From e6867a031810b86edfbf0c434ddfb6ee594f5486 Mon Sep 17 00:00:00 2001 From: Evan Kaloudis Date: Sun, 14 Apr 2024 22:17:28 -0400 Subject: [PATCH 01/24] Embedded LND: v0.17.4-beta-zeus.3 --- fetch-libraries.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fetch-libraries.sh b/fetch-libraries.sh index 655037447..624589d74 100644 --- a/fetch-libraries.sh +++ b/fetch-libraries.sh @@ -1,10 +1,10 @@ -VERSION=v0.17.4-beta-zeus.2 +VERSION=v0.17.4-beta-zeus.3 ANDROID_FILE=Lndmobile.aar IOS_FILE=Lndmobile.xcframework -ANDROID_SHA256='a913226224ff02fadb4a2b0ccc069df132c7b9eaba48c8d27f02c8090dbb5f12' -IOS_SHA256='6defcda62b5ab9eb147a7fb6f5e3c136d3b24bc93887981e612af280ecd85471' +ANDROID_SHA256='76f25ecc784cf9a26751f1ca7406e0817c56bc0d3eb46cf56dd07bbb2c6b1dee' +IOS_SHA256='88d08bfabd8451e470b2c70ee86358dfef68fe3b07dcfab3d6c661f7c1b0fccd' FILE_PATH=https://github.com/ZeusLN/lnd/releases/download/$VERSION/ From 19925f205223801709e833c311af9c9793779300 Mon Sep 17 00:00:00 2001 From: Evan Kaloudis Date: Sun, 14 Apr 2024 22:21:33 -0400 Subject: [PATCH 02/24] LND: wire up fundPsbt, finalizePsbt, publishTransaction calls --- backends/EmbeddedLND.ts | 37 +++++++------------ backends/LND.ts | 5 +-- backends/LightningNodeConnect.ts | 12 +++---- ios/LndMobile/Lnd.swift | 5 ++- lndmobile/LndMobileInjection.ts | 39 +++++++++++++++++--- lndmobile/index.ts | 10 ++++-- lndmobile/onchain.ts | 16 ++++++--- lndmobile/wallet.ts | 61 ++++++++++++++++++++++++++++++-- 8 files changed, 139 insertions(+), 46 deletions(-) diff --git a/backends/EmbeddedLND.ts b/backends/EmbeddedLND.ts index c3c9ab471..28af654cb 100644 --- a/backends/EmbeddedLND.ts +++ b/backends/EmbeddedLND.ts @@ -34,7 +34,9 @@ const { bumpFee, fundPsbt, finalizePsbt, - publishTransaction + publishTransaction, + listAccounts, + importAccount } = lndMobile.wallet; const { walletBalance, newAddress, getTransactions, sendCoins } = lndMobile.onchain; @@ -45,7 +47,7 @@ export default class EmbeddedLND extends LND { getPendingChannels = async () => await pendingChannels(); getClosedChannels = async () => await closedChannels(); getChannelInfo = async (chanId: string) => await getChanInfo(chanId); - getBlockchainBalance = async () => await walletBalance(); + getBlockchainBalance = async (data: any) => await walletBalance(data); getLightningBalance = async () => await channelBalance(); sendCoins = async (data: any) => await sendCoins( @@ -69,7 +71,8 @@ export default class EmbeddedLND extends LND { preimage: data.preimage }); getPayments = async () => await listPayments(); - getNewAddress = async (data: any) => await newAddress(data.type); + getNewAddress = async (data: any) => + await newAddress(data.type, data.account); openChannel = async (data: OpenChannelRequest) => await openChannel( data.node_pubkey_string, @@ -152,30 +155,16 @@ export default class EmbeddedLND extends LND { urlParams && (await queryRoutes(urlParams[0], urlParams[1])); // getForwardingHistory = () => N/A // // Coin Control - fundPsbt = async (data: any) => - await fundPsbt({ - raw: data.raw, - spend_unconfirmed: data.spend_unconfirmed, - sat_per_vbyte: data.sat_per_vbyte - }); - finalizePsbt = async (data: any) => - await finalizePsbt({ - funded_psbt: data.funded_psbt - }); - publishTransaction = async (data: any) => - await publishTransaction({ - tx_hex: data.tx_hex - }); + fundPsbt = async (data: any) => await fundPsbt(data); + finalizePsbt = async (data: any) => await finalizePsbt(data); + publishTransaction = async (data: any) => await publishTransaction(data); - getUTXOs = async () => await listUnspent(); + getUTXOs = async (data: any) => await listUnspent(data); bumpFee = async (data: any) => await bumpFee(data); lookupInvoice = async (data: any) => await lookupInvoice(data.r_hash); - // TODO inject - // listAccounts = () => this.getRequest('/v2/wallet/accounts'); - // TODO inject - // importAccount = (data: any) => - // this.postRequest('/v2/wallet/accounts/import', data); + listAccounts = async () => await listAccounts(); + importAccount = async (data: any) => await importAccount(data); // TODO rewrite subscription logic, starting on Receive view // subscribeInvoice = (r_hash: string) => @@ -199,7 +188,7 @@ export default class EmbeddedLND extends LND { supportsHopPicking = () => this.supports('v0.11.0'); // TODO wire up accounts // supportsAccounts = () => this.supports('v0.13.0'); - supportsAccounts = () => false; + supportsAccounts = () => true; supportsRouting = () => false; supportsNodeInfo = () => true; singleFeesEarnedTotal = () => false; diff --git a/backends/LND.ts b/backends/LND.ts index e94c5770f..5f7b06068 100644 --- a/backends/LND.ts +++ b/backends/LND.ts @@ -240,7 +240,8 @@ export default class LND { getClosedChannels = () => this.getRequest('/v1/channels/closed'); getChannelInfo = (chanId: string) => this.getRequest(`/v1/graph/edge/${chanId}`); - getBlockchainBalance = () => this.getRequest('/v1/balance/blockchain'); + getBlockchainBalance = (data: any) => + this.getRequest('/v1/balance/blockchain', data); getLightningBalance = () => this.getRequest('/v1/balance/channels'); sendCoins = (data: any) => this.postRequest('/v1/transactions', { @@ -406,7 +407,7 @@ export default class LND { finalizePsbt = (data: any) => this.postRequest('/v2/wallet/psbt/finalize', data); publishTransaction = (data: any) => this.postRequest('/v2/wallet/tx', data); - getUTXOs = () => this.getRequest('/v1/utxos?min_confs=0&max_confs=200000'); + getUTXOs = (data: any) => this.postRequest('/v2/wallet/utxos', data); bumpFee = (data: any) => this.postRequest('/v2/wallet/bumpfee', data); listAccounts = () => this.getRequest('/v2/wallet/accounts'); importAccount = (data: any) => diff --git a/backends/LightningNodeConnect.ts b/backends/LightningNodeConnect.ts index 5a4819f1a..a8ba28142 100644 --- a/backends/LightningNodeConnect.ts +++ b/backends/LightningNodeConnect.ts @@ -102,13 +102,13 @@ export default class LightningNodeConnect { .getChanInfo(request) .then((data: lnrpc.ChannelEdge) => snakeize(data)); }; - getBlockchainBalance = async () => + getBlockchainBalance = async (req: lnrpc.WalletBalanceRequest) => await this.lnc.lnd.lightning - .walletBalance({}) + .walletBalance(req) .then((data: lnrpc.WalletBalanceResponse) => snakeize(data)); - getLightningBalance = async () => + getLightningBalance = async (req: lnrpc.ChannelBalanceRequest) => await this.lnc.lnd.lightning - .channelBalance({}) + .channelBalance(req) .then((data: lnrpc.ChannelBalanceResponse) => snakeize(data)); sendCoins = async (data: any) => await this.lnc.lnd.lightning @@ -311,9 +311,9 @@ export default class LightningNodeConnect { await this.lnc.lnd.walletKit .publishTransaction(req) .then((data: walletrpc.PublishResponse) => snakeize(data)); - getUTXOs = async () => + getUTXOs = async (req: walletrpc.ListUnspentRequest) => await this.lnc.lnd.walletKit - .listUnspent({ min_confs: 0, max_confs: 200000 }) + .listUnspent(req) .then((data: walletrpc.ListUnspentResponse) => snakeize(data)); bumpFee = async (req: walletrpc.BumpFeeRequest) => await this.lnc.lnd.walletKit diff --git a/ios/LndMobile/Lnd.swift b/ios/LndMobile/Lnd.swift index 536734b20..16468acb9 100644 --- a/ios/LndMobile/Lnd.swift +++ b/ios/LndMobile/Lnd.swift @@ -119,7 +119,10 @@ open class Lnd { "FundPsbt": { bytes, cb in LndmobileWalletKitFundPsbt(bytes, cb) }, "FinalizePsbt": { bytes, cb in LndmobileWalletKitFinalizePsbt(bytes, cb) }, "PublishTransaction": { bytes, cb in LndmobileWalletKitPublishTransaction(bytes, cb) }, - // derivePrivateKey + "ListAccounts": { bytes, cb in LndmobileWalletKitListAccounts(bytes, cb) }, + "ImportAccount": { bytes, cb in LndmobileWalletKitImportAccount(bytes, cb) }, + + // derivePrivateKey "VerifyMessage": { bytes, cb in LndmobileVerifyMessage(bytes, cb) }, "SignMessage": { bytes, cb in LndmobileSignMessage(bytes, cb) }, "SignerSignMessage": { bytes, cb in LndmobileSignerSignMessage(bytes, cb) }, diff --git a/lndmobile/LndMobileInjection.ts b/lndmobile/LndMobileInjection.ts index 75767899d..4bd2bd652 100644 --- a/lndmobile/LndMobileInjection.ts +++ b/lndmobile/LndMobileInjection.ts @@ -78,7 +78,9 @@ import { bumpFee, fundPsbt, finalizePsbt, - publishTransaction + publishTransaction, + listAccounts, + importAccount } from './wallet'; import { status, modifyStatus, queryScores, setScores } from './autopilot'; import { checkScheduledSyncWorkStatus } from './scheduled-sync'; // TODO(hsjoberg): This could be its own injection "LndMobileScheduledSync" @@ -143,7 +145,11 @@ export interface ILndMobileInjections { ) => Promise; decodePayReq: (bolt11: string) => Promise; getRecoveryInfo: () => Promise; - listUnspent: () => Promise; + listUnspent: ({ + account + }: { + account?: string; + }) => Promise; resetMissionControl: () => Promise; getInfo: () => Promise; getNetworkInfo: () => Promise; @@ -273,7 +279,8 @@ export interface ILndMobileInjections { onchain: { getTransactions: () => Promise; newAddress: ( - type: lnrpc.AddressType + type: lnrpc.AddressType, + account?: string ) => Promise; sendCoins: ( address: string, @@ -286,7 +293,11 @@ export interface ILndMobileInjections { address: string, feeRate?: number ) => Promise; - walletBalance: () => Promise; + walletBalance: ({ + account + }: { + account?: string; + }) => Promise; subscribeTransactions: () => Promise; }; wallet: { @@ -335,10 +346,12 @@ export interface ILndMobileInjections { sat_per_vbyte?: Long; }) => Promise; fundPsbt: ({ + account, raw, spend_unconfirmed, sat_per_vbyte }: { + account?: string; raw: walletrpc.TxTemplate; spend_unconfirmed?: boolean; sat_per_vbyte?: Long; @@ -353,6 +366,20 @@ export interface ILndMobileInjections { }: { tx_hex: Uint8Array; }) => Promise; + listAccounts: () => Promise; + importAccount: ({ + name, + extended_public_key, + master_key_fingerprint, + address_type, + dry_run + }: { + name: string; + extended_public_key: string; + master_key_fingerprint?: Uint8Array; + address_type?: number; + dry_run: boolean; + }) => Promise; }; autopilot: { status: () => Promise; @@ -446,7 +473,9 @@ export default { bumpFee, fundPsbt, finalizePsbt, - publishTransaction + publishTransaction, + listAccounts, + importAccount }, autopilot: { status, diff --git a/lndmobile/index.ts b/lndmobile/index.ts index 4f4676a68..c9bc4a6c4 100644 --- a/lndmobile/index.ts +++ b/lndmobile/index.ts @@ -757,7 +757,11 @@ export const getRecoveryInfo = /** * @throws */ -export const listUnspent = async (): Promise => { +export const listUnspent = async ({ + account = 'default' +}: { + account: string; +}): Promise => { const response = await sendCommand< lnrpc.IListUnspentRequest, lnrpc.ListUnspentRequest, @@ -766,7 +770,9 @@ export const listUnspent = async (): Promise => { request: lnrpc.ListUnspentRequest, response: lnrpc.ListUnspentResponse, method: 'WalletKitListUnspent', - options: {} + options: { + account + } }); return response; }; diff --git a/lndmobile/onchain.ts b/lndmobile/onchain.ts index 9edd3d530..d0e31116e 100644 --- a/lndmobile/onchain.ts +++ b/lndmobile/onchain.ts @@ -23,7 +23,8 @@ export const getTransactions = async (): Promise => { * @throws */ export const newAddress = async ( - type: lnrpc.AddressType = lnrpc.AddressType.UNUSED_WITNESS_PUBKEY_HASH + type: lnrpc.AddressType = lnrpc.AddressType.UNUSED_WITNESS_PUBKEY_HASH, + account: string = 'default' ): Promise => { const response = await sendCommand< lnrpc.INewAddressRequest, @@ -34,7 +35,8 @@ export const newAddress = async ( response: lnrpc.NewAddressResponse, method: 'NewAddress', options: { - type + type, + account } }); return response; @@ -43,7 +45,11 @@ export const newAddress = async ( /** * @throws */ -export const walletBalance = async (): Promise => { +export const walletBalance = async ({ + account +}: { + account?: string; +}): Promise => { const response = await sendCommand< lnrpc.IWalletBalanceRequest, lnrpc.WalletBalanceRequest, @@ -52,7 +58,9 @@ export const walletBalance = async (): Promise => { request: lnrpc.WalletBalanceRequest, response: lnrpc.WalletBalanceResponse, method: 'WalletBalance', - options: {} + options: { + account + } }); return response; }; diff --git a/lndmobile/wallet.ts b/lndmobile/wallet.ts index a6937db38..c0338597d 100644 --- a/lndmobile/wallet.ts +++ b/lndmobile/wallet.ts @@ -25,7 +25,7 @@ export const bumpFee = async ({ outpoint, target_conf, force, - sat_per_vbyte + sat_per_vbyte: sat_per_vbyte ? Long.fromValue(sat_per_vbyte) : undefined }; const response = await sendCommand< walletrpc.IBumpFeeRequest, @@ -44,18 +44,21 @@ export const bumpFee = async ({ * @throws */ export const fundPsbt = async ({ + account, raw, spend_unconfirmed, sat_per_vbyte }: { + account?: string; raw: walletrpc.TxTemplate; spend_unconfirmed?: boolean; sat_per_vbyte?: Long; }): Promise => { const options: walletrpc.IFundPsbtRequest = { + account, raw, spend_unconfirmed, - sat_per_vbyte + sat_per_vbyte: sat_per_vbyte ? Long.fromValue(sat_per_vbyte) : undefined }; const response = await sendCommand< walletrpc.IFundPsbtRequest, @@ -94,6 +97,60 @@ export const finalizePsbt = async ({ return response; }; +/** + * @throws + */ +export const listAccounts = + async (): Promise => { + const response = await sendCommand< + walletrpc.IListAccountsRequest, + walletrpc.ListAccountsRequest, + walletrpc.ListAccountsResponse + >({ + request: walletrpc.ListAccountsRequest, + response: walletrpc.ListAccountsResponse, + method: 'ListAccounts', + options: {} + }); + return response; + }; + +/** + * @throws + */ +export const importAccount = async ({ + name, + extended_public_key, + master_key_fingerprint, + address_type, + dry_run +}: { + name: string; + extended_public_key: string; + master_key_fingerprint?: Uint8Array; + address_type?: number; + dry_run: boolean; +}): Promise => { + const options: walletrpc.IImportAccountRequest = { + name, + extended_public_key, + master_key_fingerprint, + address_type, + dry_run + }; + const response = await sendCommand< + walletrpc.IImportAccountRequest, + walletrpc.ImportAccountRequest, + walletrpc.ImportAccountResponse + >({ + request: walletrpc.ImportAccountRequest, + response: walletrpc.ImportAccountResponse, + method: 'ImportAccount', + options + }); + return response; +}; + /** * @throws */ From 571e0afa695157779b862fc0fce750524521db2a Mon Sep 17 00:00:00 2001 From: Evan Kaloudis Date: Sun, 14 Apr 2024 23:39:03 -0400 Subject: [PATCH 03/24] External Accounts: importing, and UTXO picker enhancements --- Navigation.ts | 4 + assets/images/SVG/Account.svg | 1 + assets/images/SVG/DynamicSVG/MatiSvg.tsx | 32 + components/AccountFilter.tsx | 109 ++++ .../LayerBalances/OnchainSwipeableRow.tsx | 9 +- components/LayerBalances/index.tsx | 236 +++++-- components/Pill.tsx | 6 +- components/UTXOPicker.tsx | 103 ++- locales/en.json | 15 +- models/Account.ts | 18 + models/FundedPsbt.ts | 18 + models/TransactionRequest.ts | 1 + stores/BalanceStore.ts | 26 +- stores/UTXOsStore.ts | 81 ++- utils/AddressUtils.test.ts | 28 + utils/AddressUtils.ts | 5 + utils/Base64Utils.test.ts | 6 + utils/Base64Utils.ts | 29 + utils/BbqrUtils.ts | 603 ++++++++++++++++++ utils/ThemeUtils.ts | 1 + utils/handleAnything.ts | 10 +- views/Accounts/Accounts.tsx | 34 +- views/Accounts/ImportAccount.tsx | 290 ++++++--- views/Accounts/ImportingAccount.tsx | 191 ++++++ views/PSBT.tsx | 275 ++++++++ views/Receive.tsx | 39 +- views/Settings/Settings.tsx | 48 +- views/UTXOs/CoinControl.tsx | 47 +- views/Wallet/Wallet.tsx | 5 +- 29 files changed, 2016 insertions(+), 254 deletions(-) create mode 100644 assets/images/SVG/Account.svg create mode 100644 assets/images/SVG/DynamicSVG/MatiSvg.tsx create mode 100644 components/AccountFilter.tsx create mode 100644 models/Account.ts create mode 100644 models/FundedPsbt.ts create mode 100644 utils/BbqrUtils.ts create mode 100644 views/Accounts/ImportingAccount.tsx create mode 100644 views/PSBT.tsx diff --git a/Navigation.ts b/Navigation.ts index 153dcf3d6..b602c1950 100644 --- a/Navigation.ts +++ b/Navigation.ts @@ -98,6 +98,7 @@ import CoinControl from './views/UTXOs/CoinControl'; import Utxo from './views/UTXOs/UTXO'; import Accounts from './views/Accounts/Accounts'; import ImportAccount from './views/Accounts/ImportAccount'; +import ImportingAccount from './views/Accounts/ImportingAccount'; import ImportAccountQRScanner from './views/Accounts/ImportAccountQRScanner'; import BumpFee from './views/BumpFee'; import QR from './views/QR'; @@ -286,6 +287,9 @@ const AppScenes = { ImportAccount: { screen: ImportAccount }, + ImportingAccount: { + screen: ImportingAccount + }, HandleAnythingQRScanner: { screen: HandleAnythingQRScanner }, diff --git a/assets/images/SVG/Account.svg b/assets/images/SVG/Account.svg new file mode 100644 index 000000000..1227c6954 --- /dev/null +++ b/assets/images/SVG/Account.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/SVG/DynamicSVG/MatiSvg.tsx b/assets/images/SVG/DynamicSVG/MatiSvg.tsx new file mode 100644 index 000000000..8d550ec1d --- /dev/null +++ b/assets/images/SVG/DynamicSVG/MatiSvg.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Svg, Circle, Path } from 'react-native-svg'; +import { themeColor } from '../../../../utils/ThemeUtils'; + +export default function MatiSvg({ width = 70, height = 70 }) { + const svgProps = { + width: `${width}`, + height: `${height}`, + viewBox: '0 0 50 50', + fill: 'none', + xmlns: 'http://www.w3.org/2000/svg' + }; + + const circleProps = { + cx: '25', + cy: '25', + r: '20', + fill: themeColor('background') + }; + + const path1Props = { + d: 'M34.45 28.3045l-9.4486 4.6305L15.55 28.3045l4.1137 -2.0184c1.0616 1.8058 3.055 3.0196 5.3348 3.0196s4.2732 -1.2137 5.3348 -3.0196zM24.9986 17.065L15.55 21.6955l4.1137 2.0184c1.0616 -1.8058 3.0535 -3.0196 5.3348 -3.0196c2.2828 0 4.2732 1.2137 5.3348 3.0196L34.45 21.6955zM24.9986 23.0067c-1.1252 0 -2.0391 0.8933 -2.0391 1.9933s0.914 1.9948 2.0391 1.9948S27.0376 26.1001 27.0376 25s-0.914 -1.9933 -2.0391 -1.9933zM24.9986 23.0067', + fill: themeColor('chain') + }; + + return React.createElement( + Svg, + svgProps, + React.createElement(Circle, circleProps), + React.createElement(Path, path1Props) + ); +} diff --git a/components/AccountFilter.tsx b/components/AccountFilter.tsx new file mode 100644 index 000000000..a5610c5bc --- /dev/null +++ b/components/AccountFilter.tsx @@ -0,0 +1,109 @@ +import React, { useState, useEffect } from 'react'; +import { ScrollView, View } from 'react-native'; + +import Pill from './Pill'; +import { themeColor } from './../utils/ThemeUtils'; + +interface PillProps { + name: string; + textColor?: string; + borderColor?: string; + backgroundColor?: string; +} + +interface AccountFilterProps { + items: Array; + refresh: Function; + onChangeAccount?: Function; + default?: string; + showAll?: boolean; +} + +function AccountFilter(props: AccountFilterProps) { + const [selectedPin, setPin] = useState(props.default); + + useEffect(() => { + props.refresh(selectedPin); + }, [selectedPin]); + + const pills = []; + + if (props.showAll) { + pills.push( + + { + if (props.onChangeAccount) props.onChangeAccount(); + setPin(''); + }} + /> + + ); + } + + pills.push( + + { + if (props.onChangeAccount) props.onChangeAccount(); + setPin('default'); + }} + /> + + ); + + for (const item in props.items) { + const account = props.items[item]; + const { name } = account; + + pills.push( + + { + if (props.onChangeAccount) props.onChangeAccount(); + setPin(name); + }} + /> + + ); + } + + return ( + + {pills} + + ); +} + +export default AccountFilter; diff --git a/components/LayerBalances/OnchainSwipeableRow.tsx b/components/LayerBalances/OnchainSwipeableRow.tsx index 08cb16854..08ed02bb6 100644 --- a/components/LayerBalances/OnchainSwipeableRow.tsx +++ b/components/LayerBalances/OnchainSwipeableRow.tsx @@ -23,6 +23,7 @@ interface OnchainSwipeableRowProps { value?: string; amount?: string; locked?: boolean; + account?: string; } export default class OnchainSwipeableRow extends Component< @@ -34,6 +35,7 @@ export default class OnchainSwipeableRow extends Component< x: number, progress: Animated.AnimatedInterpolation ) => { + const { account, navigation } = this.props; const transTranslateX = progress.interpolate({ inputRange: [0.25, 1], outputRange: [x, 0] @@ -46,14 +48,15 @@ export default class OnchainSwipeableRow extends Component< this.close(); if (text === localeString('general.receive')) { - this.props.navigation.navigate('Receive', { + navigation.navigate('Receive', { + account: account === 'On-chain' ? 'default' : account, selectedIndex: 2, autoGenerateOnChain: true }); } else if (text === localeString('general.coins')) { - this.props.navigation.navigate('CoinControl'); + navigation.navigate('CoinControl', { account }); } else if (text === localeString('general.send')) { - this.props.navigation.navigate('Send'); + navigation.navigate('Send'); } }; diff --git a/components/LayerBalances/index.tsx b/components/LayerBalances/index.tsx index a52b3386d..a3c165fe1 100644 --- a/components/LayerBalances/index.tsx +++ b/components/LayerBalances/index.tsx @@ -1,6 +1,13 @@ import React, { Component } from 'react'; -import { FlatList, StyleSheet, Text, View, I18nManager } from 'react-native'; - +import { + FlatList, + StyleSheet, + Text, + TouchableOpacity, + View, + I18nManager +} from 'react-native'; +import LinearGradient from 'react-native-linear-gradient'; import { RectButton } from 'react-native-gesture-handler'; import { inject, observer } from 'mobx-react'; @@ -11,6 +18,7 @@ import LightningSwipeableRow from './LightningSwipeableRow'; import BalanceStore from './../../stores/BalanceStore'; import UnitsStore from './../../stores/UnitsStore'; +import UTXOsStore from '../../stores/UTXOsStore'; import BackendUtils from '../../utils/BackendUtils'; import { localeString } from './../../utils/LocaleUtils'; @@ -18,10 +26,11 @@ import { themeColor } from './../../utils/ThemeUtils'; import OnChainSvg from '../../assets/images/SVG/DynamicSVG/OnChainSvg'; import LightningSvg from '../../assets/images/SVG/DynamicSVG/LightningSvg'; -import LinearGradient from 'react-native-linear-gradient'; +import MatiSvg from '../../assets/images/SVG/DynamicSVG/MatiSvg'; interface LayerBalancesProps { BalanceStore: BalanceStore; + UTXOsStore: UTXOsStore; UnitsStore: UnitsStore; navigation: any; onRefresh?: any; @@ -29,6 +38,7 @@ interface LayerBalancesProps { amount?: string; lightning?: string; locked?: boolean; + consolidated?: boolean; } // To toggle LTR/RTL change to `true` @@ -36,50 +46,104 @@ I18nManager.allowRTL(false); type DataRow = { layer: string; + subtitle?: string; balance: string | number; + // TODO check if exists + count: number; + watchOnly?: boolean; }; -const Row = ({ item }: { item: DataRow }) => ( - - - - {item.layer === 'On-chain' ? : } - - - {item.layer === 'Lightning' - ? localeString('general.lightning') - : localeString('general.onchain')} - - +const Row = ({ item }: { item: DataRow }) => { + const moreAccounts = + item.layer === localeString('components.LayerBalances.moreAccounts'); + return ( + + + + {item.watchOnly ? ( + + ) : item.layer === 'On-chain' ? ( + + ) : item.layer === 'Lightning' ? ( + + ) : moreAccounts ? null : ( + + )} + + + + {item.layer === 'Lightning' + ? localeString('general.lightning') + : item.layer === 'On-chain' + ? localeString('general.onchain') + : item.layer} + + {item.subtitle && ( + + {item.subtitle} + + )} + + - - - -); + {!moreAccounts ? ( + + ) : ( + + {`+${item.count - 1}`} + + )} + + + ); +}; const SwipeableRow = ({ item, @@ -99,6 +163,18 @@ const SwipeableRow = ({ lightning?: string; locked?: boolean; }) => { + if (index === 0) { + return ( + + + + ); + } + if (index === 1) { return ( navigation.navigate('Accounts')}> + + + ); + } + return ( - - + ); }; -@inject() +@inject('BalanceStore', 'UTXOsStore') @observer export default class LayerBalances extends Component { render() { @@ -134,11 +220,14 @@ export default class LayerBalances extends Component { amount, lightning, onRefresh, - locked + locked, + consolidated } = this.props; const { totalBlockchainBalance, lightningBalance } = BalanceStore; + const otherAccounts = this.props.UTXOsStore.accounts; + let DATA: DataRow[] = [ { layer: 'Lightning', @@ -154,8 +243,41 @@ export default class LayerBalances extends Component { }); } + if (Object.keys(otherAccounts).length > 0 && !consolidated) { + for (let i = 0; i < otherAccounts.length; i++) { + DATA.push({ + layer: otherAccounts[i].name, + subtitle: otherAccounts[i].XFP, + balance: otherAccounts[i].balance || 0, + watchOnly: otherAccounts[i].watch_only || false + }); + } + } + + if (Object.keys(otherAccounts).length > 0 && consolidated) { + let n = 0; + for (let i = 0; i < otherAccounts.length; i++) { + while (n < 1) { + DATA.push({ + layer: otherAccounts[i].name, + subtitle: otherAccounts[i].XFP, + balance: otherAccounts[i].balance || 0, + watchOnly: otherAccounts[i].watch_only || false + }); + n++; + } + } + } + + if (Object.keys(otherAccounts).length > 1 && consolidated) { + DATA.push({ + layer: localeString('components.LayerBalances.moreAccounts'), + count: Object.keys(otherAccounts).length + }); + } + return ( - <> + ( @@ -174,17 +296,18 @@ export default class LayerBalances extends Component { /> )} keyExtractor={(_item, index) => `message ${index}`} - style={{ top: 20 }} + style={{ marginTop: 20 }} onRefresh={() => onRefresh()} refreshing={false} /> - + ); } } const styles = StyleSheet.create({ rectButton: { + flex: 1, height: 80, paddingVertical: 10, paddingLeft: 6, @@ -196,6 +319,17 @@ const styles = StyleSheet.create({ marginRight: 15, borderRadius: 50 }, + moreButton: { + height: 40, + paddingVertical: 10, + paddingHorizontal: 20, + justifyContent: 'space-between', + alignItems: 'center', + flexDirection: 'row', + marginLeft: 15, + marginRight: 15, + borderRadius: 15 + }, left: { flexDirection: 'row', alignItems: 'center', diff --git a/components/Pill.tsx b/components/Pill.tsx index a1e01dde1..a80db10cd 100644 --- a/components/Pill.tsx +++ b/components/Pill.tsx @@ -7,15 +7,16 @@ interface PillProps { textColor?: string; borderColor?: string; backgroundColor?: string; + onPress?: () => void; } function Pill(props: PillProps) { - const { title, textColor, borderColor, backgroundColor } = props; + const { title, textColor, borderColor, backgroundColor, onPress } = props; return ( void; + onValueChange: (value: any, balance: number, account: string) => void; UTXOsStore: UTXOsStore; } @@ -34,6 +36,7 @@ interface UTXOPickerState { showUtxoModal: boolean; selectedBalance: number; setBalance: number; + account: string; } const DEFAULT_TITLE = localeString('components.UTXOPicker.defaultTitle'); @@ -49,11 +52,17 @@ export default class UTXOPicker extends React.Component< utxosSet: [], showUtxoModal: false, selectedBalance: 0, - setBalance: 0 + setBalance: 0, + account: 'default' }; + UNSAFE_componentWillMount() { + if (BackendUtils.supportsAccounts()) + this.props.UTXOsStore.listAccounts(); + } + openPicker() { - stores.utxosStore.getUTXOs(); + stores.utxosStore.getUTXOs(this.state.account); this.setState({ utxosSelected: [], showUtxoModal: true, @@ -68,7 +77,7 @@ export default class UTXOPicker extends React.Component< selectedBalance: 0, setBalance: 0 }); - this.props.onValueChange([], 0); + this.props.onValueChange([], 0, 'default'); } displayValues(): string { @@ -103,9 +112,14 @@ export default class UTXOPicker extends React.Component< render() { const { title, onValueChange, UTXOsStore } = this.props; - const { utxosSelected, utxosSet, showUtxoModal, selectedBalance } = - this.state; - const { utxos, loading, getUTXOs } = UTXOsStore; + const { + utxosSelected, + utxosSet, + showUtxoModal, + selectedBalance, + account + } = this.state; + const { utxos, loading, getUTXOs, accounts } = UTXOsStore; const utxosPicked: string[] = []; utxosSelected.forEach((utxo: string) => utxosPicked.push(utxo)); @@ -163,11 +177,33 @@ export default class UTXOPicker extends React.Component< /> + {BackendUtils.supportsAccounts() && ( + { + getUTXOs({ + account: newAccount + }); + this.setState({ + account: newAccount + }); + }} + onChangeAccount={() => { + this.setState({ + utxosSelected: [], + selectedBalance: 0 + }); + }} + /> + )} + {loading && } {!loading && utxos.length === 0 && ( `${item.txid}-${index}`} onEndReachedThreshold={50} refreshing={loading} - onRefresh={() => getUTXOs()} + onRefresh={() => getUTXOs(account)} /> )} - {!loading && utxos.length > 0 && ( + {!loading && ( <> - -