From 09bbdb3a538212c804954840c828b078f12a9427 Mon Sep 17 00:00:00 2001 From: louisinger Date: Thu, 14 Oct 2021 11:29:10 +0200 Subject: [PATCH 01/52] cosigner pair view --- src/background/backend.ts | 13 ++++--- src/presentation/cosigner/pair.tsx | 56 ++++++++++++++++++++++++++++ src/presentation/routes/constants.ts | 5 +++ src/presentation/routes/index.tsx | 4 ++ 4 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 src/presentation/cosigner/pair.tsx diff --git a/src/background/backend.ts b/src/background/backend.ts index f6d0d755..95cf8452 100644 --- a/src/background/backend.ts +++ b/src/background/backend.ts @@ -51,6 +51,11 @@ import { flushTx } from '../application/redux/actions/connect'; const UPDATE_ALARM = 'UPDATE_ALARM'; +const getAddresses = async (state: RootReducerState) => { + const xpub = await getRestoredXPub(state); + return (await xpub.getAddresses()).reverse(); +} + /** * fetch and unblind the utxos and then refresh it. */ @@ -61,10 +66,7 @@ export function fetchAndUpdateUtxos(): ThunkAction ({ @@ -156,8 +158,7 @@ export function updateTxsHistory(): ThunkAction address.toOutputScript(a.confidentialAddress).toString('hex') ); diff --git a/src/presentation/cosigner/pair.tsx b/src/presentation/cosigner/pair.tsx new file mode 100644 index 00000000..6d824cb7 --- /dev/null +++ b/src/presentation/cosigner/pair.tsx @@ -0,0 +1,56 @@ +import { Field, Form, FormikProps, withFormik } from 'formik'; +import React from 'react'; +import Shell from '../components/shell'; +import * as Yup from 'yup'; +import Button from '../components/button'; + +interface OptInFormProps { + onSubmit: (values: OptInFormValues) => Promise; +} + +interface OptInFormValues { + cosignerURL: string; +} + +const optInForm = (props: FormikProps) => { + const { touched, errors, isSubmitting } = props; + return ( +
+ + {touched.cosignerURL && errors.cosignerURL &&
{errors.cosignerURL}
} + + + + ) +} + +const OptInFormikForm = withFormik({ + validationSchema: Yup.object().shape({ + cosignerURL: Yup.string() + .required('Please input cosignerURL') + .url('Not a valid URL') + }), + + handleSubmit: async (values, { props }) => { + await props.onSubmit(values); + }, + + displayName: 'OptInForm', +})(optInForm); + +const PairCosigner: React.FC = () => { + const onSubmit = (values: OptInFormValues) => { + console.log(values); + return Promise.resolve(); + } + + return +

{'Restore a wallet from a mnemonic phrase'}

+

{'Pair a new cosigner, endpoints must be compatible with marina cosigner API.'}

+ +
+} + +export default PairCosigner; diff --git a/src/presentation/routes/constants.ts b/src/presentation/routes/constants.ts index 88f884d5..019e7b7d 100644 --- a/src/presentation/routes/constants.ts +++ b/src/presentation/routes/constants.ts @@ -50,6 +50,9 @@ const SETTINGS_CREDITS_ROUTE = '/settings/info/credits'; const SETTINGS_TERMS_ROUTE = '/settings/info/terms-of-service'; const SETTINGS_DEEP_RESTORER_ROUTE = '/settings/info/deep-restorer'; +// Cosigner Opt in +const PAIR_COSIGNER_ROUTE = '/cosigner/pair'; + export { //Connect CONNECT_ENABLE_ROUTE, @@ -90,4 +93,6 @@ export { SETTINGS_CREDITS_ROUTE, SETTINGS_TERMS_ROUTE, SETTINGS_DEEP_RESTORER_ROUTE, + + PAIR_COSIGNER_ROUTE }; diff --git a/src/presentation/routes/index.tsx b/src/presentation/routes/index.tsx index 768ab5e1..9bbccbb2 100644 --- a/src/presentation/routes/index.tsx +++ b/src/presentation/routes/index.tsx @@ -37,6 +37,7 @@ import { BACKUP_UNLOCK_ROUTE, SETTINGS_DEEP_RESTORER_ROUTE, RECEIVE_ADDRESS_ROUTE, + PAIR_COSIGNER_ROUTE, } from './constants'; // Connect @@ -79,6 +80,7 @@ import SettingsExplorer from '../settings/explorer'; import SettingsNetworks from '../../application/redux/containers/settings-networks.container'; import SettingsCredits from '../settings/credits'; import SettingsTerms from '../settings/terms'; +import PairCosigner from '../cosigner/pair'; const Routes: React.FC = () => { return ( @@ -123,6 +125,8 @@ const Routes: React.FC = () => { + + ); }; From ca4b1fbfdb57b54546c10f93eb5ac74ffdc4cf1d Mon Sep 17 00:00:00 2001 From: louisinger Date: Thu, 14 Oct 2021 15:53:51 +0200 Subject: [PATCH 02/52] add cosigner in settings menu --- src/presentation/routes/constants.ts | 2 ++ src/presentation/routes/index.tsx | 3 +++ src/presentation/settings/cosigners.tsx | 28 +++++++++++++++++++++ src/presentation/settings/menu-settings.tsx | 10 ++++++++ 4 files changed, 43 insertions(+) create mode 100644 src/presentation/settings/cosigners.tsx diff --git a/src/presentation/routes/constants.ts b/src/presentation/routes/constants.ts index 019e7b7d..2dd47368 100644 --- a/src/presentation/routes/constants.ts +++ b/src/presentation/routes/constants.ts @@ -49,6 +49,7 @@ const SETTINGS_MENU_INFO_ROUTE = '/settings/info'; const SETTINGS_CREDITS_ROUTE = '/settings/info/credits'; const SETTINGS_TERMS_ROUTE = '/settings/info/terms-of-service'; const SETTINGS_DEEP_RESTORER_ROUTE = '/settings/info/deep-restorer'; +const SETTINGS_COSIGNERS_ROUTE = '/settings/info/cosigners'; // Cosigner Opt in const PAIR_COSIGNER_ROUTE = '/cosigner/pair'; @@ -93,6 +94,7 @@ export { SETTINGS_CREDITS_ROUTE, SETTINGS_TERMS_ROUTE, SETTINGS_DEEP_RESTORER_ROUTE, + SETTINGS_COSIGNERS_ROUTE, PAIR_COSIGNER_ROUTE }; diff --git a/src/presentation/routes/index.tsx b/src/presentation/routes/index.tsx index 9bbccbb2..38156356 100644 --- a/src/presentation/routes/index.tsx +++ b/src/presentation/routes/index.tsx @@ -38,6 +38,7 @@ import { SETTINGS_DEEP_RESTORER_ROUTE, RECEIVE_ADDRESS_ROUTE, PAIR_COSIGNER_ROUTE, + SETTINGS_COSIGNERS_ROUTE, } from './constants'; // Connect @@ -81,6 +82,7 @@ import SettingsNetworks from '../../application/redux/containers/settings-networ import SettingsCredits from '../settings/credits'; import SettingsTerms from '../settings/terms'; import PairCosigner from '../cosigner/pair'; +import SettingsCosigners from '../settings/cosigners'; const Routes: React.FC = () => { return ( @@ -117,6 +119,7 @@ const Routes: React.FC = () => { + {/*Login*/} diff --git a/src/presentation/settings/cosigners.tsx b/src/presentation/settings/cosigners.tsx new file mode 100644 index 00000000..c8e04506 --- /dev/null +++ b/src/presentation/settings/cosigners.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import ShellPopUp from '../components/shell-popup'; +import browser from 'webextension-polyfill'; +import { PAIR_COSIGNER_ROUTE } from '../routes/constants'; + +const SettingsCosigners: React.FC = () => { + const openAddCosignerTab = async () => { + const url = browser.runtime.getURL(`home.html#${PAIR_COSIGNER_ROUTE}`); + await browser.tabs.create({ url }); + }; + + return ( + +

Cosigners

+
+ + Add cosigner + +
+
+ ); +}; + +export default SettingsCosigners; \ No newline at end of file diff --git a/src/presentation/settings/menu-settings.tsx b/src/presentation/settings/menu-settings.tsx index bda046af..bad49475 100644 --- a/src/presentation/settings/menu-settings.tsx +++ b/src/presentation/settings/menu-settings.tsx @@ -7,14 +7,17 @@ import { SETTINGS_EXPLORER_ROUTE, SETTINGS_NETWORKS_ROUTE, DEFAULT_ROUTE, + SETTINGS_COSIGNERS_ROUTE, } from '../routes/constants'; const SettingsMenuSettings: React.FC = () => { const history = useHistory(); + const handleChangeCurrency = () => history.push(SETTINGS_CURRENCY_ROUTE); const handleExplorer = () => history.push(SETTINGS_EXPLORER_ROUTE); const handleNetworks = () => history.push(SETTINGS_NETWORKS_ROUTE); const handleDeepRestorer = () => history.push(SETTINGS_DEEP_RESTORER_ROUTE); + const handleCosigners = () => history.push(SETTINGS_COSIGNERS_ROUTE); return ( { chevron + + ); From e2ee44d04d12b9cd0d389dc0af7b1c5498592d4b Mon Sep 17 00:00:00 2001 From: louisinger Date: Thu, 14 Oct 2021 15:54:00 +0200 Subject: [PATCH 03/52] bump ldk --- yarn.lock | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index cdc6de43..2ceb064f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6027,16 +6027,16 @@ lcid@^3.0.0: dependencies: invert-kv "^3.0.0" -ldk@^0.3.9: - version "0.3.9" - resolved "https://registry.yarnpkg.com/ldk/-/ldk-0.3.9.tgz#50ec067e31a2d8d235dc0e072a877e00b7133d15" - integrity sha512-SSSpS5r/Fbj5ZFseZcLsud9ErP5p/5dclmptfaI05bnejs5Iys47t0IggBGBhrsLggfmy7119swJMs0hyA2VXA== +ldk@^0.3.13: + version "0.3.13" + resolved "https://registry.yarnpkg.com/ldk/-/ldk-0.3.13.tgz#351c2c9a1a83f6aab692590df8cc0865a6b82956" + integrity sha512-IEYztFTS53yTvyKCNIFXThti1hk4Xir10KgRaySj9b/Exas589I+PgQvputvRKKIWRTe3wMQYQChuSGWb3dzjg== dependencies: axios "^0.21.1" bip32 "^2.0.6" bip39 "^3.0.3" bs58check "^2.1.2" - liquidjs-lib "^5.1.15" + liquidjs-lib "^5.2.2" marina-provider "^1.0.0" slip77 "^0.1.1" @@ -6086,12 +6086,13 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= -liquidjs-lib@^5.1.15: - version "5.1.15" - resolved "https://registry.yarnpkg.com/liquidjs-lib/-/liquidjs-lib-5.1.15.tgz#43cf25006da11386e23561178a260264318467a9" - integrity sha512-shrt3KBUnFWoAAqBa5iyz8v62g1P9JIlMtvW+C2GH9J3HqYFV6Hs+Rx7OtNQ1z7xTNttPEbJMGn360JwIT9D0g== +liquidjs-lib@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/liquidjs-lib/-/liquidjs-lib-5.2.2.tgz#72a6fa4a32b74c49b7f9e9f9e58e31e55ff6a264" + integrity sha512-wHwJGGQi0zU3DrOaFPqfa36i3W0XojA/aoXbr66ph1NDc3nA1CJzdsagMx++iqY23qftqgUo0rKN9WDz2wmr/Q== dependencies: "@vulpemventures/secp256k1-zkp" "^2.0.0" + axios "^0.21.1" bech32 "^1.1.2" bip174-liquid "^1.0.3" bip32 "^2.0.4" From cd5297ddc7b851ca2d702ac86425822e950b9734 Mon Sep 17 00:00:00 2001 From: louisinger Date: Thu, 14 Oct 2021 17:24:03 +0200 Subject: [PATCH 04/52] add account domain and MainAccount in Wallet reducer --- package.json | 2 +- .../containers/address-amount.container.ts | 5 +-- .../redux/containers/choose-fee.container.ts | 5 +-- .../redux/containers/end-of-flow.container.ts | 5 +-- .../redux/containers/receive.container.ts | 5 +-- src/application/redux/reducers/index.ts | 2 +- .../redux/reducers/wallet-reducer.ts | 37 +++++++++------- .../redux/selectors/wallet.selector.ts | 31 ++++++-------- src/application/utils/crypto.ts | 16 ++++--- src/application/utils/restorer.ts | 38 ++++++++++++++--- src/application/utils/transaction.ts | 6 ++- src/background/backend.ts | 29 +++++++------ src/background/background-script.ts | 4 +- src/content/marinaBroker.ts | 21 +++------- src/domain/account.ts | 31 ++++++++++++++ src/domain/wallet.ts | 11 ++--- .../components/address-amount-form.tsx | 7 ++-- src/presentation/connect/sign-msg.tsx | 2 +- src/presentation/connect/sign-pset.tsx | 16 ++----- src/presentation/connect/spend.tsx | 17 ++------ src/presentation/cosigner/pair.tsx | 42 +++++++++++-------- .../onboarding/backup-unlock/index.tsx | 4 +- src/presentation/routes/constants.ts | 3 +- src/presentation/settings/cosigners.tsx | 2 +- src/presentation/settings/menu-settings.tsx | 2 +- src/presentation/settings/show-mnemonic.tsx | 2 +- src/presentation/wallet/receive/index.tsx | 9 ++-- .../wallet/send/address-amount.tsx | 11 ++--- src/presentation/wallet/send/choose-fee.tsx | 18 +++----- src/presentation/wallet/send/end-of-flow.tsx | 24 +++++------ 30 files changed, 212 insertions(+), 195 deletions(-) create mode 100644 src/domain/account.ts diff --git a/package.json b/package.json index c39b02ee..2e7b61ce 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "decimal.js": "^10.2.1", "formik": "^2.2.6", "google-protobuf": "^3.15.8", - "ldk": "^0.3.9", + "ldk": "^0.3.13", "lodash.debounce": "^4.0.8", "lottie-web": "^5.7.8", "marina-provider": "^1.4.3", diff --git a/src/application/redux/containers/address-amount.container.ts b/src/application/redux/containers/address-amount.container.ts index a932d774..ea697014 100644 --- a/src/application/redux/containers/address-amount.container.ts +++ b/src/application/redux/containers/address-amount.container.ts @@ -5,15 +5,14 @@ import AddressAmountView, { AddressAmountProps, } from '../../../presentation/wallet/send/address-amount'; import { balancesSelector } from '../selectors/balance.selector'; -import { masterPubKeySelector, restorerOptsSelector } from '../selectors/wallet.selector'; +import { selectMainAccount } from '../selectors/wallet.selector'; const mapStateToProps = (state: RootReducerState): AddressAmountProps => ({ + mainAccount: selectMainAccount(state), network: state.app.network, transaction: state.transaction, assets: state.assets, balances: balancesSelector(state), - masterPubKey: masterPubKeySelector(state), - restorerOpts: restorerOptsSelector(state), transactionAsset: assetGetterFromIAssets(state.assets)(state.transaction.sendAsset), }); diff --git a/src/application/redux/containers/choose-fee.container.ts b/src/application/redux/containers/choose-fee.container.ts index 48ab556c..28e83d88 100644 --- a/src/application/redux/containers/choose-fee.container.ts +++ b/src/application/redux/containers/choose-fee.container.ts @@ -3,7 +3,7 @@ import { RootReducerState } from '../../../domain/common'; import ChooseFeeView, { ChooseFeeProps } from '../../../presentation/wallet/send/choose-fee'; import { lbtcAssetByNetwork } from '../../utils'; import { balancesSelector } from '../selectors/balance.selector'; -import { masterPubKeySelector, restorerOptsSelector } from '../selectors/wallet.selector'; +import { selectMainAccount } from '../selectors/wallet.selector'; const mapStateToProps = (state: RootReducerState): ChooseFeeProps => ({ wallet: state.wallet, @@ -12,12 +12,11 @@ const mapStateToProps = (state: RootReducerState): ChooseFeeProps => ({ balances: balancesSelector(state), taxiAssets: state.taxi.taxiAssets, lbtcAssetHash: lbtcAssetByNetwork(state.app.network), - masterPubKey: masterPubKeySelector(state), - restorerOpts: restorerOptsSelector(state), sendAddress: state.transaction.sendAddress, changeAddress: state.transaction.changeAddress, sendAsset: state.transaction.sendAsset, sendAmount: state.transaction.sendAmount, + mainAccount: selectMainAccount(state), }); const ChooseFee = connect(mapStateToProps)(ChooseFeeView); diff --git a/src/application/redux/containers/end-of-flow.container.ts b/src/application/redux/containers/end-of-flow.container.ts index 4f46a882..00cd7212 100644 --- a/src/application/redux/containers/end-of-flow.container.ts +++ b/src/application/redux/containers/end-of-flow.container.ts @@ -2,11 +2,10 @@ import { connect } from 'react-redux'; import { RootReducerState } from '../../../domain/common'; import EndOfFlow, { EndOfFlowProps } from '../../../presentation/wallet/send/end-of-flow'; import { getExplorerURLSelector } from '../selectors/app.selector'; +import { selectMainAccount } from '../selectors/wallet.selector'; const mapStateToProps = (state: RootReducerState): EndOfFlowProps => ({ - wallet: state.wallet, - network: state.app.network, - restorerOpts: state.wallet.restorerOpts, + mainAccount: selectMainAccount(state), pset: state.transaction.pset, explorerURL: getExplorerURLSelector(state), recipientAddress: state.transaction.sendAddress?.value, diff --git a/src/application/redux/containers/receive.container.ts b/src/application/redux/containers/receive.container.ts index 528ec620..f623ac0b 100644 --- a/src/application/redux/containers/receive.container.ts +++ b/src/application/redux/containers/receive.container.ts @@ -1,11 +1,10 @@ import { connect } from 'react-redux'; import { RootReducerState } from '../../../domain/common'; import ReceiveView, { ReceiveProps } from '../../../presentation/wallet/receive'; -import { masterPubKeySelector, restorerOptsSelector } from '../selectors/wallet.selector'; +import { selectMainAccount } from '../selectors/wallet.selector'; const mapStateToProps = (state: RootReducerState): ReceiveProps => ({ - pubKey: masterPubKeySelector(state), - restorerOpts: restorerOptsSelector(state), + mainAccount: selectMainAccount(state), }); const Receive = connect(mapStateToProps)(ReceiveView); diff --git a/src/application/redux/reducers/index.ts b/src/application/redux/reducers/index.ts index 53c58d74..a36f1ef7 100644 --- a/src/application/redux/reducers/index.ts +++ b/src/application/redux/reducers/index.ts @@ -98,7 +98,7 @@ const marinaReducer = combineReducers({ reducer: walletReducer, key: 'wallet', blacklist: ['deepRestorer'], - version: 1, + version: 2, initialState: walletInitState, }), taxi: persist({ diff --git a/src/application/redux/reducers/wallet-reducer.ts b/src/application/redux/reducers/wallet-reducer.ts index 3b7effc1..a9ad966a 100644 --- a/src/application/redux/reducers/wallet-reducer.ts +++ b/src/application/redux/reducers/wallet-reducer.ts @@ -6,13 +6,15 @@ import { AnyAction } from 'redux'; import { UtxoInterface } from 'ldk'; export const walletInitState: IWallet = { - restorerOpts: { - lastUsedExternalIndex: 0, - lastUsedInternalIndex: 0, + mainAccount: { + encryptedMnemonic: '', + masterBlindingKey: '', + masterXPub: '', + restorerOpts: { + lastUsedExternalIndex: 0, + lastUsedInternalIndex: 0, + }, }, - encryptedMnemonic: '', - masterXPub: '', - masterBlindingKey: '', passwordHash: '', utxoMap: {}, deepRestorer: { @@ -34,20 +36,20 @@ export function walletReducer( case ACTION_TYPES.WALLET_SET_DATA: { return { ...state, - masterXPub: payload.masterXPub, - masterBlindingKey: payload.masterBlindingKey, - encryptedMnemonic: payload.encryptedMnemonic, passwordHash: payload.passwordHash, - restorerOpts: payload.restorerOpts, + mainAccount: { ...payload }, }; } case ACTION_TYPES.NEW_CHANGE_ADDRESS_SUCCESS: { return { ...state, - restorerOpts: { - ...state.restorerOpts, - lastUsedInternalIndex: (state.restorerOpts.lastUsedInternalIndex ?? -1) + 1, + mainAccount: { + ...state.mainAccount, + restorerOpts: { + ...state.mainAccount.restorerOpts, + lastUsedInternalIndex: (state.mainAccount.restorerOpts.lastUsedInternalIndex ?? 0) + 1, + }, }, }; } @@ -55,9 +57,12 @@ export function walletReducer( case ACTION_TYPES.NEW_ADDRESS_SUCCESS: { return { ...state, - restorerOpts: { - ...state.restorerOpts, - lastUsedExternalIndex: (state.restorerOpts.lastUsedExternalIndex ?? -1) + 1, + mainAccount: { + ...state.mainAccount, + restorerOpts: { + ...state.mainAccount.restorerOpts, + lastUsedExternalIndex: (state.mainAccount.restorerOpts.lastUsedExternalIndex ?? 0) + 1, + }, }, }; } diff --git a/src/application/redux/selectors/wallet.selector.ts b/src/application/redux/selectors/wallet.selector.ts index d2b44759..1b10ab20 100644 --- a/src/application/redux/selectors/wallet.selector.ts +++ b/src/application/redux/selectors/wallet.selector.ts @@ -1,23 +1,9 @@ -import { IdentityType, MasterPublicKey, StateRestorerOpts, UtxoInterface } from 'ldk'; +import { MasterPublicKey, UtxoInterface } from 'ldk'; +import { createMnemonicAccount, MainAccount } from '../../../domain/account'; import { RootReducerState } from '../../../domain/common'; -export function masterPubKeySelector(state: RootReducerState): MasterPublicKey { - const { masterBlindingKey, masterXPub } = state.wallet; - const network = state.app.network; - const pubKeyWallet = new MasterPublicKey({ - chain: network, - type: IdentityType.MasterPublicKey, - opts: { - masterPublicKey: masterXPub, - masterBlindingKey: masterBlindingKey, - }, - }); - - return pubKeyWallet; -} - -export function restorerOptsSelector(state: RootReducerState): StateRestorerOpts { - return state.wallet.restorerOpts; +export function masterPubKeySelector(state: RootReducerState): Promise { + return selectMainAccount(state).getWatchIdentity(); } export function utxosSelector(state: RootReducerState): UtxoInterface[] { @@ -25,5 +11,12 @@ export function utxosSelector(state: RootReducerState): UtxoInterface[] { } export function hasMnemonicSelector(state: RootReducerState): boolean { - return state.wallet.encryptedMnemonic !== '' && state.wallet.encryptedMnemonic !== undefined; + return ( + state.wallet.mainAccount.encryptedMnemonic !== '' && + state.wallet.mainAccount.encryptedMnemonic !== undefined + ); +} + +export function selectMainAccount(state: RootReducerState): MainAccount { + return createMnemonicAccount(state.wallet.mainAccount, state.app.network); } diff --git a/src/application/utils/crypto.ts b/src/application/utils/crypto.ts index f9b90db4..8888401a 100644 --- a/src/application/utils/crypto.ts +++ b/src/application/utils/crypto.ts @@ -15,12 +15,16 @@ export function encrypt(payload: Mnemonic, password: Password): EncryptedMnemoni } export function decrypt(encrypted: EncryptedMnemonic, password: Password): Mnemonic { - const hash = crypto.createHash('sha1').update(password); - const secret = hash.digest().slice(0, 16); - const key = crypto.createDecipheriv('aes-128-cbc', secret, iv); - let decrypted = key.update(encrypted, 'hex', 'utf8'); - decrypted += key.final('utf8'); - return createMnemonic(decrypted); + try { + const hash = crypto.createHash('sha1').update(password); + const secret = hash.digest().slice(0, 16); + const key = crypto.createDecipheriv('aes-128-cbc', secret, iv); + let decrypted = key.update(encrypted, 'hex', 'utf8'); + decrypted += key.final('utf8'); + return createMnemonic(decrypted); + } catch { + throw new Error('invalid password'); + } } export function sha256Hash(str: string): string { diff --git a/src/application/utils/restorer.ts b/src/application/utils/restorer.ts index e48f6818..66118b98 100644 --- a/src/application/utils/restorer.ts +++ b/src/application/utils/restorer.ts @@ -1,5 +1,15 @@ -import { StateRestorerOpts, Mnemonic, IdentityType, mnemonicRestorerFromState } from 'ldk'; +import { + StateRestorerOpts, + Mnemonic, + IdentityType, + mnemonicRestorerFromState, + MasterPublicKey, + masterPubKeyRestorerFromState, +} from 'ldk'; import { Address } from '../../domain/address'; +import { MasterBlindingKey } from '../../domain/master-blinding-key'; +import { MasterXPub } from '../../domain/master-extended-pub'; +import { Network } from '../../domain/network'; export function getStateRestorerOptsFromAddresses(addresses: Address[]): StateRestorerOpts { const derivationPaths = addresses.map((addr) => addr.derivationPath); @@ -27,16 +37,34 @@ export function getStateRestorerOptsFromAddresses(addresses: Address[]): StateRe }; } -export function mnemonicWallet( +export function restoredMnemonic( mnemonic: string, restorerOpts: StateRestorerOpts, - chain: string + chain: Network ): Promise { - const mnemonicWallet = new Mnemonic({ + const mnemonicID = new Mnemonic({ chain, type: IdentityType.Mnemonic, opts: { mnemonic }, }); - return mnemonicRestorerFromState(mnemonicWallet)(restorerOpts); + return mnemonicRestorerFromState(mnemonicID)(restorerOpts); +} + +export function restoredMasterPublicKey( + masterXPub: MasterXPub, + masterBlindingKey: MasterBlindingKey, + restorerOpts: StateRestorerOpts, + network: Network +) { + const xpub = new MasterPublicKey({ + chain: network, + type: IdentityType.MasterPublicKey, + opts: { + masterPublicKey: masterXPub, + masterBlindingKey: masterBlindingKey, + }, + }); + + return masterPubKeyRestorerFromState(xpub)(restorerOpts); } diff --git a/src/application/utils/transaction.ts b/src/application/utils/transaction.ts index 689b586e..8fbb5c2f 100644 --- a/src/application/utils/transaction.ts +++ b/src/application/utils/transaction.ts @@ -270,7 +270,11 @@ function getTransfers( }; for (const input of vin) { - if (!isBlindedOutputInterface(input.prevout) && walletScripts.includes(input.prevout.script)) { + if ( + input.prevout && + !isBlindedOutputInterface(input.prevout) && + walletScripts.includes(input.prevout.script) + ) { addToTransfers(-1 * input.prevout.value, input.prevout.asset); } } diff --git a/src/background/backend.ts b/src/background/backend.ts index 95cf8452..4f5ddbb9 100644 --- a/src/background/backend.ts +++ b/src/background/backend.ts @@ -12,8 +12,8 @@ import { fetchAndUnblindUtxosGenerator, masterPubKeyRestorerFromEsplora, MasterPublicKey, - masterPubKeyRestorerFromState, utxoWithPrevout, + IdentityType, } from 'ldk'; import { fetchAssetsFromTaxi, @@ -29,10 +29,7 @@ import { } from '../application/redux/actions/wallet'; import { createAddress } from '../domain/address'; import { setTaxiAssets, updateTaxiAssets } from '../application/redux/actions/taxi'; -import { - masterPubKeySelector, - restorerOptsSelector, -} from '../application/redux/selectors/wallet.selector'; +import { selectMainAccount } from '../application/redux/selectors/wallet.selector'; import { addUtxo, deleteUtxo, updateUtxos } from '../application/redux/actions/utxos'; import { addAsset } from '../application/redux/actions/asset'; import { ThunkAction } from 'redux-thunk'; @@ -52,9 +49,9 @@ import { flushTx } from '../application/redux/actions/connect'; const UPDATE_ALARM = 'UPDATE_ALARM'; const getAddresses = async (state: RootReducerState) => { - const xpub = await getRestoredXPub(state); + const xpub = await selectMainAccount(state).getWatchIdentity(); return (await xpub.getAddresses()).reverse(); -} +}; /** * fetch and unblind the utxos and then refresh it. @@ -259,7 +256,14 @@ export function deepRestorer(): ThunkAction { const state = getState(); const { isLoading, gapLimit } = state.wallet.deepRestorer; - const toRestore = masterPubKeySelector(state); + const toRestore = new MasterPublicKey({ + chain: state.app.network, + type: IdentityType.MasterPublicKey, + opts: { + masterPublicKey: state.wallet.mainAccount.masterXPub, + masterBlindingKey: state.wallet.mainAccount.masterBlindingKey, + }, + }); const explorer = getExplorerURLSelector(getState()); if (isLoading) return; @@ -275,9 +279,10 @@ export function deepRestorer(): ThunkAction { - const xPubKey = masterPubKeySelector(state); - const opts = restorerOptsSelector(state); - return masterPubKeyRestorerFromState(xPubKey)(opts); -} - // reset all the reducers except the `assets` reducer (shared data). export function resetAll(): ThunkAction { return (dispatch) => { diff --git a/src/background/background-script.ts b/src/background/background-script.ts index 76dc9c8f..a41b6ff8 100644 --- a/src/background/background-script.ts +++ b/src/background/background-script.ts @@ -52,7 +52,7 @@ browser.runtime.onInstalled.addListener(({ reason }) => { // /!\ FIX: prevent opening the onboarding page if the browser has been closed browser.runtime.onStartup.addListener(() => { (async () => { - if (marinaStore.getState().wallet.encryptedMnemonic !== '') { + if (marinaStore.getState().wallet.mainAccount.encryptedMnemonic !== '') { // Everytime the browser starts up we need to set up the popup page await browser.browserAction.setPopup({ popup: 'popup.html' }); } @@ -71,7 +71,7 @@ browser.browserAction.onClicked.addListener(() => { // the wallet creation process, we let user re-open it // Check if wallet exists in storage and if not we open the // onboarding page again. - if (marinaStore.getState().wallet.encryptedMnemonic === '') { + if (marinaStore.getState().wallet.mainAccount.encryptedMnemonic === '') { welcomeTabID = await openInitializeWelcomeRoute(); return; } else { diff --git a/src/content/marinaBroker.ts b/src/content/marinaBroker.ts index 5c7d84ad..ff9c091f 100644 --- a/src/content/marinaBroker.ts +++ b/src/content/marinaBroker.ts @@ -18,12 +18,7 @@ import { setTx, setTxData, } from '../application/redux/actions/connect'; -import { - masterPubKeySelector, - restorerOptsSelector, - utxosSelector, -} from '../application/redux/selectors/wallet.selector'; -import { masterPubKeyRestorerFromState, MasterPublicKey } from 'ldk'; +import { selectMainAccount, utxosSelector } from '../application/redux/selectors/wallet.selector'; import { incrementAddressIndex, incrementChangeAddressIndex, @@ -122,13 +117,13 @@ export default class MarinaBroker extends Broker { case Marina.prototype.getAddresses.name: { this.checkHostnameAuthorization(state); - const xpub = await getRestoredXPub(state); + const xpub = await selectMainAccount(state).getWatchIdentity(); return successMsg(await xpub.getAddresses()); } case Marina.prototype.getNextAddress.name: { this.checkHostnameAuthorization(state); - const xpub = await getRestoredXPub(state); + const xpub = await selectMainAccount(state).getWatchIdentity(); const nextAddress = await xpub.getNextAddress(); await this.store.dispatchAsync(incrementAddressIndex()); return successMsg(nextAddress); @@ -136,7 +131,7 @@ export default class MarinaBroker extends Broker { case Marina.prototype.getNextChangeAddress.name: { this.checkHostnameAuthorization(state); - const xpub = await getRestoredXPub(state); + const xpub = await selectMainAccount(state).getWatchIdentity(); const nextChangeAddress = await xpub.getNextChangeAddress(); await this.store.dispatchAsync(incrementChangeAddressIndex()); return successMsg(nextChangeAddress); @@ -223,7 +218,7 @@ export default class MarinaBroker extends Broker { case Marina.prototype.isReady.name: { try { - await getRestoredXPub(state); // check if Xpub is valid + await selectMainAccount(state).getWatchIdentity(); // check if Xpub is valid return successMsg(state.app.isOnboardingCompleted); } catch { // catch error = not ready @@ -246,9 +241,3 @@ export default class MarinaBroker extends Broker { } }; } - -function getRestoredXPub(state: RootReducerState): Promise { - const xPubKey = masterPubKeySelector(state); - const opts = restorerOptsSelector(state); - return masterPubKeyRestorerFromState(xPubKey)(opts); -} diff --git a/src/domain/account.ts b/src/domain/account.ts new file mode 100644 index 00000000..7e7ad20f --- /dev/null +++ b/src/domain/account.ts @@ -0,0 +1,31 @@ +import { IdentityInterface, MasterPublicKey, Mnemonic, StateRestorerOpts } from 'ldk'; +import { decrypt } from '../application/utils'; +import { restoredMasterPublicKey, restoredMnemonic } from '../application/utils/restorer'; +import { EncryptedMnemonic } from './encrypted-mnemonic'; +import { MasterBlindingKey } from './master-blinding-key'; +import { MasterXPub } from './master-extended-pub'; +import { Network } from './network'; + +export interface Account { + getSigningIdentity(password: string): Promise; + getWatchIdentity(): Promise; + [propName: string]: any; +} + +export type MainAccount = Account; + +export interface MnemonicAccountData { + encryptedMnemonic: EncryptedMnemonic; + restorerOpts: StateRestorerOpts; + masterXPub: MasterXPub; + masterBlindingKey: MasterBlindingKey; +} + +export function createMnemonicAccount(data: MnemonicAccountData, network: Network): MainAccount { + return { + getSigningIdentity: (password: string) => + restoredMnemonic(decrypt(data.encryptedMnemonic, password), data.restorerOpts, network), + getWatchIdentity: () => + restoredMasterPublicKey(data.masterXPub, data.masterBlindingKey, data.restorerOpts, network), + }; +} diff --git a/src/domain/wallet.ts b/src/domain/wallet.ts index 025a0782..46b865e6 100644 --- a/src/domain/wallet.ts +++ b/src/domain/wallet.ts @@ -1,18 +1,13 @@ -import { UtxoInterface, StateRestorerOpts } from 'ldk'; +import { UtxoInterface } from 'ldk'; +import { MnemonicAccountData } from './account'; import { IError } from './common'; -import { EncryptedMnemonic } from './encrypted-mnemonic'; -import { MasterBlindingKey } from './master-blinding-key'; -import { MasterXPub } from './master-extended-pub'; import { PasswordHash } from './password-hash'; export interface IWallet { - encryptedMnemonic: EncryptedMnemonic; + mainAccount: MnemonicAccountData; errors?: Record; - masterXPub: MasterXPub; - masterBlindingKey: MasterBlindingKey; passwordHash: PasswordHash; utxoMap: Record; - restorerOpts: StateRestorerOpts; deepRestorer: { gapLimit: number; isLoading: boolean; diff --git a/src/presentation/components/address-amount-form.tsx b/src/presentation/components/address-amount-form.tsx index 3e1618cc..ba879610 100644 --- a/src/presentation/components/address-amount-form.tsx +++ b/src/presentation/components/address-amount-form.tsx @@ -1,5 +1,4 @@ import { FormikProps, withFormik } from 'formik'; -import { masterPubKeyRestorerFromState, MasterPublicKey, StateRestorerOpts } from 'ldk'; import { RouteComponentProps } from 'react-router'; import { ProxyStoreDispatch } from '../../application/redux/proxyStore'; import cx from 'classnames'; @@ -14,6 +13,7 @@ import { TransactionState } from '../../application/redux/reducers/transaction-r import { IAssets } from '../../domain/assets'; import { Network } from '../../domain/network'; import { incrementChangeAddressIndex } from '../../application/redux/actions/wallet'; +import { MainAccount } from '../../domain/account'; interface AddressAmountFormValues { address: string; @@ -28,11 +28,10 @@ interface AddressAmountFormProps { dispatch: ProxyStoreDispatch; assetPrecision: number; history: RouteComponentProps['history']; - pubKey: MasterPublicKey; - restorerOpts: StateRestorerOpts; transaction: TransactionState; assets: IAssets; network: Network; + mainAccount: MainAccount; } const AddressAmountForm = (props: FormikProps) => { @@ -157,7 +156,7 @@ const AddressAmountEnhancedForm = withFormik { - const masterPubKey = await masterPubKeyRestorerFromState(props.pubKey)(props.restorerOpts); + const masterPubKey = await props.mainAccount.getWatchIdentity(); const changeAddressGenerated = await masterPubKey.getNextChangeAddress(); const changeAddress = createAddress( changeAddressGenerated.confidentialAddress, diff --git a/src/presentation/connect/sign-msg.tsx b/src/presentation/connect/sign-msg.tsx index 7d35979a..392ed5e3 100644 --- a/src/presentation/connect/sign-msg.tsx +++ b/src/presentation/connect/sign-msg.tsx @@ -39,7 +39,7 @@ const ConnectSignMsg: React.FC = ({ connectData }) => { const [error, setError] = useState(''); const network = useSelector((state: RootReducerState) => state.app.network); const encryptedMnemonic = useSelector( - (state: RootReducerState) => state.wallet.encryptedMnemonic + (state: RootReducerState) => state.wallet.mainAccount.encryptedMnemonic ); const popupWindowProxy = new PopupWindowProxy(); diff --git a/src/presentation/connect/sign-pset.tsx b/src/presentation/connect/sign-pset.tsx index 57ab30b9..71b37bf2 100644 --- a/src/presentation/connect/sign-pset.tsx +++ b/src/presentation/connect/sign-pset.tsx @@ -8,9 +8,7 @@ import { WithConnectDataProps, } from '../../application/redux/containers/with-connect-data.container'; import { useSelector } from 'react-redux'; -import { restorerOptsSelector } from '../../application/redux/selectors/wallet.selector'; -import { RootReducerState } from '../../domain/common'; -import { decrypt, mnemonicWallet } from '../../application/utils'; +import { selectMainAccount } from '../../application/redux/selectors/wallet.selector'; import PopupWindowProxy from './popupWindowProxy'; export interface SignTransactionPopupResponse { @@ -24,11 +22,7 @@ const ConnectSignTransaction: React.FC = ({ connectData }) const [isModalUnlockOpen, showUnlockModal] = useState(false); const [error, setError] = useState(''); - const network = useSelector((state: RootReducerState) => state.app.network); - const restorerOpts = useSelector(restorerOptsSelector); - const encryptedMnemonic = useSelector( - (state: RootReducerState) => state.wallet.encryptedMnemonic - ); + const mainAccount = useSelector(selectMainAccount); const handleModalUnlockClose = () => showUnlockModal(false); const handleUnlockModalOpen = () => showUnlockModal(true); @@ -52,11 +46,7 @@ const ConnectSignTransaction: React.FC = ({ connectData }) const { tx } = connectData; if (!tx || !tx.pset) throw new Error('No transaction to sign'); - const mnemo = await mnemonicWallet( - decrypt(encryptedMnemonic, password), - restorerOpts, - network - ); + const mnemo = await mainAccount.getSigningIdentity(password); const signedPset = await mnemo.signPset(tx.pset); await sendResponseMessage(true, signedPset); diff --git a/src/presentation/connect/spend.tsx b/src/presentation/connect/spend.tsx index a721be98..ccbe88c1 100644 --- a/src/presentation/connect/spend.tsx +++ b/src/presentation/connect/spend.tsx @@ -15,14 +15,12 @@ import { ProxyStoreDispatch } from '../../application/redux/proxyStore'; import { flushTx } from '../../application/redux/actions/connect'; import { Network } from '../../domain/network'; import { ConnectData } from '../../domain/connect'; -import { mnemonicWallet } from '../../application/utils/restorer'; import { blindAndSignPset, createSendPset } from '../../application/utils/transaction'; import { incrementChangeAddressIndex } from '../../application/redux/actions/wallet'; import { - restorerOptsSelector, + selectMainAccount, utxosSelector, } from '../../application/redux/selectors/wallet.selector'; -import { decrypt } from '../../application/utils/crypto'; import PopupWindowProxy from './popupWindowProxy'; export interface SpendPopupResponse { @@ -32,12 +30,9 @@ export interface SpendPopupResponse { const ConnectSpend: React.FC = ({ connectData }) => { const assets = useSelector((state: RootReducerState) => state.assets); - const coins = useSelector(utxosSelector); - const restorerOpts = useSelector(restorerOptsSelector); - const encryptedMnemonic = useSelector( - (state: RootReducerState) => state.wallet.encryptedMnemonic - ); + const mainAccount = useSelector(selectMainAccount); const network = useSelector((state: RootReducerState) => state.app.network); + const coins = useSelector(utxosSelector); const dispatch = useDispatch(); @@ -70,11 +65,7 @@ const ConnectSpend: React.FC = ({ connectData }) => { if (!password || password.length === 0) return; try { - const mnemonicIdentity = await mnemonicWallet( - decrypt(encryptedMnemonic, password), - restorerOpts, - network - ); + const mnemonicIdentity = await mainAccount.getSigningIdentity(password); const signedTxHex = await makeTransaction( mnemonicIdentity, coins, diff --git a/src/presentation/cosigner/pair.tsx b/src/presentation/cosigner/pair.tsx index 6d824cb7..33300dbf 100644 --- a/src/presentation/cosigner/pair.tsx +++ b/src/presentation/cosigner/pair.tsx @@ -14,23 +14,30 @@ interface OptInFormValues { const optInForm = (props: FormikProps) => { const { touched, errors, isSubmitting } = props; + + const touchedAndError = (value: keyof OptInFormValues) => touched[value] && errors[value]; + return (
- - {touched.cosignerURL && errors.cosignerURL &&
{errors.cosignerURL}
} - - - ) -} + ); +}; const OptInFormikForm = withFormik({ validationSchema: Yup.object().shape({ - cosignerURL: Yup.string() - .required('Please input cosignerURL') - .url('Not a valid URL') + cosignerURL: Yup.string().required('Please input cosignerURL').url('Not a valid URL'), }), handleSubmit: async (values, { props }) => { @@ -44,13 +51,14 @@ const PairCosigner: React.FC = () => { const onSubmit = (values: OptInFormValues) => { console.log(values); return Promise.resolve(); - } - - return -

{'Restore a wallet from a mnemonic phrase'}

-

{'Pair a new cosigner, endpoints must be compatible with marina cosigner API.'}

- -
-} + }; + + return ( + +

Add a new 2-of-2 Account

+ +
+ ); +}; export default PairCosigner; diff --git a/src/presentation/onboarding/backup-unlock/index.tsx b/src/presentation/onboarding/backup-unlock/index.tsx index d0d94b82..2792ccfe 100644 --- a/src/presentation/onboarding/backup-unlock/index.tsx +++ b/src/presentation/onboarding/backup-unlock/index.tsx @@ -69,7 +69,9 @@ const BackUpUnlockEnhancedForm = withFormik { const history = useHistory(); const dispatch = useDispatch(); - const encryptedMnemonic = useSelector((s: RootReducerState) => s.wallet.encryptedMnemonic); + const encryptedMnemonic = useSelector( + (s: RootReducerState) => s.wallet.mainAccount.encryptedMnemonic + ); return ( diff --git a/src/presentation/routes/constants.ts b/src/presentation/routes/constants.ts index 2dd47368..9b67bbdc 100644 --- a/src/presentation/routes/constants.ts +++ b/src/presentation/routes/constants.ts @@ -95,6 +95,5 @@ export { SETTINGS_TERMS_ROUTE, SETTINGS_DEEP_RESTORER_ROUTE, SETTINGS_COSIGNERS_ROUTE, - - PAIR_COSIGNER_ROUTE + PAIR_COSIGNER_ROUTE, }; diff --git a/src/presentation/settings/cosigners.tsx b/src/presentation/settings/cosigners.tsx index c8e04506..8471e87d 100644 --- a/src/presentation/settings/cosigners.tsx +++ b/src/presentation/settings/cosigners.tsx @@ -25,4 +25,4 @@ const SettingsCosigners: React.FC = () => { ); }; -export default SettingsCosigners; \ No newline at end of file +export default SettingsCosigners; diff --git a/src/presentation/settings/menu-settings.tsx b/src/presentation/settings/menu-settings.tsx index bad49475..52c9834b 100644 --- a/src/presentation/settings/menu-settings.tsx +++ b/src/presentation/settings/menu-settings.tsx @@ -12,7 +12,7 @@ import { const SettingsMenuSettings: React.FC = () => { const history = useHistory(); - + const handleChangeCurrency = () => history.push(SETTINGS_CURRENCY_ROUTE); const handleExplorer = () => history.push(SETTINGS_EXPLORER_ROUTE); const handleNetworks = () => history.push(SETTINGS_NETWORKS_ROUTE); diff --git a/src/presentation/settings/show-mnemonic.tsx b/src/presentation/settings/show-mnemonic.tsx index cb6ec459..ca060cbc 100644 --- a/src/presentation/settings/show-mnemonic.tsx +++ b/src/presentation/settings/show-mnemonic.tsx @@ -21,7 +21,7 @@ const SettingsShowMnemonicView: React.FC = ({ wallet if (!match(password, wallet.passwordHash)) { throw new Error('Invalid password'); } - const mnemo = decrypt(wallet.encryptedMnemonic, createPassword(password)); + const mnemo = decrypt(wallet.mainAccount.encryptedMnemonic, createPassword(password)); setMnemonic(mnemo); showUnlockModal(false); }; diff --git a/src/presentation/wallet/receive/index.tsx b/src/presentation/wallet/receive/index.tsx index c3332756..945a63fa 100644 --- a/src/presentation/wallet/receive/index.tsx +++ b/src/presentation/wallet/receive/index.tsx @@ -7,15 +7,14 @@ import { formatAddress } from '../../utils'; import { useDispatch } from 'react-redux'; import { updateUtxos } from '../../../application/redux/actions/utxos'; import { ProxyStoreDispatch } from '../../../application/redux/proxyStore'; -import { masterPubKeyRestorerFromState, MasterPublicKey, StateRestorerOpts } from 'ldk'; import { incrementAddressIndex } from '../../../application/redux/actions/wallet'; +import { MainAccount } from '../../../domain/account'; export interface ReceiveProps { - pubKey: MasterPublicKey; - restorerOpts: StateRestorerOpts; + mainAccount: MainAccount; } -const ReceiveView: React.FC = ({ pubKey, restorerOpts }) => { +const ReceiveView: React.FC = ({ mainAccount }) => { const history = useHistory(); const dispatch = useDispatch(); @@ -33,7 +32,7 @@ const ReceiveView: React.FC = ({ pubKey, restorerOpts }) => { useEffect(() => { (async () => { - const publicKey = await masterPubKeyRestorerFromState(pubKey)(restorerOpts); + const publicKey = await mainAccount.getWatchIdentity(); const addr = await publicKey.getNextAddress(); setConfidentialAddress(addr.confidentialAddress); await dispatch(incrementAddressIndex()); // persist address diff --git a/src/presentation/wallet/send/address-amount.tsx b/src/presentation/wallet/send/address-amount.tsx index 54476630..f22fcbdd 100644 --- a/src/presentation/wallet/send/address-amount.tsx +++ b/src/presentation/wallet/send/address-amount.tsx @@ -9,15 +9,14 @@ import { flushPendingTx } from '../../../application/redux/actions/transaction'; import { BalancesByAsset } from '../../../application/redux/selectors/balance.selector'; import { ProxyStoreDispatch } from '../../../application/redux/proxyStore'; import AddressAmountEnhancedForm from '../../components/address-amount-form'; -import { MasterPublicKey, StateRestorerOpts } from 'ldk'; import { Network } from '../../../domain/network'; import { TransactionState } from '../../../application/redux/reducers/transaction-reducer'; import { Asset, IAssets } from '../../../domain/assets'; import { DEFAULT_ROUTE } from '../../routes/constants'; +import { MainAccount } from '../../../domain/account'; export interface AddressAmountProps { - masterPubKey: MasterPublicKey; - restorerOpts: StateRestorerOpts; + mainAccount: MainAccount; network: Network; transaction: TransactionState; balances: BalancesByAsset; @@ -26,8 +25,7 @@ export interface AddressAmountProps { } const AddressAmountView: React.FC = ({ - masterPubKey, - restorerOpts, + mainAccount, network, transaction, balances, @@ -66,11 +64,10 @@ const AddressAmountView: React.FC = ({ history={history} balances={balances} transaction={transaction} - restorerOpts={restorerOpts} network={network} - pubKey={masterPubKey} assets={assets} assetPrecision={transactionAsset.precision} + mainAccount={mainAccount} /> ); diff --git a/src/presentation/wallet/send/choose-fee.tsx b/src/presentation/wallet/send/choose-fee.tsx index fd31d7c9..5532d670 100644 --- a/src/presentation/wallet/send/choose-fee.tsx +++ b/src/presentation/wallet/send/choose-fee.tsx @@ -1,13 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router'; -import { - greedyCoinSelector, - masterPubKeyRestorerFromState, - MasterPublicKey, - RecipientInterface, - StateRestorerOpts, - walletFromCoins, -} from 'ldk'; +import { greedyCoinSelector, RecipientInterface, walletFromCoins } from 'ldk'; import Balance from '../../components/balance'; import Button from '../../components/button'; import ShellPopUp from '../../components/shell-popup'; @@ -38,6 +31,7 @@ import { ProxyStoreDispatch } from '../../../application/redux/proxyStore'; import { Address, createAddress } from '../../../domain/address'; import { Topup } from 'taxi-protobuf/generated/js/taxi_pb'; import { incrementChangeAddressIndex } from '../../../application/redux/actions/wallet'; +import { MainAccount } from '../../../domain/account'; export interface ChooseFeeProps { network: Network; @@ -50,8 +44,7 @@ export interface ChooseFeeProps { balances: BalancesByAsset; taxiAssets: string[]; lbtcAssetHash: string; - masterPubKey: MasterPublicKey; - restorerOpts: StateRestorerOpts; + mainAccount: MainAccount; } const ChooseFeeView: React.FC = ({ @@ -65,8 +58,7 @@ const ChooseFeeView: React.FC = ({ balances, taxiAssets, lbtcAssetHash, - masterPubKey, - restorerOpts, + mainAccount, }) => { const history = useHistory(); const dispatch = useDispatch(); @@ -152,7 +144,7 @@ const ChooseFeeView: React.FC = ({ let nextChangeAddr = feeChange; if (!nextChangeAddr) { - const restored = await masterPubKeyRestorerFromState(masterPubKey)(restorerOpts); + const restored = await mainAccount.getWatchIdentity(); const next = await restored.getNextChangeAddress(); nextChangeAddr = createAddress(next.confidentialAddress, next.derivationPath); setFeeChange(nextChangeAddr); diff --git a/src/presentation/wallet/send/end-of-flow.tsx b/src/presentation/wallet/send/end-of-flow.tsx index f577a67e..b9698284 100644 --- a/src/presentation/wallet/send/end-of-flow.tsx +++ b/src/presentation/wallet/send/end-of-flow.tsx @@ -3,7 +3,12 @@ import { useHistory } from 'react-router'; import Button from '../../components/button'; import ModalUnlock from '../../components/modal-unlock'; import ShellPopUp from '../../components/shell-popup'; -import { blindAndSignPset, broadcastTx, decrypt, mnemonicWallet } from '../../../application/utils'; +import { + blindAndSignPset, + broadcastTx, + decrypt, + restoredMnemonic, +} from '../../../application/utils'; import { SEND_PAYMENT_ERROR_ROUTE, SEND_PAYMENT_SUCCESS_ROUTE } from '../../routes/constants'; import { debounce } from 'lodash'; import { IWallet } from '../../../domain/wallet'; @@ -12,21 +17,18 @@ import { createPassword } from '../../../domain/password'; import { match } from '../../../domain/password-hash'; import { StateRestorerOpts } from 'ldk'; import { extractErrorMessage } from '../../utils/error'; +import { MainAccount } from '../../../domain/account'; export interface EndOfFlowProps { - wallet: IWallet; - network: Network; - restorerOpts: StateRestorerOpts; + mainAccount: MainAccount; pset?: string; explorerURL: string; recipientAddress?: string; } const EndOfFlow: React.FC = ({ - wallet, - network, + mainAccount, pset, - restorerOpts, explorerURL, recipientAddress, }) => { @@ -41,13 +43,7 @@ const EndOfFlow: React.FC = ({ if (!pset || !recipientAddress) return; try { const pass = createPassword(password); - if (!match(password, wallet.passwordHash)) { - throw new Error('Invalid password'); - } - - const mnemonic = decrypt(wallet.encryptedMnemonic, pass); - - const mnemo = await mnemonicWallet(mnemonic, restorerOpts, network); + const mnemo = await mainAccount.getSigningIdentity(pass); tx = await blindAndSignPset(mnemo, pset, [recipientAddress]); const txid = await broadcastTx(explorerURL, tx); From 87c03a4c03d4a270b9f03428650de0e304689b42 Mon Sep 17 00:00:00 2001 From: louisinger Date: Thu, 14 Oct 2021 17:26:27 +0200 Subject: [PATCH 05/52] clean unused import --- src/presentation/wallet/send/end-of-flow.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/presentation/wallet/send/end-of-flow.tsx b/src/presentation/wallet/send/end-of-flow.tsx index b9698284..9b306516 100644 --- a/src/presentation/wallet/send/end-of-flow.tsx +++ b/src/presentation/wallet/send/end-of-flow.tsx @@ -6,16 +6,10 @@ import ShellPopUp from '../../components/shell-popup'; import { blindAndSignPset, broadcastTx, - decrypt, - restoredMnemonic, } from '../../../application/utils'; import { SEND_PAYMENT_ERROR_ROUTE, SEND_PAYMENT_SUCCESS_ROUTE } from '../../routes/constants'; import { debounce } from 'lodash'; -import { IWallet } from '../../../domain/wallet'; -import { Network } from '../../../domain/network'; import { createPassword } from '../../../domain/password'; -import { match } from '../../../domain/password-hash'; -import { StateRestorerOpts } from 'ldk'; import { extractErrorMessage } from '../../utils/error'; import { MainAccount } from '../../../domain/account'; From 66b888875712b63579c5e9579e848ea90c622d95 Mon Sep 17 00:00:00 2001 From: louisinger Date: Fri, 15 Oct 2021 11:12:17 +0200 Subject: [PATCH 06/52] wip multisig account --- .../redux/reducers/wallet-reducer.ts | 4 +-- src/application/utils/restorer.ts | 31 +++++++++++++++++++ src/domain/account.ts | 5 +-- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/application/redux/reducers/wallet-reducer.ts b/src/application/redux/reducers/wallet-reducer.ts index a9ad966a..37f827e1 100644 --- a/src/application/redux/reducers/wallet-reducer.ts +++ b/src/application/redux/reducers/wallet-reducer.ts @@ -48,7 +48,7 @@ export function walletReducer( ...state.mainAccount, restorerOpts: { ...state.mainAccount.restorerOpts, - lastUsedInternalIndex: (state.mainAccount.restorerOpts.lastUsedInternalIndex ?? 0) + 1, + lastUsedInternalIndex: (state.mainAccount.restorerOpts.lastUsedInternalIndex ?? -1) + 1, }, }, }; @@ -61,7 +61,7 @@ export function walletReducer( ...state.mainAccount, restorerOpts: { ...state.mainAccount.restorerOpts, - lastUsedExternalIndex: (state.mainAccount.restorerOpts.lastUsedExternalIndex ?? 0) + 1, + lastUsedExternalIndex: (state.mainAccount.restorerOpts.lastUsedExternalIndex ?? -1) + 1, }, }, }; diff --git a/src/application/utils/restorer.ts b/src/application/utils/restorer.ts index 66118b98..87e81e54 100644 --- a/src/application/utils/restorer.ts +++ b/src/application/utils/restorer.ts @@ -5,6 +5,10 @@ import { mnemonicRestorerFromState, MasterPublicKey, masterPubKeyRestorerFromState, + Multisig, + DEFAULT_BASE_DERIVATION_PATH, + CosignerMultisig, + IdentityInterface, } from 'ldk'; import { Address } from '../../domain/address'; import { MasterBlindingKey } from '../../domain/master-blinding-key'; @@ -37,6 +41,8 @@ export function getStateRestorerOptsFromAddresses(addresses: Address[]): StateRe }; } +// create a Mnemonic Identity +// restore it from restorer's state export function restoredMnemonic( mnemonic: string, restorerOpts: StateRestorerOpts, @@ -51,6 +57,8 @@ export function restoredMnemonic( return mnemonicRestorerFromState(mnemonicID)(restorerOpts); } +// create a MasterPublicKey Identity +// restore it using StateRestorerOpts export function restoredMasterPublicKey( masterXPub: MasterXPub, masterBlindingKey: MasterBlindingKey, @@ -68,3 +76,26 @@ export function restoredMasterPublicKey( return masterPubKeyRestorerFromState(xpub)(restorerOpts); } + + +export function restoredMultisig( + mnemonic: string, + cosigners: CosignerMultisig[], + requiredSignatures: number, + network: Network +) { + const multisigID = new Multisig({ + chain: network, + type: IdentityType.Multisig, + opts: { + requiredSignatures, + signer: { + mnemonic, + baseDerivationPath: DEFAULT_BASE_DERIVATION_PATH, + }, + cosigners + } + }); + + return restore(multisigID as IdentityInterface) +} \ No newline at end of file diff --git a/src/domain/account.ts b/src/domain/account.ts index 7e7ad20f..b09e211d 100644 --- a/src/domain/account.ts +++ b/src/domain/account.ts @@ -1,4 +1,4 @@ -import { IdentityInterface, MasterPublicKey, Mnemonic, StateRestorerOpts } from 'ldk'; +import { IdentityInterface, MasterPublicKey, Mnemonic, Multisig, MultisigWatchOnly, StateRestorerOpts } from 'ldk'; import { decrypt } from '../application/utils'; import { restoredMasterPublicKey, restoredMnemonic } from '../application/utils/restorer'; import { EncryptedMnemonic } from './encrypted-mnemonic'; @@ -6,13 +6,14 @@ import { MasterBlindingKey } from './master-blinding-key'; import { MasterXPub } from './master-extended-pub'; import { Network } from './network'; -export interface Account { +export interface Account { getSigningIdentity(password: string): Promise; getWatchIdentity(): Promise; [propName: string]: any; } export type MainAccount = Account; +export type MultisigAccount = Account; export interface MnemonicAccountData { encryptedMnemonic: EncryptedMnemonic; From 442e46ab974b49fbce733d09dbfe1cf992e3eb16 Mon Sep 17 00:00:00 2001 From: louisinger Date: Mon, 18 Oct 2021 13:44:34 +0200 Subject: [PATCH 07/52] comment account domain --- src/domain/account.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/domain/account.ts b/src/domain/account.ts index b09e211d..4aba6ce6 100644 --- a/src/domain/account.ts +++ b/src/domain/account.ts @@ -6,14 +6,24 @@ import { MasterBlindingKey } from './master-blinding-key'; import { MasterXPub } from './master-extended-pub'; import { Network } from './network'; +/** + * Account domain represents the keys of the User + * + * - each Account is a derived of master private key (computed from mnemonic). + * - an Account returns two type of identities: a WatchOnly identity and a signing Identity (computed from user's password). + * - + * + */ + export interface Account { getSigningIdentity(password: string): Promise; getWatchIdentity(): Promise; [propName: string]: any; } +// Main Account uses the default Mnemonic derivation path +// single-sig account used to send/receive regular assets export type MainAccount = Account; -export type MultisigAccount = Account; export interface MnemonicAccountData { encryptedMnemonic: EncryptedMnemonic; @@ -30,3 +40,7 @@ export function createMnemonicAccount(data: MnemonicAccountData, network: Networ restoredMasterPublicKey(data.masterXPub, data.masterBlindingKey, data.restorerOpts, network), }; } + +// MultisigAccount aims to handle cosigner +// use master extended public keys from cosigners and xpub derived from master private key (mnemonic) +export type MultisigAccount = Account; From 6132cd7dd763d3cdae02acf6004371c178e7442f Mon Sep 17 00:00:00 2001 From: louisinger Date: Mon, 18 Oct 2021 16:16:02 +0200 Subject: [PATCH 08/52] move utxos and txs to wallet reducer --- src/application/redux/actions/utxos.ts | 9 +- .../redux}/backend.ts | 107 ++++++++++-------- .../containers/address-amount.container.ts | 5 +- .../redux/containers/choose-fee.container.ts | 5 +- .../redux/containers/home.container.ts | 5 +- .../receive-select-asset.container.ts | 5 +- .../containers/send-select-asset.container.ts | 5 +- src/application/redux/reducers/index.ts | 10 +- .../redux/reducers/txs-history-reducer.ts | 31 ----- .../redux/reducers/wallet-reducer.ts | 56 ++++++--- .../redux/selectors/balance.selector.ts | 9 +- .../redux/selectors/transaction.selector.ts | 4 - .../redux/selectors/wallet.selector.ts | 11 +- src/application/redux/store.ts | 11 +- src/application/utils/restorer.ts | 20 ++++ src/content/marinaBroker.ts | 12 +- src/domain/account.ts | 16 ++- src/domain/common.ts | 2 - src/domain/transaction.ts | 7 +- src/domain/wallet.ts | 6 +- src/presentation/connect/spend.tsx | 5 +- 21 files changed, 188 insertions(+), 153 deletions(-) rename src/{background => application/redux}/backend.ts (80%) delete mode 100644 src/application/redux/reducers/txs-history-reducer.ts diff --git a/src/application/redux/actions/utxos.ts b/src/application/redux/actions/utxos.ts index e4c69a2e..af132d18 100644 --- a/src/application/redux/actions/utxos.ts +++ b/src/application/redux/actions/utxos.ts @@ -1,19 +1,20 @@ import { UtxoInterface } from 'ldk'; import { AnyAction } from 'redux'; +import { AccountID } from '../../../domain/account'; import { ADD_UTXO, DELETE_UTXO, FLUSH_UTXOS, UPDATE_UTXOS } from './action-types'; export function updateUtxos(): AnyAction { return { type: UPDATE_UTXOS }; } -export function addUtxo(utxo: UtxoInterface): AnyAction { - return { type: ADD_UTXO, payload: { utxo } }; +export function addUtxo(accountID: AccountID, utxo: UtxoInterface): AnyAction { + return { type: ADD_UTXO, payload: { accountID, utxo } }; } export function deleteUtxo(txid: string, vout: number): AnyAction { return { type: DELETE_UTXO, payload: { txid, vout } }; } -export function flushUtxos(): AnyAction { - return { type: FLUSH_UTXOS }; +export function flushUtxos(accountID: AccountID): AnyAction { + return { type: FLUSH_UTXOS, payload: { accountID } }; } diff --git a/src/background/backend.ts b/src/application/redux/backend.ts similarity index 80% rename from src/background/backend.ts rename to src/application/redux/backend.ts index 4f5ddbb9..31c6fca2 100644 --- a/src/background/backend.ts +++ b/src/application/redux/backend.ts @@ -1,5 +1,5 @@ -import { RootReducerState } from '../domain/common'; -import { defaultPrecision } from '../application/utils/constants'; +import { RootReducerState } from '../../domain/common'; +import { defaultPrecision } from '../utils/constants'; import axios from 'axios'; import browser from 'webextension-polyfill'; import { @@ -14,6 +14,7 @@ import { MasterPublicKey, utxoWithPrevout, IdentityType, + AddressInterface, } from 'ldk'; import { fetchAssetsFromTaxi, @@ -21,52 +22,73 @@ import { taxiURL, toDisplayTransaction, toStringOutpoint, -} from '../application/utils'; +} from '../utils'; import { setDeepRestorerError, setDeepRestorerIsLoading, setWalletData, -} from '../application/redux/actions/wallet'; -import { createAddress } from '../domain/address'; -import { setTaxiAssets, updateTaxiAssets } from '../application/redux/actions/taxi'; -import { selectMainAccount } from '../application/redux/selectors/wallet.selector'; -import { addUtxo, deleteUtxo, updateUtxos } from '../application/redux/actions/utxos'; -import { addAsset } from '../application/redux/actions/asset'; +} from './actions/wallet'; +import { createAddress } from '../../domain/address'; +import { setTaxiAssets, updateTaxiAssets } from './actions/taxi'; +import { addUtxo, deleteUtxo, updateUtxos } from './actions/utxos'; +import { addAsset } from './actions/asset'; import { ThunkAction } from 'redux-thunk'; import { AnyAction, Dispatch } from 'redux'; -import { IAssets } from '../domain/assets'; -import { addTx, updateTxs } from '../application/redux/actions/transaction'; -import { getExplorerURLSelector } from '../application/redux/selectors/app.selector'; +import { IAssets } from '../../domain/assets'; +import { addTx, updateTxs } from './actions/transaction'; +import { getExplorerURLSelector } from './selectors/app.selector'; import { RESET_APP, RESET_CONNECT, RESET_TAXI, RESET_TXS, RESET_WALLET, -} from '../application/redux/actions/action-types'; -import { flushTx } from '../application/redux/actions/connect'; +} from './actions/action-types'; +import { flushTx } from './actions/connect'; +import { Account } from '../../domain/account'; +import { selectMainAccount } from './selectors/wallet.selector'; const UPDATE_ALARM = 'UPDATE_ALARM'; -const getAddresses = async (state: RootReducerState) => { - const xpub = await selectMainAccount(state).getWatchIdentity(); - return (await xpub.getAddresses()).reverse(); -}; +type AccountSelector = (state: RootReducerState) => Account; /** - * fetch and unblind the utxos and then refresh it. + * fetch the asset infos from explorer (ticker, precision etc...) */ -export function fetchAndUpdateUtxos(): ThunkAction { +async function fetchAssetInfos( + assetHash: string, + explorerUrl: string, + assetsState: IAssets, + dispatch: Dispatch +) { + if (assetsState[assetHash] !== undefined) return; // do not update + + const assetInfos = (await axios.get(`${explorerUrl}/asset/${assetHash}`)).data; + const name = assetInfos?.name ? assetInfos.name : 'Unknown'; + const ticker = assetInfos?.ticker ? assetInfos.ticker : assetHash.slice(0, 4).toUpperCase(); + const precision = assetInfos.precision !== undefined ? assetInfos.precision : defaultPrecision; + + dispatch(addAsset(assetHash, { name, ticker, precision })); +} + +async function getAddressesFromAccount(account: Account): Promise { + return (await account.getWatchIdentity()).getAddresses(); +} + +// fetch and unblind the utxos and then refresh it. +export function makeUtxosUpdaterThunk(selectAccount: AccountSelector): ThunkAction { return async (dispatch, getState) => { try { const state = getState(); - const { wallet, app } = state; + const { app } = state; if (!app.isAuthenticated) return; - const addrs = await getAddresses(state); + + const account = selectAccount(state); const explorer = getExplorerURLSelector(getState()); + const utxosMap = state.wallet.unspentsAndTransactions[account.accountID].utxosMap; - const currentOutpoints = Object.values(wallet.utxoMap).map(({ txid, vout }) => ({ + const currentOutpoints = Object.values(utxosMap || {}).map(({ txid, vout }) => ({ txid, vout, })); @@ -75,12 +97,12 @@ export function fetchAndUpdateUtxos(): ThunkAction { const outpoint = toStringOutpoint(utxo); - const skip = wallet.utxoMap[outpoint] !== undefined; + const skip = utxosMap[outpoint] !== undefined; if (skip) skippedOutpoints.push(toStringOutpoint(utxo)); @@ -101,7 +123,7 @@ export function fetchAndUpdateUtxos(): ThunkAction { +export function makeTxsUpdaterThunk(selectAccount: AccountSelector): ThunkAction { return async (dispatch, getState) => { try { const state = getState(); - const { app, txsHistory } = state; + const { app } = state; if (!app.isAuthenticated) return; + + const account = selectAccount(state); + const txsHistory = state.wallet.unspentsAndTransactions[account.accountID].transactions[app.network] || {}; + // Initialize txs to txsHistory shallow clone - const addressInterfaces = await getAddresses(state); + const addressInterfaces = await getAddressesFromAccount(account); const walletScripts = addressInterfaces.map((a) => address.toOutputScript(a.confidentialAddress).toString('hex') ); @@ -183,7 +190,7 @@ export function updateTxsHistory(): ThunkAction txsHistory[app.network][tx.txid] !== undefined + (tx) => txsHistory[tx.txid] !== undefined ); let it = await txsGen.next(); @@ -287,7 +294,7 @@ export function deepRestorer(): ThunkAction ({ @@ -12,7 +13,7 @@ const mapStateToProps = (state: RootReducerState): AddressAmountProps => ({ network: state.app.network, transaction: state.transaction, assets: state.assets, - balances: balancesSelector(state), + balances: selectBalances(MainAccountID)(state), transactionAsset: assetGetterFromIAssets(state.assets)(state.transaction.sendAsset), }); diff --git a/src/application/redux/containers/choose-fee.container.ts b/src/application/redux/containers/choose-fee.container.ts index 28e83d88..44da992f 100644 --- a/src/application/redux/containers/choose-fee.container.ts +++ b/src/application/redux/containers/choose-fee.container.ts @@ -1,15 +1,16 @@ import { connect } from 'react-redux'; +import { MainAccountID } from '../../../domain/account'; import { RootReducerState } from '../../../domain/common'; import ChooseFeeView, { ChooseFeeProps } from '../../../presentation/wallet/send/choose-fee'; import { lbtcAssetByNetwork } from '../../utils'; -import { balancesSelector } from '../selectors/balance.selector'; +import { selectBalances } from '../selectors/balance.selector'; import { selectMainAccount } from '../selectors/wallet.selector'; const mapStateToProps = (state: RootReducerState): ChooseFeeProps => ({ wallet: state.wallet, network: state.app.network, assets: state.assets, - balances: balancesSelector(state), + balances: selectBalances(MainAccountID)(state), taxiAssets: state.taxi.taxiAssets, lbtcAssetHash: lbtcAssetByNetwork(state.app.network), sendAddress: state.transaction.sendAddress, diff --git a/src/application/redux/containers/home.container.ts b/src/application/redux/containers/home.container.ts index d8a9a64f..6e5522c1 100644 --- a/src/application/redux/containers/home.container.ts +++ b/src/application/redux/containers/home.container.ts @@ -1,15 +1,16 @@ import { RootReducerState } from './../../../domain/common'; import { connect } from 'react-redux'; import HomeView, { HomeProps } from '../../../presentation/wallet/home'; -import { balancesSelector } from '../selectors/balance.selector'; +import { selectBalances } from '../selectors/balance.selector'; import { assetGetterFromIAssets } from '../../../domain/assets'; import { lbtcAssetByNetwork } from '../../utils'; +import { MainAccountID } from '../../../domain/account'; const mapStateToProps = (state: RootReducerState): HomeProps => ({ lbtcAssetHash: lbtcAssetByNetwork(state.app.network), network: state.app.network, transactionStep: state.transaction.step, - assetsBalance: balancesSelector(state), + assetsBalance: selectBalances(MainAccountID)(state), getAsset: assetGetterFromIAssets(state.assets), isWalletVerified: state.wallet.isVerified, }); diff --git a/src/application/redux/containers/receive-select-asset.container.ts b/src/application/redux/containers/receive-select-asset.container.ts index 7dbdea4b..fe26deb9 100644 --- a/src/application/redux/containers/receive-select-asset.container.ts +++ b/src/application/redux/containers/receive-select-asset.container.ts @@ -1,13 +1,14 @@ import { connect } from 'react-redux'; +import { MainAccountID } from '../../../domain/account'; import { assetGetterFromIAssets } from '../../../domain/assets'; import { RootReducerState } from '../../../domain/common'; import ReceiveSelectAssetView, { ReceiveSelectAssetProps, } from '../../../presentation/wallet/receive/receive-select-asset'; -import { balancesSelector } from '../selectors/balance.selector'; +import { selectBalances } from '../selectors/balance.selector'; const mapStateToProps = (state: RootReducerState): ReceiveSelectAssetProps => { - const balances = balancesSelector(state); + const balances = selectBalances(MainAccountID)(state); const getAsset = assetGetterFromIAssets(state.assets); return { network: state.app.network, diff --git a/src/application/redux/containers/send-select-asset.container.ts b/src/application/redux/containers/send-select-asset.container.ts index b9ce2281..148e4ac6 100644 --- a/src/application/redux/containers/send-select-asset.container.ts +++ b/src/application/redux/containers/send-select-asset.container.ts @@ -1,13 +1,14 @@ import { connect } from 'react-redux'; +import { MainAccountID } from '../../../domain/account'; import { assetGetterFromIAssets } from '../../../domain/assets'; import { RootReducerState } from '../../../domain/common'; import SendSelectAssetView, { SendSelectAssetProps, } from '../../../presentation/wallet/send/send-select-asset'; -import { balancesSelector } from '../selectors/balance.selector'; +import { selectBalances } from '../selectors/balance.selector'; const mapStateToProps = (state: RootReducerState): SendSelectAssetProps => { - const balances = balancesSelector(state); + const balances = selectBalances(MainAccountID)(state); const getAsset = assetGetterFromIAssets(state.assets); return { network: state.app.network, diff --git a/src/application/redux/reducers/index.ts b/src/application/redux/reducers/index.ts index a36f1ef7..386b4ad7 100644 --- a/src/application/redux/reducers/index.ts +++ b/src/application/redux/reducers/index.ts @@ -1,14 +1,12 @@ import { assetInitState, assetReducer } from './asset-reducer'; import { onboardingReducer } from './onboarding-reducer'; import { transactionReducer, TransactionState, transactionInitState } from './transaction-reducer'; -import { txsHistoryReducer, txsHistoryInitState } from './txs-history-reducer'; import { AnyAction, combineReducers, Reducer } from 'redux'; import { Storage } from 'redux-persist'; import { parse, stringify } from '../../utils/browser-storage-converters'; import browser from 'webextension-polyfill'; import persistReducer, { PersistPartial } from 'redux-persist/es/persistReducer'; import { IApp } from '../../../domain/app'; -import { TxsHistoryByNetwork } from '../../../domain/transaction'; import { IWallet } from '../../../domain/wallet'; import { taxiReducer, TaxiState, taxiInitState } from './taxi-reducer'; import { ConnectData } from '../../../domain/connect'; @@ -88,17 +86,11 @@ const marinaReducer = combineReducers({ version: 1, initialState: transactionInitState, }), - txsHistory: persist({ - reducer: txsHistoryReducer, - key: 'txsHistory', - version: 2, - initialState: txsHistoryInitState, - }), wallet: persist({ reducer: walletReducer, key: 'wallet', blacklist: ['deepRestorer'], - version: 2, + version: 3, initialState: walletInitState, }), taxi: persist({ diff --git a/src/application/redux/reducers/txs-history-reducer.ts b/src/application/redux/reducers/txs-history-reducer.ts deleted file mode 100644 index 5b89879b..00000000 --- a/src/application/redux/reducers/txs-history-reducer.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as ACTION_TYPES from '../actions/action-types'; -import { TxDisplayInterface, TxsHistoryByNetwork } from '../../../domain/transaction'; -import { AnyAction } from 'redux'; - -export const txsHistoryInitState: TxsHistoryByNetwork = { regtest: {}, liquid: {} }; - -export function txsHistoryReducer( - state: TxsHistoryByNetwork = txsHistoryInitState, - { type, payload }: AnyAction -): TxsHistoryByNetwork { - switch (type) { - case ACTION_TYPES.RESET_TXS: { - return txsHistoryInitState; - } - - case ACTION_TYPES.ADD_TX: { - let newLiquidTxsHistory = state.liquid; - let newRegtestTxsHistory = state.regtest; - const toAddTx = payload.tx as TxDisplayInterface; - if (payload.network === 'liquid') { - newLiquidTxsHistory = { ...state.liquid, [toAddTx.txId]: toAddTx }; - } else { - newRegtestTxsHistory = { ...state.regtest, [toAddTx.txId]: toAddTx }; - } - return { regtest: newRegtestTxsHistory, liquid: newLiquidTxsHistory }; - } - - default: - return state; - } -} diff --git a/src/application/redux/reducers/wallet-reducer.ts b/src/application/redux/reducers/wallet-reducer.ts index 37f827e1..ca3de125 100644 --- a/src/application/redux/reducers/wallet-reducer.ts +++ b/src/application/redux/reducers/wallet-reducer.ts @@ -4,9 +4,11 @@ import * as ACTION_TYPES from '../actions/action-types'; import { IWallet } from '../../../domain/wallet'; import { AnyAction } from 'redux'; import { UtxoInterface } from 'ldk'; +import { AccountID, MainAccountID } from '../../../domain/account'; export const walletInitState: IWallet = { mainAccount: { + accountID: MainAccountID, encryptedMnemonic: '', masterBlindingKey: '', masterXPub: '', @@ -15,8 +17,13 @@ export const walletInitState: IWallet = { lastUsedInternalIndex: 0, }, }, + unspentsAndTransactions: { + MainAccountID: { + utxosMap: { }, + transactions: { regtest: { }, liquid: { } } + } + }, passwordHash: '', - utxoMap: {}, deepRestorer: { gapLimit: 20, isLoading: false, @@ -24,6 +31,22 @@ export const walletInitState: IWallet = { isVerified: false, }; +const addUnspent = (state: IWallet) => (accountID: AccountID, utxo: UtxoInterface): IWallet => { + return { + ...state, + unspentsAndTransactions: { + ...state.unspentsAndTransactions, + [accountID]: { + ...state.unspentsAndTransactions[accountID], + utxosMap: { + ...state.unspentsAndTransactions[accountID].utxosMap, + [toStringOutpoint(utxo)]: utxo, + } + } + } + } +} + export function walletReducer( state: IWallet = walletInitState, { type, payload }: AnyAction @@ -68,23 +91,24 @@ export function walletReducer( } case ACTION_TYPES.ADD_UTXO: { - return { - ...state, - utxoMap: { - ...state.utxoMap, - [toStringOutpoint(payload.utxo as UtxoInterface)]: payload.utxo, - }, - }; + return addUnspent(state)(payload.accountID, payload.utxo); } case ACTION_TYPES.DELETE_UTXO: { const { [toStringOutpoint({ txid: payload.txid, vout: payload.vout })]: deleted, - ...utxoMap - } = state.utxoMap; + ...utxosMap + } = state.unspentsAndTransactions[payload.accountID].utxosMap; + return { ...state, - utxoMap, + unspentsAndTransactions: { + ...state.unspentsAndTransactions, + [payload.accountID]: { + ...state.unspentsAndTransactions[payload.accountID], + utxosMap, + } + } }; } @@ -112,8 +136,14 @@ export function walletReducer( case ACTION_TYPES.FLUSH_UTXOS: { return { ...state, - utxoMap: {}, - }; + unspentsAndTransactions: { + ...state.unspentsAndTransactions, + [payload.accountID]: { + ...state.unspentsAndTransactions[payload.accountID], + utxosMap: {}, + } + } + } } case ACTION_TYPES.SET_VERIFIED: { diff --git a/src/application/redux/selectors/balance.selector.ts b/src/application/redux/selectors/balance.selector.ts index a178eb45..c095de8e 100644 --- a/src/application/redux/selectors/balance.selector.ts +++ b/src/application/redux/selectors/balance.selector.ts @@ -1,6 +1,7 @@ +import { AccountID } from '../../../domain/account'; import { RootReducerState } from '../../../domain/common'; import { lbtcAssetByNetwork } from '../../utils'; -import { walletTransactions } from './transaction.selector'; +import { selectTransactions, selectUtxos } from './wallet.selector'; export type BalancesByAsset = { [assetHash: string]: number }; /** @@ -8,8 +9,8 @@ export type BalancesByAsset = { [assetHash: string]: number }; * @param onSuccess * @param onError */ -export function balancesSelector(state: RootReducerState): BalancesByAsset { - const utxos = Object.values(state.wallet.utxoMap); +export const selectBalances = (accountID: AccountID) => (state: RootReducerState): BalancesByAsset => { + const utxos = selectUtxos(accountID)(state); const balancesFromUtxos = utxos.reduce((acc, curr) => { if (!curr.asset || !curr.value) { return acc; @@ -17,7 +18,7 @@ export function balancesSelector(state: RootReducerState): BalancesByAsset { return { ...acc, [curr.asset]: curr.value + (curr.asset in acc ? acc[curr.asset] : 0) }; }, {} as BalancesByAsset); - const txs = walletTransactions(state); + const txs = selectTransactions(accountID)(state); const assets = Object.keys(balancesFromUtxos); for (const tx of txs) { diff --git a/src/application/redux/selectors/transaction.selector.ts b/src/application/redux/selectors/transaction.selector.ts index d742b06b..67b9fe4e 100644 --- a/src/application/redux/selectors/transaction.selector.ts +++ b/src/application/redux/selectors/transaction.selector.ts @@ -1,10 +1,6 @@ import { RootReducerState } from '../../../domain/common'; import { TxDisplayInterface } from '../../../domain/transaction'; -export function walletTransactions(state: RootReducerState): TxDisplayInterface[] { - return Object.values(state.txsHistory[state.app.network]); -} - export const txHasAsset = (assetHash: string) => (tx: TxDisplayInterface): boolean => { diff --git a/src/application/redux/selectors/wallet.selector.ts b/src/application/redux/selectors/wallet.selector.ts index 1b10ab20..083d78bd 100644 --- a/src/application/redux/selectors/wallet.selector.ts +++ b/src/application/redux/selectors/wallet.selector.ts @@ -1,13 +1,18 @@ import { MasterPublicKey, UtxoInterface } from 'ldk'; -import { createMnemonicAccount, MainAccount } from '../../../domain/account'; +import { AccountID, createMnemonicAccount, MainAccount } from '../../../domain/account'; import { RootReducerState } from '../../../domain/common'; +import { TxDisplayInterface } from '../../../domain/transaction'; export function masterPubKeySelector(state: RootReducerState): Promise { return selectMainAccount(state).getWatchIdentity(); } -export function utxosSelector(state: RootReducerState): UtxoInterface[] { - return Object.values(state.wallet.utxoMap); +export const selectUtxos = (accountID: AccountID) => (state: RootReducerState): UtxoInterface[] => { + return Object.values(state.wallet.unspentsAndTransactions[accountID].utxosMap || {}); +} + +export const selectTransactions = (accountID: AccountID) => (state: RootReducerState): TxDisplayInterface[] => { + return Object.values(state.wallet.unspentsAndTransactions[accountID].transactions[state.app.network] || {}); } export function hasMnemonicSelector(state: RootReducerState): boolean { diff --git a/src/application/redux/store.ts b/src/application/redux/store.ts index 57574a71..cfa8e47d 100644 --- a/src/application/redux/store.ts +++ b/src/application/redux/store.ts @@ -11,15 +11,16 @@ import { alias, wrapStore } from 'webext-redux'; import marinaReducer from './reducers'; import { fetchAndSetTaxiAssets, - updateTxsHistory, - fetchAndUpdateUtxos, + makeTxsUpdaterThunk, + makeUtxosUpdaterThunk, startAlarmUpdater, deepRestorer, resetAll, -} from '../../background/backend'; +} from './backend'; import persistStore from 'redux-persist/es/persistStore'; import { parse, stringify } from '../utils/browser-storage-converters'; import thunk from 'redux-thunk'; +import { selectMainAccount } from './selectors/wallet.selector'; export const serializerAndDeserializer = { serializer: (payload: any) => stringify(payload), @@ -27,8 +28,8 @@ export const serializerAndDeserializer = { }; const backgroundAliases = { - [UPDATE_UTXOS]: () => fetchAndUpdateUtxos(), - [UPDATE_TXS]: () => updateTxsHistory(), + [UPDATE_UTXOS]: () => makeUtxosUpdaterThunk(selectMainAccount), + [UPDATE_TXS]: () => makeTxsUpdaterThunk(selectMainAccount), [UPDATE_TAXI_ASSETS]: () => fetchAndSetTaxiAssets(), [START_PERIODIC_UPDATE]: () => startAlarmUpdater(), [START_DEEP_RESTORATION]: () => deepRestorer(), diff --git a/src/application/utils/restorer.ts b/src/application/utils/restorer.ts index 87e81e54..420fdb62 100644 --- a/src/application/utils/restorer.ts +++ b/src/application/utils/restorer.ts @@ -9,6 +9,8 @@ import { DEFAULT_BASE_DERIVATION_PATH, CosignerMultisig, IdentityInterface, + MultisigWatchOnly, + XPub, } from 'ldk'; import { Address } from '../../domain/address'; import { MasterBlindingKey } from '../../domain/master-blinding-key'; @@ -97,5 +99,23 @@ export function restoredMultisig( } }); + return restore(multisigID as IdentityInterface) +} + +export function restoredWatchOnlyMultisig( + signerXPub: XPub, + cosigners: CosignerMultisig[], + requiredSignatures: number, + network: Network +) { + const multisigID = new MultisigWatchOnly({ + chain: network, + type: IdentityType.MultisigWatchOnly, + opts: { + requiredSignatures, + cosigners: cosigners.concat([signerXPub]) + } + }); + return restore(multisigID as IdentityInterface) } \ No newline at end of file diff --git a/src/content/marinaBroker.ts b/src/content/marinaBroker.ts index ff9c091f..aa78708f 100644 --- a/src/content/marinaBroker.ts +++ b/src/content/marinaBroker.ts @@ -18,19 +18,19 @@ import { setTx, setTxData, } from '../application/redux/actions/connect'; -import { selectMainAccount, utxosSelector } from '../application/redux/selectors/wallet.selector'; +import { selectMainAccount, selectTransactions, selectUtxos } from '../application/redux/selectors/wallet.selector'; import { incrementAddressIndex, incrementChangeAddressIndex, } from '../application/redux/actions/wallet'; import { lbtcAssetByNetwork, sortRecipients } from '../application/utils'; -import { walletTransactions } from '../application/redux/selectors/transaction.selector'; -import { balancesSelector } from '../application/redux/selectors/balance.selector'; +import { selectBalances } from '../application/redux/selectors/balance.selector'; import { assetGetterFromIAssets } from '../domain/assets'; import { Balance, Recipient } from 'marina-provider'; import { SignTransactionPopupResponse } from '../presentation/connect/sign-pset'; import { SpendPopupResponse } from '../presentation/connect/spend'; import { SignMessagePopupResponse } from '../presentation/connect/sign-msg'; +import { MainAccountID } from '../domain/account'; export default class MarinaBroker extends Broker { private static NotSetUpError = new Error('proxy store and/or cache are not set up'); @@ -195,19 +195,19 @@ export default class MarinaBroker extends Broker { case Marina.prototype.getTransactions.name: { this.checkHostnameAuthorization(state); - const transactions = walletTransactions(state); + const transactions = selectTransactions(MainAccountID)(state); return successMsg(transactions); } case Marina.prototype.getCoins.name: { this.checkHostnameAuthorization(state); - const coins = utxosSelector(state); + const coins = selectUtxos(MainAccountID)(state); return successMsg(coins); } case Marina.prototype.getBalances.name: { this.checkHostnameAuthorization(state); - const balances = balancesSelector(state); + const balances = selectBalances(MainAccountID)(state); const assetGetter = assetGetterFromIAssets(state.assets); const balancesResult: Balance[] = []; for (const [assetHash, amount] of Object.entries(balances)) { diff --git a/src/domain/account.ts b/src/domain/account.ts index 4aba6ce6..b5e7d1b9 100644 --- a/src/domain/account.ts +++ b/src/domain/account.ts @@ -6,19 +6,21 @@ import { MasterBlindingKey } from './master-blinding-key'; import { MasterXPub } from './master-extended-pub'; import { Network } from './network'; +export type AccountID = string; +export const MainAccountID: AccountID = 'main'; + /** * Account domain represents the keys of the User * * - each Account is a derived of master private key (computed from mnemonic). - * - an Account returns two type of identities: a WatchOnly identity and a signing Identity (computed from user's password). - * - - * + * - an Account returns two types of identities: a WatchOnly identity and a signing Identity. + * the watch-only identity is used to update utxos and transactions state + * the signing identity is used to sign inputs. it needs the user's password to decrypt the mnemonic. */ - export interface Account { + accountID: AccountID; getSigningIdentity(password: string): Promise; getWatchIdentity(): Promise; - [propName: string]: any; } // Main Account uses the default Mnemonic derivation path @@ -26,6 +28,7 @@ export interface Account; export interface MnemonicAccountData { + accountID: AccountID; encryptedMnemonic: EncryptedMnemonic; restorerOpts: StateRestorerOpts; masterXPub: MasterXPub; @@ -34,6 +37,7 @@ export interface MnemonicAccountData { export function createMnemonicAccount(data: MnemonicAccountData, network: Network): MainAccount { return { + accountID: data.accountID, getSigningIdentity: (password: string) => restoredMnemonic(decrypt(data.encryptedMnemonic, password), data.restorerOpts, network), getWatchIdentity: () => @@ -41,6 +45,6 @@ export function createMnemonicAccount(data: MnemonicAccountData, network: Networ }; } -// MultisigAccount aims to handle cosigner +// MultisigAccount aims to handle account with cosigner(s) // use master extended public keys from cosigners and xpub derived from master private key (mnemonic) export type MultisigAccount = Account; diff --git a/src/domain/common.ts b/src/domain/common.ts index c8aa67c2..eb83d62b 100644 --- a/src/domain/common.ts +++ b/src/domain/common.ts @@ -3,7 +3,6 @@ import { IWallet } from './wallet'; import { IApp } from './app'; import { OnboardingState } from '../application/redux/reducers/onboarding-reducer'; import { TransactionState } from '../application/redux/reducers/transaction-reducer'; -import { TxsHistoryByNetwork } from './transaction'; import { Action } from 'redux'; import { TaxiState } from '../application/redux/reducers/taxi-reducer'; import { IAssets } from './assets'; @@ -13,7 +12,6 @@ export interface RootReducerState { assets: IAssets; onboarding: OnboardingState; transaction: TransactionState; - txsHistory: TxsHistoryByNetwork; wallet: IWallet; connect: ConnectData; taxi: TaxiState; diff --git a/src/domain/transaction.ts b/src/domain/transaction.ts index 14d1ecfb..2ef08e64 100644 --- a/src/domain/transaction.ts +++ b/src/domain/transaction.ts @@ -1,8 +1,13 @@ -import { address, decodePset } from 'ldk'; +import { address, decodePset, UtxoInterface } from 'ldk'; import { Address } from './address'; import { IError } from './common'; import { Network } from './network'; +export interface UtxosAndTxsHistory { + utxosMap: Record; + transactions: TxsHistoryByNetwork; +} + export type TxsHistory = Record; export type TxsHistoryByNetwork = Record & Partial>; diff --git a/src/domain/wallet.ts b/src/domain/wallet.ts index 46b865e6..d0433f6a 100644 --- a/src/domain/wallet.ts +++ b/src/domain/wallet.ts @@ -1,13 +1,13 @@ -import { UtxoInterface } from 'ldk'; -import { MnemonicAccountData } from './account'; +import { AccountID, MnemonicAccountData } from './account'; import { IError } from './common'; import { PasswordHash } from './password-hash'; +import { UtxosAndTxsHistory } from './transaction'; export interface IWallet { mainAccount: MnemonicAccountData; + unspentsAndTransactions: Record; errors?: Record; passwordHash: PasswordHash; - utxoMap: Record; deepRestorer: { gapLimit: number; isLoading: boolean; diff --git a/src/presentation/connect/spend.tsx b/src/presentation/connect/spend.tsx index ccbe88c1..621cf69a 100644 --- a/src/presentation/connect/spend.tsx +++ b/src/presentation/connect/spend.tsx @@ -19,9 +19,10 @@ import { blindAndSignPset, createSendPset } from '../../application/utils/transa import { incrementChangeAddressIndex } from '../../application/redux/actions/wallet'; import { selectMainAccount, - utxosSelector, + selectUtxos, } from '../../application/redux/selectors/wallet.selector'; import PopupWindowProxy from './popupWindowProxy'; +import { MainAccountID } from '../../domain/account'; export interface SpendPopupResponse { accepted: boolean; @@ -32,7 +33,7 @@ const ConnectSpend: React.FC = ({ connectData }) => { const assets = useSelector((state: RootReducerState) => state.assets); const mainAccount = useSelector(selectMainAccount); const network = useSelector((state: RootReducerState) => state.app.network); - const coins = useSelector(utxosSelector); + const coins = useSelector(selectUtxos(MainAccountID)); const dispatch = useDispatch(); From 5356c12a2162937c96dacfad5b71a18e4d3ed611 Mon Sep 17 00:00:00 2001 From: louisinger Date: Tue, 19 Oct 2021 13:37:36 +0200 Subject: [PATCH 09/52] refacto WalletState (former 'IWallet') --- .../redux/containers/choose-fee.container.ts | 4 +- src/application/redux/reducers/index.ts | 4 +- .../redux/reducers/wallet-reducer.ts | 11 ++--- .../redux/selectors/wallet.selector.ts | 13 +++++- src/application/utils/restorer.ts | 13 +++--- src/domain/account.ts | 40 ++++++++++++++++++- src/domain/common.ts | 4 +- src/domain/wallet.ts | 12 ++++-- src/presentation/settings/cosigners.tsx | 18 ++++++++- src/presentation/settings/show-mnemonic.tsx | 4 +- src/presentation/wallet/send/choose-fee.tsx | 11 +++-- 11 files changed, 98 insertions(+), 36 deletions(-) diff --git a/src/application/redux/containers/choose-fee.container.ts b/src/application/redux/containers/choose-fee.container.ts index 44da992f..eb8d45d8 100644 --- a/src/application/redux/containers/choose-fee.container.ts +++ b/src/application/redux/containers/choose-fee.container.ts @@ -4,10 +4,9 @@ import { RootReducerState } from '../../../domain/common'; import ChooseFeeView, { ChooseFeeProps } from '../../../presentation/wallet/send/choose-fee'; import { lbtcAssetByNetwork } from '../../utils'; import { selectBalances } from '../selectors/balance.selector'; -import { selectMainAccount } from '../selectors/wallet.selector'; +import { selectMainAccount, selectUtxos } from '../selectors/wallet.selector'; const mapStateToProps = (state: RootReducerState): ChooseFeeProps => ({ - wallet: state.wallet, network: state.app.network, assets: state.assets, balances: selectBalances(MainAccountID)(state), @@ -18,6 +17,7 @@ const mapStateToProps = (state: RootReducerState): ChooseFeeProps => ({ sendAsset: state.transaction.sendAsset, sendAmount: state.transaction.sendAmount, mainAccount: selectMainAccount(state), + mainAccountUtxos: selectUtxos(MainAccountID)(state) }); const ChooseFee = connect(mapStateToProps)(ChooseFeeView); diff --git a/src/application/redux/reducers/index.ts b/src/application/redux/reducers/index.ts index 386b4ad7..138124aa 100644 --- a/src/application/redux/reducers/index.ts +++ b/src/application/redux/reducers/index.ts @@ -7,7 +7,7 @@ import { parse, stringify } from '../../utils/browser-storage-converters'; import browser from 'webextension-polyfill'; import persistReducer, { PersistPartial } from 'redux-persist/es/persistReducer'; import { IApp } from '../../../domain/app'; -import { IWallet } from '../../../domain/wallet'; +import { WalletState } from '../../../domain/wallet'; import { taxiReducer, TaxiState, taxiInitState } from './taxi-reducer'; import { ConnectData } from '../../../domain/connect'; import { IAssets } from '../../../domain/assets'; @@ -86,7 +86,7 @@ const marinaReducer = combineReducers({ version: 1, initialState: transactionInitState, }), - wallet: persist({ + wallet: persist({ reducer: walletReducer, key: 'wallet', blacklist: ['deepRestorer'], diff --git a/src/application/redux/reducers/wallet-reducer.ts b/src/application/redux/reducers/wallet-reducer.ts index ca3de125..b99f26ce 100644 --- a/src/application/redux/reducers/wallet-reducer.ts +++ b/src/application/redux/reducers/wallet-reducer.ts @@ -1,12 +1,12 @@ /* eslint-disable @typescript-eslint/restrict-plus-operands */ import { toStringOutpoint } from './../../utils/utxos'; import * as ACTION_TYPES from '../actions/action-types'; -import { IWallet } from '../../../domain/wallet'; +import { WalletState } from '../../../domain/wallet'; import { AnyAction } from 'redux'; import { UtxoInterface } from 'ldk'; import { AccountID, MainAccountID } from '../../../domain/account'; -export const walletInitState: IWallet = { +export const walletInitState: WalletState = { mainAccount: { accountID: MainAccountID, encryptedMnemonic: '', @@ -17,6 +17,7 @@ export const walletInitState: IWallet = { lastUsedInternalIndex: 0, }, }, + restrictedAssetAccounts: {}, unspentsAndTransactions: { MainAccountID: { utxosMap: { }, @@ -31,7 +32,7 @@ export const walletInitState: IWallet = { isVerified: false, }; -const addUnspent = (state: IWallet) => (accountID: AccountID, utxo: UtxoInterface): IWallet => { +const addUnspent = (state: WalletState) => (accountID: AccountID, utxo: UtxoInterface): WalletState => { return { ...state, unspentsAndTransactions: { @@ -48,9 +49,9 @@ const addUnspent = (state: IWallet) => (accountID: AccountID, utxo: UtxoInterfac } export function walletReducer( - state: IWallet = walletInitState, + state: WalletState = walletInitState, { type, payload }: AnyAction -): IWallet { +): WalletState { switch (type) { case ACTION_TYPES.RESET_WALLET: { return walletInitState; diff --git a/src/application/redux/selectors/wallet.selector.ts b/src/application/redux/selectors/wallet.selector.ts index 083d78bd..f9fda513 100644 --- a/src/application/redux/selectors/wallet.selector.ts +++ b/src/application/redux/selectors/wallet.selector.ts @@ -1,7 +1,8 @@ -import { MasterPublicKey, UtxoInterface } from 'ldk'; -import { AccountID, createMnemonicAccount, MainAccount } from '../../../domain/account'; +import { MasterPublicKey, UtxoInterface, XPub } from 'ldk'; +import { AccountID, createMnemonicAccount, createMultisigAccount, MainAccount, MultisigAccount, MultisigAccountData } from '../../../domain/account'; import { RootReducerState } from '../../../domain/common'; import { TxDisplayInterface } from '../../../domain/transaction'; +import { CosignerExtraData } from '../../../domain/wallet'; export function masterPubKeySelector(state: RootReducerState): Promise { return selectMainAccount(state).getWatchIdentity(); @@ -25,3 +26,11 @@ export function hasMnemonicSelector(state: RootReducerState): boolean { export function selectMainAccount(state: RootReducerState): MainAccount { return createMnemonicAccount(state.wallet.mainAccount, state.app.network); } + +export const selectRestrictedAssetAccount = (cosignerXPub: XPub) => function (state: RootReducerState): MultisigAccount { + return createMultisigAccount(state.wallet.mainAccount.encryptedMnemonic, state.wallet.restrictedAssetAccounts[cosignerXPub], state.app.network); +} + +export function selectAllRestrictedAssetAccounts(state: RootReducerState): MultisigAccountData[] { + return Object.values(state.wallet.restrictedAssetAccounts); +} \ No newline at end of file diff --git a/src/application/utils/restorer.ts b/src/application/utils/restorer.ts index 420fdb62..26ddb975 100644 --- a/src/application/utils/restorer.ts +++ b/src/application/utils/restorer.ts @@ -6,11 +6,11 @@ import { MasterPublicKey, masterPubKeyRestorerFromState, Multisig, - DEFAULT_BASE_DERIVATION_PATH, CosignerMultisig, IdentityInterface, MultisigWatchOnly, XPub, + HDSignerMultisig, } from 'ldk'; import { Address } from '../../domain/address'; import { MasterBlindingKey } from '../../domain/master-blinding-key'; @@ -81,7 +81,7 @@ export function restoredMasterPublicKey( export function restoredMultisig( - mnemonic: string, + signer: HDSignerMultisig, cosigners: CosignerMultisig[], requiredSignatures: number, network: Network @@ -91,15 +91,12 @@ export function restoredMultisig( type: IdentityType.Multisig, opts: { requiredSignatures, - signer: { - mnemonic, - baseDerivationPath: DEFAULT_BASE_DERIVATION_PATH, - }, + signer, cosigners } }); - return restore(multisigID as IdentityInterface) + return restoreFrom(multisigID); } export function restoredWatchOnlyMultisig( @@ -118,4 +115,4 @@ export function restoredWatchOnlyMultisig( }); return restore(multisigID as IdentityInterface) -} \ No newline at end of file +} diff --git a/src/domain/account.ts b/src/domain/account.ts index b5e7d1b9..1fc27e69 100644 --- a/src/domain/account.ts +++ b/src/domain/account.ts @@ -1,6 +1,6 @@ -import { IdentityInterface, MasterPublicKey, Mnemonic, Multisig, MultisigWatchOnly, StateRestorerOpts } from 'ldk'; +import { DEFAULT_BASE_DERIVATION_PATH, HDSignerMultisig, IdentityInterface, IdentityType, MasterPublicKey, Mnemonic, Multisig, MultisigWatchOnly, StateRestorerOpts, XPub } from 'ldk'; import { decrypt } from '../application/utils'; -import { restoredMasterPublicKey, restoredMnemonic } from '../application/utils/restorer'; +import { restoredMasterPublicKey, restoredMnemonic, restoredMultisig, restoredWatchOnlyMultisig } from '../application/utils/restorer'; import { EncryptedMnemonic } from './encrypted-mnemonic'; import { MasterBlindingKey } from './master-blinding-key'; import { MasterXPub } from './master-extended-pub'; @@ -48,3 +48,39 @@ export function createMnemonicAccount(data: MnemonicAccountData, network: Networ // MultisigAccount aims to handle account with cosigner(s) // use master extended public keys from cosigners and xpub derived from master private key (mnemonic) export type MultisigAccount = Account; + +export interface MultisigAccountData { + baseDerivationPath: string; // we'll use the MainAccount in order to generate + signerXPub: XPub; + cosignerXPubs: XPub[]; + requiredSignature: number; + extraData: ExtraDataT; +} + +export function create2of2MultisigAccountData(signer: HDSignerMultisig, cosignerXPub: XPub, network: Network, extraData: T): MultisigAccountData { + const multisigID = new Multisig({ + chain: network, + type: IdentityType.Multisig, + opts: { + requiredSignatures: 2, + signer, + cosigners: [cosignerXPub] + } + }) + + return { + baseDerivationPath: signer.baseDerivationPath || DEFAULT_BASE_DERIVATION_PATH, + signerXPub: multisigID.getXPub(), + cosignerXPubs: [cosignerXPub], + requiredSignature: 2, + extraData + } +} + +export function createMultisigAccount(encryptedMnemonic: EncryptedMnemonic, data: MultisigAccountData, network: Network): MultisigAccount { + return { + accountID: data.signerXPub, + getSigningIdentity: (password: string) => restoredMultisig({ mnemonic: decrypt(encryptedMnemonic, password), baseDerivationPath: data.baseDerivationPath }, data.cosignerXPubs, data.requiredSignature, network), + getWatchIdentity: () => restoredWatchOnlyMultisig(data.signerXPub, data.cosignerXPubs, data.requiredSignature, network) + } +} \ No newline at end of file diff --git a/src/domain/common.ts b/src/domain/common.ts index eb83d62b..5b419827 100644 --- a/src/domain/common.ts +++ b/src/domain/common.ts @@ -1,5 +1,5 @@ import { ConnectData } from './connect'; -import { IWallet } from './wallet'; +import { WalletState } from './wallet'; import { IApp } from './app'; import { OnboardingState } from '../application/redux/reducers/onboarding-reducer'; import { TransactionState } from '../application/redux/reducers/transaction-reducer'; @@ -12,7 +12,7 @@ export interface RootReducerState { assets: IAssets; onboarding: OnboardingState; transaction: TransactionState; - wallet: IWallet; + wallet: WalletState; connect: ConnectData; taxi: TaxiState; } diff --git a/src/domain/wallet.ts b/src/domain/wallet.ts index d0433f6a..efa1c164 100644 --- a/src/domain/wallet.ts +++ b/src/domain/wallet.ts @@ -1,12 +1,12 @@ -import { AccountID, MnemonicAccountData } from './account'; -import { IError } from './common'; +import { XPub } from 'ldk'; +import { AccountID, MnemonicAccountData, MultisigAccountData } from './account'; import { PasswordHash } from './password-hash'; import { UtxosAndTxsHistory } from './transaction'; -export interface IWallet { +export interface WalletState { mainAccount: MnemonicAccountData; + restrictedAssetAccounts: Record>, unspentsAndTransactions: Record; - errors?: Record; passwordHash: PasswordHash; deepRestorer: { gapLimit: number; @@ -15,3 +15,7 @@ export interface IWallet { }; isVerified: boolean; } + +export interface CosignerExtraData { + cosignerURL: string; +} \ No newline at end of file diff --git a/src/presentation/settings/cosigners.tsx b/src/presentation/settings/cosigners.tsx index 8471e87d..f6eaa7bc 100644 --- a/src/presentation/settings/cosigners.tsx +++ b/src/presentation/settings/cosigners.tsx @@ -2,8 +2,14 @@ import React from 'react'; import ShellPopUp from '../components/shell-popup'; import browser from 'webextension-polyfill'; import { PAIR_COSIGNER_ROUTE } from '../routes/constants'; +import { MultisigAccountData } from '../../domain/account'; +import { CosignerExtraData } from '../../domain/wallet'; -const SettingsCosigners: React.FC = () => { +export interface SettingsCosignersProps { + multisigAccountsData: MultisigAccountData[]; +} + +const SettingsCosigners: React.FC = ({ multisigAccountsData }) => { const openAddCosignerTab = async () => { const url = browser.runtime.getURL(`home.html#${PAIR_COSIGNER_ROUTE}`); await browser.tabs.create({ url }); @@ -16,6 +22,16 @@ const SettingsCosigners: React.FC = () => { currentPage="Change currency" >

Cosigners

+
+
    + {multisigAccountsData.map(({ extraData }) => +
  • + { extraData.cosignerURL } +
  • + )} +
+
+
Add cosigner diff --git a/src/presentation/settings/show-mnemonic.tsx b/src/presentation/settings/show-mnemonic.tsx index ca060cbc..afa1ac6d 100644 --- a/src/presentation/settings/show-mnemonic.tsx +++ b/src/presentation/settings/show-mnemonic.tsx @@ -4,12 +4,12 @@ import { decrypt } from '../../application/utils'; import ModalUnlock from '../components/modal-unlock'; import RevealMnemonic from '../components/reveal-mnemonic'; import ShellPopUp from '../components/shell-popup'; -import { IWallet } from '../../domain/wallet'; +import { WalletState } from '../../domain/wallet'; import { match } from '../../domain/password-hash'; import { createPassword } from '../../domain/password'; export interface SettingsShowMnemonicProps { - wallet: IWallet; + wallet: WalletState; } const SettingsShowMnemonicView: React.FC = ({ wallet }) => { diff --git a/src/presentation/wallet/send/choose-fee.tsx b/src/presentation/wallet/send/choose-fee.tsx index 5532d670..ff67970c 100644 --- a/src/presentation/wallet/send/choose-fee.tsx +++ b/src/presentation/wallet/send/choose-fee.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router'; -import { greedyCoinSelector, RecipientInterface, walletFromCoins } from 'ldk'; +import { greedyCoinSelector, RecipientInterface, UtxoInterface, walletFromCoins } from 'ldk'; import Balance from '../../components/balance'; import Button from '../../components/button'; import ShellPopUp from '../../components/shell-popup'; @@ -17,7 +17,6 @@ import { } from '../../../application/utils'; import { formatDecimalAmount, fromSatoshi, fromSatoshiStr } from '../../utils'; import useLottieLoader from '../../hooks/use-lottie-loader'; -import { IWallet } from '../../../domain/wallet'; import { IAssets } from '../../../domain/assets'; import { useDispatch } from 'react-redux'; import { BalancesByAsset } from '../../../application/redux/selectors/balance.selector'; @@ -40,11 +39,11 @@ export interface ChooseFeeProps { sendAmount: number; sendAddress?: Address; sendAsset: string; - wallet: IWallet; balances: BalancesByAsset; taxiAssets: string[]; lbtcAssetHash: string; mainAccount: MainAccount; + mainAccountUtxos: UtxoInterface[]; } const ChooseFeeView: React.FC = ({ @@ -54,11 +53,11 @@ const ChooseFeeView: React.FC = ({ sendAmount, sendAddress, sendAsset, - wallet, balances, taxiAssets, lbtcAssetHash, mainAccount, + mainAccountUtxos }) => { const history = useHistory(); const dispatch = useDispatch(); @@ -112,7 +111,7 @@ const ChooseFeeView: React.FC = ({ const createTx = (recipients: RecipientInterface[]) => { // no taxi setFeeChange(undefined); - const w = walletFromCoins(Object.values(wallet.utxoMap), network); + const w = walletFromCoins(mainAccountUtxos, network); const currentSatsPerByte = feeLevelToSatsPerByte[feeLevel]; if (!changeAddress) throw new Error('change address is not defined'); @@ -159,7 +158,7 @@ const ChooseFeeView: React.FC = ({ const tx: string = createTaxiTxFromTopup( taxiTopup, - Object.values(wallet.utxoMap), + mainAccountUtxos, recipients, greedyCoinSelector(), changeGetter From 3c0a54404ec3602ba74ffaa75030e47e26b4df9b Mon Sep 17 00:00:00 2001 From: louisinger Date: Tue, 19 Oct 2021 14:01:02 +0200 Subject: [PATCH 10/52] flush all accounts utxos in networks menu --- .../redux/containers/settings-networks.container.ts | 1 + src/presentation/settings/networks.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/application/redux/containers/settings-networks.container.ts b/src/application/redux/containers/settings-networks.container.ts index 7a6050a9..d9442b65 100644 --- a/src/application/redux/containers/settings-networks.container.ts +++ b/src/application/redux/containers/settings-networks.container.ts @@ -6,6 +6,7 @@ import { RootReducerState } from '../../../domain/common'; const mapStateToProps = (state: RootReducerState): SettingsNetworksProps => ({ restorationLoading: state.wallet.deepRestorer.isLoading, + accountsIDs: Object.keys(state.wallet.unspentsAndTransactions), error: state.wallet.deepRestorer.error, }); diff --git a/src/presentation/settings/networks.tsx b/src/presentation/settings/networks.tsx index 626923b1..fd7fadef 100644 --- a/src/presentation/settings/networks.tsx +++ b/src/presentation/settings/networks.tsx @@ -4,6 +4,7 @@ import { changeNetwork } from '../../application/redux/actions/app'; import { flushUtxos } from '../../application/redux/actions/utxos'; import { setDeepRestorerGapLimit, startDeepRestorer } from '../../application/redux/actions/wallet'; import { ProxyStoreDispatch } from '../../application/redux/proxyStore'; +import { AccountID } from '../../domain/account'; import { RootReducerState } from '../../domain/common'; import { createNetwork } from '../../domain/network'; import Select from '../components/select'; @@ -15,10 +16,11 @@ const formattedNetworks = networks.map((n) => formatNetwork(n)); export interface SettingsNetworksProps { restorationLoading: boolean; + accountsIDs: AccountID[]; error?: string; } -const SettingsNetworksView: React.FC = ({ restorationLoading, error }) => { +const SettingsNetworksView: React.FC = ({ restorationLoading, accountsIDs, error }) => { const [isLoading, setIsLoading] = useState(false); const network = useSelector((state: RootReducerState) => state.app.network); @@ -30,7 +32,7 @@ const SettingsNetworksView: React.FC = ({ restorationLoad setIsLoading(true); const newNetwork = createNetwork(net.toLowerCase()); await dispatch(changeNetwork(newNetwork)); - await dispatch(flushUtxos()); + await Promise.all(accountsIDs.map(flushUtxos).map(dispatch)); await dispatch(setDeepRestorerGapLimit(20)); await dispatch(startDeepRestorer()); setIsLoading(false); From ed0a562a8698307fded48355044885efe8621646 Mon Sep 17 00:00:00 2001 From: louisinger Date: Tue, 19 Oct 2021 14:38:18 +0200 Subject: [PATCH 11/52] fix imports --- .../redux/containers/transactions.container.ts | 5 +++-- src/application/utils/restorer.ts | 6 ++++-- src/content/store-cache.ts | 5 +++-- src/presentation/cosigner/pair.tsx | 15 +++++++++++++++ .../onboarding/wallet-restore/index.tsx | 9 +++------ 5 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/application/redux/containers/transactions.container.ts b/src/application/redux/containers/transactions.container.ts index 5c3a0ec9..f5c3653d 100644 --- a/src/application/redux/containers/transactions.container.ts +++ b/src/application/redux/containers/transactions.container.ts @@ -1,12 +1,13 @@ import { connect } from 'react-redux'; +import { MainAccountID } from '../../../domain/account'; import { RootReducerState } from '../../../domain/common'; import TransactionsView, { TransactionsProps } from '../../../presentation/wallet/transactions'; -import { walletTransactions } from '../selectors/transaction.selector'; +import { selectTransactions } from '../selectors/wallet.selector'; const mapStateToProps = (state: RootReducerState): TransactionsProps => ({ assets: state.assets, network: state.app.network, - transactions: walletTransactions(state), + transactions: selectTransactions(MainAccountID)(state), webExplorerURL: state.app.explorerByNetwork[state.app.network].electrsURL, }); diff --git a/src/application/utils/restorer.ts b/src/application/utils/restorer.ts index 26ddb975..3756f9a0 100644 --- a/src/application/utils/restorer.ts +++ b/src/application/utils/restorer.ts @@ -96,7 +96,8 @@ export function restoredMultisig( } }); - return restoreFrom(multisigID); + // return restoreFrom(multisigID); + return Promise.resolve(multisigID); } export function restoredWatchOnlyMultisig( @@ -114,5 +115,6 @@ export function restoredWatchOnlyMultisig( } }); - return restore(multisigID as IdentityInterface) + // return restoreFromState(multisigID as IdentityInterface) + return Promise.resolve(multisigID); } diff --git a/src/content/store-cache.ts b/src/content/store-cache.ts index eaf748c2..074568b9 100644 --- a/src/content/store-cache.ts +++ b/src/content/store-cache.ts @@ -6,6 +6,7 @@ import { compareUtxoState, networkChange, } from '../application/utils/marina-event'; +import { MainAccountID } from '../domain/account'; import { RootReducerState } from '../domain/common'; import { Network } from '../domain/network'; import { TxsHistory } from '../domain/transaction'; @@ -48,8 +49,8 @@ export function compareCacheForEvents( // create cache from State. export function newCacheFromState(state: RootReducerState): StoreCache { return { - utxoState: state.wallet.utxoMap, - txsHistoryState: state.txsHistory[state.app.network], + utxoState: state.wallet.unspentsAndTransactions[MainAccountID].utxosMap, + txsHistoryState: state.wallet.unspentsAndTransactions[MainAccountID].transactions[state.app.network], enabledWebsitesState: state.connect.enabledSites, network: state.app.network, }; diff --git a/src/presentation/cosigner/pair.tsx b/src/presentation/cosigner/pair.tsx index 33300dbf..34621318 100644 --- a/src/presentation/cosigner/pair.tsx +++ b/src/presentation/cosigner/pair.tsx @@ -3,6 +3,8 @@ import React from 'react'; import Shell from '../components/shell'; import * as Yup from 'yup'; import Button from '../components/button'; +import { create2of2MultisigAccountData } from '../../domain/account'; +import { CosignerExtraData } from '../../domain/wallet'; interface OptInFormProps { onSubmit: (values: OptInFormValues) => Promise; @@ -10,6 +12,7 @@ interface OptInFormProps { interface OptInFormValues { cosignerURL: string; + password: string; } const optInForm = (props: FormikProps) => { @@ -28,6 +31,13 @@ const optInForm = (props: FormikProps) => { /> {touchedAndError('cosignerURL') &&
{errors.cosignerURL}
} + + {touchedAndError('password') &&
{errors.password}
} + @@ -38,6 +48,7 @@ const optInForm = (props: FormikProps) => { const OptInFormikForm = withFormik({ validationSchema: Yup.object().shape({ cosignerURL: Yup.string().required('Please input cosignerURL').url('Not a valid URL'), + password: Yup.string().required() }), handleSubmit: async (values, { props }) => { @@ -49,6 +60,10 @@ const OptInFormikForm = withFormik({ const PairCosigner: React.FC = () => { const onSubmit = (values: OptInFormValues) => { + // const multisigAccountData = create2of2MultisigAccountData( + + // ) + console.log(values); return Promise.resolve(); }; diff --git a/src/presentation/onboarding/wallet-restore/index.tsx b/src/presentation/onboarding/wallet-restore/index.tsx index af82ccd9..29961a3d 100644 --- a/src/presentation/onboarding/wallet-restore/index.tsx +++ b/src/presentation/onboarding/wallet-restore/index.tsx @@ -5,10 +5,10 @@ import { withFormik, FormikProps } from 'formik'; import * as Yup from 'yup'; import Button from '../../components/button'; import Shell from '../../components/shell'; -import { IError, RootReducerState } from '../../../domain/common'; +import { IError } from '../../../domain/common'; import { INITIALIZE_END_OF_FLOW_ROUTE } from '../../routes/constants'; import { setPasswordAndOnboardingMnemonic } from '../../../application/redux/actions/onboarding'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { ProxyStoreDispatch } from '../../../application/redux/proxyStore'; import { setVerified } from '../../../application/redux/actions/wallet'; @@ -20,7 +20,6 @@ interface WalletRestoreFormValues { } interface WalletRestoreFormProps { - ctxErrors?: Record; dispatch: ProxyStoreDispatch; history: RouteComponentProps['history']; } @@ -122,7 +121,6 @@ const WalletRestoreEnhancedForm = withFormik ({ confirmPassword: '', - ctxErrors: props.ctxErrors, mnemonic: '', password: '', }), @@ -158,13 +156,12 @@ const WalletRestoreEnhancedForm = withFormik = () => { const dispatch = useDispatch(); const history = useHistory(); - const errors = useSelector((state: RootReducerState) => state.wallet?.errors); return (

{'Restore a wallet from a mnemonic phrase'}

{'Enter your secret twelve words of your mnemonic phrase to Restore your wallet'}

- +
); }; From e63852a58a72737d694ae9237977852019aa7f26 Mon Sep 17 00:00:00 2001 From: louisinger Date: Tue, 19 Oct 2021 16:26:19 +0200 Subject: [PATCH 12/52] updaters depends on Account domain --- src/application/redux/actions/transaction.ts | 5 +- src/application/redux/backend.ts | 29 ++++--- .../redux/containers/choose-fee.container.ts | 2 +- .../redux/reducers/wallet-reducer.ts | 84 +++++++++++++------ .../redux/selectors/balance.selector.ts | 48 ++++++----- .../redux/selectors/wallet.selector.ts | 50 ++++++++--- src/application/utils/restorer.ts | 9 +- src/content/marinaBroker.ts | 6 +- src/content/store-cache.ts | 3 +- src/domain/account.ts | 77 +++++++++++++---- src/domain/transaction.ts | 2 +- src/domain/wallet.ts | 4 +- src/presentation/connect/spend.tsx | 5 +- src/presentation/cosigner/pair.tsx | 6 +- src/presentation/settings/cosigners.tsx | 14 ++-- src/presentation/settings/networks.tsx | 6 +- src/presentation/wallet/send/choose-fee.tsx | 2 +- src/presentation/wallet/send/end-of-flow.tsx | 5 +- 18 files changed, 235 insertions(+), 122 deletions(-) diff --git a/src/application/redux/actions/transaction.ts b/src/application/redux/actions/transaction.ts index f90752c9..1beccb2a 100644 --- a/src/application/redux/actions/transaction.ts +++ b/src/application/redux/actions/transaction.ts @@ -12,6 +12,7 @@ import { AnyAction } from 'redux'; import { Address } from '../../../domain/address'; import { TxDisplayInterface } from '../../../domain/transaction'; import { Network } from '../../../domain/network'; +import { AccountID } from '../../../domain/account'; export function setAsset(asset: string): AnyAction { return { type: PENDING_TX_SET_ASSET, payload: { asset } }; @@ -53,9 +54,9 @@ export function setPset(pset: string): AnyAction { }; } -export function addTx(tx: TxDisplayInterface, network: Network): AnyAction { +export function addTx(accountID: AccountID, tx: TxDisplayInterface, network: Network): AnyAction { return { type: ADD_TX, - payload: { tx, network }, + payload: { tx, network, accountID }, }; } diff --git a/src/application/redux/backend.ts b/src/application/redux/backend.ts index 31c6fca2..1ec85ef7 100644 --- a/src/application/redux/backend.ts +++ b/src/application/redux/backend.ts @@ -23,11 +23,7 @@ import { toDisplayTransaction, toStringOutpoint, } from '../utils'; -import { - setDeepRestorerError, - setDeepRestorerIsLoading, - setWalletData, -} from './actions/wallet'; +import { setDeepRestorerError, setDeepRestorerIsLoading, setWalletData } from './actions/wallet'; import { createAddress } from '../../domain/address'; import { setTaxiAssets, updateTaxiAssets } from './actions/taxi'; import { addUtxo, deleteUtxo, updateUtxos } from './actions/utxos'; @@ -46,7 +42,11 @@ import { } from './actions/action-types'; import { flushTx } from './actions/connect'; import { Account } from '../../domain/account'; -import { selectMainAccount } from './selectors/wallet.selector'; +import { + selectMainAccount, + selectUnspentsAndTransactions, + selectUtxos, +} from './selectors/wallet.selector'; const UPDATE_ALARM = 'UPDATE_ALARM'; @@ -76,17 +76,18 @@ async function getAddressesFromAccount(account: Account): Promise { +export function makeUtxosUpdaterThunk( + selectAccount: AccountSelector +): ThunkAction { return async (dispatch, getState) => { try { const state = getState(); const { app } = state; if (!app.isAuthenticated) return; - const account = selectAccount(state); const explorer = getExplorerURLSelector(getState()); - const utxosMap = state.wallet.unspentsAndTransactions[account.accountID].utxosMap; + const utxosMap = selectUnspentsAndTransactions(account.accountID)(state).utxosMap; const currentOutpoints = Object.values(utxosMap || {}).map(({ txid, vout }) => ({ txid, @@ -151,7 +152,9 @@ export function makeUtxosUpdaterThunk(selectAccount: AccountSelector): ThunkActi /** * use fetchAndUnblindTxsGenerator to update the tx history */ -export function makeTxsUpdaterThunk(selectAccount: AccountSelector): ThunkAction { +export function makeTxsUpdaterThunk( + selectAccount: AccountSelector +): ThunkAction { return async (dispatch, getState) => { try { const state = getState(); @@ -159,7 +162,9 @@ export function makeTxsUpdaterThunk(selectAccount: AccountSelector): ThunkAction if (!app.isAuthenticated) return; const account = selectAccount(state); - const txsHistory = state.wallet.unspentsAndTransactions[account.accountID].transactions[app.network] || {}; + const txsHistory = selectUnspentsAndTransactions(account.accountID)(state).transactions[ + app.network + ]; // Initialize txs to txsHistory shallow clone const addressInterfaces = await getAddressesFromAccount(account); @@ -204,7 +209,7 @@ export function makeTxsUpdaterThunk(selectAccount: AccountSelector): ThunkAction const tx = it.value; // Update all txsHistory state at each single new tx const toAdd = toDisplayTransaction(tx, walletScripts, networks[app.network]); - dispatch(addTx(toAdd, app.network)); + dispatch(addTx(account.accountID, toAdd, app.network)); it = await txsGen.next(); } } catch (error) { diff --git a/src/application/redux/containers/choose-fee.container.ts b/src/application/redux/containers/choose-fee.container.ts index eb8d45d8..fb3c0e3e 100644 --- a/src/application/redux/containers/choose-fee.container.ts +++ b/src/application/redux/containers/choose-fee.container.ts @@ -17,7 +17,7 @@ const mapStateToProps = (state: RootReducerState): ChooseFeeProps => ({ sendAsset: state.transaction.sendAsset, sendAmount: state.transaction.sendAmount, mainAccount: selectMainAccount(state), - mainAccountUtxos: selectUtxos(MainAccountID)(state) + mainAccountUtxos: selectUtxos(MainAccountID)(state), }); const ChooseFee = connect(mapStateToProps)(ChooseFeeView); diff --git a/src/application/redux/reducers/wallet-reducer.ts b/src/application/redux/reducers/wallet-reducer.ts index b99f26ce..bf68fc24 100644 --- a/src/application/redux/reducers/wallet-reducer.ts +++ b/src/application/redux/reducers/wallet-reducer.ts @@ -5,6 +5,8 @@ import { WalletState } from '../../../domain/wallet'; import { AnyAction } from 'redux'; import { UtxoInterface } from 'ldk'; import { AccountID, MainAccountID } from '../../../domain/account'; +import { TxDisplayInterface } from '../../../domain/transaction'; +import { Network } from '../../../domain/network'; export const walletInitState: WalletState = { mainAccount: { @@ -20,9 +22,9 @@ export const walletInitState: WalletState = { restrictedAssetAccounts: {}, unspentsAndTransactions: { MainAccountID: { - utxosMap: { }, - transactions: { regtest: { }, liquid: { } } - } + utxosMap: {}, + transactions: { regtest: {}, liquid: {} }, + }, }, passwordHash: '', deepRestorer: { @@ -32,21 +34,44 @@ export const walletInitState: WalletState = { isVerified: false, }; -const addUnspent = (state: WalletState) => (accountID: AccountID, utxo: UtxoInterface): WalletState => { - return { - ...state, - unspentsAndTransactions: { - ...state.unspentsAndTransactions, - [accountID]: { - ...state.unspentsAndTransactions[accountID], - utxosMap: { - ...state.unspentsAndTransactions[accountID].utxosMap, - [toStringOutpoint(utxo)]: utxo, - } - } - } - } -} +const addUnspent = + (state: WalletState) => + (accountID: AccountID, utxo: UtxoInterface): WalletState => { + return { + ...state, + unspentsAndTransactions: { + ...state.unspentsAndTransactions, + [accountID]: { + ...state.unspentsAndTransactions[accountID], + utxosMap: { + ...state.unspentsAndTransactions[accountID].utxosMap, + [toStringOutpoint(utxo)]: utxo, + }, + }, + }, + }; + }; + +const addTx = + (state: WalletState) => + (accountID: AccountID, tx: TxDisplayInterface, network: Network): WalletState => { + return { + ...state, + unspentsAndTransactions: { + ...state.unspentsAndTransactions, + [accountID]: { + ...state.unspentsAndTransactions[accountID], + transactions: { + ...state.unspentsAndTransactions[accountID].transactions, + [network]: { + ...state.unspentsAndTransactions[accountID].transactions[network], + [tx.txId]: tx, + }, + }, + }, + }, + }; + }; export function walletReducer( state: WalletState = walletInitState, @@ -61,7 +86,14 @@ export function walletReducer( return { ...state, passwordHash: payload.passwordHash, - mainAccount: { ...payload }, + mainAccount: { accountID: MainAccountID, ...payload }, + unspentsAndTransactions: { + ...state.unspentsAndTransactions, + [MainAccountID]: { + utxosMap: {}, + transactions: { regtest: {}, liquid: {} }, + }, + }, }; } @@ -108,11 +140,15 @@ export function walletReducer( [payload.accountID]: { ...state.unspentsAndTransactions[payload.accountID], utxosMap, - } - } + }, + }, }; } + case ACTION_TYPES.ADD_TX: { + return addTx(state)(payload.accountID, payload.tx, payload.network); + } + case ACTION_TYPES.SET_DEEP_RESTORER_GAP_LIMIT: { return { ...state, @@ -142,9 +178,9 @@ export function walletReducer( [payload.accountID]: { ...state.unspentsAndTransactions[payload.accountID], utxosMap: {}, - } - } - } + }, + }, + }; } case ACTION_TYPES.SET_VERIFIED: { diff --git a/src/application/redux/selectors/balance.selector.ts b/src/application/redux/selectors/balance.selector.ts index c095de8e..3d2b2dd0 100644 --- a/src/application/redux/selectors/balance.selector.ts +++ b/src/application/redux/selectors/balance.selector.ts @@ -9,33 +9,35 @@ export type BalancesByAsset = { [assetHash: string]: number }; * @param onSuccess * @param onError */ -export const selectBalances = (accountID: AccountID) => (state: RootReducerState): BalancesByAsset => { - const utxos = selectUtxos(accountID)(state); - const balancesFromUtxos = utxos.reduce((acc, curr) => { - if (!curr.asset || !curr.value) { - return acc; - } - return { ...acc, [curr.asset]: curr.value + (curr.asset in acc ? acc[curr.asset] : 0) }; - }, {} as BalancesByAsset); +export const selectBalances = + (accountID: AccountID) => + (state: RootReducerState): BalancesByAsset => { + const utxos = selectUtxos(accountID)(state); + const balancesFromUtxos = utxos.reduce((acc, curr) => { + if (!curr.asset || !curr.value) { + return acc; + } + return { ...acc, [curr.asset]: curr.value + (curr.asset in acc ? acc[curr.asset] : 0) }; + }, {} as BalancesByAsset); - const txs = selectTransactions(accountID)(state); - const assets = Object.keys(balancesFromUtxos); + const txs = selectTransactions(accountID)(state); + const assets = Object.keys(balancesFromUtxos); - for (const tx of txs) { - const allTxAssets = tx.transfers.map((t) => t.asset); - for (const a of allTxAssets) { - if (!assets.includes(a)) { - balancesFromUtxos[a] = 0; - assets.push(a); + for (const tx of txs) { + const allTxAssets = tx.transfers.map((t) => t.asset); + for (const a of allTxAssets) { + if (!assets.includes(a)) { + balancesFromUtxos[a] = 0; + assets.push(a); + } } } - } - const lbtcAssetHash = lbtcAssetByNetwork(state.app.network); + const lbtcAssetHash = lbtcAssetByNetwork(state.app.network); - if (balancesFromUtxos[lbtcAssetHash] === undefined) { - balancesFromUtxos[lbtcAssetHash] = 0; - } + if (balancesFromUtxos[lbtcAssetHash] === undefined) { + balancesFromUtxos[lbtcAssetHash] = 0; + } - return balancesFromUtxos; -} + return balancesFromUtxos; + }; diff --git a/src/application/redux/selectors/wallet.selector.ts b/src/application/redux/selectors/wallet.selector.ts index f9fda513..c5782e2b 100644 --- a/src/application/redux/selectors/wallet.selector.ts +++ b/src/application/redux/selectors/wallet.selector.ts @@ -1,5 +1,12 @@ import { MasterPublicKey, UtxoInterface, XPub } from 'ldk'; -import { AccountID, createMnemonicAccount, createMultisigAccount, MainAccount, MultisigAccount, MultisigAccountData } from '../../../domain/account'; +import { + AccountID, + createMnemonicAccount, + createMultisigAccount, + MainAccount, + MultisigAccount, + MultisigAccountData, +} from '../../../domain/account'; import { RootReducerState } from '../../../domain/common'; import { TxDisplayInterface } from '../../../domain/transaction'; import { CosignerExtraData } from '../../../domain/wallet'; @@ -8,13 +15,19 @@ export function masterPubKeySelector(state: RootReducerState): Promise (state: RootReducerState): UtxoInterface[] => { - return Object.values(state.wallet.unspentsAndTransactions[accountID].utxosMap || {}); -} +export const selectUtxos = + (accountID: AccountID) => + (state: RootReducerState): UtxoInterface[] => { + return Object.values(selectUnspentsAndTransactions(accountID)(state).utxosMap); + }; -export const selectTransactions = (accountID: AccountID) => (state: RootReducerState): TxDisplayInterface[] => { - return Object.values(state.wallet.unspentsAndTransactions[accountID].transactions[state.app.network] || {}); -} +export const selectTransactions = + (accountID: AccountID) => + (state: RootReducerState): TxDisplayInterface[] => { + return Object.values( + selectUnspentsAndTransactions(accountID)(state).transactions[state.app.network] + ); + }; export function hasMnemonicSelector(state: RootReducerState): boolean { return ( @@ -27,10 +40,23 @@ export function selectMainAccount(state: RootReducerState): MainAccount { return createMnemonicAccount(state.wallet.mainAccount, state.app.network); } -export const selectRestrictedAssetAccount = (cosignerXPub: XPub) => function (state: RootReducerState): MultisigAccount { - return createMultisigAccount(state.wallet.mainAccount.encryptedMnemonic, state.wallet.restrictedAssetAccounts[cosignerXPub], state.app.network); -} +export const selectRestrictedAssetAccount = (cosignerXPub: XPub) => + function (state: RootReducerState): MultisigAccount { + return createMultisigAccount( + state.wallet.mainAccount.encryptedMnemonic, + state.wallet.restrictedAssetAccounts[cosignerXPub], + state.app.network + ); + }; -export function selectAllRestrictedAssetAccounts(state: RootReducerState): MultisigAccountData[] { +export function selectAllRestrictedAssetAccounts( + state: RootReducerState +): MultisigAccountData[] { return Object.values(state.wallet.restrictedAssetAccounts); -} \ No newline at end of file +} + +export const selectUnspentsAndTransactions = (accountID: AccountID) => (state: RootReducerState) => + state.wallet.unspentsAndTransactions[accountID] ?? { + utxosMap: {}, + transactions: { regtest: {}, liquid: {} }, + }; diff --git a/src/application/utils/restorer.ts b/src/application/utils/restorer.ts index 3756f9a0..bc68b722 100644 --- a/src/application/utils/restorer.ts +++ b/src/application/utils/restorer.ts @@ -79,7 +79,6 @@ export function restoredMasterPublicKey( return masterPubKeyRestorerFromState(xpub)(restorerOpts); } - export function restoredMultisig( signer: HDSignerMultisig, cosigners: CosignerMultisig[], @@ -92,8 +91,8 @@ export function restoredMultisig( opts: { requiredSignatures, signer, - cosigners - } + cosigners, + }, }); // return restoreFrom(multisigID); @@ -111,8 +110,8 @@ export function restoredWatchOnlyMultisig( type: IdentityType.MultisigWatchOnly, opts: { requiredSignatures, - cosigners: cosigners.concat([signerXPub]) - } + cosigners: cosigners.concat([signerXPub]), + }, }); // return restoreFromState(multisigID as IdentityInterface) diff --git a/src/content/marinaBroker.ts b/src/content/marinaBroker.ts index aa78708f..622a0df7 100644 --- a/src/content/marinaBroker.ts +++ b/src/content/marinaBroker.ts @@ -18,7 +18,11 @@ import { setTx, setTxData, } from '../application/redux/actions/connect'; -import { selectMainAccount, selectTransactions, selectUtxos } from '../application/redux/selectors/wallet.selector'; +import { + selectMainAccount, + selectTransactions, + selectUtxos, +} from '../application/redux/selectors/wallet.selector'; import { incrementAddressIndex, incrementChangeAddressIndex, diff --git a/src/content/store-cache.ts b/src/content/store-cache.ts index 074568b9..7af695c8 100644 --- a/src/content/store-cache.ts +++ b/src/content/store-cache.ts @@ -50,7 +50,8 @@ export function compareCacheForEvents( export function newCacheFromState(state: RootReducerState): StoreCache { return { utxoState: state.wallet.unspentsAndTransactions[MainAccountID].utxosMap, - txsHistoryState: state.wallet.unspentsAndTransactions[MainAccountID].transactions[state.app.network], + txsHistoryState: + state.wallet.unspentsAndTransactions[MainAccountID].transactions[state.app.network], enabledWebsitesState: state.connect.enabledSites, network: state.app.network, }; diff --git a/src/domain/account.ts b/src/domain/account.ts index 1fc27e69..0586be0c 100644 --- a/src/domain/account.ts +++ b/src/domain/account.ts @@ -1,6 +1,22 @@ -import { DEFAULT_BASE_DERIVATION_PATH, HDSignerMultisig, IdentityInterface, IdentityType, MasterPublicKey, Mnemonic, Multisig, MultisigWatchOnly, StateRestorerOpts, XPub } from 'ldk'; +import { + DEFAULT_BASE_DERIVATION_PATH, + HDSignerMultisig, + IdentityInterface, + IdentityType, + MasterPublicKey, + Mnemonic, + Multisig, + MultisigWatchOnly, + StateRestorerOpts, + XPub, +} from 'ldk'; import { decrypt } from '../application/utils'; -import { restoredMasterPublicKey, restoredMnemonic, restoredMultisig, restoredWatchOnlyMultisig } from '../application/utils/restorer'; +import { + restoredMasterPublicKey, + restoredMnemonic, + restoredMultisig, + restoredWatchOnlyMultisig, +} from '../application/utils/restorer'; import { EncryptedMnemonic } from './encrypted-mnemonic'; import { MasterBlindingKey } from './master-blinding-key'; import { MasterXPub } from './master-extended-pub'; @@ -11,13 +27,16 @@ export const MainAccountID: AccountID = 'main'; /** * Account domain represents the keys of the User - * + * * - each Account is a derived of master private key (computed from mnemonic). * - an Account returns two types of identities: a WatchOnly identity and a signing Identity. * the watch-only identity is used to update utxos and transactions state * the signing identity is used to sign inputs. it needs the user's password to decrypt the mnemonic. */ -export interface Account { +export interface Account< + SignID extends IdentityInterface = IdentityInterface, + WatchID extends IdentityInterface = IdentityInterface +> { accountID: AccountID; getSigningIdentity(password: string): Promise; getWatchIdentity(): Promise; @@ -50,37 +69,61 @@ export function createMnemonicAccount(data: MnemonicAccountData, network: Networ export type MultisigAccount = Account; export interface MultisigAccountData { - baseDerivationPath: string; // we'll use the MainAccount in order to generate + baseDerivationPath: string; // we'll use the MainAccount in order to generate signerXPub: XPub; cosignerXPubs: XPub[]; requiredSignature: number; extraData: ExtraDataT; } -export function create2of2MultisigAccountData(signer: HDSignerMultisig, cosignerXPub: XPub, network: Network, extraData: T): MultisigAccountData { +export function create2of2MultisigAccountData( + signer: HDSignerMultisig, + cosignerXPub: XPub, + network: Network, + extraData: T +): MultisigAccountData { const multisigID = new Multisig({ chain: network, type: IdentityType.Multisig, opts: { requiredSignatures: 2, signer, - cosigners: [cosignerXPub] - } - }) - + cosigners: [cosignerXPub], + }, + }); + return { baseDerivationPath: signer.baseDerivationPath || DEFAULT_BASE_DERIVATION_PATH, signerXPub: multisigID.getXPub(), cosignerXPubs: [cosignerXPub], requiredSignature: 2, - extraData - } + extraData, + }; } -export function createMultisigAccount(encryptedMnemonic: EncryptedMnemonic, data: MultisigAccountData, network: Network): MultisigAccount { +export function createMultisigAccount( + encryptedMnemonic: EncryptedMnemonic, + data: MultisigAccountData, + network: Network +): MultisigAccount { return { accountID: data.signerXPub, - getSigningIdentity: (password: string) => restoredMultisig({ mnemonic: decrypt(encryptedMnemonic, password), baseDerivationPath: data.baseDerivationPath }, data.cosignerXPubs, data.requiredSignature, network), - getWatchIdentity: () => restoredWatchOnlyMultisig(data.signerXPub, data.cosignerXPubs, data.requiredSignature, network) - } -} \ No newline at end of file + getSigningIdentity: (password: string) => + restoredMultisig( + { + mnemonic: decrypt(encryptedMnemonic, password), + baseDerivationPath: data.baseDerivationPath, + }, + data.cosignerXPubs, + data.requiredSignature, + network + ), + getWatchIdentity: () => + restoredWatchOnlyMultisig( + data.signerXPub, + data.cosignerXPubs, + data.requiredSignature, + network + ), + }; +} diff --git a/src/domain/transaction.ts b/src/domain/transaction.ts index 2ef08e64..39c3b98d 100644 --- a/src/domain/transaction.ts +++ b/src/domain/transaction.ts @@ -6,7 +6,7 @@ import { Network } from './network'; export interface UtxosAndTxsHistory { utxosMap: Record; transactions: TxsHistoryByNetwork; -} +} export type TxsHistory = Record; diff --git a/src/domain/wallet.ts b/src/domain/wallet.ts index efa1c164..041b8ec9 100644 --- a/src/domain/wallet.ts +++ b/src/domain/wallet.ts @@ -5,7 +5,7 @@ import { UtxosAndTxsHistory } from './transaction'; export interface WalletState { mainAccount: MnemonicAccountData; - restrictedAssetAccounts: Record>, + restrictedAssetAccounts: Record>; unspentsAndTransactions: Record; passwordHash: PasswordHash; deepRestorer: { @@ -18,4 +18,4 @@ export interface WalletState { export interface CosignerExtraData { cosignerURL: string; -} \ No newline at end of file +} diff --git a/src/presentation/connect/spend.tsx b/src/presentation/connect/spend.tsx index 621cf69a..ce66f434 100644 --- a/src/presentation/connect/spend.tsx +++ b/src/presentation/connect/spend.tsx @@ -17,10 +17,7 @@ import { Network } from '../../domain/network'; import { ConnectData } from '../../domain/connect'; import { blindAndSignPset, createSendPset } from '../../application/utils/transaction'; import { incrementChangeAddressIndex } from '../../application/redux/actions/wallet'; -import { - selectMainAccount, - selectUtxos, -} from '../../application/redux/selectors/wallet.selector'; +import { selectMainAccount, selectUtxos } from '../../application/redux/selectors/wallet.selector'; import PopupWindowProxy from './popupWindowProxy'; import { MainAccountID } from '../../domain/account'; diff --git a/src/presentation/cosigner/pair.tsx b/src/presentation/cosigner/pair.tsx index 34621318..bdf8c967 100644 --- a/src/presentation/cosigner/pair.tsx +++ b/src/presentation/cosigner/pair.tsx @@ -31,7 +31,7 @@ const optInForm = (props: FormikProps) => { /> {touchedAndError('cosignerURL') &&
{errors.cosignerURL}
} - ) => { const OptInFormikForm = withFormik({ validationSchema: Yup.object().shape({ cosignerURL: Yup.string().required('Please input cosignerURL').url('Not a valid URL'), - password: Yup.string().required() + password: Yup.string().required(), }), handleSubmit: async (values, { props }) => { @@ -61,7 +61,7 @@ const OptInFormikForm = withFormik({ const PairCosigner: React.FC = () => { const onSubmit = (values: OptInFormValues) => { // const multisigAccountData = create2of2MultisigAccountData( - + // ) console.log(values); diff --git a/src/presentation/settings/cosigners.tsx b/src/presentation/settings/cosigners.tsx index f6eaa7bc..ddf21061 100644 --- a/src/presentation/settings/cosigners.tsx +++ b/src/presentation/settings/cosigners.tsx @@ -22,16 +22,14 @@ const SettingsCosigners: React.FC = ({ multisigAccountsD currentPage="Change currency" >

Cosigners

-
+
    - {multisigAccountsData.map(({ extraData }) => -
  • - { extraData.cosignerURL } -
  • - )} + {multisigAccountsData.map(({ extraData }) => ( +
  • {extraData.cosignerURL}
  • + ))}
-
- +
+
Add cosigner diff --git a/src/presentation/settings/networks.tsx b/src/presentation/settings/networks.tsx index fd7fadef..46168ae3 100644 --- a/src/presentation/settings/networks.tsx +++ b/src/presentation/settings/networks.tsx @@ -20,7 +20,11 @@ export interface SettingsNetworksProps { error?: string; } -const SettingsNetworksView: React.FC = ({ restorationLoading, accountsIDs, error }) => { +const SettingsNetworksView: React.FC = ({ + restorationLoading, + accountsIDs, + error, +}) => { const [isLoading, setIsLoading] = useState(false); const network = useSelector((state: RootReducerState) => state.app.network); diff --git a/src/presentation/wallet/send/choose-fee.tsx b/src/presentation/wallet/send/choose-fee.tsx index ff67970c..a7ef27b5 100644 --- a/src/presentation/wallet/send/choose-fee.tsx +++ b/src/presentation/wallet/send/choose-fee.tsx @@ -57,7 +57,7 @@ const ChooseFeeView: React.FC = ({ taxiAssets, lbtcAssetHash, mainAccount, - mainAccountUtxos + mainAccountUtxos, }) => { const history = useHistory(); const dispatch = useDispatch(); diff --git a/src/presentation/wallet/send/end-of-flow.tsx b/src/presentation/wallet/send/end-of-flow.tsx index 9b306516..89a34349 100644 --- a/src/presentation/wallet/send/end-of-flow.tsx +++ b/src/presentation/wallet/send/end-of-flow.tsx @@ -3,10 +3,7 @@ import { useHistory } from 'react-router'; import Button from '../../components/button'; import ModalUnlock from '../../components/modal-unlock'; import ShellPopUp from '../../components/shell-popup'; -import { - blindAndSignPset, - broadcastTx, -} from '../../../application/utils'; +import { blindAndSignPset, broadcastTx } from '../../../application/utils'; import { SEND_PAYMENT_ERROR_ROUTE, SEND_PAYMENT_SUCCESS_ROUTE } from '../../routes/constants'; import { debounce } from 'lodash'; import { createPassword } from '../../../domain/password'; From 4b0d1f979d205c841107eeaa70d0d9edec28e280 Mon Sep 17 00:00:00 2001 From: louisinger Date: Tue, 19 Oct 2021 20:57:15 +0200 Subject: [PATCH 13/52] ADD_RESTRICTED_ACCOUNT_DATA action --- src/application/redux/actions/action-types.ts | 1 + src/application/redux/actions/wallet.ts | 10 ++++++++++ src/application/redux/reducers/wallet-reducer.ts | 15 +++++++++++++-- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/application/redux/actions/action-types.ts b/src/application/redux/actions/action-types.ts index 62f86954..0abccc61 100644 --- a/src/application/redux/actions/action-types.ts +++ b/src/application/redux/actions/action-types.ts @@ -1,5 +1,6 @@ // Wallet export const WALLET_SET_DATA = 'WALLET_SET_DATA'; +export const WALLET_ADD_RESTRICTED_ASSET_ACCOUNT = 'WALLET_ADD_RESTRICTED_ASSET_ACCOUNT'; export const ADD_UTXO = 'ADD_UTXO'; export const DELETE_UTXO = 'DELETE_UTXO'; export const FLUSH_UTXOS = 'FLUSH_UTXOS'; diff --git a/src/application/redux/actions/wallet.ts b/src/application/redux/actions/wallet.ts index d0677fa1..0b4bd035 100644 --- a/src/application/redux/actions/wallet.ts +++ b/src/application/redux/actions/wallet.ts @@ -7,10 +7,20 @@ import { NEW_ADDRESS_SUCCESS, NEW_CHANGE_ADDRESS_SUCCESS, SET_VERIFIED, + WALLET_ADD_RESTRICTED_ASSET_ACCOUNT, } from './action-types'; import { AnyAction } from 'redux'; import { WalletData } from '../../utils/wallet'; import { extractErrorMessage } from '../../../presentation/utils/error'; +import { MultisigAccountData } from '../../../domain/account'; +import { CosignerExtraData } from '../../../domain/wallet'; + +export function addRestrictedAssetData(multisigAccountData: MultisigAccountData) { + return { + type: WALLET_ADD_RESTRICTED_ASSET_ACCOUNT, + payload: { multisigAccountData } + } +} export function setWalletData(walletData: WalletData): AnyAction { return { diff --git a/src/application/redux/reducers/wallet-reducer.ts b/src/application/redux/reducers/wallet-reducer.ts index bf68fc24..f54593f5 100644 --- a/src/application/redux/reducers/wallet-reducer.ts +++ b/src/application/redux/reducers/wallet-reducer.ts @@ -1,10 +1,10 @@ /* eslint-disable @typescript-eslint/restrict-plus-operands */ import { toStringOutpoint } from './../../utils/utxos'; import * as ACTION_TYPES from '../actions/action-types'; -import { WalletState } from '../../../domain/wallet'; +import { CosignerExtraData, WalletState } from '../../../domain/wallet'; import { AnyAction } from 'redux'; import { UtxoInterface } from 'ldk'; -import { AccountID, MainAccountID } from '../../../domain/account'; +import { AccountID, MainAccountID, MultisigAccountData } from '../../../domain/account'; import { TxDisplayInterface } from '../../../domain/transaction'; import { Network } from '../../../domain/network'; @@ -96,6 +96,17 @@ export function walletReducer( }, }; } + + case ACTION_TYPES.WALLET_ADD_RESTRICTED_ASSET_ACCOUNT: { + const data = payload.multisigAccountData as MultisigAccountData; + return { + ...state, + restrictedAssetAccounts: { + ...state.restrictedAssetAccounts, + [data.cosignerXPubs[0]]: data + } + } + } case ACTION_TYPES.NEW_CHANGE_ADDRESS_SUCCESS: { return { From f8392d4c2f234b1572f7253b735a09d2e09ece48 Mon Sep 17 00:00:00 2001 From: louisinger Date: Wed, 20 Oct 2021 11:14:49 +0200 Subject: [PATCH 14/52] UI cleaning --- src/application/redux/actions/wallet.ts | 8 +- src/application/redux/backend.ts | 1 - .../redux/containers/cosigners.container.ts | 14 ++++ .../redux/containers/pair.container.ts | 14 ++++ .../redux/reducers/wallet-reducer.ts | 8 +- .../redux/selectors/wallet.selector.ts | 1 + src/application/utils/restorer.ts | 1 - src/domain/cosigner.ts | 73 +++++++++++++++++++ src/presentation/cosigner/pair.tsx | 72 +++++++++++++++--- src/presentation/routes/index.tsx | 6 +- src/presentation/settings/cosigners.tsx | 26 ++++--- 11 files changed, 193 insertions(+), 31 deletions(-) create mode 100644 src/application/redux/containers/cosigners.container.ts create mode 100644 src/application/redux/containers/pair.container.ts create mode 100644 src/domain/cosigner.ts diff --git a/src/application/redux/actions/wallet.ts b/src/application/redux/actions/wallet.ts index 0b4bd035..374e2084 100644 --- a/src/application/redux/actions/wallet.ts +++ b/src/application/redux/actions/wallet.ts @@ -15,11 +15,13 @@ import { extractErrorMessage } from '../../../presentation/utils/error'; import { MultisigAccountData } from '../../../domain/account'; import { CosignerExtraData } from '../../../domain/wallet'; -export function addRestrictedAssetData(multisigAccountData: MultisigAccountData) { +export function addRestrictedAssetData( + multisigAccountData: MultisigAccountData +) { return { type: WALLET_ADD_RESTRICTED_ASSET_ACCOUNT, - payload: { multisigAccountData } - } + payload: { multisigAccountData }, + }; } export function setWalletData(walletData: WalletData): AnyAction { diff --git a/src/application/redux/backend.ts b/src/application/redux/backend.ts index 1ec85ef7..e99e79fb 100644 --- a/src/application/redux/backend.ts +++ b/src/application/redux/backend.ts @@ -45,7 +45,6 @@ import { Account } from '../../domain/account'; import { selectMainAccount, selectUnspentsAndTransactions, - selectUtxos, } from './selectors/wallet.selector'; const UPDATE_ALARM = 'UPDATE_ALARM'; diff --git a/src/application/redux/containers/cosigners.container.ts b/src/application/redux/containers/cosigners.container.ts new file mode 100644 index 00000000..37244cc5 --- /dev/null +++ b/src/application/redux/containers/cosigners.container.ts @@ -0,0 +1,14 @@ +import { connect } from 'react-redux'; +import { RootReducerState } from '../../../domain/common'; +import SettingsCosignersView, { + SettingsCosignersProps, +} from '../../../presentation/settings/cosigners'; +import { selectAllRestrictedAssetAccounts } from '../selectors/wallet.selector'; + +const SettingsCosigner = connect( + (state: RootReducerState): SettingsCosignersProps => ({ + multisigAccountsData: selectAllRestrictedAssetAccounts(state), + }) +)(SettingsCosignersView); + +export default SettingsCosigner; diff --git a/src/application/redux/containers/pair.container.ts b/src/application/redux/containers/pair.container.ts new file mode 100644 index 00000000..f2d5d289 --- /dev/null +++ b/src/application/redux/containers/pair.container.ts @@ -0,0 +1,14 @@ +import { connect } from 'react-redux'; +import { RootReducerState } from '../../../domain/common'; +import PairCosignerView, { PairCosignerProps } from '../../../presentation/cosigner/pair'; +import { getExplorerURLSelector } from '../selectors/app.selector'; + +const mapStateToProps = (state: RootReducerState): PairCosignerProps => ({ + encryptedMnemonic: state.wallet.mainAccount.encryptedMnemonic, + explorerURL: getExplorerURLSelector(state), + network: state.app.network, +}); + +const PairCosigner = connect(mapStateToProps)(PairCosignerView); + +export default PairCosigner; diff --git a/src/application/redux/reducers/wallet-reducer.ts b/src/application/redux/reducers/wallet-reducer.ts index f54593f5..e5b018f4 100644 --- a/src/application/redux/reducers/wallet-reducer.ts +++ b/src/application/redux/reducers/wallet-reducer.ts @@ -96,16 +96,16 @@ export function walletReducer( }, }; } - + case ACTION_TYPES.WALLET_ADD_RESTRICTED_ASSET_ACCOUNT: { const data = payload.multisigAccountData as MultisigAccountData; return { ...state, restrictedAssetAccounts: { ...state.restrictedAssetAccounts, - [data.cosignerXPubs[0]]: data - } - } + [data.cosignerXPubs[0]]: data, + }, + }; } case ACTION_TYPES.NEW_CHANGE_ADDRESS_SUCCESS: { diff --git a/src/application/redux/selectors/wallet.selector.ts b/src/application/redux/selectors/wallet.selector.ts index c5782e2b..4dc3f17a 100644 --- a/src/application/redux/selectors/wallet.selector.ts +++ b/src/application/redux/selectors/wallet.selector.ts @@ -52,6 +52,7 @@ export const selectRestrictedAssetAccount = (cosignerXPub: XPub) => export function selectAllRestrictedAssetAccounts( state: RootReducerState ): MultisigAccountData[] { + console.log(state.wallet.restrictedAssetAccounts); return Object.values(state.wallet.restrictedAssetAccounts); } diff --git a/src/application/utils/restorer.ts b/src/application/utils/restorer.ts index bc68b722..5ef7aba7 100644 --- a/src/application/utils/restorer.ts +++ b/src/application/utils/restorer.ts @@ -7,7 +7,6 @@ import { masterPubKeyRestorerFromState, Multisig, CosignerMultisig, - IdentityInterface, MultisigWatchOnly, XPub, HDSignerMultisig, diff --git a/src/domain/cosigner.ts b/src/domain/cosigner.ts new file mode 100644 index 00000000..08444598 --- /dev/null +++ b/src/domain/cosigner.ts @@ -0,0 +1,73 @@ +import { fromPublicKey, fromSeed } from 'bip32'; +import { mnemonicToSeedSync } from 'bip39'; +import { + DEFAULT_BASE_DERIVATION_PATH, + HDSignerMultisig, + IdentityType, + Mnemonic, + Multisig, + multisigFromEsplora, + toXpub, + XPub, +} from 'ldk'; +import { networkFromString } from '../application/utils'; +import { Network } from './network'; + +export interface Cosigner { + requestXPub(signerXPub: XPub): Promise; + signPset(pset: string, signWith: XPub): Promise; +} + +export function HDSignerToXPub(signer: HDSignerMultisig, network: Network) { + const walletSeed = mnemonicToSeedSync(signer.mnemonic); + const net = networkFromString(network); + const baseNode = fromSeed(walletSeed, net).derivePath( + signer.baseDerivationPath || DEFAULT_BASE_DERIVATION_PATH + ); + return toXpub(fromPublicKey(baseNode.publicKey, baseNode.chainCode, net).toBase58()); +} + +export class MockedCosigner implements Cosigner { + private mnemonic: Mnemonic; + private cosignerXPub: XPub | undefined; + private network: Network; + private esploraURL: string; + + constructor(network: Network, esploraURL: string) { + this.mnemonic = Mnemonic.Random(network, DEFAULT_BASE_DERIVATION_PATH); + this.network = network; + this.esploraURL = esploraURL; + } + + requestXPub(singerXPub: XPub) { + this.cosignerXPub = singerXPub; + return Promise.resolve(this.mnemonic.getXPub()); + } + + async signPset(pset: string, signWith: XPub) { + if (this.cosignerXPub === undefined) { + throw new Error('pairing is not done'); + } + + if (signWith !== this.mnemonic.getXPub()) { + throw new Error(`can only sign with ${this.mnemonic.getXPub()}`); + } + + const multisigID = await multisigFromEsplora( + new Multisig({ + chain: this.network, + type: IdentityType.Multisig, + opts: { + requiredSignatures: 2, + cosigners: [this.cosignerXPub], + signer: { + mnemonic: this.mnemonic.mnemonic, + baseDerivationPath: DEFAULT_BASE_DERIVATION_PATH, + }, + }, + }) + )({ esploraURL: this.esploraURL, gapLimit: 20 }); + + return multisigID.signPset(pset); + } +} diff --git a/src/presentation/cosigner/pair.tsx b/src/presentation/cosigner/pair.tsx index bdf8c967..b0cfad7e 100644 --- a/src/presentation/cosigner/pair.tsx +++ b/src/presentation/cosigner/pair.tsx @@ -5,6 +5,14 @@ import * as Yup from 'yup'; import Button from '../components/button'; import { create2of2MultisigAccountData } from '../../domain/account'; import { CosignerExtraData } from '../../domain/wallet'; +import { decrypt } from '../../application/utils'; +import { EncryptedMnemonic } from '../../domain/encrypted-mnemonic'; +import { Cosigner, HDSignerToXPub, MockedCosigner } from '../../domain/cosigner'; +import { Network } from '../../domain/network'; +import { useDispatch } from 'react-redux'; +import { ProxyStoreDispatch } from '../../application/redux/proxyStore'; +import { addRestrictedAssetData } from '../../application/redux/actions/wallet'; +import { DEFAULT_BASE_DERIVATION_PATH } from 'ldk'; interface OptInFormProps { onSubmit: (values: OptInFormValues) => Promise; @@ -13,6 +21,7 @@ interface OptInFormProps { interface OptInFormValues { cosignerURL: string; password: string; + derivationPath: string; } const optInForm = (props: FormikProps) => { @@ -26,18 +35,28 @@ const optInForm = (props: FormikProps) => { {touchedAndError('cosignerURL') &&
{errors.cosignerURL}
} +

Password

{touchedAndError('password') &&
{errors.password}
} +

Derivation path

+ + + {touchedAndError('derivationPath') &&
{errors.derivationPath}
} @@ -47,8 +66,17 @@ const optInForm = (props: FormikProps) => { const OptInFormikForm = withFormik({ validationSchema: Yup.object().shape({ - cosignerURL: Yup.string().required('Please input cosignerURL').url('Not a valid URL'), + cosignerURL: Yup.string().required().url('invalid URL'), password: Yup.string().required(), + derivationPath: Yup.string() + .required() + .matches(/^(m\/)?(\d+'?\/)*\d+'?$/, () => 'invalid BIP32 derivation path'), + }), + + mapPropsToValues: () => ({ + cosignerURL: '', + derivationPath: DEFAULT_BASE_DERIVATION_PATH, + password: '', }), handleSubmit: async (values, { props }) => { @@ -58,14 +86,40 @@ const OptInFormikForm = withFormik({ displayName: 'OptInForm', })(optInForm); -const PairCosigner: React.FC = () => { - const onSubmit = (values: OptInFormValues) => { - // const multisigAccountData = create2of2MultisigAccountData( +export interface PairCosignerProps { + encryptedMnemonic: EncryptedMnemonic; + network: Network; + explorerURL: string; +} + +const PairCosignerView: React.FC = ({ + encryptedMnemonic, + network, + explorerURL, +}) => { + const dispatch = useDispatch(); + + const onSubmit = async (values: OptInFormValues) => { + const walletSignerData = { + mnemonic: decrypt(encryptedMnemonic, values.password), + baseDerivationPath: values.derivationPath, + }; + const walletXPub = HDSignerToXPub(walletSignerData, network); + + // cosigner should be created from values.cosignerURL + const cosigner: Cosigner = new MockedCosigner(network, explorerURL); + const requestedXPub = await cosigner.requestXPub(walletXPub); + + const multisigAccountData = create2of2MultisigAccountData( + walletSignerData, + requestedXPub, + network, + { cosignerURL: values.cosignerURL } + ); - // ) + await dispatch(addRestrictedAssetData(multisigAccountData)); - console.log(values); - return Promise.resolve(); + console.log(multisigAccountData); }; return ( @@ -76,4 +130,4 @@ const PairCosigner: React.FC = () => { ); }; -export default PairCosigner; +export default PairCosignerView; diff --git a/src/presentation/routes/index.tsx b/src/presentation/routes/index.tsx index 38156356..e26d48b2 100644 --- a/src/presentation/routes/index.tsx +++ b/src/presentation/routes/index.tsx @@ -81,8 +81,8 @@ import SettingsExplorer from '../settings/explorer'; import SettingsNetworks from '../../application/redux/containers/settings-networks.container'; import SettingsCredits from '../settings/credits'; import SettingsTerms from '../settings/terms'; -import PairCosigner from '../cosigner/pair'; -import SettingsCosigners from '../settings/cosigners'; +import PairCosigner from '../../application/redux/containers/pair.container'; +import SettingsCosigner from '../../application/redux/containers/cosigners.container'; const Routes: React.FC = () => { return ( @@ -119,7 +119,7 @@ const Routes: React.FC = () => { - + {/*Login*/} diff --git a/src/presentation/settings/cosigners.tsx b/src/presentation/settings/cosigners.tsx index ddf21061..2716199a 100644 --- a/src/presentation/settings/cosigners.tsx +++ b/src/presentation/settings/cosigners.tsx @@ -4,12 +4,14 @@ import browser from 'webextension-polyfill'; import { PAIR_COSIGNER_ROUTE } from '../routes/constants'; import { MultisigAccountData } from '../../domain/account'; import { CosignerExtraData } from '../../domain/wallet'; +import ButtonList from '../components/button-list'; +import Button from '../components/button'; export interface SettingsCosignersProps { multisigAccountsData: MultisigAccountData[]; } -const SettingsCosigners: React.FC = ({ multisigAccountsData }) => { +const SettingsCosignersView: React.FC = ({ multisigAccountsData }) => { const openAddCosignerTab = async () => { const url = browser.runtime.getURL(`home.html#${PAIR_COSIGNER_ROUTE}`); await browser.tabs.create({ url }); @@ -21,22 +23,26 @@ const SettingsCosigners: React.FC = ({ multisigAccountsD className="h-popupContent container pb-20 mx-auto text-center bg-bottom bg-no-repeat" currentPage="Change currency" > -

Cosigners

-
-
    - {multisigAccountsData.map(({ extraData }) => ( -
  • {extraData.cosignerURL}
  • +
    + + {multisigAccountsData.map(({ extraData }, index) => ( +
    + + Cosigner #{index}
    +
    + {extraData.cosignerURL} +
    ))} -
+
- +
); }; -export default SettingsCosigners; +export default SettingsCosignersView; From 9ff562b98bbe72c48367914f158b91b6ddf865bf Mon Sep 17 00:00:00 2001 From: louisinger Date: Wed, 20 Oct 2021 11:58:22 +0200 Subject: [PATCH 15/52] use restored from state for Multisig accounts --- package.json | 2 +- src/application/redux/actions/action-types.ts | 4 +- src/application/redux/actions/wallet.ts | 14 ++-- src/application/redux/backend.ts | 5 +- .../containers/address-amount.container.ts | 2 +- .../redux/containers/choose-fee.container.ts | 2 +- .../redux/containers/end-of-flow.container.ts | 2 +- .../redux/containers/receive.container.ts | 2 +- .../redux/reducers/wallet-reducer.ts | 64 +++++++++++++++---- .../redux/selectors/wallet.selector.ts | 5 +- src/application/utils/restorer.ts | 13 ++-- src/application/utils/transaction.ts | 16 +++-- src/content/marinaBroker.ts | 10 +-- src/domain/account.ts | 38 ++++++++--- .../components/address-amount-form.tsx | 8 +-- src/presentation/connect/sign-pset.tsx | 1 - src/presentation/cosigner/pair.tsx | 7 +- src/presentation/settings/cosigners.tsx | 5 +- src/presentation/wallet/receive/index.tsx | 10 +-- .../wallet/send/address-amount.tsx | 8 +-- src/presentation/wallet/send/choose-fee.tsx | 10 +-- src/presentation/wallet/send/end-of-flow.tsx | 22 +++---- yarn.lock | 8 +-- 23 files changed, 164 insertions(+), 94 deletions(-) diff --git a/package.json b/package.json index 2e7b61ce..e8bf1c32 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "decimal.js": "^10.2.1", "formik": "^2.2.6", "google-protobuf": "^3.15.8", - "ldk": "^0.3.13", + "ldk": "^0.3.14", "lodash.debounce": "^4.0.8", "lottie-web": "^5.7.8", "marina-provider": "^1.4.3", diff --git a/src/application/redux/actions/action-types.ts b/src/application/redux/actions/action-types.ts index 0abccc61..cffc02d0 100644 --- a/src/application/redux/actions/action-types.ts +++ b/src/application/redux/actions/action-types.ts @@ -8,8 +8,8 @@ export const UPDATE_UTXOS = 'UPDATE_UTXOS'; export const SET_DEEP_RESTORER_GAP_LIMIT = 'SET_DEEP_RESTORER_GAP_LIMIT'; export const SET_DEEP_RESTORER_IS_LOADING = 'SET_DEEP_RESTORER_IS_LOADING'; export const SET_DEEP_RESTORER_ERROR = 'SET_DEEP_RESTORER_ERROR'; -export const NEW_ADDRESS_SUCCESS = 'NEW_ADDRESS_SUCCESS'; -export const NEW_CHANGE_ADDRESS_SUCCESS = 'NEW_CHANGE_ADDRESS_SUCCESS'; +export const INCREMENT_EXTERNAL_ADDRESS_INDEX = 'INCREMENT_EXTERNAL_ADDRESS_INDEX'; +export const INCREMENT_INTERNAL_ADDRESS_INDEX = 'INCREMENT_INTERNAL_ADDRESS_INDEX'; export const SET_VERIFIED = 'SET_VERIFIED'; export const RESET_WALLET = 'RESET_WALLET'; diff --git a/src/application/redux/actions/wallet.ts b/src/application/redux/actions/wallet.ts index 374e2084..c1ae23c1 100644 --- a/src/application/redux/actions/wallet.ts +++ b/src/application/redux/actions/wallet.ts @@ -4,15 +4,15 @@ import { SET_DEEP_RESTORER_GAP_LIMIT, SET_DEEP_RESTORER_ERROR, START_DEEP_RESTORATION, - NEW_ADDRESS_SUCCESS, - NEW_CHANGE_ADDRESS_SUCCESS, + INCREMENT_EXTERNAL_ADDRESS_INDEX, + INCREMENT_INTERNAL_ADDRESS_INDEX, SET_VERIFIED, WALLET_ADD_RESTRICTED_ASSET_ACCOUNT, } from './action-types'; import { AnyAction } from 'redux'; import { WalletData } from '../../utils/wallet'; import { extractErrorMessage } from '../../../presentation/utils/error'; -import { MultisigAccountData } from '../../../domain/account'; +import { AccountID, MultisigAccountData } from '../../../domain/account'; import { CosignerExtraData } from '../../../domain/wallet'; export function addRestrictedAssetData( @@ -31,12 +31,12 @@ export function setWalletData(walletData: WalletData): AnyAction { }; } -export function incrementAddressIndex(): AnyAction { - return { type: NEW_ADDRESS_SUCCESS }; +export function incrementAddressIndex(accountID: AccountID): AnyAction { + return { type: INCREMENT_EXTERNAL_ADDRESS_INDEX, payload: { accountID } }; } -export function incrementChangeAddressIndex(): AnyAction { - return { type: NEW_CHANGE_ADDRESS_SUCCESS }; +export function incrementChangeAddressIndex(accountID: AccountID): AnyAction { + return { type: INCREMENT_INTERNAL_ADDRESS_INDEX, payload: { accountID } }; } export function setDeepRestorerIsLoading(isLoading: boolean): AnyAction { diff --git a/src/application/redux/backend.ts b/src/application/redux/backend.ts index e99e79fb..a85a1f67 100644 --- a/src/application/redux/backend.ts +++ b/src/application/redux/backend.ts @@ -42,10 +42,7 @@ import { } from './actions/action-types'; import { flushTx } from './actions/connect'; import { Account } from '../../domain/account'; -import { - selectMainAccount, - selectUnspentsAndTransactions, -} from './selectors/wallet.selector'; +import { selectMainAccount, selectUnspentsAndTransactions } from './selectors/wallet.selector'; const UPDATE_ALARM = 'UPDATE_ALARM'; diff --git a/src/application/redux/containers/address-amount.container.ts b/src/application/redux/containers/address-amount.container.ts index 475a5595..b087d587 100644 --- a/src/application/redux/containers/address-amount.container.ts +++ b/src/application/redux/containers/address-amount.container.ts @@ -9,7 +9,7 @@ import { selectBalances } from '../selectors/balance.selector'; import { selectMainAccount } from '../selectors/wallet.selector'; const mapStateToProps = (state: RootReducerState): AddressAmountProps => ({ - mainAccount: selectMainAccount(state), + account: selectMainAccount(state), network: state.app.network, transaction: state.transaction, assets: state.assets, diff --git a/src/application/redux/containers/choose-fee.container.ts b/src/application/redux/containers/choose-fee.container.ts index fb3c0e3e..373a99b7 100644 --- a/src/application/redux/containers/choose-fee.container.ts +++ b/src/application/redux/containers/choose-fee.container.ts @@ -16,7 +16,7 @@ const mapStateToProps = (state: RootReducerState): ChooseFeeProps => ({ changeAddress: state.transaction.changeAddress, sendAsset: state.transaction.sendAsset, sendAmount: state.transaction.sendAmount, - mainAccount: selectMainAccount(state), + account: selectMainAccount(state), mainAccountUtxos: selectUtxos(MainAccountID)(state), }); diff --git a/src/application/redux/containers/end-of-flow.container.ts b/src/application/redux/containers/end-of-flow.container.ts index 00cd7212..7a6e4c2f 100644 --- a/src/application/redux/containers/end-of-flow.container.ts +++ b/src/application/redux/containers/end-of-flow.container.ts @@ -5,7 +5,7 @@ import { getExplorerURLSelector } from '../selectors/app.selector'; import { selectMainAccount } from '../selectors/wallet.selector'; const mapStateToProps = (state: RootReducerState): EndOfFlowProps => ({ - mainAccount: selectMainAccount(state), + account: selectMainAccount(state), pset: state.transaction.pset, explorerURL: getExplorerURLSelector(state), recipientAddress: state.transaction.sendAddress?.value, diff --git a/src/application/redux/containers/receive.container.ts b/src/application/redux/containers/receive.container.ts index f623ac0b..851c6033 100644 --- a/src/application/redux/containers/receive.container.ts +++ b/src/application/redux/containers/receive.container.ts @@ -4,7 +4,7 @@ import ReceiveView, { ReceiveProps } from '../../../presentation/wallet/receive' import { selectMainAccount } from '../selectors/wallet.selector'; const mapStateToProps = (state: RootReducerState): ReceiveProps => ({ - mainAccount: selectMainAccount(state), + account: selectMainAccount(state), }); const Receive = connect(mapStateToProps)(ReceiveView); diff --git a/src/application/redux/reducers/wallet-reducer.ts b/src/application/redux/reducers/wallet-reducer.ts index e5b018f4..5f71f71b 100644 --- a/src/application/redux/reducers/wallet-reducer.ts +++ b/src/application/redux/reducers/wallet-reducer.ts @@ -108,27 +108,67 @@ export function walletReducer( }; } - case ACTION_TYPES.NEW_CHANGE_ADDRESS_SUCCESS: { + case ACTION_TYPES.INCREMENT_INTERNAL_ADDRESS_INDEX: { + const accountID = payload.accountID as AccountID; + if (accountID === MainAccountID) { + return { + ...state, + mainAccount: { + ...state.mainAccount, + restorerOpts: { + ...state.mainAccount.restorerOpts, + lastUsedInternalIndex: + (state.mainAccount.restorerOpts.lastUsedInternalIndex ?? -1) + 1, + }, + }, + }; + } + return { ...state, - mainAccount: { - ...state.mainAccount, - restorerOpts: { - ...state.mainAccount.restorerOpts, - lastUsedInternalIndex: (state.mainAccount.restorerOpts.lastUsedInternalIndex ?? -1) + 1, + restrictedAssetAccounts: { + ...state.restrictedAssetAccounts, + [accountID]: { + ...state.restrictedAssetAccounts[accountID], + restorerOpts: { + ...state.restrictedAssetAccounts[accountID].restorerOpts, + lastUsedInternalIndex: + (state.restrictedAssetAccounts[accountID].restorerOpts.lastUsedInternalIndex ?? + -1) + 1, + }, }, }, }; } - case ACTION_TYPES.NEW_ADDRESS_SUCCESS: { + case ACTION_TYPES.INCREMENT_EXTERNAL_ADDRESS_INDEX: { + const accountID = payload.accountID as AccountID; + if (accountID === MainAccountID) { + return { + ...state, + mainAccount: { + ...state.mainAccount, + restorerOpts: { + ...state.mainAccount.restorerOpts, + lastUsedExternalIndex: + (state.mainAccount.restorerOpts.lastUsedExternalIndex ?? -1) + 1, + }, + }, + }; + } + return { ...state, - mainAccount: { - ...state.mainAccount, - restorerOpts: { - ...state.mainAccount.restorerOpts, - lastUsedExternalIndex: (state.mainAccount.restorerOpts.lastUsedExternalIndex ?? -1) + 1, + restrictedAssetAccounts: { + ...state.restrictedAssetAccounts, + [accountID]: { + ...state.restrictedAssetAccounts[accountID], + restorerOpts: { + ...state.restrictedAssetAccounts[accountID].restorerOpts, + lastUsedExternalIndex: + (state.restrictedAssetAccounts[accountID].restorerOpts.lastUsedExternalIndex ?? + -1) + 1, + }, }, }, }; diff --git a/src/application/redux/selectors/wallet.selector.ts b/src/application/redux/selectors/wallet.selector.ts index 4dc3f17a..d9aaf249 100644 --- a/src/application/redux/selectors/wallet.selector.ts +++ b/src/application/redux/selectors/wallet.selector.ts @@ -3,9 +3,9 @@ import { AccountID, createMnemonicAccount, createMultisigAccount, - MainAccount, MultisigAccount, MultisigAccountData, + MnemonicAccount, } from '../../../domain/account'; import { RootReducerState } from '../../../domain/common'; import { TxDisplayInterface } from '../../../domain/transaction'; @@ -36,7 +36,7 @@ export function hasMnemonicSelector(state: RootReducerState): boolean { ); } -export function selectMainAccount(state: RootReducerState): MainAccount { +export function selectMainAccount(state: RootReducerState): MnemonicAccount { return createMnemonicAccount(state.wallet.mainAccount, state.app.network); } @@ -52,7 +52,6 @@ export const selectRestrictedAssetAccount = (cosignerXPub: XPub) => export function selectAllRestrictedAssetAccounts( state: RootReducerState ): MultisigAccountData[] { - console.log(state.wallet.restrictedAssetAccounts); return Object.values(state.wallet.restrictedAssetAccounts); } diff --git a/src/application/utils/restorer.ts b/src/application/utils/restorer.ts index 5ef7aba7..e34a7ce7 100644 --- a/src/application/utils/restorer.ts +++ b/src/application/utils/restorer.ts @@ -10,6 +10,7 @@ import { MultisigWatchOnly, XPub, HDSignerMultisig, + restorerFromState, } from 'ldk'; import { Address } from '../../domain/address'; import { MasterBlindingKey } from '../../domain/master-blinding-key'; @@ -78,10 +79,13 @@ export function restoredMasterPublicKey( return masterPubKeyRestorerFromState(xpub)(restorerOpts); } +// create a Multisig Identity +// restore it using StateRestorerOpts export function restoredMultisig( signer: HDSignerMultisig, cosigners: CosignerMultisig[], requiredSignatures: number, + restorerOpts: StateRestorerOpts, network: Network ) { const multisigID = new Multisig({ @@ -94,14 +98,16 @@ export function restoredMultisig( }, }); - // return restoreFrom(multisigID); - return Promise.resolve(multisigID); + return restorerFromState(multisigID)(restorerOpts); } +// create a MultisigWatchOnly Identity +// restore it using StateRestorerOpts export function restoredWatchOnlyMultisig( signerXPub: XPub, cosigners: CosignerMultisig[], requiredSignatures: number, + restorerOpts: StateRestorerOpts, network: Network ) { const multisigID = new MultisigWatchOnly({ @@ -113,6 +119,5 @@ export function restoredWatchOnlyMultisig( }, }); - // return restoreFromState(multisigID as IdentityInterface) - return Promise.resolve(multisigID); + return restorerFromState(multisigID)(restorerOpts); } diff --git a/src/application/utils/transaction.ts b/src/application/utils/transaction.ts index 8fbb5c2f..9a56d160 100644 --- a/src/application/utils/transaction.ts +++ b/src/application/utils/transaction.ts @@ -8,9 +8,9 @@ import { decodePset, getUnblindURLFromTx, greedyCoinSelector, + IdentityInterface, InputInterface, isBlindedOutputInterface, - Mnemonic, psetToUnsignedTx, RecipientInterface, TxInterface, @@ -44,23 +44,27 @@ function outPubKeysMap(pset: string, outputAddresses: string[]): Map { - const outputAddresses = (await mnemonic.getAddresses()).map((a) => a.confidentialAddress); + const outputAddresses = (await signerIdentity.getAddresses()).map((a) => a.confidentialAddress); const outputPubKeys = outPubKeysMap(psetBase64, outputAddresses.concat(recipientAddresses)); const outputsToBlind = Array.from(outputPubKeys.keys()); - const blindedPset: string = await mnemonic.blindPset(psetBase64, outputsToBlind, outputPubKeys); + const blindedPset: string = await signerIdentity.blindPset( + psetBase64, + outputsToBlind, + outputPubKeys + ); - const signedPset: string = await mnemonic.signPset(blindedPset); + const signedPset: string = await signerIdentity.signPset(blindedPset); const ptx = decodePset(signedPset); if (!ptx.validateSignaturesOfAllInputs()) { diff --git a/src/content/marinaBroker.ts b/src/content/marinaBroker.ts index 622a0df7..6f4011a4 100644 --- a/src/content/marinaBroker.ts +++ b/src/content/marinaBroker.ts @@ -127,17 +127,19 @@ export default class MarinaBroker extends Broker { case Marina.prototype.getNextAddress.name: { this.checkHostnameAuthorization(state); - const xpub = await selectMainAccount(state).getWatchIdentity(); + const account = selectMainAccount(state); + const xpub = await account.getWatchIdentity(); const nextAddress = await xpub.getNextAddress(); - await this.store.dispatchAsync(incrementAddressIndex()); + await this.store.dispatchAsync(incrementAddressIndex(account.getAccountID())); return successMsg(nextAddress); } case Marina.prototype.getNextChangeAddress.name: { this.checkHostnameAuthorization(state); - const xpub = await selectMainAccount(state).getWatchIdentity(); + const account = selectMainAccount(state); + const xpub = await account.getWatchIdentity(); const nextChangeAddress = await xpub.getNextChangeAddress(); - await this.store.dispatchAsync(incrementChangeAddressIndex()); + await this.store.dispatchAsync(incrementChangeAddressIndex(account.getAccountID())); return successMsg(nextChangeAddress); } diff --git a/src/domain/account.ts b/src/domain/account.ts index 0586be0c..6f0f3c48 100644 --- a/src/domain/account.ts +++ b/src/domain/account.ts @@ -9,14 +9,17 @@ import { MultisigWatchOnly, StateRestorerOpts, XPub, + multisigFromEsplora, } from 'ldk'; import { decrypt } from '../application/utils'; import { + getStateRestorerOptsFromAddresses, restoredMasterPublicKey, restoredMnemonic, restoredMultisig, restoredWatchOnlyMultisig, } from '../application/utils/restorer'; +import { createAddress } from './address'; import { EncryptedMnemonic } from './encrypted-mnemonic'; import { MasterBlindingKey } from './master-blinding-key'; import { MasterXPub } from './master-extended-pub'; @@ -37,14 +40,14 @@ export interface Account< SignID extends IdentityInterface = IdentityInterface, WatchID extends IdentityInterface = IdentityInterface > { - accountID: AccountID; + getAccountID(): AccountID; getSigningIdentity(password: string): Promise; getWatchIdentity(): Promise; } // Main Account uses the default Mnemonic derivation path // single-sig account used to send/receive regular assets -export type MainAccount = Account; +export type MnemonicAccount = Account; export interface MnemonicAccountData { accountID: AccountID; @@ -54,9 +57,12 @@ export interface MnemonicAccountData { masterBlindingKey: MasterBlindingKey; } -export function createMnemonicAccount(data: MnemonicAccountData, network: Network): MainAccount { +export function createMnemonicAccount( + data: MnemonicAccountData, + network: Network +): MnemonicAccount { return { - accountID: data.accountID, + getAccountID: () => data.accountID, getSigningIdentity: (password: string) => restoredMnemonic(decrypt(data.encryptedMnemonic, password), data.restorerOpts, network), getWatchIdentity: () => @@ -72,16 +78,20 @@ export interface MultisigAccountData { baseDerivationPath: string; // we'll use the MainAccount in order to generate signerXPub: XPub; cosignerXPubs: XPub[]; + restorerOpts: StateRestorerOpts; requiredSignature: number; extraData: ExtraDataT; } -export function create2of2MultisigAccountData( +// create account data +// restore the Identity from esplora URL in order to compute the StateRestorerOpts +export async function create2of2MultisigAccountData( signer: HDSignerMultisig, cosignerXPub: XPub, network: Network, - extraData: T -): MultisigAccountData { + extraData: T, + explorerURL: string +): Promise> { const multisigID = new Multisig({ chain: network, type: IdentityType.Multisig, @@ -92,12 +102,22 @@ export function create2of2MultisigAccountData( }, }); + const restoredFromExplorer = await multisigFromEsplora(multisigID)({ + esploraURL: explorerURL, + gapLimit: 30, + }); + const addresses = (await restoredFromExplorer.getAddresses()).map((a) => + createAddress(a.confidentialAddress, a.derivationPath) + ); + const restorerOpts = getStateRestorerOptsFromAddresses(addresses); + return { baseDerivationPath: signer.baseDerivationPath || DEFAULT_BASE_DERIVATION_PATH, signerXPub: multisigID.getXPub(), cosignerXPubs: [cosignerXPub], requiredSignature: 2, extraData, + restorerOpts, }; } @@ -107,7 +127,7 @@ export function createMultisigAccount( network: Network ): MultisigAccount { return { - accountID: data.signerXPub, + getAccountID: () => data.signerXPub, getSigningIdentity: (password: string) => restoredMultisig( { @@ -116,6 +136,7 @@ export function createMultisigAccount( }, data.cosignerXPubs, data.requiredSignature, + data.restorerOpts, network ), getWatchIdentity: () => @@ -123,6 +144,7 @@ export function createMultisigAccount( data.signerXPub, data.cosignerXPubs, data.requiredSignature, + data.restorerOpts, network ), }; diff --git a/src/presentation/components/address-amount-form.tsx b/src/presentation/components/address-amount-form.tsx index ba879610..69ed994e 100644 --- a/src/presentation/components/address-amount-form.tsx +++ b/src/presentation/components/address-amount-form.tsx @@ -13,7 +13,7 @@ import { TransactionState } from '../../application/redux/reducers/transaction-r import { IAssets } from '../../domain/assets'; import { Network } from '../../domain/network'; import { incrementChangeAddressIndex } from '../../application/redux/actions/wallet'; -import { MainAccount } from '../../domain/account'; +import { Account } from '../../domain/account'; interface AddressAmountFormValues { address: string; @@ -31,7 +31,7 @@ interface AddressAmountFormProps { transaction: TransactionState; assets: IAssets; network: Network; - mainAccount: MainAccount; + account: Account; } const AddressAmountForm = (props: FormikProps) => { @@ -156,14 +156,14 @@ const AddressAmountEnhancedForm = withFormik { - const masterPubKey = await props.mainAccount.getWatchIdentity(); + const masterPubKey = await props.account.getWatchIdentity(); const changeAddressGenerated = await masterPubKey.getNextChangeAddress(); const changeAddress = createAddress( changeAddressGenerated.confidentialAddress, changeAddressGenerated.derivationPath ); - await props.dispatch(incrementChangeAddressIndex()); // persist address in wallet + await props.dispatch(incrementChangeAddressIndex(props.account.getAccountID())); // persist address in wallet await props .dispatch( diff --git a/src/presentation/connect/sign-pset.tsx b/src/presentation/connect/sign-pset.tsx index 71b37bf2..54499c3b 100644 --- a/src/presentation/connect/sign-pset.tsx +++ b/src/presentation/connect/sign-pset.tsx @@ -63,7 +63,6 @@ const ConnectSignTransaction: React.FC = ({ connectData }) debounce(signTx, 2000, { leading: true, trailing: false }) ).current; - console.log(connectData.tx?.pset); return ( = ({ const cosigner: Cosigner = new MockedCosigner(network, explorerURL); const requestedXPub = await cosigner.requestXPub(walletXPub); - const multisigAccountData = create2of2MultisigAccountData( + const multisigAccountData = await create2of2MultisigAccountData( walletSignerData, requestedXPub, network, - { cosignerURL: values.cosignerURL } + { cosignerURL: values.cosignerURL }, + explorerURL ); await dispatch(addRestrictedAssetData(multisigAccountData)); - - console.log(multisigAccountData); }; return ( diff --git a/src/presentation/settings/cosigners.tsx b/src/presentation/settings/cosigners.tsx index 2716199a..4de1fd0c 100644 --- a/src/presentation/settings/cosigners.tsx +++ b/src/presentation/settings/cosigners.tsx @@ -26,7 +26,10 @@ const SettingsCosignersView: React.FC = ({ multisigAccou
{multisigAccountsData.map(({ extraData }, index) => ( -
+
Cosigner #{index}
diff --git a/src/presentation/wallet/receive/index.tsx b/src/presentation/wallet/receive/index.tsx index 945a63fa..e964ebad 100644 --- a/src/presentation/wallet/receive/index.tsx +++ b/src/presentation/wallet/receive/index.tsx @@ -8,13 +8,13 @@ import { useDispatch } from 'react-redux'; import { updateUtxos } from '../../../application/redux/actions/utxos'; import { ProxyStoreDispatch } from '../../../application/redux/proxyStore'; import { incrementAddressIndex } from '../../../application/redux/actions/wallet'; -import { MainAccount } from '../../../domain/account'; +import { Account } from '../../../domain/account'; export interface ReceiveProps { - mainAccount: MainAccount; + account: Account; } -const ReceiveView: React.FC = ({ mainAccount }) => { +const ReceiveView: React.FC = ({ account }) => { const history = useHistory(); const dispatch = useDispatch(); @@ -32,10 +32,10 @@ const ReceiveView: React.FC = ({ mainAccount }) => { useEffect(() => { (async () => { - const publicKey = await mainAccount.getWatchIdentity(); + const publicKey = await account.getWatchIdentity(); const addr = await publicKey.getNextAddress(); setConfidentialAddress(addr.confidentialAddress); - await dispatch(incrementAddressIndex()); // persist address + await dispatch(incrementAddressIndex(account.getAccountID())); // persist address setTimeout(() => { dispatch(updateUtxos()).catch(console.error); }, 8000); diff --git a/src/presentation/wallet/send/address-amount.tsx b/src/presentation/wallet/send/address-amount.tsx index f22fcbdd..b5ba388e 100644 --- a/src/presentation/wallet/send/address-amount.tsx +++ b/src/presentation/wallet/send/address-amount.tsx @@ -13,10 +13,10 @@ import { Network } from '../../../domain/network'; import { TransactionState } from '../../../application/redux/reducers/transaction-reducer'; import { Asset, IAssets } from '../../../domain/assets'; import { DEFAULT_ROUTE } from '../../routes/constants'; -import { MainAccount } from '../../../domain/account'; +import { Account } from '../../../domain/account'; export interface AddressAmountProps { - mainAccount: MainAccount; + account: Account; network: Network; transaction: TransactionState; balances: BalancesByAsset; @@ -25,7 +25,7 @@ export interface AddressAmountProps { } const AddressAmountView: React.FC = ({ - mainAccount, + account, network, transaction, balances, @@ -67,7 +67,7 @@ const AddressAmountView: React.FC = ({ network={network} assets={assets} assetPrecision={transactionAsset.precision} - mainAccount={mainAccount} + account={account} /> ); diff --git a/src/presentation/wallet/send/choose-fee.tsx b/src/presentation/wallet/send/choose-fee.tsx index a7ef27b5..60add1d0 100644 --- a/src/presentation/wallet/send/choose-fee.tsx +++ b/src/presentation/wallet/send/choose-fee.tsx @@ -30,7 +30,7 @@ import { ProxyStoreDispatch } from '../../../application/redux/proxyStore'; import { Address, createAddress } from '../../../domain/address'; import { Topup } from 'taxi-protobuf/generated/js/taxi_pb'; import { incrementChangeAddressIndex } from '../../../application/redux/actions/wallet'; -import { MainAccount } from '../../../domain/account'; +import { Account } from '../../../domain/account'; export interface ChooseFeeProps { network: Network; @@ -42,7 +42,7 @@ export interface ChooseFeeProps { balances: BalancesByAsset; taxiAssets: string[]; lbtcAssetHash: string; - mainAccount: MainAccount; + account: Account; mainAccountUtxos: UtxoInterface[]; } @@ -56,7 +56,7 @@ const ChooseFeeView: React.FC = ({ balances, taxiAssets, lbtcAssetHash, - mainAccount, + account, mainAccountUtxos, }) => { const history = useHistory(); @@ -143,7 +143,7 @@ const ChooseFeeView: React.FC = ({ let nextChangeAddr = feeChange; if (!nextChangeAddr) { - const restored = await mainAccount.getWatchIdentity(); + const restored = await account.getWatchIdentity(); const next = await restored.getNextChangeAddress(); nextChangeAddr = createAddress(next.confidentialAddress, next.derivationPath); setFeeChange(nextChangeAddr); @@ -189,7 +189,7 @@ const ChooseFeeView: React.FC = ({ if (feeChange) { await Promise.all([ dispatch(setFeeChangeAddress(feeChange)), - dispatch(incrementChangeAddressIndex()), + dispatch(incrementChangeAddressIndex(account.getAccountID())), ]); } diff --git a/src/presentation/wallet/send/end-of-flow.tsx b/src/presentation/wallet/send/end-of-flow.tsx index 89a34349..7e63cf6b 100644 --- a/src/presentation/wallet/send/end-of-flow.tsx +++ b/src/presentation/wallet/send/end-of-flow.tsx @@ -8,21 +8,17 @@ import { SEND_PAYMENT_ERROR_ROUTE, SEND_PAYMENT_SUCCESS_ROUTE } from '../../rout import { debounce } from 'lodash'; import { createPassword } from '../../../domain/password'; import { extractErrorMessage } from '../../utils/error'; -import { MainAccount } from '../../../domain/account'; +import { Account, MainAccountID } from '../../../domain/account'; +import { Transaction } from 'liquidjs-lib'; export interface EndOfFlowProps { - mainAccount: MainAccount; + account: Account; pset?: string; explorerURL: string; recipientAddress?: string; } -const EndOfFlow: React.FC = ({ - mainAccount, - pset, - explorerURL, - recipientAddress, -}) => { +const EndOfFlow: React.FC = ({ account, pset, explorerURL, recipientAddress }) => { const history = useHistory(); const [isModalUnlockOpen, showUnlockModal] = useState(true); @@ -34,10 +30,14 @@ const EndOfFlow: React.FC = ({ if (!pset || !recipientAddress) return; try { const pass = createPassword(password); - const mnemo = await mainAccount.getSigningIdentity(pass); - tx = await blindAndSignPset(mnemo, pset, [recipientAddress]); + const signer = await account.getSigningIdentity(pass); + tx = await blindAndSignPset(signer, pset, [recipientAddress]); + + const txid = Transaction.fromHex(tx).getId(); + if (account.getAccountID() === MainAccountID) { + await broadcastTx(explorerURL, tx); + } - const txid = await broadcastTx(explorerURL, tx); history.push({ pathname: SEND_PAYMENT_SUCCESS_ROUTE, state: { txid }, diff --git a/yarn.lock b/yarn.lock index 2ceb064f..448aa2f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6027,10 +6027,10 @@ lcid@^3.0.0: dependencies: invert-kv "^3.0.0" -ldk@^0.3.13: - version "0.3.13" - resolved "https://registry.yarnpkg.com/ldk/-/ldk-0.3.13.tgz#351c2c9a1a83f6aab692590df8cc0865a6b82956" - integrity sha512-IEYztFTS53yTvyKCNIFXThti1hk4Xir10KgRaySj9b/Exas589I+PgQvputvRKKIWRTe3wMQYQChuSGWb3dzjg== +ldk@^0.3.14: + version "0.3.14" + resolved "https://registry.yarnpkg.com/ldk/-/ldk-0.3.14.tgz#6294dafe48172a266a0490dba0ec5e5751cd9297" + integrity sha512-G8IQQto20WOSUbPc9mXwcCK5Ba+AsQdkWf5SkbisRQdKcS8E+A4dLpMv8np18e0/NxAJQ+eRhox49ypQLYzCLQ== dependencies: axios "^0.21.1" bip32 "^2.0.6" From 941747f960730029423b3e3305aa08e23a776025 Mon Sep 17 00:00:00 2001 From: louisinger Date: Wed, 20 Oct 2021 13:36:35 +0200 Subject: [PATCH 16/52] pair-sucess page --- src/application/redux/backend.ts | 8 +-- .../redux/reducers/wallet-reducer.ts | 12 ++--- src/presentation/connect/spend.tsx | 6 ++- src/presentation/cosigner/pair-success.tsx | 13 +++++ src/presentation/cosigner/pair.tsx | 4 ++ src/presentation/routes/constants.ts | 2 + src/presentation/routes/guards.tsx | 7 ++- src/presentation/routes/index.tsx | 53 +++++++++++-------- 8 files changed, 68 insertions(+), 37 deletions(-) create mode 100644 src/presentation/cosigner/pair-success.tsx diff --git a/src/application/redux/backend.ts b/src/application/redux/backend.ts index a85a1f67..015cc66a 100644 --- a/src/application/redux/backend.ts +++ b/src/application/redux/backend.ts @@ -83,7 +83,7 @@ export function makeUtxosUpdaterThunk( const account = selectAccount(state); const explorer = getExplorerURLSelector(getState()); - const utxosMap = selectUnspentsAndTransactions(account.accountID)(state).utxosMap; + const utxosMap = selectUnspentsAndTransactions(account.getAccountID())(state).utxosMap; const currentOutpoints = Object.values(utxosMap || {}).map(({ txid, vout }) => ({ txid, @@ -120,7 +120,7 @@ export function makeUtxosUpdaterThunk( utxo = await utxoWithPrevout(utxo, explorer); } - dispatch(addUtxo(account.accountID, utxo)); + dispatch(addUtxo(account.getAccountID(), utxo)); } utxoIterator = await utxos.next(); } @@ -158,7 +158,7 @@ export function makeTxsUpdaterThunk( if (!app.isAuthenticated) return; const account = selectAccount(state); - const txsHistory = selectUnspentsAndTransactions(account.accountID)(state).transactions[ + const txsHistory = selectUnspentsAndTransactions(account.getAccountID())(state).transactions[ app.network ]; @@ -205,7 +205,7 @@ export function makeTxsUpdaterThunk( const tx = it.value; // Update all txsHistory state at each single new tx const toAdd = toDisplayTransaction(tx, walletScripts, networks[app.network]); - dispatch(addTx(account.accountID, toAdd, app.network)); + dispatch(addTx(account.getAccountID(), toAdd, app.network)); it = await txsGen.next(); } } catch (error) { diff --git a/src/application/redux/reducers/wallet-reducer.ts b/src/application/redux/reducers/wallet-reducer.ts index 5f71f71b..f0484ec6 100644 --- a/src/application/redux/reducers/wallet-reducer.ts +++ b/src/application/redux/reducers/wallet-reducer.ts @@ -118,7 +118,7 @@ export function walletReducer( restorerOpts: { ...state.mainAccount.restorerOpts, lastUsedInternalIndex: - (state.mainAccount.restorerOpts.lastUsedInternalIndex ?? -1) + 1, + (state.mainAccount.restorerOpts.lastUsedInternalIndex ?? 0) + 1, }, }, }; @@ -133,8 +133,8 @@ export function walletReducer( restorerOpts: { ...state.restrictedAssetAccounts[accountID].restorerOpts, lastUsedInternalIndex: - (state.restrictedAssetAccounts[accountID].restorerOpts.lastUsedInternalIndex ?? - -1) + 1, + (state.restrictedAssetAccounts[accountID].restorerOpts.lastUsedInternalIndex ?? 0) + + 1, }, }, }, @@ -151,7 +151,7 @@ export function walletReducer( restorerOpts: { ...state.mainAccount.restorerOpts, lastUsedExternalIndex: - (state.mainAccount.restorerOpts.lastUsedExternalIndex ?? -1) + 1, + (state.mainAccount.restorerOpts.lastUsedExternalIndex ?? 0) + 1, }, }, }; @@ -166,8 +166,8 @@ export function walletReducer( restorerOpts: { ...state.restrictedAssetAccounts[accountID].restorerOpts, lastUsedExternalIndex: - (state.restrictedAssetAccounts[accountID].restorerOpts.lastUsedExternalIndex ?? - -1) + 1, + (state.restrictedAssetAccounts[accountID].restorerOpts.lastUsedExternalIndex ?? 0) + + 1, }, }, }, diff --git a/src/presentation/connect/spend.tsx b/src/presentation/connect/spend.tsx index ce66f434..444b6bbf 100644 --- a/src/presentation/connect/spend.tsx +++ b/src/presentation/connect/spend.tsx @@ -19,7 +19,7 @@ import { blindAndSignPset, createSendPset } from '../../application/utils/transa import { incrementChangeAddressIndex } from '../../application/redux/actions/wallet'; import { selectMainAccount, selectUtxos } from '../../application/redux/selectors/wallet.selector'; import PopupWindowProxy from './popupWindowProxy'; -import { MainAccountID } from '../../domain/account'; +import { AccountID, MainAccountID } from '../../domain/account'; export interface SpendPopupResponse { accepted: boolean; @@ -65,6 +65,7 @@ const ConnectSpend: React.FC = ({ connectData }) => { try { const mnemonicIdentity = await mainAccount.getSigningIdentity(password); const signedTxHex = await makeTransaction( + mainAccount.getAccountID(), mnemonicIdentity, coins, connectData.tx, @@ -149,6 +150,7 @@ const ConnectSpend: React.FC = ({ connectData }) => { export default connectWithConnectData(ConnectSpend); async function makeTransaction( + accountID: AccountID, mnemonic: Mnemonic, coins: UtxoInterface[], connectDataTx: ConnectData['tx'], @@ -173,7 +175,7 @@ async function makeTransaction( const changeAddressGetter = (asset: string) => { if (!assets.includes(asset)) return undefined; // will throw an error in coin selector if (!persisted[asset]) { - dispatch(incrementChangeAddressIndex()).catch(console.error); + dispatch(incrementChangeAddressIndex(accountID)).catch(console.error); persisted[asset] = true; } return changeAddresses[asset].confidentialAddress; diff --git a/src/presentation/cosigner/pair-success.tsx b/src/presentation/cosigner/pair-success.tsx new file mode 100644 index 00000000..c39dc0d2 --- /dev/null +++ b/src/presentation/cosigner/pair-success.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import Shell from '../components/shell'; + +const PairSuccess: React.FC = () => { + return ( + +

New account successfully restored and add to Marina! You can close this page.

+ mermaid +
+ ); +}; + +export default PairSuccess; diff --git a/src/presentation/cosigner/pair.tsx b/src/presentation/cosigner/pair.tsx index 81ac1e8c..5098c85e 100644 --- a/src/presentation/cosigner/pair.tsx +++ b/src/presentation/cosigner/pair.tsx @@ -13,6 +13,8 @@ import { useDispatch } from 'react-redux'; import { ProxyStoreDispatch } from '../../application/redux/proxyStore'; import { addRestrictedAssetData } from '../../application/redux/actions/wallet'; import { DEFAULT_BASE_DERIVATION_PATH } from 'ldk'; +import { useHistory } from 'react-router'; +import { PAIR_SUCCESS_COSIGNER_ROUTE } from '../routes/constants'; interface OptInFormProps { onSubmit: (values: OptInFormValues) => Promise; @@ -98,6 +100,7 @@ const PairCosignerView: React.FC = ({ explorerURL, }) => { const dispatch = useDispatch(); + const history = useHistory(); const onSubmit = async (values: OptInFormValues) => { const walletSignerData = { @@ -119,6 +122,7 @@ const PairCosignerView: React.FC = ({ ); await dispatch(addRestrictedAssetData(multisigAccountData)); + history.push(PAIR_SUCCESS_COSIGNER_ROUTE); }; return ( diff --git a/src/presentation/routes/constants.ts b/src/presentation/routes/constants.ts index 9b67bbdc..c3ca8a9d 100644 --- a/src/presentation/routes/constants.ts +++ b/src/presentation/routes/constants.ts @@ -53,6 +53,7 @@ const SETTINGS_COSIGNERS_ROUTE = '/settings/info/cosigners'; // Cosigner Opt in const PAIR_COSIGNER_ROUTE = '/cosigner/pair'; +const PAIR_SUCCESS_COSIGNER_ROUTE = '/cosigner/pair/success'; export { //Connect @@ -96,4 +97,5 @@ export { SETTINGS_DEEP_RESTORER_ROUTE, SETTINGS_COSIGNERS_ROUTE, PAIR_COSIGNER_ROUTE, + PAIR_SUCCESS_COSIGNER_ROUTE, }; diff --git a/src/presentation/routes/guards.tsx b/src/presentation/routes/guards.tsx index e1192442..80ebe41f 100644 --- a/src/presentation/routes/guards.tsx +++ b/src/presentation/routes/guards.tsx @@ -15,10 +15,13 @@ const ALLOWED_REDIRECT_ROUTE = [CONNECT_ENABLE_ROUTE, CONNECT_SPEND_ROUTE]; */ interface ProtectedRouteProps extends RouteProps { - comp: React.ComponentType> | React.ComponentType; + component: React.ComponentType> | React.ComponentType; } -export const ProtectedRoute: React.FC = ({ comp: Component, ...rest }) => { +export const ProtectedRoute: React.FC = ({ + component: Component, + ...rest +}) => { const isAuthenticated = useSelector((state: RootReducerState) => state.app.isAuthenticated); // we check if an optional param is given diff --git a/src/presentation/routes/index.tsx b/src/presentation/routes/index.tsx index e26d48b2..1ae20482 100644 --- a/src/presentation/routes/index.tsx +++ b/src/presentation/routes/index.tsx @@ -39,6 +39,7 @@ import { RECEIVE_ADDRESS_ROUTE, PAIR_COSIGNER_ROUTE, SETTINGS_COSIGNERS_ROUTE, + PAIR_SUCCESS_COSIGNER_ROUTE, } from './constants'; // Connect @@ -83,6 +84,7 @@ import SettingsCredits from '../settings/credits'; import SettingsTerms from '../settings/terms'; import PairCosigner from '../../application/redux/containers/pair.container'; import SettingsCosigner from '../../application/redux/containers/cosigners.container'; +import PairSuccess from '../cosigner/pair-success'; const Routes: React.FC = () => { return ( @@ -97,29 +99,33 @@ const Routes: React.FC = () => { {/*Wallet*/} - - - - - - - - - - - + + + + + + + + + + + {/*Settings*/} - - - - - - - - - - - + + + + + + + + + + + {/*Login*/} @@ -129,7 +135,8 @@ const Routes: React.FC = () => { - + + ); }; From e53da0a354b1cf1b19b5306f6437adbaeb0b9532 Mon Sep 17 00:00:00 2001 From: louisinger Date: Wed, 20 Oct 2021 14:13:50 +0200 Subject: [PATCH 17/52] drop isRegtestAsset --- src/application/redux/reducers/asset-reducer.ts | 3 +-- src/domain/assets.ts | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/application/redux/reducers/asset-reducer.ts b/src/application/redux/reducers/asset-reducer.ts index 1363111d..9ce74bae 100644 --- a/src/application/redux/reducers/asset-reducer.ts +++ b/src/application/redux/reducers/asset-reducer.ts @@ -3,7 +3,7 @@ import { IAssets } from '../../../domain/assets'; import * as ACTION_TYPES from '../actions/action-types'; export const assetInitState: IAssets = { - ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2: { + 'ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2': { name: 'Tether USD', precision: 8, ticker: 'USDt', @@ -17,7 +17,6 @@ export const assetInitState: IAssets = { name: 'Liquid Bitcoin', precision: 8, ticker: 'L-BTC', - isRegtestAsset: true, }, }; diff --git a/src/domain/assets.ts b/src/domain/assets.ts index d7b930d0..3c1bc929 100644 --- a/src/domain/assets.ts +++ b/src/domain/assets.ts @@ -5,7 +5,6 @@ export type Asset = { name: string; precision: number; ticker: string; - isRegtestAsset?: boolean; }; export type AssetGetter = (assetHash: string) => Asset & { assetHash: string }; From f6a87dfad8dea8a594d0d8993e7d15653e5b11d7 Mon Sep 17 00:00:00 2001 From: louisinger Date: Wed, 20 Oct 2021 15:37:07 +0200 Subject: [PATCH 18/52] MultisigWithCosigner Identity --- src/application/utils/restorer.ts | 9 +++--- src/domain/account.ts | 16 +++++++---- src/domain/cosigner.ts | 45 ++++++++++++++++++++++-------- src/presentation/cosigner/pair.tsx | 2 +- 4 files changed, 49 insertions(+), 23 deletions(-) diff --git a/src/application/utils/restorer.ts b/src/application/utils/restorer.ts index e34a7ce7..30d026c8 100644 --- a/src/application/utils/restorer.ts +++ b/src/application/utils/restorer.ts @@ -5,7 +5,6 @@ import { mnemonicRestorerFromState, MasterPublicKey, masterPubKeyRestorerFromState, - Multisig, CosignerMultisig, MultisigWatchOnly, XPub, @@ -13,6 +12,7 @@ import { restorerFromState, } from 'ldk'; import { Address } from '../../domain/address'; +import { Cosigner, MultisigWithCosigner } from '../../domain/cosigner'; import { MasterBlindingKey } from '../../domain/master-blinding-key'; import { MasterXPub } from '../../domain/master-extended-pub'; import { Network } from '../../domain/network'; @@ -86,9 +86,10 @@ export function restoredMultisig( cosigners: CosignerMultisig[], requiredSignatures: number, restorerOpts: StateRestorerOpts, + cosigner: Cosigner, network: Network ) { - const multisigID = new Multisig({ + const multisigID = new MultisigWithCosigner({ chain: network, type: IdentityType.Multisig, opts: { @@ -96,9 +97,9 @@ export function restoredMultisig( signer, cosigners, }, - }); + }, cosigner); - return restorerFromState(multisigID)(restorerOpts); + return restorerFromState(multisigID)(restorerOpts); } // create a MultisigWatchOnly Identity diff --git a/src/domain/account.ts b/src/domain/account.ts index 6f0f3c48..18faf528 100644 --- a/src/domain/account.ts +++ b/src/domain/account.ts @@ -20,10 +20,12 @@ import { restoredWatchOnlyMultisig, } from '../application/utils/restorer'; import { createAddress } from './address'; +import { HDSignerToXPub, MockedCosigner, MultisigWithCosigner } from './cosigner'; import { EncryptedMnemonic } from './encrypted-mnemonic'; import { MasterBlindingKey } from './master-blinding-key'; import { MasterXPub } from './master-extended-pub'; import { Network } from './network'; +import { CosignerExtraData } from './wallet'; export type AccountID = string; export const MainAccountID: AccountID = 'main'; @@ -72,7 +74,7 @@ export function createMnemonicAccount( // MultisigAccount aims to handle account with cosigner(s) // use master extended public keys from cosigners and xpub derived from master private key (mnemonic) -export type MultisigAccount = Account; +export type MultisigAccount = Account; export interface MultisigAccountData { baseDerivationPath: string; // we'll use the MainAccount in order to generate @@ -81,6 +83,7 @@ export interface MultisigAccountData { restorerOpts: StateRestorerOpts; requiredSignature: number; extraData: ExtraDataT; + network: Network; } // create account data @@ -113,18 +116,18 @@ export async function create2of2MultisigAccountData( return { baseDerivationPath: signer.baseDerivationPath || DEFAULT_BASE_DERIVATION_PATH, - signerXPub: multisigID.getXPub(), + signerXPub: HDSignerToXPub(signer, network), cosignerXPubs: [cosignerXPub], requiredSignature: 2, extraData, restorerOpts, + network }; } export function createMultisigAccount( encryptedMnemonic: EncryptedMnemonic, - data: MultisigAccountData, - network: Network + data: MultisigAccountData, ): MultisigAccount { return { getAccountID: () => data.signerXPub, @@ -137,7 +140,8 @@ export function createMultisigAccount( data.cosignerXPubs, data.requiredSignature, data.restorerOpts, - network + new MockedCosigner(data.network, data.signerXPub), + data.network ), getWatchIdentity: () => restoredWatchOnlyMultisig( @@ -145,7 +149,7 @@ export function createMultisigAccount( data.cosignerXPubs, data.requiredSignature, data.restorerOpts, - network + data.network ), }; } diff --git a/src/domain/cosigner.ts b/src/domain/cosigner.ts index 08444598..531f0f1d 100644 --- a/src/domain/cosigner.ts +++ b/src/domain/cosigner.ts @@ -3,19 +3,37 @@ import { mnemonicToSeedSync } from 'bip39'; import { DEFAULT_BASE_DERIVATION_PATH, HDSignerMultisig, + IdentityInterface, + IdentityOpts, IdentityType, Mnemonic, Multisig, multisigFromEsplora, + MultisigOpts, toXpub, XPub, } from 'ldk'; import { networkFromString } from '../application/utils'; +import { BlockstreamExplorerURLs, NigiriDefaultExplorerURLs } from './app'; import { Network } from './network'; +export class MultisigWithCosigner extends Multisig implements IdentityInterface { + private cosigner: Cosigner; + + constructor(opts: IdentityOpts, cosigner: Cosigner) { + super(opts); + this.cosigner = cosigner; + } + + async signPset(pset: string): Promise { + const signed = await super.signPset(pset) + return this.cosigner.signPset(signed) + } +} + export interface Cosigner { requestXPub(signerXPub: XPub): Promise; - signPset(pset: string, signWith: XPub): Promise; + signPset(pset: string): Promise; } export function HDSignerToXPub(signer: HDSignerMultisig, network: Network) { @@ -29,30 +47,33 @@ export function HDSignerToXPub(signer: HDSignerMultisig, network: Network) { export class MockedCosigner implements Cosigner { private mnemonic: Mnemonic; - private cosignerXPub: XPub | undefined; + private cosignerXPub: XPub; private network: Network; private esploraURL: string; - constructor(network: Network, esploraURL: string) { - this.mnemonic = Mnemonic.Random(network, DEFAULT_BASE_DERIVATION_PATH); + constructor(network: Network, cosignerXPub: XPub) { + this.mnemonic = new Mnemonic({ + chain: network, + type: IdentityType.Mnemonic, + opts: { + mnemonic: 'sponsor envelope waste fork indicate board survey tobacco laugh cover guitar layer', + baseDerivationPath: DEFAULT_BASE_DERIVATION_PATH + } + }); this.network = network; - this.esploraURL = esploraURL; + this.esploraURL = network === 'liquid' ? BlockstreamExplorerURLs.esploraURL : NigiriDefaultExplorerURLs.esploraURL; + this.cosignerXPub = cosignerXPub; } - requestXPub(singerXPub: XPub) { - this.cosignerXPub = singerXPub; + requestXPub(_: XPub) { return Promise.resolve(this.mnemonic.getXPub()); } - async signPset(pset: string, signWith: XPub) { + async signPset(pset: string) { if (this.cosignerXPub === undefined) { throw new Error('pairing is not done'); } - if (signWith !== this.mnemonic.getXPub()) { - throw new Error(`can only sign with ${this.mnemonic.getXPub()}`); - } - const multisigID = await multisigFromEsplora( new Multisig({ chain: this.network, diff --git a/src/presentation/cosigner/pair.tsx b/src/presentation/cosigner/pair.tsx index 5098c85e..272680ca 100644 --- a/src/presentation/cosigner/pair.tsx +++ b/src/presentation/cosigner/pair.tsx @@ -110,7 +110,7 @@ const PairCosignerView: React.FC = ({ const walletXPub = HDSignerToXPub(walletSignerData, network); // cosigner should be created from values.cosignerURL - const cosigner: Cosigner = new MockedCosigner(network, explorerURL); + const cosigner: Cosigner = new MockedCosigner(network, walletXPub); const requestedXPub = await cosigner.requestXPub(walletXPub); const multisigAccountData = await create2of2MultisigAccountData( From 639a2f1188d448510bb34390427ae948838445e8 Mon Sep 17 00:00:00 2001 From: louisinger Date: Thu, 21 Oct 2021 13:16:27 +0200 Subject: [PATCH 19/52] updaterWorker logic --- src/application/redux/actions/action-types.ts | 7 +- src/application/redux/actions/transaction.ts | 7 - src/application/redux/actions/updater.ts | 27 ++ src/application/redux/actions/utxos.ts | 6 +- src/application/redux/backend.ts | 320 ------------------ .../redux/reducers/asset-reducer.ts | 2 +- src/application/redux/reducers/index.ts | 2 + .../redux/reducers/updater-reducer.ts | 36 ++ .../redux/reducers/wallet-reducer.ts | 11 +- .../redux/selectors/wallet.selector.ts | 14 +- src/application/redux/store.ts | 134 +++++++- src/application/utils/restorer.ts | 19 +- src/background/backend.ts | 188 ++++++++++ src/background/background-script.ts | 2 + src/background/updater-worker.ts | 38 +++ src/domain/account.ts | 4 +- src/domain/common.ts | 2 + src/domain/cosigner.ts | 16 +- src/presentation/components/shell-popup.tsx | 9 +- src/presentation/wallet/receive/index.tsx | 5 +- src/presentation/wallet/send/end-of-flow.tsx | 2 +- .../wallet/send/payment-success.tsx | 10 +- .../wallet/transactions/index.tsx | 7 +- 23 files changed, 477 insertions(+), 391 deletions(-) create mode 100644 src/application/redux/actions/updater.ts delete mode 100644 src/application/redux/backend.ts create mode 100644 src/application/redux/reducers/updater-reducer.ts create mode 100644 src/background/backend.ts create mode 100644 src/background/updater-worker.ts diff --git a/src/application/redux/actions/action-types.ts b/src/application/redux/actions/action-types.ts index cffc02d0..9c2b10e4 100644 --- a/src/application/redux/actions/action-types.ts +++ b/src/application/redux/actions/action-types.ts @@ -4,7 +4,6 @@ export const WALLET_ADD_RESTRICTED_ASSET_ACCOUNT = 'WALLET_ADD_RESTRICTED_ASSET_ export const ADD_UTXO = 'ADD_UTXO'; export const DELETE_UTXO = 'DELETE_UTXO'; export const FLUSH_UTXOS = 'FLUSH_UTXOS'; -export const UPDATE_UTXOS = 'UPDATE_UTXOS'; export const SET_DEEP_RESTORER_GAP_LIMIT = 'SET_DEEP_RESTORER_GAP_LIMIT'; export const SET_DEEP_RESTORER_IS_LOADING = 'SET_DEEP_RESTORER_IS_LOADING'; export const SET_DEEP_RESTORER_ERROR = 'SET_DEEP_RESTORER_ERROR'; @@ -28,9 +27,7 @@ export const ONBOARDING_FLUSH = 'ONBOARDING_FLUSH'; export const ONBOARDING_SET_IS_FROM_POPUP_FLOW = 'ONBOARDING_SET_IS_FROM_POPUP_FLOW'; // Transactions history -export const UPDATE_TXS = 'UPDATE_TXS'; export const ADD_TX = 'ADD_TX'; -export const RESET_TXS = 'RESET_TXS'; // Pending transaction export const PENDING_TX_SET_ASSET = 'PENDING_TX_SET_ASSET'; @@ -67,3 +64,7 @@ export const START_DEEP_RESTORATION = 'START_DEEP_RESTORATION'; // Reset export const RESET = 'RESET'; + +// Updater +export const PUSH_UPDATER_TASK = 'PUSH_UPDATER_TASK'; +export const POP_UPDATER_TASK = 'POP_UPDATER_TASK'; diff --git a/src/application/redux/actions/transaction.ts b/src/application/redux/actions/transaction.ts index 1beccb2a..4cb0938b 100644 --- a/src/application/redux/actions/transaction.ts +++ b/src/application/redux/actions/transaction.ts @@ -4,7 +4,6 @@ import { PENDING_TX_SET_ADDRESSES_AND_AMOUNT, PENDING_TX_SET_FEE_CHANGE_ADDRESS, PENDING_TX_SET_FEE_AMOUNT_AND_ASSET, - UPDATE_TXS, PENDING_TX_SET_PSET, ADD_TX, } from './action-types'; @@ -41,12 +40,6 @@ export function flushPendingTx(): AnyAction { return { type: PENDING_TX_FLUSH }; } -export function updateTxs(): AnyAction { - return { - type: UPDATE_TXS, - }; -} - export function setPset(pset: string): AnyAction { return { type: PENDING_TX_SET_PSET, diff --git a/src/application/redux/actions/updater.ts b/src/application/redux/actions/updater.ts new file mode 100644 index 00000000..c206e3cf --- /dev/null +++ b/src/application/redux/actions/updater.ts @@ -0,0 +1,27 @@ +import { AccountID } from '../../../domain/account'; +import { UpdaterTaskType } from '../reducers/updater-reducer'; +import { PUSH_UPDATER_TASK } from './action-types'; + +export function utxosUpdateTask(accountID: AccountID) { + return { + type: PUSH_UPDATER_TASK, + payload: { + updaterTask: { + accountID, + type: UpdaterTaskType.UTXO, + }, + }, + }; +} + +export function txsUpdateTask(accountID: AccountID) { + return { + type: PUSH_UPDATER_TASK, + payload: { + updaterTask: { + accountID, + type: UpdaterTaskType.TX, + }, + }, + }; +} diff --git a/src/application/redux/actions/utxos.ts b/src/application/redux/actions/utxos.ts index af132d18..eb5386ba 100644 --- a/src/application/redux/actions/utxos.ts +++ b/src/application/redux/actions/utxos.ts @@ -1,11 +1,7 @@ import { UtxoInterface } from 'ldk'; import { AnyAction } from 'redux'; import { AccountID } from '../../../domain/account'; -import { ADD_UTXO, DELETE_UTXO, FLUSH_UTXOS, UPDATE_UTXOS } from './action-types'; - -export function updateUtxos(): AnyAction { - return { type: UPDATE_UTXOS }; -} +import { ADD_UTXO, DELETE_UTXO, FLUSH_UTXOS } from './action-types'; export function addUtxo(accountID: AccountID, utxo: UtxoInterface): AnyAction { return { type: ADD_UTXO, payload: { accountID, utxo } }; diff --git a/src/application/redux/backend.ts b/src/application/redux/backend.ts deleted file mode 100644 index 015cc66a..00000000 --- a/src/application/redux/backend.ts +++ /dev/null @@ -1,320 +0,0 @@ -import { RootReducerState } from '../../domain/common'; -import { defaultPrecision } from '../utils/constants'; -import axios from 'axios'; -import browser from 'webextension-polyfill'; -import { - address as addressLDK, - networks, - isBlindedUtxo, - BlindingKeyGetter, - address, - fetchAndUnblindTxsGenerator, - fetchAndUnblindUtxosGenerator, - masterPubKeyRestorerFromEsplora, - MasterPublicKey, - utxoWithPrevout, - IdentityType, - AddressInterface, -} from 'ldk'; -import { - fetchAssetsFromTaxi, - getStateRestorerOptsFromAddresses, - taxiURL, - toDisplayTransaction, - toStringOutpoint, -} from '../utils'; -import { setDeepRestorerError, setDeepRestorerIsLoading, setWalletData } from './actions/wallet'; -import { createAddress } from '../../domain/address'; -import { setTaxiAssets, updateTaxiAssets } from './actions/taxi'; -import { addUtxo, deleteUtxo, updateUtxos } from './actions/utxos'; -import { addAsset } from './actions/asset'; -import { ThunkAction } from 'redux-thunk'; -import { AnyAction, Dispatch } from 'redux'; -import { IAssets } from '../../domain/assets'; -import { addTx, updateTxs } from './actions/transaction'; -import { getExplorerURLSelector } from './selectors/app.selector'; -import { - RESET_APP, - RESET_CONNECT, - RESET_TAXI, - RESET_TXS, - RESET_WALLET, -} from './actions/action-types'; -import { flushTx } from './actions/connect'; -import { Account } from '../../domain/account'; -import { selectMainAccount, selectUnspentsAndTransactions } from './selectors/wallet.selector'; - -const UPDATE_ALARM = 'UPDATE_ALARM'; - -type AccountSelector = (state: RootReducerState) => Account; - -/** - * fetch the asset infos from explorer (ticker, precision etc...) - */ -async function fetchAssetInfos( - assetHash: string, - explorerUrl: string, - assetsState: IAssets, - dispatch: Dispatch -) { - if (assetsState[assetHash] !== undefined) return; // do not update - - const assetInfos = (await axios.get(`${explorerUrl}/asset/${assetHash}`)).data; - const name = assetInfos?.name ? assetInfos.name : 'Unknown'; - const ticker = assetInfos?.ticker ? assetInfos.ticker : assetHash.slice(0, 4).toUpperCase(); - const precision = assetInfos.precision !== undefined ? assetInfos.precision : defaultPrecision; - - dispatch(addAsset(assetHash, { name, ticker, precision })); -} - -async function getAddressesFromAccount(account: Account): Promise { - return (await account.getWatchIdentity()).getAddresses(); -} - -// fetch and unblind the utxos and then refresh it. -export function makeUtxosUpdaterThunk( - selectAccount: AccountSelector -): ThunkAction { - return async (dispatch, getState) => { - try { - const state = getState(); - const { app } = state; - if (!app.isAuthenticated) return; - - const account = selectAccount(state); - const explorer = getExplorerURLSelector(getState()); - const utxosMap = selectUnspentsAndTransactions(account.getAccountID())(state).utxosMap; - - const currentOutpoints = Object.values(utxosMap || {}).map(({ txid, vout }) => ({ - txid, - vout, - })); - - const skippedOutpoints: string[] = []; // for deleting - - // Fetch utxo(s). Return blinded utxo(s) if unblinding has been skipped - const utxos = fetchAndUnblindUtxosGenerator( - await getAddressesFromAccount(account), - explorer, - // Skip unblinding if utxo exists in current state - (utxo) => { - const outpoint = toStringOutpoint(utxo); - const skip = utxosMap[outpoint] !== undefined; - - if (skip) skippedOutpoints.push(toStringOutpoint(utxo)); - - return skip; - } - ); - - let utxoIterator = await utxos.next(); - while (!utxoIterator.done) { - let utxo = utxoIterator?.value; - if (!isBlindedUtxo(utxo)) { - if (utxo.asset) { - const assets = getState().assets; - await fetchAssetInfos(utxo.asset, explorer, assets, dispatch).catch(console.error); - } - - if (!utxo.prevout) { - utxo = await utxoWithPrevout(utxo, explorer); - } - - dispatch(addUtxo(account.getAccountID(), utxo)); - } - utxoIterator = await utxos.next(); - } - - if (utxoIterator.done) { - console.info(`number of utxos fetched: ${utxoIterator.value.numberOfUtxos}`); - if (utxoIterator.value.errors.length > 0) { - console.warn( - `${utxoIterator.value.errors.length} errors occurs during utxos updater generator` - ); - } - } - - for (const outpoint of currentOutpoints) { - if (skippedOutpoints.includes(toStringOutpoint(outpoint))) continue; - // if not skipped, it means the utxo has been spent - dispatch(deleteUtxo(outpoint.txid, outpoint.vout)); - } - } catch (error) { - console.error(`fetchAndUpdateUtxos error: ${error}`); - } - }; -} - -/** - * use fetchAndUnblindTxsGenerator to update the tx history - */ -export function makeTxsUpdaterThunk( - selectAccount: AccountSelector -): ThunkAction { - return async (dispatch, getState) => { - try { - const state = getState(); - const { app } = state; - if (!app.isAuthenticated) return; - - const account = selectAccount(state); - const txsHistory = selectUnspentsAndTransactions(account.getAccountID())(state).transactions[ - app.network - ]; - - // Initialize txs to txsHistory shallow clone - const addressInterfaces = await getAddressesFromAccount(account); - const walletScripts = addressInterfaces.map((a) => - address.toOutputScript(a.confidentialAddress).toString('hex') - ); - - const explorer = getExplorerURLSelector(getState()); - - const identityBlindKeyGetter: BlindingKeyGetter = (script: string) => { - try { - const address = addressLDK.fromOutputScript( - Buffer.from(script, 'hex'), - networks[app.network] - ); - return addressInterfaces.find( - (addr) => - addressLDK.fromConfidential(addr.confidentialAddress).unconfidentialAddress === - address - )?.blindingPrivateKey; - } catch (_) { - return undefined; - } - }; - - const txsGen = fetchAndUnblindTxsGenerator( - addressInterfaces.map((a) => a.confidentialAddress), - identityBlindKeyGetter, - explorer, - // Check if tx exists in React state - (tx) => txsHistory[tx.txid] !== undefined - ); - - let it = await txsGen.next(); - - // If no new tx already in state then return txsHistory of current network - if (it.done) { - return; - } - - while (!it.done) { - const tx = it.value; - // Update all txsHistory state at each single new tx - const toAdd = toDisplayTransaction(tx, walletScripts, networks[app.network]); - dispatch(addTx(account.getAccountID(), toAdd, app.network)); - it = await txsGen.next(); - } - } catch (error) { - console.error(`fetchAndUnblindTxs: ${error}`); - } - }; -} - -/** - * fetch assets from taxi daemon endpoint (make a grpc call) - * and then set assets in store. - */ -export function fetchAndSetTaxiAssets(): ThunkAction { - return async (dispatch, getState) => { - const state = getState(); - const assets = await fetchAssetsFromTaxi(taxiURL[state.app.network]); - - const currentAssets = state.taxi.taxiAssets; - const sortAndJoin = (a: string[]) => a.sort().join(''); - - if (sortAndJoin(currentAssets) === sortAndJoin(assets)) { - return; // skip if same assets state - } - - dispatch(setTaxiAssets(assets)); - }; -} - -// Start the periodic updater (for utxos and txs fetching) -export function startAlarmUpdater(): ThunkAction { - return (dispatch) => { - dispatch(updateUtxos()); - - browser.alarms.onAlarm.addListener((alarm) => { - switch (alarm.name) { - case UPDATE_ALARM: - dispatch(updateTxs()); - dispatch(updateUtxos()); - dispatch(updateTaxiAssets()); - break; - - default: - break; - } - }); - - browser.alarms.create(UPDATE_ALARM, { - when: Date.now(), - periodInMinutes: 1, - }); - }; -} - -// Using to generate addresses and use the explorer to test them -export function deepRestorer(): ThunkAction { - return async (dispatch, getState) => { - const state = getState(); - const { isLoading, gapLimit } = state.wallet.deepRestorer; - const toRestore = new MasterPublicKey({ - chain: state.app.network, - type: IdentityType.MasterPublicKey, - opts: { - masterPublicKey: state.wallet.mainAccount.masterXPub, - masterBlindingKey: state.wallet.mainAccount.masterBlindingKey, - }, - }); - const explorer = getExplorerURLSelector(getState()); - if (isLoading) return; - - try { - dispatch(setDeepRestorerIsLoading(true)); - const opts = { gapLimit, esploraURL: explorer }; - const publicKey = await masterPubKeyRestorerFromEsplora(toRestore)(opts); - const addresses = (await publicKey.getAddresses()).map((a) => - createAddress(a.confidentialAddress, a.derivationPath) - ); - - const restorerOpts = getStateRestorerOptsFromAddresses(addresses); - - dispatch( - setWalletData({ - ...state.wallet.mainAccount, - restorerOpts, - confidentialAddresses: addresses, - passwordHash: state.wallet.passwordHash, - }) - ); - - dispatch(updateUtxos()); - dispatch(makeTxsUpdaterThunk(selectMainAccount)); - dispatch(fetchAndSetTaxiAssets()); - - dispatch(setDeepRestorerError(undefined)); - } catch (err: any) { - dispatch(setDeepRestorerError(err.message || err)); - } finally { - dispatch(setDeepRestorerIsLoading(false)); - } - }; -} - -// reset all the reducers except the `assets` reducer (shared data). -export function resetAll(): ThunkAction { - return (dispatch) => { - dispatch({ type: RESET_TAXI }); - dispatch({ type: RESET_TXS }); - dispatch({ type: RESET_APP }); - dispatch({ type: RESET_WALLET }); - dispatch({ type: RESET_CONNECT }); - dispatch(flushTx()); - }; -} diff --git a/src/application/redux/reducers/asset-reducer.ts b/src/application/redux/reducers/asset-reducer.ts index 9ce74bae..a4324f1f 100644 --- a/src/application/redux/reducers/asset-reducer.ts +++ b/src/application/redux/reducers/asset-reducer.ts @@ -3,7 +3,7 @@ import { IAssets } from '../../../domain/assets'; import * as ACTION_TYPES from '../actions/action-types'; export const assetInitState: IAssets = { - 'ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2': { + ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2: { name: 'Tether USD', precision: 8, ticker: 'USDt', diff --git a/src/application/redux/reducers/index.ts b/src/application/redux/reducers/index.ts index 138124aa..52d7e2c4 100644 --- a/src/application/redux/reducers/index.ts +++ b/src/application/redux/reducers/index.ts @@ -15,6 +15,7 @@ import { PersistConfig } from 'redux-persist/lib/types'; import { appReducer, appInitState } from './app-reducer'; import { walletInitState, walletReducer } from './wallet-reducer'; import { connectDataReducer, connectDataInitState } from './connect-data-reducer'; +import { updaterReducer } from './updater-reducer'; const browserLocalStorage: Storage = { getItem: async (key: string) => { @@ -106,6 +107,7 @@ const marinaReducer = combineReducers({ version: 1, initialState: connectDataInitState, }), + updater: updaterReducer, }); export default marinaReducer; diff --git a/src/application/redux/reducers/updater-reducer.ts b/src/application/redux/reducers/updater-reducer.ts new file mode 100644 index 00000000..8e284019 --- /dev/null +++ b/src/application/redux/reducers/updater-reducer.ts @@ -0,0 +1,36 @@ +import { AnyAction } from 'redux'; +import { AccountID } from '../../../domain/account'; +import { POP_UPDATER_TASK, PUSH_UPDATER_TASK } from '../actions/action-types'; + +export enum UpdaterTaskType { + TX = 0, + UTXO, +} + +export interface UpdaterTask { + accountID: AccountID; + type: UpdaterTaskType; +} + +export interface UpdaterState { + stack: UpdaterTask[]; +} + +export function updaterReducer( + state: UpdaterState = { stack: [] }, + { type, payload }: AnyAction +): UpdaterState { + switch (type) { + case PUSH_UPDATER_TASK: { + if (payload.updaterTask) return { ...state, stack: [...state.stack, payload.updaterTask] }; + return state; + } + + case POP_UPDATER_TASK: { + return { ...state, stack: state.stack.slice(0, -1) }; + } + + default: + return state; + } +} diff --git a/src/application/redux/reducers/wallet-reducer.ts b/src/application/redux/reducers/wallet-reducer.ts index f0484ec6..6ba4504d 100644 --- a/src/application/redux/reducers/wallet-reducer.ts +++ b/src/application/redux/reducers/wallet-reducer.ts @@ -21,7 +21,7 @@ export const walletInitState: WalletState = { }, restrictedAssetAccounts: {}, unspentsAndTransactions: { - MainAccountID: { + [MainAccountID]: { utxosMap: {}, transactions: { regtest: {}, liquid: {} }, }, @@ -103,7 +103,14 @@ export function walletReducer( ...state, restrictedAssetAccounts: { ...state.restrictedAssetAccounts, - [data.cosignerXPubs[0]]: data, + [data.signerXPub]: data, + }, + unspentsAndTransactions: { + ...state.unspentsAndTransactions, + [data.signerXPub]: { + utxosMap: {}, + transactions: { liquid: {}, regtest: {} }, + }, }, }; } diff --git a/src/application/redux/selectors/wallet.selector.ts b/src/application/redux/selectors/wallet.selector.ts index d9aaf249..8374d8b4 100644 --- a/src/application/redux/selectors/wallet.selector.ts +++ b/src/application/redux/selectors/wallet.selector.ts @@ -6,6 +6,7 @@ import { MultisigAccount, MultisigAccountData, MnemonicAccount, + MainAccountID, } from '../../../domain/account'; import { RootReducerState } from '../../../domain/common'; import { TxDisplayInterface } from '../../../domain/transaction'; @@ -40,23 +41,26 @@ export function selectMainAccount(state: RootReducerState): MnemonicAccount { return createMnemonicAccount(state.wallet.mainAccount, state.app.network); } -export const selectRestrictedAssetAccount = (cosignerXPub: XPub) => +const selectRestrictedAssetAccount = (signerXPub: AccountID) => function (state: RootReducerState): MultisigAccount { return createMultisigAccount( state.wallet.mainAccount.encryptedMnemonic, - state.wallet.restrictedAssetAccounts[cosignerXPub], - state.app.network + state.wallet.restrictedAssetAccounts[signerXPub] ); }; +export const selectAccount = (accountID: AccountID) => + accountID === MainAccountID ? selectMainAccount : selectRestrictedAssetAccount(accountID); + export function selectAllRestrictedAssetAccounts( state: RootReducerState ): MultisigAccountData[] { return Object.values(state.wallet.restrictedAssetAccounts); } -export const selectUnspentsAndTransactions = (accountID: AccountID) => (state: RootReducerState) => - state.wallet.unspentsAndTransactions[accountID] ?? { +export const selectUnspentsAndTransactions = (accountID: AccountID) => (state: RootReducerState) => { + return state.wallet.unspentsAndTransactions[accountID] ?? { utxosMap: {}, transactions: { regtest: {}, liquid: {} }, }; +} \ No newline at end of file diff --git a/src/application/redux/store.ts b/src/application/redux/store.ts index cfa8e47d..da793691 100644 --- a/src/application/redux/store.ts +++ b/src/application/redux/store.ts @@ -1,26 +1,30 @@ import { RESET, + RESET_APP, + RESET_CONNECT, + RESET_TAXI, + RESET_WALLET, START_DEEP_RESTORATION, START_PERIODIC_UPDATE, UPDATE_TAXI_ASSETS, - UPDATE_TXS, - UPDATE_UTXOS, } from './actions/action-types'; -import { createStore, applyMiddleware, Store } from 'redux'; +import { createStore, applyMiddleware, Store, AnyAction } from 'redux'; import { alias, wrapStore } from 'webext-redux'; import marinaReducer from './reducers'; -import { - fetchAndSetTaxiAssets, - makeTxsUpdaterThunk, - makeUtxosUpdaterThunk, - startAlarmUpdater, - deepRestorer, - resetAll, -} from './backend'; import persistStore from 'redux-persist/es/persistStore'; import { parse, stringify } from '../utils/browser-storage-converters'; -import thunk from 'redux-thunk'; -import { selectMainAccount } from './selectors/wallet.selector'; +import thunk, { ThunkAction } from 'redux-thunk'; +import { RootReducerState } from '../../domain/common'; +import { fetchAssetsFromTaxi, getStateRestorerOptsFromAddresses, taxiURL } from '../utils'; +import { setTaxiAssets, updateTaxiAssets } from './actions/taxi'; +import browser from 'webextension-polyfill'; +import { IdentityType, masterPubKeyRestorerFromEsplora, MasterPublicKey } from 'ldk'; +import { getExplorerURLSelector } from './selectors/app.selector'; +import { setDeepRestorerError, setDeepRestorerIsLoading, setWalletData } from './actions/wallet'; +import { createAddress } from '../../domain/address'; +import { flushTx } from './actions/connect'; +import { txsUpdateTask, utxosUpdateTask } from './actions/updater'; +import { MainAccountID } from '../../domain/account'; export const serializerAndDeserializer = { serializer: (payload: any) => stringify(payload), @@ -28,8 +32,6 @@ export const serializerAndDeserializer = { }; const backgroundAliases = { - [UPDATE_UTXOS]: () => makeUtxosUpdaterThunk(selectMainAccount), - [UPDATE_TXS]: () => makeTxsUpdaterThunk(selectMainAccount), [UPDATE_TAXI_ASSETS]: () => fetchAndSetTaxiAssets(), [START_PERIODIC_UPDATE]: () => startAlarmUpdater(), [START_DEEP_RESTORATION]: () => deepRestorer(), @@ -38,6 +40,108 @@ const backgroundAliases = { const create = () => createStore(marinaReducer, applyMiddleware(alias(backgroundAliases), thunk)); +// fetch assets from taxi daemon endpoint (make a grpc call) +// and then set assets in store. +function fetchAndSetTaxiAssets(): ThunkAction { + return async (dispatch, getState) => { + const state = getState(); + const assets = await fetchAssetsFromTaxi(taxiURL[state.app.network]); + + const currentAssets = state.taxi.taxiAssets; + const sortAndJoin = (a: string[]) => a.sort().join(''); + + if (sortAndJoin(currentAssets) === sortAndJoin(assets)) { + return; // skip if same assets state + } + + dispatch(setTaxiAssets(assets)); + }; +} + +// Start the periodic updater (for utxos and txs fetching) +export function startAlarmUpdater(): ThunkAction { + return (dispatch) => { + dispatch(utxosUpdateTask(MainAccountID)); + + browser.alarms.onAlarm.addListener((alarm) => { + switch (alarm.name) { + case 'UPDATE_ALARM': + dispatch(txsUpdateTask(MainAccountID)); + dispatch(utxosUpdateTask(MainAccountID)); + dispatch(updateTaxiAssets()); + break; + + default: + break; + } + }); + + browser.alarms.create('UPDATE_ALARM', { + when: Date.now(), + periodInMinutes: 1, + }); + }; +} + +// Using to generate addresses and use the explorer to test them +export function deepRestorer(): ThunkAction { + return async (dispatch, getState) => { + const state = getState(); + const { isLoading, gapLimit } = state.wallet.deepRestorer; + const toRestore = new MasterPublicKey({ + chain: state.app.network, + type: IdentityType.MasterPublicKey, + opts: { + masterPublicKey: state.wallet.mainAccount.masterXPub, + masterBlindingKey: state.wallet.mainAccount.masterBlindingKey, + }, + }); + const explorer = getExplorerURLSelector(getState()); + if (isLoading) return; + + try { + dispatch(setDeepRestorerIsLoading(true)); + const opts = { gapLimit, esploraURL: explorer }; + const publicKey = await masterPubKeyRestorerFromEsplora(toRestore)(opts); + const addresses = (await publicKey.getAddresses()).map((a) => + createAddress(a.confidentialAddress, a.derivationPath) + ); + + const restorerOpts = getStateRestorerOptsFromAddresses(addresses); + + dispatch( + setWalletData({ + ...state.wallet.mainAccount, + restorerOpts, + confidentialAddresses: addresses, + passwordHash: state.wallet.passwordHash, + }) + ); + + dispatch(utxosUpdateTask(MainAccountID)); + dispatch(txsUpdateTask(MainAccountID)); + dispatch(fetchAndSetTaxiAssets()); + + dispatch(setDeepRestorerError(undefined)); + } catch (err: any) { + dispatch(setDeepRestorerError(err.message || err)); + } finally { + dispatch(setDeepRestorerIsLoading(false)); + } + }; +} + +// reset all the reducers except the `assets` reducer (shared data). +export function resetAll(): ThunkAction { + return (dispatch) => { + dispatch({ type: RESET_TAXI }); + dispatch({ type: RESET_APP }); + dispatch({ type: RESET_WALLET }); + dispatch({ type: RESET_CONNECT }); + dispatch(flushTx()); + }; +} + export const marinaStore = create(); export const persistor = persistStore(marinaStore); diff --git a/src/application/utils/restorer.ts b/src/application/utils/restorer.ts index 30d026c8..bf8bb188 100644 --- a/src/application/utils/restorer.ts +++ b/src/application/utils/restorer.ts @@ -89,15 +89,18 @@ export function restoredMultisig( cosigner: Cosigner, network: Network ) { - const multisigID = new MultisigWithCosigner({ - chain: network, - type: IdentityType.Multisig, - opts: { - requiredSignatures, - signer, - cosigners, + const multisigID = new MultisigWithCosigner( + { + chain: network, + type: IdentityType.Multisig, + opts: { + requiredSignatures, + signer, + cosigners, + }, }, - }, cosigner); + cosigner + ); return restorerFromState(multisigID)(restorerOpts); } diff --git a/src/background/backend.ts b/src/background/backend.ts new file mode 100644 index 00000000..db7fdab6 --- /dev/null +++ b/src/background/backend.ts @@ -0,0 +1,188 @@ +import axios from 'axios'; +import { + address, + AddressInterface, + BlindingKeyGetter, + fetchAndUnblindTxsGenerator, + fetchAndUnblindUtxosGenerator, + isBlindedUtxo, + networks, + utxoWithPrevout, +} from 'ldk'; +import { Store } from 'redux'; +import { addAsset } from '../application/redux/actions/asset'; +import { addTx } from '../application/redux/actions/transaction'; +import { addUtxo, deleteUtxo } from '../application/redux/actions/utxos'; +import { getExplorerURLSelector } from '../application/redux/selectors/app.selector'; +import { selectUnspentsAndTransactions } from '../application/redux/selectors/wallet.selector'; +import { defaultPrecision, toDisplayTransaction, toStringOutpoint } from '../application/utils'; +import { Account } from '../domain/account'; +import { RootReducerState } from '../domain/common'; + +type AccountSelector = (state: RootReducerState) => Account; + +/** + * fetch the asset infos from explorer (ticker, precision etc...) + */ +async function fetchAssetInfos( + assetHash: string, + explorerUrl: string, + store: Store +) { + if (store.getState().assets[assetHash] !== undefined) return; // do not update + + const assetInfos = (await axios.get(`${explorerUrl}/asset/${assetHash}`)).data; + const name = assetInfos?.name ? assetInfos.name : 'Unknown'; + const ticker = assetInfos?.ticker ? assetInfos.ticker : assetHash.slice(0, 4).toUpperCase(); + const precision = assetInfos.precision !== undefined ? assetInfos.precision : defaultPrecision; + + store.dispatch(addAsset(assetHash, { name, ticker, precision })); +} + +async function getAddressesFromAccount(account: Account): Promise { + return (await account.getWatchIdentity()).getAddresses(); +} + +// fetch and unblind the utxos and then refresh it. +export function makeUtxosUpdater( + selectAccount: AccountSelector +): (store: Store) => Promise { + return async (store: Store) => { + try { + const state = store.getState(); + const dispatch = store.dispatch; + const { app } = state; + if (!app.isAuthenticated) return; + + const account = selectAccount(state); + const explorer = getExplorerURLSelector(state); + const currentCacheState = selectUnspentsAndTransactions(account.getAccountID())(state); + const utxosMap = currentCacheState === undefined ? {} : currentCacheState.utxosMap; + + const currentOutpoints = Object.values(utxosMap || {}).map(({ txid, vout }) => ({ + txid, + vout, + })); + + const skippedOutpoints: string[] = []; // for deleting + + // Fetch utxo(s). Return blinded utxo(s) if unblinding has been skipped + const utxos = fetchAndUnblindUtxosGenerator( + await getAddressesFromAccount(account), + explorer, + // Skip unblinding if utxo exists in current state + (utxo) => { + const outpoint = toStringOutpoint(utxo); + const skip = utxosMap[outpoint] !== undefined; + + if (skip) skippedOutpoints.push(toStringOutpoint(utxo)); + + return skip; + } + ); + + let utxoIterator = await utxos.next(); + while (!utxoIterator.done) { + let utxo = utxoIterator?.value; + if (!isBlindedUtxo(utxo)) { + if (utxo.asset) { + await fetchAssetInfos(utxo.asset, explorer, store).catch(console.error); + } + + if (!utxo.prevout) { + utxo = await utxoWithPrevout(utxo, explorer); + } + + dispatch(addUtxo(account.getAccountID(), utxo)); + } + utxoIterator = await utxos.next(); + } + + if (utxoIterator.done) { + console.info(`number of utxos fetched: ${utxoIterator.value.numberOfUtxos}`); + if (utxoIterator.value.errors.length > 0) { + console.warn( + `${utxoIterator.value.errors.length} errors occurs during utxos updater generator` + ); + } + } + + for (const outpoint of currentOutpoints) { + if (skippedOutpoints.includes(toStringOutpoint(outpoint))) continue; + // if not skipped, it means the utxo has been spent + dispatch(deleteUtxo(outpoint.txid, outpoint.vout)); + } + } catch (error) { + console.error(`fetchAndUpdateUtxos error: ${error}`); + } + }; +} + +/** + * use fetchAndUnblindTxsGenerator to update the tx history + */ +export function makeTxsUpdater( + selectAccount: AccountSelector +): (store: Store) => Promise { + return async (store: Store) => { + try { + const state = store.getState(); + const { app } = state; + if (!app.isAuthenticated) return; + + const account = selectAccount(state); + const txsHistory = selectUnspentsAndTransactions(account.getAccountID())(state).transactions[ + app.network + ]; + + // Initialize txs to txsHistory shallow clone + const addressInterfaces = await getAddressesFromAccount(account); + const walletScripts = addressInterfaces.map((a) => + address.toOutputScript(a.confidentialAddress).toString('hex') + ); + + const explorer = getExplorerURLSelector(state); + + const identityBlindKeyGetter: BlindingKeyGetter = (script: string) => { + try { + const addressFromScript = address.fromOutputScript( + Buffer.from(script, 'hex'), + networks[app.network] + ); + return addressInterfaces.find( + (addr) => + address.fromConfidential(addr.confidentialAddress).unconfidentialAddress === + addressFromScript + )?.blindingPrivateKey; + } catch (_) { + return undefined; + } + }; + + const txsGen = fetchAndUnblindTxsGenerator( + addressInterfaces.map((a) => a.confidentialAddress), + identityBlindKeyGetter, + explorer, + // Check if tx exists in React state + (tx) => txsHistory[tx.txid] !== undefined + ); + + let it = await txsGen.next(); + + // If no new tx already in state then return txsHistory of current network + if (it.done) { + return; + } + + while (!it.done) { + const tx = it.value; + // Update all txsHistory state at each single new tx + const toAdd = toDisplayTransaction(tx, walletScripts, networks[app.network]); + store.dispatch(addTx(account.getAccountID(), toAdd, app.network)); + it = await txsGen.next(); + } + } catch (error) { + console.error(`fetchAndUnblindTxs: ${error}`); + } + }; +} diff --git a/src/background/background-script.ts b/src/background/background-script.ts index a41b6ff8..deb90d6b 100644 --- a/src/background/background-script.ts +++ b/src/background/background-script.ts @@ -16,12 +16,14 @@ import { } from '../domain/message'; import { POPUP_RESPONSE } from '../presentation/connect/popupBroker'; import { INITIALIZE_WELCOME_ROUTE } from '../presentation/routes/constants'; +import { startUpdaterWorker } from './updater-worker'; // MUST be > 15 seconds const IDLE_TIMEOUT_IN_SECONDS = 300; // 5 minutes let welcomeTabID: number | undefined = undefined; wrapMarinaStore(marinaStore); // wrap store to proxy store +startUpdaterWorker(marinaStore); // start an async subscriber to store in order to handle updater task /** * Fired when the extension is first installed, when the extension is updated to a new version, diff --git a/src/background/updater-worker.ts b/src/background/updater-worker.ts new file mode 100644 index 00000000..5d522310 --- /dev/null +++ b/src/background/updater-worker.ts @@ -0,0 +1,38 @@ +import { Store, Unsubscribe } from 'redux'; +import { POP_UPDATER_TASK } from '../application/redux/actions/action-types'; +import { UpdaterTask, UpdaterTaskType } from '../application/redux/reducers/updater-reducer'; +import { selectAccount } from '../application/redux/selectors/wallet.selector'; +import { RootReducerState } from '../domain/common'; +import { makeTxsUpdater, makeUtxosUpdater } from './backend'; + +function nextUpdaterTask(store: Store): Promise { + const next = store.getState().updater.stack.pop(); + if (next) return Promise.resolve(next); + + let unsubscribe: Unsubscribe; + + return new Promise((resolve) => { + unsubscribe = store.subscribe(() => { + const next = store.getState().updater.stack.pop(); + if (next) return resolve(next); + }); + }).then((task) => { + unsubscribe(); + return task; + }); +} + +export async function startUpdaterWorker(store: Store): Promise { + console.warn('start updater worker') + while (true) { + try { + const nextTask = await nextUpdaterTask(store); // if stack = [] this freeze the loop + console.warn('next task:', nextTask); + store.dispatch({ type: POP_UPDATER_TASK }); // pop the task from the stack + const taskResolver = nextTask.type === UpdaterTaskType.TX ? makeTxsUpdater : makeUtxosUpdater; + await taskResolver(selectAccount(nextTask.accountID))(store); + } catch { + console.error('updater error') + } + } +} diff --git a/src/domain/account.ts b/src/domain/account.ts index 18faf528..d2688524 100644 --- a/src/domain/account.ts +++ b/src/domain/account.ts @@ -121,13 +121,13 @@ export async function create2of2MultisigAccountData( requiredSignature: 2, extraData, restorerOpts, - network + network, }; } export function createMultisigAccount( encryptedMnemonic: EncryptedMnemonic, - data: MultisigAccountData, + data: MultisigAccountData ): MultisigAccount { return { getAccountID: () => data.signerXPub, diff --git a/src/domain/common.ts b/src/domain/common.ts index 5b419827..f349d6e7 100644 --- a/src/domain/common.ts +++ b/src/domain/common.ts @@ -6,6 +6,7 @@ import { TransactionState } from '../application/redux/reducers/transaction-redu import { Action } from 'redux'; import { TaxiState } from '../application/redux/reducers/taxi-reducer'; import { IAssets } from './assets'; +import { UpdaterState } from '../application/redux/reducers/updater-reducer'; export interface RootReducerState { app: IApp; @@ -15,6 +16,7 @@ export interface RootReducerState { wallet: WalletState; connect: ConnectData; taxi: TaxiState; + updater: UpdaterState; } export interface ActionWithPayload extends Action { diff --git a/src/domain/cosigner.ts b/src/domain/cosigner.ts index 531f0f1d..b3be1c9e 100644 --- a/src/domain/cosigner.ts +++ b/src/domain/cosigner.ts @@ -26,8 +26,8 @@ export class MultisigWithCosigner extends Multisig implements IdentityInterface } async signPset(pset: string): Promise { - const signed = await super.signPset(pset) - return this.cosigner.signPset(signed) + const signed = await super.signPset(pset); + return this.cosigner.signPset(signed); } } @@ -56,12 +56,16 @@ export class MockedCosigner implements Cosigner { chain: network, type: IdentityType.Mnemonic, opts: { - mnemonic: 'sponsor envelope waste fork indicate board survey tobacco laugh cover guitar layer', - baseDerivationPath: DEFAULT_BASE_DERIVATION_PATH - } + mnemonic: + 'sponsor envelope waste fork indicate board survey tobacco laugh cover guitar layer', + baseDerivationPath: DEFAULT_BASE_DERIVATION_PATH, + }, }); this.network = network; - this.esploraURL = network === 'liquid' ? BlockstreamExplorerURLs.esploraURL : NigiriDefaultExplorerURLs.esploraURL; + this.esploraURL = + network === 'liquid' + ? BlockstreamExplorerURLs.esploraURL + : NigiriDefaultExplorerURLs.esploraURL; this.cosignerXPub = cosignerXPub; } diff --git a/src/presentation/components/shell-popup.tsx b/src/presentation/components/shell-popup.tsx index c119a541..9d67e05f 100644 --- a/src/presentation/components/shell-popup.tsx +++ b/src/presentation/components/shell-popup.tsx @@ -4,9 +4,10 @@ import ModalMenu from './modal-menu'; import { DEFAULT_ROUTE } from '../routes/constants'; import { useDispatch, useSelector } from 'react-redux'; import { ProxyStoreDispatch } from '../../application/redux/proxyStore'; -import { updateUtxos } from '../../application/redux/actions/utxos'; -import { flushPendingTx, updateTxs } from '../../application/redux/actions/transaction'; +import { flushPendingTx } from '../../application/redux/actions/transaction'; import { RootReducerState } from '../../domain/common'; +import { MainAccountID } from '../../domain/account'; +import { txsUpdateTask, utxosUpdateTask } from '../../application/redux/actions/updater'; interface Props { btnDisabled?: boolean; @@ -42,8 +43,8 @@ const ShellPopUp: React.FC = ({ const goToHome = async () => { // If already home, refresh state and return balances if (history.location.pathname === '/') { - dispatch(updateUtxos()).catch(console.error); - dispatch(updateTxs()).catch(console.error); + dispatch(utxosUpdateTask(MainAccountID)).catch(console.error); + dispatch(txsUpdateTask(MainAccountID)).catch(console.error); } await dispatch(flushPendingTx()); history.push(DEFAULT_ROUTE); diff --git a/src/presentation/wallet/receive/index.tsx b/src/presentation/wallet/receive/index.tsx index e964ebad..226c93ce 100644 --- a/src/presentation/wallet/receive/index.tsx +++ b/src/presentation/wallet/receive/index.tsx @@ -5,10 +5,10 @@ import Button from '../../components/button'; import ShellPopUp from '../../components/shell-popup'; import { formatAddress } from '../../utils'; import { useDispatch } from 'react-redux'; -import { updateUtxos } from '../../../application/redux/actions/utxos'; import { ProxyStoreDispatch } from '../../../application/redux/proxyStore'; import { incrementAddressIndex } from '../../../application/redux/actions/wallet'; import { Account } from '../../../domain/account'; +import { txsUpdateTask, utxosUpdateTask } from '../../../application/redux/actions/updater'; export interface ReceiveProps { account: Account; @@ -37,7 +37,8 @@ const ReceiveView: React.FC = ({ account }) => { setConfidentialAddress(addr.confidentialAddress); await dispatch(incrementAddressIndex(account.getAccountID())); // persist address setTimeout(() => { - dispatch(updateUtxos()).catch(console.error); + dispatch(utxosUpdateTask(account.getAccountID())).catch(console.error); + dispatch(txsUpdateTask(account.getAccountID())).catch(console.error); }, 8000); })().catch(console.error); }, []); diff --git a/src/presentation/wallet/send/end-of-flow.tsx b/src/presentation/wallet/send/end-of-flow.tsx index 7e63cf6b..7b0d97fc 100644 --- a/src/presentation/wallet/send/end-of-flow.tsx +++ b/src/presentation/wallet/send/end-of-flow.tsx @@ -40,7 +40,7 @@ const EndOfFlow: React.FC = ({ account, pset, explorerURL, recip history.push({ pathname: SEND_PAYMENT_SUCCESS_ROUTE, - state: { txid }, + state: { txid, accountID: account.getAccountID() }, }); } catch (error: unknown) { return history.push({ diff --git a/src/presentation/wallet/send/payment-success.tsx b/src/presentation/wallet/send/payment-success.tsx index d90ea474..c74069c8 100644 --- a/src/presentation/wallet/send/payment-success.tsx +++ b/src/presentation/wallet/send/payment-success.tsx @@ -6,12 +6,14 @@ import Button from '../../components/button'; import browser from 'webextension-polyfill'; import { DEFAULT_ROUTE } from '../../routes/constants'; import { useDispatch } from 'react-redux'; -import { flushPendingTx, updateTxs } from '../../../application/redux/actions/transaction'; +import { flushPendingTx } from '../../../application/redux/actions/transaction'; import { ProxyStoreDispatch } from '../../../application/redux/proxyStore'; -import { updateUtxos } from '../../../application/redux/actions/utxos'; +import { AccountID } from '../../../domain/account'; +import { txsUpdateTask, utxosUpdateTask } from '../../../application/redux/actions/updater'; interface LocationState { txid: string; + accountID: AccountID; } export interface PaymentSuccessProps { @@ -39,8 +41,8 @@ const PaymentSuccessView: React.FC = ({ electrsExplorerURL useEffect(() => { void (async () => { await dispatch(flushPendingTx()); - await dispatch(updateUtxos()).catch(console.error); - await dispatch(updateTxs()).catch(console.error); + await dispatch(utxosUpdateTask(state.accountID)).catch(console.error); + await dispatch(txsUpdateTask(state.accountID)).catch(console.error); })(); }, []); diff --git a/src/presentation/wallet/transactions/index.tsx b/src/presentation/wallet/transactions/index.tsx index 563bcc53..4746befd 100644 --- a/src/presentation/wallet/transactions/index.tsx +++ b/src/presentation/wallet/transactions/index.tsx @@ -18,7 +18,7 @@ import { imgPathMapMainnet, imgPathMapRegtest, txTypeAsString } from '../../../a import { fromSatoshiStr } from '../../utils'; import { TxDisplayInterface } from '../../../domain/transaction'; import { IAssets } from '../../../domain/assets'; -import { updateTxs, setAsset } from '../../../application/redux/actions/transaction'; +import { setAsset } from '../../../application/redux/actions/transaction'; import { useDispatch } from 'react-redux'; import { Network } from '../../../domain/network'; import { txHasAsset } from '../../../application/redux/selectors/transaction.selector'; @@ -75,11 +75,6 @@ const TransactionsView: React.FC = ({ await browser.tabs.create({ url, active: false }); }; - // Update txs history once at first render - useEffect(() => { - dispatch(updateTxs()).catch(console.error); - }, []); - return ( Date: Thu, 21 Oct 2021 14:09:18 +0200 Subject: [PATCH 20/52] fix deleteUtxo action --- src/application/redux/actions/utxos.ts | 4 ++-- .../redux/reducers/wallet-reducer.ts | 4 ++++ .../redux/selectors/wallet.selector.ts | 13 +++++++----- src/application/redux/store.ts | 21 ++++++++++++------- src/background/backend.ts | 3 ++- src/background/updater-worker.ts | 4 +--- 6 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/application/redux/actions/utxos.ts b/src/application/redux/actions/utxos.ts index eb5386ba..4794a1a6 100644 --- a/src/application/redux/actions/utxos.ts +++ b/src/application/redux/actions/utxos.ts @@ -7,8 +7,8 @@ export function addUtxo(accountID: AccountID, utxo: UtxoInterface): AnyAction { return { type: ADD_UTXO, payload: { accountID, utxo } }; } -export function deleteUtxo(txid: string, vout: number): AnyAction { - return { type: DELETE_UTXO, payload: { txid, vout } }; +export function deleteUtxo(accountID: AccountID, txid: string, vout: number): AnyAction { + return { type: DELETE_UTXO, payload: { txid, vout, accountID } }; } export function flushUtxos(accountID: AccountID): AnyAction { diff --git a/src/application/redux/reducers/wallet-reducer.ts b/src/application/redux/reducers/wallet-reducer.ts index 6ba4504d..fa5a4cf8 100644 --- a/src/application/redux/reducers/wallet-reducer.ts +++ b/src/application/redux/reducers/wallet-reducer.ts @@ -186,6 +186,10 @@ export function walletReducer( } case ACTION_TYPES.DELETE_UTXO: { + if (!state.unspentsAndTransactions[payload.accountID]) { + return state; + } + const { [toStringOutpoint({ txid: payload.txid, vout: payload.vout })]: deleted, ...utxosMap diff --git a/src/application/redux/selectors/wallet.selector.ts b/src/application/redux/selectors/wallet.selector.ts index 8374d8b4..21a1c5bb 100644 --- a/src/application/redux/selectors/wallet.selector.ts +++ b/src/application/redux/selectors/wallet.selector.ts @@ -58,9 +58,12 @@ export function selectAllRestrictedAssetAccounts( return Object.values(state.wallet.restrictedAssetAccounts); } -export const selectUnspentsAndTransactions = (accountID: AccountID) => (state: RootReducerState) => { - return state.wallet.unspentsAndTransactions[accountID] ?? { - utxosMap: {}, - transactions: { regtest: {}, liquid: {} }, +export const selectUnspentsAndTransactions = + (accountID: AccountID) => (state: RootReducerState) => { + return ( + state.wallet.unspentsAndTransactions[accountID] ?? { + utxosMap: {}, + transactions: { regtest: {}, liquid: {} }, + } + ); }; -} \ No newline at end of file diff --git a/src/application/redux/store.ts b/src/application/redux/store.ts index da793691..eeba776e 100644 --- a/src/application/redux/store.ts +++ b/src/application/redux/store.ts @@ -25,6 +25,7 @@ import { createAddress } from '../../domain/address'; import { flushTx } from './actions/connect'; import { txsUpdateTask, utxosUpdateTask } from './actions/updater'; import { MainAccountID } from '../../domain/account'; +import { extractErrorMessage } from '../../presentation/utils/error'; export const serializerAndDeserializer = { serializer: (payload: any) => stringify(payload), @@ -44,17 +45,21 @@ const create = () => createStore(marinaReducer, applyMiddleware(alias(background // and then set assets in store. function fetchAndSetTaxiAssets(): ThunkAction { return async (dispatch, getState) => { - const state = getState(); - const assets = await fetchAssetsFromTaxi(taxiURL[state.app.network]); + try { + const state = getState(); + const assets = await fetchAssetsFromTaxi(taxiURL[state.app.network]); - const currentAssets = state.taxi.taxiAssets; - const sortAndJoin = (a: string[]) => a.sort().join(''); + const currentAssets = state.taxi.taxiAssets; + const sortAndJoin = (a: string[]) => a.sort().join(''); - if (sortAndJoin(currentAssets) === sortAndJoin(assets)) { - return; // skip if same assets state - } + if (sortAndJoin(currentAssets) === sortAndJoin(assets)) { + return; // skip if same assets state + } - dispatch(setTaxiAssets(assets)); + dispatch(setTaxiAssets(assets)); + } catch (err) { + console.error('an error happen while fetching taxi assets:', extractErrorMessage(err)); + } }; } diff --git a/src/background/backend.ts b/src/background/backend.ts index db7fdab6..bcf0070e 100644 --- a/src/background/backend.ts +++ b/src/background/backend.ts @@ -16,6 +16,7 @@ import { addUtxo, deleteUtxo } from '../application/redux/actions/utxos'; import { getExplorerURLSelector } from '../application/redux/selectors/app.selector'; import { selectUnspentsAndTransactions } from '../application/redux/selectors/wallet.selector'; import { defaultPrecision, toDisplayTransaction, toStringOutpoint } from '../application/utils'; +import { stringify } from '../application/utils/browser-storage-converters'; import { Account } from '../domain/account'; import { RootReducerState } from '../domain/common'; @@ -110,7 +111,7 @@ export function makeUtxosUpdater( for (const outpoint of currentOutpoints) { if (skippedOutpoints.includes(toStringOutpoint(outpoint))) continue; // if not skipped, it means the utxo has been spent - dispatch(deleteUtxo(outpoint.txid, outpoint.vout)); + dispatch(deleteUtxo(account.getAccountID(), outpoint.txid, outpoint.vout)); } } catch (error) { console.error(`fetchAndUpdateUtxos error: ${error}`); diff --git a/src/background/updater-worker.ts b/src/background/updater-worker.ts index 5d522310..45543565 100644 --- a/src/background/updater-worker.ts +++ b/src/background/updater-worker.ts @@ -23,16 +23,14 @@ function nextUpdaterTask(store: Store): Promise { } export async function startUpdaterWorker(store: Store): Promise { - console.warn('start updater worker') while (true) { try { const nextTask = await nextUpdaterTask(store); // if stack = [] this freeze the loop - console.warn('next task:', nextTask); store.dispatch({ type: POP_UPDATER_TASK }); // pop the task from the stack const taskResolver = nextTask.type === UpdaterTaskType.TX ? makeTxsUpdater : makeUtxosUpdater; await taskResolver(selectAccount(nextTask.accountID))(store); } catch { - console.error('updater error') + console.error('updater error'); } } } From 8dbc7e22ef2819b4b77463b9bb2fdc8cba4bdba2 Mon Sep 17 00:00:00 2001 From: louisinger Date: Thu, 21 Oct 2021 14:27:28 +0200 Subject: [PATCH 21/52] cleaning and prettier --- src/application/redux/selectors/wallet.selector.ts | 2 +- src/background/backend.ts | 1 - src/background/background-script.ts | 6 +++++- src/background/updater-worker.ts | 6 +++++- src/presentation/components/shell-popup.tsx | 3 ++- src/presentation/wallet/transactions/index.tsx | 2 +- 6 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/application/redux/selectors/wallet.selector.ts b/src/application/redux/selectors/wallet.selector.ts index 21a1c5bb..1e3fa146 100644 --- a/src/application/redux/selectors/wallet.selector.ts +++ b/src/application/redux/selectors/wallet.selector.ts @@ -1,4 +1,4 @@ -import { MasterPublicKey, UtxoInterface, XPub } from 'ldk'; +import { MasterPublicKey, UtxoInterface } from 'ldk'; import { AccountID, createMnemonicAccount, diff --git a/src/background/backend.ts b/src/background/backend.ts index bcf0070e..87a64ed5 100644 --- a/src/background/backend.ts +++ b/src/background/backend.ts @@ -16,7 +16,6 @@ import { addUtxo, deleteUtxo } from '../application/redux/actions/utxos'; import { getExplorerURLSelector } from '../application/redux/selectors/app.selector'; import { selectUnspentsAndTransactions } from '../application/redux/selectors/wallet.selector'; import { defaultPrecision, toDisplayTransaction, toStringOutpoint } from '../application/utils'; -import { stringify } from '../application/utils/browser-storage-converters'; import { Account } from '../domain/account'; import { RootReducerState } from '../domain/common'; diff --git a/src/background/background-script.ts b/src/background/background-script.ts index deb90d6b..cf576439 100644 --- a/src/background/background-script.ts +++ b/src/background/background-script.ts @@ -16,6 +16,7 @@ import { } from '../domain/message'; import { POPUP_RESPONSE } from '../presentation/connect/popupBroker'; import { INITIALIZE_WELCOME_ROUTE } from '../presentation/routes/constants'; +import { extractErrorMessage } from '../presentation/utils/error'; import { startUpdaterWorker } from './updater-worker'; // MUST be > 15 seconds @@ -23,7 +24,10 @@ const IDLE_TIMEOUT_IN_SECONDS = 300; // 5 minutes let welcomeTabID: number | undefined = undefined; wrapMarinaStore(marinaStore); // wrap store to proxy store -startUpdaterWorker(marinaStore); // start an async subscriber to store in order to handle updater task +// start an async subscriber to store in order to handle updater task +startUpdaterWorker(marinaStore).catch((err) => + console.error(`CRITICAL: updater worker not started (${extractErrorMessage(err)})`) +); /** * Fired when the extension is first installed, when the extension is updated to a new version, diff --git a/src/background/updater-worker.ts b/src/background/updater-worker.ts index 45543565..6531a2d0 100644 --- a/src/background/updater-worker.ts +++ b/src/background/updater-worker.ts @@ -23,13 +23,17 @@ function nextUpdaterTask(store: Store): Promise { } export async function startUpdaterWorker(store: Store): Promise { - while (true) { + const maxConcurrentErrors = 10000; + let errorCount = 0; + while (errorCount < maxConcurrentErrors) { try { const nextTask = await nextUpdaterTask(store); // if stack = [] this freeze the loop store.dispatch({ type: POP_UPDATER_TASK }); // pop the task from the stack const taskResolver = nextTask.type === UpdaterTaskType.TX ? makeTxsUpdater : makeUtxosUpdater; await taskResolver(selectAccount(nextTask.accountID))(store); + errorCount = 0; } catch { + errorCount++; console.error('updater error'); } } diff --git a/src/presentation/components/shell-popup.tsx b/src/presentation/components/shell-popup.tsx index 9d67e05f..086c26d0 100644 --- a/src/presentation/components/shell-popup.tsx +++ b/src/presentation/components/shell-popup.tsx @@ -45,9 +45,10 @@ const ShellPopUp: React.FC = ({ if (history.location.pathname === '/') { dispatch(utxosUpdateTask(MainAccountID)).catch(console.error); dispatch(txsUpdateTask(MainAccountID)).catch(console.error); + } else { + history.push(DEFAULT_ROUTE); } await dispatch(flushPendingTx()); - history.push(DEFAULT_ROUTE); }; const handleBackBtn = () => { if (backBtnCb) { diff --git a/src/presentation/wallet/transactions/index.tsx b/src/presentation/wallet/transactions/index.tsx index 4746befd..b8452993 100644 --- a/src/presentation/wallet/transactions/index.tsx +++ b/src/presentation/wallet/transactions/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import browser from 'webextension-polyfill'; import { From ea42cff8e92f9ea7f154152d265b14356cb6bc00 Mon Sep 17 00:00:00 2001 From: louisinger Date: Thu, 21 Oct 2021 16:08:35 +0200 Subject: [PATCH 22/52] only one account --- src/application/redux/actions/action-types.ts | 2 +- src/application/redux/actions/wallet.ts | 6 +- .../redux/containers/cosigners.container.ts | 3 +- .../redux/reducers/wallet-reducer.ts | 91 ++++++----------- .../redux/selectors/wallet.selector.ts | 23 +++-- src/domain/account.ts | 8 +- src/domain/wallet.ts | 7 +- src/presentation/cosigner/pair.tsx | 4 +- src/presentation/routes/index.tsx | 98 +++++++++---------- src/presentation/wallet/receive/index.tsx | 11 +-- .../wallet/receive/receive-select-asset.tsx | 2 +- 11 files changed, 109 insertions(+), 146 deletions(-) diff --git a/src/application/redux/actions/action-types.ts b/src/application/redux/actions/action-types.ts index 9c2b10e4..cd1e09a1 100644 --- a/src/application/redux/actions/action-types.ts +++ b/src/application/redux/actions/action-types.ts @@ -1,6 +1,6 @@ // Wallet export const WALLET_SET_DATA = 'WALLET_SET_DATA'; -export const WALLET_ADD_RESTRICTED_ASSET_ACCOUNT = 'WALLET_ADD_RESTRICTED_ASSET_ACCOUNT'; +export const SET_RESTRICTED_ASSET_ACCOUNT = 'SET_RESTRICTED_ASSET_ACCOUNT'; export const ADD_UTXO = 'ADD_UTXO'; export const DELETE_UTXO = 'DELETE_UTXO'; export const FLUSH_UTXOS = 'FLUSH_UTXOS'; diff --git a/src/application/redux/actions/wallet.ts b/src/application/redux/actions/wallet.ts index c1ae23c1..608da45b 100644 --- a/src/application/redux/actions/wallet.ts +++ b/src/application/redux/actions/wallet.ts @@ -7,7 +7,7 @@ import { INCREMENT_EXTERNAL_ADDRESS_INDEX, INCREMENT_INTERNAL_ADDRESS_INDEX, SET_VERIFIED, - WALLET_ADD_RESTRICTED_ASSET_ACCOUNT, + SET_RESTRICTED_ASSET_ACCOUNT, } from './action-types'; import { AnyAction } from 'redux'; import { WalletData } from '../../utils/wallet'; @@ -15,11 +15,11 @@ import { extractErrorMessage } from '../../../presentation/utils/error'; import { AccountID, MultisigAccountData } from '../../../domain/account'; import { CosignerExtraData } from '../../../domain/wallet'; -export function addRestrictedAssetData( +export function setRestrictedAssetData( multisigAccountData: MultisigAccountData ) { return { - type: WALLET_ADD_RESTRICTED_ASSET_ACCOUNT, + type: SET_RESTRICTED_ASSET_ACCOUNT, payload: { multisigAccountData }, }; } diff --git a/src/application/redux/containers/cosigners.container.ts b/src/application/redux/containers/cosigners.container.ts index 37244cc5..ee3aea4f 100644 --- a/src/application/redux/containers/cosigners.container.ts +++ b/src/application/redux/containers/cosigners.container.ts @@ -3,11 +3,10 @@ import { RootReducerState } from '../../../domain/common'; import SettingsCosignersView, { SettingsCosignersProps, } from '../../../presentation/settings/cosigners'; -import { selectAllRestrictedAssetAccounts } from '../selectors/wallet.selector'; const SettingsCosigner = connect( (state: RootReducerState): SettingsCosignersProps => ({ - multisigAccountsData: selectAllRestrictedAssetAccounts(state), + multisigAccountsData: state.wallet.restrictedAssetAccount ? [state.wallet.restrictedAssetAccount] : [], }) )(SettingsCosignersView); diff --git a/src/application/redux/reducers/wallet-reducer.ts b/src/application/redux/reducers/wallet-reducer.ts index fa5a4cf8..9f7101e8 100644 --- a/src/application/redux/reducers/wallet-reducer.ts +++ b/src/application/redux/reducers/wallet-reducer.ts @@ -4,12 +4,12 @@ import * as ACTION_TYPES from '../actions/action-types'; import { CosignerExtraData, WalletState } from '../../../domain/wallet'; import { AnyAction } from 'redux'; import { UtxoInterface } from 'ldk'; -import { AccountID, MainAccountID, MultisigAccountData } from '../../../domain/account'; +import { AccountID, MainAccountID, MultisigAccountData, RestrictedAssetAccountID } from '../../../domain/account'; import { TxDisplayInterface } from '../../../domain/transaction'; import { Network } from '../../../domain/network'; export const walletInitState: WalletState = { - mainAccount: { + [MainAccountID]: { accountID: MainAccountID, encryptedMnemonic: '', masterBlindingKey: '', @@ -19,12 +19,16 @@ export const walletInitState: WalletState = { lastUsedInternalIndex: 0, }, }, - restrictedAssetAccounts: {}, + [RestrictedAssetAccountID]: undefined, unspentsAndTransactions: { [MainAccountID]: { utxosMap: {}, transactions: { regtest: {}, liquid: {} }, }, + [RestrictedAssetAccountID]: { + utxosMap: {}, + transactions: { regtest: {}, liquid: {} }, + } }, passwordHash: '', deepRestorer: { @@ -97,17 +101,14 @@ export function walletReducer( }; } - case ACTION_TYPES.WALLET_ADD_RESTRICTED_ASSET_ACCOUNT: { + case ACTION_TYPES.SET_RESTRICTED_ASSET_ACCOUNT: { const data = payload.multisigAccountData as MultisigAccountData; return { ...state, - restrictedAssetAccounts: { - ...state.restrictedAssetAccounts, - [data.signerXPub]: data, - }, + restrictedAssetAccount: data, unspentsAndTransactions: { ...state.unspentsAndTransactions, - [data.signerXPub]: { + [RestrictedAssetAccountID]: { utxosMap: {}, transactions: { liquid: {}, regtest: {} }, }, @@ -117,32 +118,14 @@ export function walletReducer( case ACTION_TYPES.INCREMENT_INTERNAL_ADDRESS_INDEX: { const accountID = payload.accountID as AccountID; - if (accountID === MainAccountID) { - return { - ...state, - mainAccount: { - ...state.mainAccount, - restorerOpts: { - ...state.mainAccount.restorerOpts, - lastUsedInternalIndex: - (state.mainAccount.restorerOpts.lastUsedInternalIndex ?? 0) + 1, - }, - }, - }; - } - return { ...state, - restrictedAssetAccounts: { - ...state.restrictedAssetAccounts, - [accountID]: { - ...state.restrictedAssetAccounts[accountID], - restorerOpts: { - ...state.restrictedAssetAccounts[accountID].restorerOpts, - lastUsedInternalIndex: - (state.restrictedAssetAccounts[accountID].restorerOpts.lastUsedInternalIndex ?? 0) + - 1, - }, + mainAccount: { + ...state.mainAccount, + restorerOpts: { + ...state.mainAccount.restorerOpts, + lastUsedInternalIndex: + (state.mainAccount.restorerOpts.lastUsedInternalIndex ?? 0) + 1, }, }, }; @@ -150,32 +133,14 @@ export function walletReducer( case ACTION_TYPES.INCREMENT_EXTERNAL_ADDRESS_INDEX: { const accountID = payload.accountID as AccountID; - if (accountID === MainAccountID) { - return { - ...state, - mainAccount: { - ...state.mainAccount, - restorerOpts: { - ...state.mainAccount.restorerOpts, - lastUsedExternalIndex: - (state.mainAccount.restorerOpts.lastUsedExternalIndex ?? 0) + 1, - }, - }, - }; - } - return { ...state, - restrictedAssetAccounts: { - ...state.restrictedAssetAccounts, - [accountID]: { - ...state.restrictedAssetAccounts[accountID], - restorerOpts: { - ...state.restrictedAssetAccounts[accountID].restorerOpts, - lastUsedExternalIndex: - (state.restrictedAssetAccounts[accountID].restorerOpts.lastUsedExternalIndex ?? 0) + - 1, - }, + [accountID]: { + ...state[accountID], + restorerOpts: { + ...state[accountID]?.restorerOpts, + lastUsedExternalIndex: + (state[accountID]?.restorerOpts.lastUsedExternalIndex ?? 0) + 1, }, }, }; @@ -186,21 +151,22 @@ export function walletReducer( } case ACTION_TYPES.DELETE_UTXO: { - if (!state.unspentsAndTransactions[payload.accountID]) { + const accountID = payload.accountID as AccountID; + if (!state.unspentsAndTransactions[accountID]) { return state; } const { [toStringOutpoint({ txid: payload.txid, vout: payload.vout })]: deleted, ...utxosMap - } = state.unspentsAndTransactions[payload.accountID].utxosMap; + } = state.unspentsAndTransactions[accountID].utxosMap; return { ...state, unspentsAndTransactions: { ...state.unspentsAndTransactions, [payload.accountID]: { - ...state.unspentsAndTransactions[payload.accountID], + ...state.unspentsAndTransactions[accountID], utxosMap, }, }, @@ -233,12 +199,13 @@ export function walletReducer( } case ACTION_TYPES.FLUSH_UTXOS: { + const accountID = payload.accountID as AccountID; return { ...state, unspentsAndTransactions: { ...state.unspentsAndTransactions, - [payload.accountID]: { - ...state.unspentsAndTransactions[payload.accountID], + [accountID]: { + ...state.unspentsAndTransactions[accountID], utxosMap: {}, }, }, diff --git a/src/application/redux/selectors/wallet.selector.ts b/src/application/redux/selectors/wallet.selector.ts index 1e3fa146..6b9e24c1 100644 --- a/src/application/redux/selectors/wallet.selector.ts +++ b/src/application/redux/selectors/wallet.selector.ts @@ -41,21 +41,20 @@ export function selectMainAccount(state: RootReducerState): MnemonicAccount { return createMnemonicAccount(state.wallet.mainAccount, state.app.network); } -const selectRestrictedAssetAccount = (signerXPub: AccountID) => - function (state: RootReducerState): MultisigAccount { - return createMultisigAccount( - state.wallet.mainAccount.encryptedMnemonic, - state.wallet.restrictedAssetAccounts[signerXPub] - ); - }; +function selectRestrictedAssetAccount(state: RootReducerState): MultisigAccount | undefined { + if (!state.wallet.restrictedAssetAccount) return undefined; + + return createMultisigAccount( + state.wallet.mainAccount.encryptedMnemonic, + state.wallet.restrictedAssetAccount + ); +}; export const selectAccount = (accountID: AccountID) => - accountID === MainAccountID ? selectMainAccount : selectRestrictedAssetAccount(accountID); + accountID === MainAccountID ? selectMainAccount : selectRestrictedAssetAccount; -export function selectAllRestrictedAssetAccounts( - state: RootReducerState -): MultisigAccountData[] { - return Object.values(state.wallet.restrictedAssetAccounts); +export const selectAccountForReceive = (asset: string) => (state: RootReducerState) => { + const assets = state.assets } export const selectUnspentsAndTransactions = diff --git a/src/domain/account.ts b/src/domain/account.ts index d2688524..f54dfb88 100644 --- a/src/domain/account.ts +++ b/src/domain/account.ts @@ -27,8 +27,10 @@ import { MasterXPub } from './master-extended-pub'; import { Network } from './network'; import { CosignerExtraData } from './wallet'; -export type AccountID = string; -export const MainAccountID: AccountID = 'main'; +export const MainAccountID = 'mainAccount'; +export const RestrictedAssetAccountID = 'restrictedAssetAccount'; + +export type AccountID = 'mainAccount' | 'restrictedAssetAccount'; /** * Account domain represents the keys of the User @@ -130,7 +132,7 @@ export function createMultisigAccount( data: MultisigAccountData ): MultisigAccount { return { - getAccountID: () => data.signerXPub, + getAccountID: () => RestrictedAssetAccountID, getSigningIdentity: (password: string) => restoredMultisig( { diff --git a/src/domain/wallet.ts b/src/domain/wallet.ts index 041b8ec9..b47b1085 100644 --- a/src/domain/wallet.ts +++ b/src/domain/wallet.ts @@ -1,11 +1,10 @@ -import { XPub } from 'ldk'; -import { AccountID, MnemonicAccountData, MultisigAccountData } from './account'; +import { AccountID, MainAccountID, MnemonicAccountData, MultisigAccountData, RestrictedAssetAccountID } from './account'; import { PasswordHash } from './password-hash'; import { UtxosAndTxsHistory } from './transaction'; export interface WalletState { - mainAccount: MnemonicAccountData; - restrictedAssetAccounts: Record>; + [MainAccountID]: MnemonicAccountData; + [RestrictedAssetAccountID]?: MultisigAccountData; unspentsAndTransactions: Record; passwordHash: PasswordHash; deepRestorer: { diff --git a/src/presentation/cosigner/pair.tsx b/src/presentation/cosigner/pair.tsx index 272680ca..cfa390e0 100644 --- a/src/presentation/cosigner/pair.tsx +++ b/src/presentation/cosigner/pair.tsx @@ -11,7 +11,7 @@ import { Cosigner, HDSignerToXPub, MockedCosigner } from '../../domain/cosigner' import { Network } from '../../domain/network'; import { useDispatch } from 'react-redux'; import { ProxyStoreDispatch } from '../../application/redux/proxyStore'; -import { addRestrictedAssetData } from '../../application/redux/actions/wallet'; +import { setRestrictedAssetData } from '../../application/redux/actions/wallet'; import { DEFAULT_BASE_DERIVATION_PATH } from 'ldk'; import { useHistory } from 'react-router'; import { PAIR_SUCCESS_COSIGNER_ROUTE } from '../routes/constants'; @@ -121,7 +121,7 @@ const PairCosignerView: React.FC = ({ explorerURL ); - await dispatch(addRestrictedAssetData(multisigAccountData)); + await dispatch(setRestrictedAssetData(multisigAccountData)); history.push(PAIR_SUCCESS_COSIGNER_ROUTE); }; diff --git a/src/presentation/routes/index.tsx b/src/presentation/routes/index.tsx index 1ae20482..0e933a40 100644 --- a/src/presentation/routes/index.tsx +++ b/src/presentation/routes/index.tsx @@ -88,56 +88,56 @@ import PairSuccess from '../cosigner/pair-success'; const Routes: React.FC = () => { return ( - - {/*Onboarding*/} - - - - - - - - - {/*Wallet*/} - - - - - - - - - - - - {/*Settings*/} - - - - - - - - - - - - - {/*Login*/} - - {/*Connect*/} - - - - + + {/*Onboarding*/} + + + + + + + + + {/*Wallet*/} + + + + + + + + + + + + {/*Settings*/} + + + + + + + + + + + + + {/*Login*/} + + {/*Connect*/} + + + + - - - + + + ); }; diff --git a/src/presentation/wallet/receive/index.tsx b/src/presentation/wallet/receive/index.tsx index 226c93ce..a77e69ec 100644 --- a/src/presentation/wallet/receive/index.tsx +++ b/src/presentation/wallet/receive/index.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { useHistory } from 'react-router-dom'; +import { RouteComponentProps, useHistory } from 'react-router-dom'; import QRCode from 'qrcode.react'; import Button from '../../components/button'; import ShellPopUp from '../../components/shell-popup'; @@ -7,17 +7,14 @@ import { formatAddress } from '../../utils'; import { useDispatch } from 'react-redux'; import { ProxyStoreDispatch } from '../../../application/redux/proxyStore'; import { incrementAddressIndex } from '../../../application/redux/actions/wallet'; -import { Account } from '../../../domain/account'; import { txsUpdateTask, utxosUpdateTask } from '../../../application/redux/actions/updater'; -export interface ReceiveProps { - account: Account; -} - -const ReceiveView: React.FC = ({ account }) => { +const ReceiveView: React.FC> = ({ match }) => { const history = useHistory(); const dispatch = useDispatch(); + const account = useSelector() + const [confidentialAddress, setConfidentialAddress] = useState(''); const [buttonText, setButtonText] = useState('Copy'); const [isAddressExpanded, setAddressExpanded] = useState(false); diff --git a/src/presentation/wallet/receive/receive-select-asset.tsx b/src/presentation/wallet/receive/receive-select-asset.tsx index 4a3a4e8a..a298bebd 100644 --- a/src/presentation/wallet/receive/receive-select-asset.tsx +++ b/src/presentation/wallet/receive/receive-select-asset.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useHistory } from 'react-router'; +import { RouteComponentProps, useHistory } from 'react-router'; import { RECEIVE_ADDRESS_ROUTE } from '../../routes/constants'; import { Network } from '../../../domain/network'; import { Asset } from '../../../domain/assets'; From 9095aefde6e9c305098f4511f9a4c792f5243404 Mon Sep 17 00:00:00 2001 From: louisinger Date: Fri, 22 Oct 2021 13:15:53 +0200 Subject: [PATCH 23/52] balances for several accounts --- .../containers/address-amount.container.ts | 4 ++-- .../redux/containers/choose-fee.container.ts | 4 ++-- .../redux/containers/home.container.ts | 4 ++-- .../receive-select-asset.container.ts | 5 +++-- .../redux/containers/receive.container.ts | 12 ----------- .../containers/send-select-asset.container.ts | 4 ++-- .../containers/settings-networks.container.ts | 3 ++- .../redux/reducers/wallet-reducer.ts | 8 ++++---- .../redux/selectors/balance.selector.ts | 11 +++++++++- .../redux/selectors/wallet.selector.ts | 9 ++++++--- src/application/redux/store.ts | 9 ++++++--- src/application/utils/balances.ts | 19 ++++++++++++++++++ src/background/backend.ts | 4 +++- src/presentation/routes/index.tsx | 4 ++-- src/presentation/wallet/receive/index.tsx | 13 ++++++++---- .../wallet/receive/receive-select-asset.tsx | 20 ++++++++++++++----- 16 files changed, 87 insertions(+), 46 deletions(-) delete mode 100644 src/application/redux/containers/receive.container.ts create mode 100644 src/application/utils/balances.ts diff --git a/src/application/redux/containers/address-amount.container.ts b/src/application/redux/containers/address-amount.container.ts index b087d587..f2da6026 100644 --- a/src/application/redux/containers/address-amount.container.ts +++ b/src/application/redux/containers/address-amount.container.ts @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { MainAccountID } from '../../../domain/account'; +import { MainAccountID, RestrictedAssetAccountID } from '../../../domain/account'; import { assetGetterFromIAssets } from '../../../domain/assets'; import { RootReducerState } from '../../../domain/common'; import AddressAmountView, { @@ -13,7 +13,7 @@ const mapStateToProps = (state: RootReducerState): AddressAmountProps => ({ network: state.app.network, transaction: state.transaction, assets: state.assets, - balances: selectBalances(MainAccountID)(state), + balances: selectBalances(MainAccountID, RestrictedAssetAccountID)(state), transactionAsset: assetGetterFromIAssets(state.assets)(state.transaction.sendAsset), }); diff --git a/src/application/redux/containers/choose-fee.container.ts b/src/application/redux/containers/choose-fee.container.ts index 373a99b7..b8c2dc02 100644 --- a/src/application/redux/containers/choose-fee.container.ts +++ b/src/application/redux/containers/choose-fee.container.ts @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { MainAccountID } from '../../../domain/account'; +import { MainAccountID, RestrictedAssetAccountID } from '../../../domain/account'; import { RootReducerState } from '../../../domain/common'; import ChooseFeeView, { ChooseFeeProps } from '../../../presentation/wallet/send/choose-fee'; import { lbtcAssetByNetwork } from '../../utils'; @@ -9,7 +9,7 @@ import { selectMainAccount, selectUtxos } from '../selectors/wallet.selector'; const mapStateToProps = (state: RootReducerState): ChooseFeeProps => ({ network: state.app.network, assets: state.assets, - balances: selectBalances(MainAccountID)(state), + balances: selectBalances(MainAccountID, RestrictedAssetAccountID)(state), taxiAssets: state.taxi.taxiAssets, lbtcAssetHash: lbtcAssetByNetwork(state.app.network), sendAddress: state.transaction.sendAddress, diff --git a/src/application/redux/containers/home.container.ts b/src/application/redux/containers/home.container.ts index 6e5522c1..3c7e3152 100644 --- a/src/application/redux/containers/home.container.ts +++ b/src/application/redux/containers/home.container.ts @@ -4,13 +4,13 @@ import HomeView, { HomeProps } from '../../../presentation/wallet/home'; import { selectBalances } from '../selectors/balance.selector'; import { assetGetterFromIAssets } from '../../../domain/assets'; import { lbtcAssetByNetwork } from '../../utils'; -import { MainAccountID } from '../../../domain/account'; +import { MainAccountID, RestrictedAssetAccountID } from '../../../domain/account'; const mapStateToProps = (state: RootReducerState): HomeProps => ({ lbtcAssetHash: lbtcAssetByNetwork(state.app.network), network: state.app.network, transactionStep: state.transaction.step, - assetsBalance: selectBalances(MainAccountID)(state), + assetsBalance: selectBalances(MainAccountID, RestrictedAssetAccountID)(state), getAsset: assetGetterFromIAssets(state.assets), isWalletVerified: state.wallet.isVerified, }); diff --git a/src/application/redux/containers/receive-select-asset.container.ts b/src/application/redux/containers/receive-select-asset.container.ts index fe26deb9..82c729bd 100644 --- a/src/application/redux/containers/receive-select-asset.container.ts +++ b/src/application/redux/containers/receive-select-asset.container.ts @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { MainAccountID } from '../../../domain/account'; +import { MainAccountID, RestrictedAssetAccountID } from '../../../domain/account'; import { assetGetterFromIAssets } from '../../../domain/assets'; import { RootReducerState } from '../../../domain/common'; import ReceiveSelectAssetView, { @@ -8,11 +8,12 @@ import ReceiveSelectAssetView, { import { selectBalances } from '../selectors/balance.selector'; const mapStateToProps = (state: RootReducerState): ReceiveSelectAssetProps => { - const balances = selectBalances(MainAccountID)(state); + const balances = selectBalances(MainAccountID, RestrictedAssetAccountID)(state); const getAsset = assetGetterFromIAssets(state.assets); return { network: state.app.network, assets: Object.keys(balances).map(getAsset), + restrictedAssetSetup: state.wallet.restrictedAssetAccount !== undefined }; }; diff --git a/src/application/redux/containers/receive.container.ts b/src/application/redux/containers/receive.container.ts deleted file mode 100644 index 851c6033..00000000 --- a/src/application/redux/containers/receive.container.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { connect } from 'react-redux'; -import { RootReducerState } from '../../../domain/common'; -import ReceiveView, { ReceiveProps } from '../../../presentation/wallet/receive'; -import { selectMainAccount } from '../selectors/wallet.selector'; - -const mapStateToProps = (state: RootReducerState): ReceiveProps => ({ - account: selectMainAccount(state), -}); - -const Receive = connect(mapStateToProps)(ReceiveView); - -export default Receive; diff --git a/src/application/redux/containers/send-select-asset.container.ts b/src/application/redux/containers/send-select-asset.container.ts index 148e4ac6..ea2b6cc6 100644 --- a/src/application/redux/containers/send-select-asset.container.ts +++ b/src/application/redux/containers/send-select-asset.container.ts @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { MainAccountID } from '../../../domain/account'; +import { MainAccountID, RestrictedAssetAccountID } from '../../../domain/account'; import { assetGetterFromIAssets } from '../../../domain/assets'; import { RootReducerState } from '../../../domain/common'; import SendSelectAssetView, { @@ -8,7 +8,7 @@ import SendSelectAssetView, { import { selectBalances } from '../selectors/balance.selector'; const mapStateToProps = (state: RootReducerState): SendSelectAssetProps => { - const balances = selectBalances(MainAccountID)(state); + const balances = selectBalances(MainAccountID, RestrictedAssetAccountID)(state); const getAsset = assetGetterFromIAssets(state.assets); return { network: state.app.network, diff --git a/src/application/redux/containers/settings-networks.container.ts b/src/application/redux/containers/settings-networks.container.ts index d9442b65..27df4e77 100644 --- a/src/application/redux/containers/settings-networks.container.ts +++ b/src/application/redux/containers/settings-networks.container.ts @@ -3,10 +3,11 @@ import SettingsNetworksView, { } from './../../../presentation/settings/networks'; import { connect } from 'react-redux'; import { RootReducerState } from '../../../domain/common'; +import { MainAccountID, RestrictedAssetAccountID } from '../../../domain/account'; const mapStateToProps = (state: RootReducerState): SettingsNetworksProps => ({ restorationLoading: state.wallet.deepRestorer.isLoading, - accountsIDs: Object.keys(state.wallet.unspentsAndTransactions), + accountsIDs: [RestrictedAssetAccountID, MainAccountID], error: state.wallet.deepRestorer.error, }); diff --git a/src/application/redux/reducers/wallet-reducer.ts b/src/application/redux/reducers/wallet-reducer.ts index 9f7101e8..b25225b5 100644 --- a/src/application/redux/reducers/wallet-reducer.ts +++ b/src/application/redux/reducers/wallet-reducer.ts @@ -120,12 +120,12 @@ export function walletReducer( const accountID = payload.accountID as AccountID; return { ...state, - mainAccount: { - ...state.mainAccount, + [accountID]: { + ...state[accountID], restorerOpts: { - ...state.mainAccount.restorerOpts, + ...state[accountID]?.restorerOpts, lastUsedInternalIndex: - (state.mainAccount.restorerOpts.lastUsedInternalIndex ?? 0) + 1, + (state[accountID]?.restorerOpts.lastUsedInternalIndex ?? 0) + 1, }, }, }; diff --git a/src/application/redux/selectors/balance.selector.ts b/src/application/redux/selectors/balance.selector.ts index 3d2b2dd0..719e5a79 100644 --- a/src/application/redux/selectors/balance.selector.ts +++ b/src/application/redux/selectors/balance.selector.ts @@ -1,15 +1,24 @@ import { AccountID } from '../../../domain/account'; import { RootReducerState } from '../../../domain/common'; import { lbtcAssetByNetwork } from '../../utils'; +import { sumBalances } from '../../utils/balances'; import { selectTransactions, selectUtxos } from './wallet.selector'; export type BalancesByAsset = { [assetHash: string]: number }; + +export const selectBalances = (...accounts: AccountID[]) => { + const selectors = accounts.map(id => selectBalancesForAccount(id)) + return (state: RootReducerState) => { + return sumBalances(...selectors.map(select => select(state))); + } +} + /** * Extract balances from all unblinded utxos in state * @param onSuccess * @param onError */ -export const selectBalances = +const selectBalancesForAccount = (accountID: AccountID) => (state: RootReducerState): BalancesByAsset => { const utxos = selectUtxos(accountID)(state); diff --git a/src/application/redux/selectors/wallet.selector.ts b/src/application/redux/selectors/wallet.selector.ts index 6b9e24c1..a5f20b71 100644 --- a/src/application/redux/selectors/wallet.selector.ts +++ b/src/application/redux/selectors/wallet.selector.ts @@ -4,13 +4,11 @@ import { createMnemonicAccount, createMultisigAccount, MultisigAccount, - MultisigAccountData, MnemonicAccount, MainAccountID, } from '../../../domain/account'; import { RootReducerState } from '../../../domain/common'; import { TxDisplayInterface } from '../../../domain/transaction'; -import { CosignerExtraData } from '../../../domain/wallet'; export function masterPubKeySelector(state: RootReducerState): Promise { return selectMainAccount(state).getWatchIdentity(); @@ -54,7 +52,12 @@ export const selectAccount = (accountID: AccountID) => accountID === MainAccountID ? selectMainAccount : selectRestrictedAssetAccount; export const selectAccountForReceive = (asset: string) => (state: RootReducerState) => { - const assets = state.assets + // TODO hardcode restricted asset hashes + if (asset === 'restricted_asset') { + return selectRestrictedAssetAccount(state); + } + + return selectMainAccount(state); } export const selectUnspentsAndTransactions = diff --git a/src/application/redux/store.ts b/src/application/redux/store.ts index eeba776e..9578b8f9 100644 --- a/src/application/redux/store.ts +++ b/src/application/redux/store.ts @@ -24,7 +24,7 @@ import { setDeepRestorerError, setDeepRestorerIsLoading, setWalletData } from '. import { createAddress } from '../../domain/address'; import { flushTx } from './actions/connect'; import { txsUpdateTask, utxosUpdateTask } from './actions/updater'; -import { MainAccountID } from '../../domain/account'; +import { AccountID, MainAccountID, RestrictedAssetAccountID } from '../../domain/account'; import { extractErrorMessage } from '../../presentation/utils/error'; export const serializerAndDeserializer = { @@ -71,8 +71,11 @@ export function startAlarmUpdater(): ThunkAction { switch (alarm.name) { case 'UPDATE_ALARM': - dispatch(txsUpdateTask(MainAccountID)); - dispatch(utxosUpdateTask(MainAccountID)); + ([MainAccountID, RestrictedAssetAccountID] as AccountID[]).forEach((ID: AccountID) => { + dispatch(utxosUpdateTask(ID)); + dispatch(txsUpdateTask(ID)); + }) + dispatch(updateTaxiAssets()); break; diff --git a/src/application/utils/balances.ts b/src/application/utils/balances.ts new file mode 100644 index 00000000..db0f4b92 --- /dev/null +++ b/src/application/utils/balances.ts @@ -0,0 +1,19 @@ +import { BalancesByAsset } from "../redux/selectors/balance.selector"; + +const addBalance = (toAdd: BalancesByAsset) => (base: BalancesByAsset) => { + const result = base; + for (const asset of Object.keys(toAdd)) { + result[asset] = (result[asset] ?? 0) + toAdd[asset]; + } + + return result; +} + +export const sumBalances = (...balances: BalancesByAsset[]) => { + const [balance, ...rest] = balances; + let result = balance; + const addFns = rest.map(addBalance); + addFns.forEach((f) => result = f(result)); + + return result; +} \ No newline at end of file diff --git a/src/background/backend.ts b/src/background/backend.ts index 87a64ed5..dae00a9e 100644 --- a/src/background/backend.ts +++ b/src/background/backend.ts @@ -19,7 +19,7 @@ import { defaultPrecision, toDisplayTransaction, toStringOutpoint } from '../app import { Account } from '../domain/account'; import { RootReducerState } from '../domain/common'; -type AccountSelector = (state: RootReducerState) => Account; +type AccountSelector = (state: RootReducerState) => Account | undefined; /** * fetch the asset infos from explorer (ticker, precision etc...) @@ -55,6 +55,7 @@ export function makeUtxosUpdater( if (!app.isAuthenticated) return; const account = selectAccount(state); + if (!account) return; const explorer = getExplorerURLSelector(state); const currentCacheState = selectUnspentsAndTransactions(account.getAccountID())(state); const utxosMap = currentCacheState === undefined ? {} : currentCacheState.utxosMap; @@ -131,6 +132,7 @@ export function makeTxsUpdater( if (!app.isAuthenticated) return; const account = selectAccount(state); + if (!account) return; const txsHistory = selectUnspentsAndTransactions(account.getAccountID())(state).transactions[ app.network ]; diff --git a/src/presentation/routes/index.tsx b/src/presentation/routes/index.tsx index 0e933a40..f49965c9 100644 --- a/src/presentation/routes/index.tsx +++ b/src/presentation/routes/index.tsx @@ -61,7 +61,6 @@ import BackUpUnlock from '../onboarding/backup-unlock'; import Home from '../../application/redux/containers/home.container'; import LogIn from '../wallet/log-in'; import Transactions from '../../application/redux/containers/transactions.container'; -import ReceiveAddress from '../../application/redux/containers/receive.container'; import ReceiveSelectAsset from '../../application/redux/containers/receive-select-asset.container'; import SendSelectAsset from '../../application/redux/containers/send-select-asset.container'; import AddressAmount from '../../application/redux/containers/address-amount.container'; @@ -85,6 +84,7 @@ import SettingsTerms from '../settings/terms'; import PairCosigner from '../../application/redux/containers/pair.container'; import SettingsCosigner from '../../application/redux/containers/cosigners.container'; import PairSuccess from '../cosigner/pair-success'; +import ReceiveView from '../wallet/receive'; const Routes: React.FC = () => { return ( @@ -102,7 +102,7 @@ const Routes: React.FC = () => { - + diff --git a/src/presentation/wallet/receive/index.tsx b/src/presentation/wallet/receive/index.tsx index a77e69ec..7822e4bf 100644 --- a/src/presentation/wallet/receive/index.tsx +++ b/src/presentation/wallet/receive/index.tsx @@ -4,16 +4,17 @@ import QRCode from 'qrcode.react'; import Button from '../../components/button'; import ShellPopUp from '../../components/shell-popup'; import { formatAddress } from '../../utils'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { ProxyStoreDispatch } from '../../../application/redux/proxyStore'; import { incrementAddressIndex } from '../../../application/redux/actions/wallet'; import { txsUpdateTask, utxosUpdateTask } from '../../../application/redux/actions/updater'; +import { selectAccountForReceive } from '../../../application/redux/selectors/wallet.selector'; const ReceiveView: React.FC> = ({ match }) => { const history = useHistory(); const dispatch = useDispatch(); - const account = useSelector() + const account = useSelector(selectAccountForReceive(match.params.asset)) const [confidentialAddress, setConfidentialAddress] = useState(''); const [buttonText, setButtonText] = useState('Copy'); @@ -29,8 +30,12 @@ const ReceiveView: React.FC> = ({ match } useEffect(() => { (async () => { - const publicKey = await account.getWatchIdentity(); - const addr = await publicKey.getNextAddress(); + if (account === undefined) { + throw new Error('multisig account for restricted asset is not set') + } + + const identity = await account.getWatchIdentity(); + const addr = await identity.getNextAddress(); setConfidentialAddress(addr.confidentialAddress); await dispatch(incrementAddressIndex(account.getAccountID())); // persist address setTimeout(() => { diff --git a/src/presentation/wallet/receive/receive-select-asset.tsx b/src/presentation/wallet/receive/receive-select-asset.tsx index a298bebd..90f6eccc 100644 --- a/src/presentation/wallet/receive/receive-select-asset.tsx +++ b/src/presentation/wallet/receive/receive-select-asset.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { RouteComponentProps, useHistory } from 'react-router'; +import { useHistory } from 'react-router'; import { RECEIVE_ADDRESS_ROUTE } from '../../routes/constants'; import { Network } from '../../../domain/network'; import { Asset } from '../../../domain/assets'; @@ -8,13 +8,14 @@ import AssetListScreen from '../../components/asset-list-screen'; export interface ReceiveSelectAssetProps { network: Network; assets: Array; + restrictedAssetSetup: boolean; } -const ReceiveSelectAssetView: React.FC = ({ network, assets }) => { +const ReceiveSelectAssetView: React.FC = ({ network, assets, restrictedAssetSetup }) => { const history = useHistory(); - const handleSend = (_: string) => { - return Promise.resolve(history.push(RECEIVE_ADDRESS_ROUTE)); + const handleSend = (asset: string) => { + return Promise.resolve(history.push(`${RECEIVE_ADDRESS_ROUTE}/${asset}`)); }; return ( @@ -22,7 +23,9 @@ const ReceiveSelectAssetView: React.FC = ({ network, as title="Receive Asset" onClick={handleSend} network={network} - assets={[UnknowAsset].concat(assets)} + assets={[UnknowAsset] + .concat(assets) + .concat(restrictedAssetSetup ? [RestrictedAsset] : [])} /> ); }; @@ -34,4 +37,11 @@ const UnknowAsset: Asset & { assetHash: string } = { assetHash: 'new_asset', }; +const RestrictedAsset: Asset & { assetHash: string } = { + ticker: 'Any', + name: 'Restricted assets', + precision: 8, + assetHash: 'restricted_asset', +}; + export default ReceiveSelectAssetView; From b50eecb03b2429f3ce7d01498ace3a1785de7268 Mon Sep 17 00:00:00 2001 From: louisinger Date: Fri, 22 Oct 2021 13:24:05 +0200 Subject: [PATCH 24/52] selectTransactions and selectUtxos for all accounts --- .../redux/containers/choose-fee.container.ts | 2 +- .../redux/containers/transactions.container.ts | 4 ++-- src/application/redux/selectors/wallet.selector.ts | 12 ++++++++++-- src/presentation/wallet/send/choose-fee.tsx | 8 ++++---- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/application/redux/containers/choose-fee.container.ts b/src/application/redux/containers/choose-fee.container.ts index b8c2dc02..f1bcea83 100644 --- a/src/application/redux/containers/choose-fee.container.ts +++ b/src/application/redux/containers/choose-fee.container.ts @@ -17,7 +17,7 @@ const mapStateToProps = (state: RootReducerState): ChooseFeeProps => ({ sendAsset: state.transaction.sendAsset, sendAmount: state.transaction.sendAmount, account: selectMainAccount(state), - mainAccountUtxos: selectUtxos(MainAccountID)(state), + utxos: selectUtxos(MainAccountID, RestrictedAssetAccountID)(state), }); const ChooseFee = connect(mapStateToProps)(ChooseFeeView); diff --git a/src/application/redux/containers/transactions.container.ts b/src/application/redux/containers/transactions.container.ts index f5c3653d..961c760e 100644 --- a/src/application/redux/containers/transactions.container.ts +++ b/src/application/redux/containers/transactions.container.ts @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { MainAccountID } from '../../../domain/account'; +import { MainAccountID, RestrictedAssetAccountID } from '../../../domain/account'; import { RootReducerState } from '../../../domain/common'; import TransactionsView, { TransactionsProps } from '../../../presentation/wallet/transactions'; import { selectTransactions } from '../selectors/wallet.selector'; @@ -7,7 +7,7 @@ import { selectTransactions } from '../selectors/wallet.selector'; const mapStateToProps = (state: RootReducerState): TransactionsProps => ({ assets: state.assets, network: state.app.network, - transactions: selectTransactions(MainAccountID)(state), + transactions: selectTransactions(MainAccountID, RestrictedAssetAccountID)(state), webExplorerURL: state.app.explorerByNetwork[state.app.network].electrsURL, }); diff --git a/src/application/redux/selectors/wallet.selector.ts b/src/application/redux/selectors/wallet.selector.ts index a5f20b71..173ab2ae 100644 --- a/src/application/redux/selectors/wallet.selector.ts +++ b/src/application/redux/selectors/wallet.selector.ts @@ -14,13 +14,21 @@ export function masterPubKeySelector(state: RootReducerState): Promise (state: RootReducerState): UtxoInterface[] => { + return accounts.flatMap(ID => selectUtxosForAccount(ID)(state)); +} + +const selectUtxosForAccount = (accountID: AccountID) => (state: RootReducerState): UtxoInterface[] => { return Object.values(selectUnspentsAndTransactions(accountID)(state).utxosMap); }; -export const selectTransactions = +export const selectTransactions = (...accounts: AccountID[]) => (state: RootReducerState) => { + return accounts.flatMap(ID => selectTransactionsForAccount(ID)(state)); +} + +const selectTransactionsForAccount = (accountID: AccountID) => (state: RootReducerState): TxDisplayInterface[] => { return Object.values( diff --git a/src/presentation/wallet/send/choose-fee.tsx b/src/presentation/wallet/send/choose-fee.tsx index 60add1d0..bf035d99 100644 --- a/src/presentation/wallet/send/choose-fee.tsx +++ b/src/presentation/wallet/send/choose-fee.tsx @@ -43,7 +43,7 @@ export interface ChooseFeeProps { taxiAssets: string[]; lbtcAssetHash: string; account: Account; - mainAccountUtxos: UtxoInterface[]; + utxos: UtxoInterface[]; } const ChooseFeeView: React.FC = ({ @@ -57,7 +57,7 @@ const ChooseFeeView: React.FC = ({ taxiAssets, lbtcAssetHash, account, - mainAccountUtxos, + utxos, }) => { const history = useHistory(); const dispatch = useDispatch(); @@ -111,7 +111,7 @@ const ChooseFeeView: React.FC = ({ const createTx = (recipients: RecipientInterface[]) => { // no taxi setFeeChange(undefined); - const w = walletFromCoins(mainAccountUtxos, network); + const w = walletFromCoins(utxos, network); const currentSatsPerByte = feeLevelToSatsPerByte[feeLevel]; if (!changeAddress) throw new Error('change address is not defined'); @@ -158,7 +158,7 @@ const ChooseFeeView: React.FC = ({ const tx: string = createTaxiTxFromTopup( taxiTopup, - mainAccountUtxos, + utxos, recipients, greedyCoinSelector(), changeGetter From 77ec2e08429f6f0f6aef35801252ab400599f933 Mon Sep 17 00:00:00 2001 From: louisinger Date: Fri, 22 Oct 2021 13:24:57 +0200 Subject: [PATCH 25/52] prettier and lint --- .../redux/containers/cosigners.container.ts | 4 +- .../receive-select-asset.container.ts | 2 +- .../redux/reducers/wallet-reducer.ts | 15 +-- .../redux/selectors/balance.selector.ts | 8 +- .../redux/selectors/wallet.selector.ts | 20 ++-- src/application/redux/store.ts | 4 +- src/application/utils/balances.ts | 8 +- src/domain/wallet.ts | 8 +- src/presentation/routes/index.tsx | 98 +++++++++---------- src/presentation/wallet/receive/index.tsx | 4 +- .../wallet/receive/receive-select-asset.tsx | 10 +- 11 files changed, 99 insertions(+), 82 deletions(-) diff --git a/src/application/redux/containers/cosigners.container.ts b/src/application/redux/containers/cosigners.container.ts index ee3aea4f..b25c5b4d 100644 --- a/src/application/redux/containers/cosigners.container.ts +++ b/src/application/redux/containers/cosigners.container.ts @@ -6,7 +6,9 @@ import SettingsCosignersView, { const SettingsCosigner = connect( (state: RootReducerState): SettingsCosignersProps => ({ - multisigAccountsData: state.wallet.restrictedAssetAccount ? [state.wallet.restrictedAssetAccount] : [], + multisigAccountsData: state.wallet.restrictedAssetAccount + ? [state.wallet.restrictedAssetAccount] + : [], }) )(SettingsCosignersView); diff --git a/src/application/redux/containers/receive-select-asset.container.ts b/src/application/redux/containers/receive-select-asset.container.ts index 82c729bd..fcefdccc 100644 --- a/src/application/redux/containers/receive-select-asset.container.ts +++ b/src/application/redux/containers/receive-select-asset.container.ts @@ -13,7 +13,7 @@ const mapStateToProps = (state: RootReducerState): ReceiveSelectAssetProps => { return { network: state.app.network, assets: Object.keys(balances).map(getAsset), - restrictedAssetSetup: state.wallet.restrictedAssetAccount !== undefined + restrictedAssetSetup: state.wallet.restrictedAssetAccount !== undefined, }; }; diff --git a/src/application/redux/reducers/wallet-reducer.ts b/src/application/redux/reducers/wallet-reducer.ts index b25225b5..105b5a83 100644 --- a/src/application/redux/reducers/wallet-reducer.ts +++ b/src/application/redux/reducers/wallet-reducer.ts @@ -4,7 +4,12 @@ import * as ACTION_TYPES from '../actions/action-types'; import { CosignerExtraData, WalletState } from '../../../domain/wallet'; import { AnyAction } from 'redux'; import { UtxoInterface } from 'ldk'; -import { AccountID, MainAccountID, MultisigAccountData, RestrictedAssetAccountID } from '../../../domain/account'; +import { + AccountID, + MainAccountID, + MultisigAccountData, + RestrictedAssetAccountID, +} from '../../../domain/account'; import { TxDisplayInterface } from '../../../domain/transaction'; import { Network } from '../../../domain/network'; @@ -28,7 +33,7 @@ export const walletInitState: WalletState = { [RestrictedAssetAccountID]: { utxosMap: {}, transactions: { regtest: {}, liquid: {} }, - } + }, }, passwordHash: '', deepRestorer: { @@ -124,8 +129,7 @@ export function walletReducer( ...state[accountID], restorerOpts: { ...state[accountID]?.restorerOpts, - lastUsedInternalIndex: - (state[accountID]?.restorerOpts.lastUsedInternalIndex ?? 0) + 1, + lastUsedInternalIndex: (state[accountID]?.restorerOpts.lastUsedInternalIndex ?? 0) + 1, }, }, }; @@ -139,8 +143,7 @@ export function walletReducer( ...state[accountID], restorerOpts: { ...state[accountID]?.restorerOpts, - lastUsedExternalIndex: - (state[accountID]?.restorerOpts.lastUsedExternalIndex ?? 0) + 1, + lastUsedExternalIndex: (state[accountID]?.restorerOpts.lastUsedExternalIndex ?? 0) + 1, }, }, }; diff --git a/src/application/redux/selectors/balance.selector.ts b/src/application/redux/selectors/balance.selector.ts index 719e5a79..c134a8c2 100644 --- a/src/application/redux/selectors/balance.selector.ts +++ b/src/application/redux/selectors/balance.selector.ts @@ -7,11 +7,11 @@ import { selectTransactions, selectUtxos } from './wallet.selector'; export type BalancesByAsset = { [assetHash: string]: number }; export const selectBalances = (...accounts: AccountID[]) => { - const selectors = accounts.map(id => selectBalancesForAccount(id)) + const selectors = accounts.map((id) => selectBalancesForAccount(id)); return (state: RootReducerState) => { - return sumBalances(...selectors.map(select => select(state))); - } -} + return sumBalances(...selectors.map((select) => select(state))); + }; +}; /** * Extract balances from all unblinded utxos in state diff --git a/src/application/redux/selectors/wallet.selector.ts b/src/application/redux/selectors/wallet.selector.ts index 173ab2ae..00c8fb7d 100644 --- a/src/application/redux/selectors/wallet.selector.ts +++ b/src/application/redux/selectors/wallet.selector.ts @@ -14,9 +14,11 @@ export function masterPubKeySelector(state: RootReducerState): Promise (state: RootReducerState): UtxoInterface[] => { - return accounts.flatMap(ID => selectUtxosForAccount(ID)(state)); -} +export const selectUtxos = + (...accounts: AccountID[]) => + (state: RootReducerState): UtxoInterface[] => { + return accounts.flatMap((ID) => selectUtxosForAccount(ID)(state)); + }; const selectUtxosForAccount = (accountID: AccountID) => @@ -24,9 +26,11 @@ const selectUtxosForAccount = return Object.values(selectUnspentsAndTransactions(accountID)(state).utxosMap); }; -export const selectTransactions = (...accounts: AccountID[]) => (state: RootReducerState) => { - return accounts.flatMap(ID => selectTransactionsForAccount(ID)(state)); -} +export const selectTransactions = + (...accounts: AccountID[]) => + (state: RootReducerState) => { + return accounts.flatMap((ID) => selectTransactionsForAccount(ID)(state)); + }; const selectTransactionsForAccount = (accountID: AccountID) => @@ -54,7 +58,7 @@ function selectRestrictedAssetAccount(state: RootReducerState): MultisigAccount state.wallet.mainAccount.encryptedMnemonic, state.wallet.restrictedAssetAccount ); -}; +} export const selectAccount = (accountID: AccountID) => accountID === MainAccountID ? selectMainAccount : selectRestrictedAssetAccount; @@ -66,7 +70,7 @@ export const selectAccountForReceive = (asset: string) => (state: RootReducerSta } return selectMainAccount(state); -} +}; export const selectUnspentsAndTransactions = (accountID: AccountID) => (state: RootReducerState) => { diff --git a/src/application/redux/store.ts b/src/application/redux/store.ts index 9578b8f9..d3782a7b 100644 --- a/src/application/redux/store.ts +++ b/src/application/redux/store.ts @@ -74,8 +74,8 @@ export function startAlarmUpdater(): ThunkAction { dispatch(utxosUpdateTask(ID)); dispatch(txsUpdateTask(ID)); - }) - + }); + dispatch(updateTaxiAssets()); break; diff --git a/src/application/utils/balances.ts b/src/application/utils/balances.ts index db0f4b92..9e1914ac 100644 --- a/src/application/utils/balances.ts +++ b/src/application/utils/balances.ts @@ -1,4 +1,4 @@ -import { BalancesByAsset } from "../redux/selectors/balance.selector"; +import { BalancesByAsset } from '../redux/selectors/balance.selector'; const addBalance = (toAdd: BalancesByAsset) => (base: BalancesByAsset) => { const result = base; @@ -7,13 +7,13 @@ const addBalance = (toAdd: BalancesByAsset) => (base: BalancesByAsset) => { } return result; -} +}; export const sumBalances = (...balances: BalancesByAsset[]) => { const [balance, ...rest] = balances; let result = balance; const addFns = rest.map(addBalance); - addFns.forEach((f) => result = f(result)); + addFns.forEach((f) => (result = f(result))); return result; -} \ No newline at end of file +}; diff --git a/src/domain/wallet.ts b/src/domain/wallet.ts index b47b1085..a911cfee 100644 --- a/src/domain/wallet.ts +++ b/src/domain/wallet.ts @@ -1,4 +1,10 @@ -import { AccountID, MainAccountID, MnemonicAccountData, MultisigAccountData, RestrictedAssetAccountID } from './account'; +import { + AccountID, + MainAccountID, + MnemonicAccountData, + MultisigAccountData, + RestrictedAssetAccountID, +} from './account'; import { PasswordHash } from './password-hash'; import { UtxosAndTxsHistory } from './transaction'; diff --git a/src/presentation/routes/index.tsx b/src/presentation/routes/index.tsx index f49965c9..069e825d 100644 --- a/src/presentation/routes/index.tsx +++ b/src/presentation/routes/index.tsx @@ -88,56 +88,56 @@ import ReceiveView from '../wallet/receive'; const Routes: React.FC = () => { return ( - - {/*Onboarding*/} - - - - - - - - - {/*Wallet*/} - - - - - - - - - - - - {/*Settings*/} - - - - - - - - - - - - - {/*Login*/} - - {/*Connect*/} - - - - + + {/*Onboarding*/} + + + + + + + + + {/*Wallet*/} + + + + + + + + + + + + {/*Settings*/} + + + + + + + + + + + + + {/*Login*/} + + {/*Connect*/} + + + + - - - + + + ); }; diff --git a/src/presentation/wallet/receive/index.tsx b/src/presentation/wallet/receive/index.tsx index 7822e4bf..fe1d7c58 100644 --- a/src/presentation/wallet/receive/index.tsx +++ b/src/presentation/wallet/receive/index.tsx @@ -14,7 +14,7 @@ const ReceiveView: React.FC> = ({ match } const history = useHistory(); const dispatch = useDispatch(); - const account = useSelector(selectAccountForReceive(match.params.asset)) + const account = useSelector(selectAccountForReceive(match.params.asset)); const [confidentialAddress, setConfidentialAddress] = useState(''); const [buttonText, setButtonText] = useState('Copy'); @@ -31,7 +31,7 @@ const ReceiveView: React.FC> = ({ match } useEffect(() => { (async () => { if (account === undefined) { - throw new Error('multisig account for restricted asset is not set') + throw new Error('multisig account for restricted asset is not set'); } const identity = await account.getWatchIdentity(); diff --git a/src/presentation/wallet/receive/receive-select-asset.tsx b/src/presentation/wallet/receive/receive-select-asset.tsx index 90f6eccc..f7f3f7f0 100644 --- a/src/presentation/wallet/receive/receive-select-asset.tsx +++ b/src/presentation/wallet/receive/receive-select-asset.tsx @@ -11,7 +11,11 @@ export interface ReceiveSelectAssetProps { restrictedAssetSetup: boolean; } -const ReceiveSelectAssetView: React.FC = ({ network, assets, restrictedAssetSetup }) => { +const ReceiveSelectAssetView: React.FC = ({ + network, + assets, + restrictedAssetSetup, +}) => { const history = useHistory(); const handleSend = (asset: string) => { @@ -23,9 +27,7 @@ const ReceiveSelectAssetView: React.FC = ({ network, as title="Receive Asset" onClick={handleSend} network={network} - assets={[UnknowAsset] - .concat(assets) - .concat(restrictedAssetSetup ? [RestrictedAsset] : [])} + assets={[UnknowAsset].concat(assets).concat(restrictedAssetSetup ? [RestrictedAsset] : [])} /> ); }; From d738c8dcef2d2164238ab76209e56eb24284a6ee Mon Sep 17 00:00:00 2001 From: louisinger Date: Fri, 22 Oct 2021 14:19:35 +0200 Subject: [PATCH 26/52] MainAccountID constant in MmenmonicAccount --- src/application/redux/reducers/wallet-reducer.ts | 1 - src/application/redux/selectors/wallet.selector.ts | 2 +- src/domain/account.ts | 3 +-- src/presentation/wallet/receive/index.tsx | 4 ++-- src/presentation/wallet/transactions/index.tsx | 4 ++-- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/application/redux/reducers/wallet-reducer.ts b/src/application/redux/reducers/wallet-reducer.ts index 105b5a83..6b0ce185 100644 --- a/src/application/redux/reducers/wallet-reducer.ts +++ b/src/application/redux/reducers/wallet-reducer.ts @@ -15,7 +15,6 @@ import { Network } from '../../../domain/network'; export const walletInitState: WalletState = { [MainAccountID]: { - accountID: MainAccountID, encryptedMnemonic: '', masterBlindingKey: '', masterXPub: '', diff --git a/src/application/redux/selectors/wallet.selector.ts b/src/application/redux/selectors/wallet.selector.ts index 00c8fb7d..80894e61 100644 --- a/src/application/redux/selectors/wallet.selector.ts +++ b/src/application/redux/selectors/wallet.selector.ts @@ -63,7 +63,7 @@ function selectRestrictedAssetAccount(state: RootReducerState): MultisigAccount export const selectAccount = (accountID: AccountID) => accountID === MainAccountID ? selectMainAccount : selectRestrictedAssetAccount; -export const selectAccountForReceive = (asset: string) => (state: RootReducerState) => { +export const selectAccountForAsset = (asset: string) => (state: RootReducerState) => { // TODO hardcode restricted asset hashes if (asset === 'restricted_asset') { return selectRestrictedAssetAccount(state); diff --git a/src/domain/account.ts b/src/domain/account.ts index f54dfb88..6cf7a082 100644 --- a/src/domain/account.ts +++ b/src/domain/account.ts @@ -54,7 +54,6 @@ export interface Account< export type MnemonicAccount = Account; export interface MnemonicAccountData { - accountID: AccountID; encryptedMnemonic: EncryptedMnemonic; restorerOpts: StateRestorerOpts; masterXPub: MasterXPub; @@ -66,7 +65,7 @@ export function createMnemonicAccount( network: Network ): MnemonicAccount { return { - getAccountID: () => data.accountID, + getAccountID: () => MainAccountID, getSigningIdentity: (password: string) => restoredMnemonic(decrypt(data.encryptedMnemonic, password), data.restorerOpts, network), getWatchIdentity: () => diff --git a/src/presentation/wallet/receive/index.tsx b/src/presentation/wallet/receive/index.tsx index fe1d7c58..49e43221 100644 --- a/src/presentation/wallet/receive/index.tsx +++ b/src/presentation/wallet/receive/index.tsx @@ -8,13 +8,13 @@ import { useDispatch, useSelector } from 'react-redux'; import { ProxyStoreDispatch } from '../../../application/redux/proxyStore'; import { incrementAddressIndex } from '../../../application/redux/actions/wallet'; import { txsUpdateTask, utxosUpdateTask } from '../../../application/redux/actions/updater'; -import { selectAccountForReceive } from '../../../application/redux/selectors/wallet.selector'; +import { selectAccountForAsset } from '../../../application/redux/selectors/wallet.selector'; const ReceiveView: React.FC> = ({ match }) => { const history = useHistory(); const dispatch = useDispatch(); - const account = useSelector(selectAccountForReceive(match.params.asset)); + const account = useSelector(selectAccountForAsset(match.params.asset)); const [confidentialAddress, setConfidentialAddress] = useState(''); const [buttonText, setButtonText] = useState('Copy'); diff --git a/src/presentation/wallet/transactions/index.tsx b/src/presentation/wallet/transactions/index.tsx index b8452993..ce71bb7e 100644 --- a/src/presentation/wallet/transactions/index.tsx +++ b/src/presentation/wallet/transactions/index.tsx @@ -3,7 +3,7 @@ import { useHistory, useLocation } from 'react-router-dom'; import browser from 'webextension-polyfill'; import { DEFAULT_ROUTE, - RECEIVE_ADDRESS_ROUTE, + RECEIVE_SELECT_ASSET_ROUTE, SEND_ADDRESS_AMOUNT_ROUTE, } from '../../routes/constants'; import Balance from '../../components/balance'; @@ -61,7 +61,7 @@ const TransactionsView: React.FC = ({ // Save mnemonic modal const [isSaveMnemonicModalOpen, showSaveMnemonicModal] = useState(false); const handleSaveMnemonicClose = () => showSaveMnemonicModal(false); - const handleSaveMnemonicConfirm = () => history.push(RECEIVE_ADDRESS_ROUTE); + const handleSaveMnemonicConfirm = () => history.push(RECEIVE_SELECT_ASSET_ROUTE); const handleReceive = () => showSaveMnemonicModal(true); const handleSend = async () => { await dispatch(setAsset(state.assetHash)); From 6457633675cfccb357b798f082383af22da3e725 Mon Sep 17 00:00:00 2001 From: louisinger Date: Fri, 22 Oct 2021 17:24:30 +0200 Subject: [PATCH 27/52] refacto choose-fee component --- src/presentation/wallet/send/choose-fee.tsx | 296 +++++++++++--------- 1 file changed, 156 insertions(+), 140 deletions(-) diff --git a/src/presentation/wallet/send/choose-fee.tsx b/src/presentation/wallet/send/choose-fee.tsx index bf035d99..da5cd423 100644 --- a/src/presentation/wallet/send/choose-fee.tsx +++ b/src/presentation/wallet/send/choose-fee.tsx @@ -7,12 +7,10 @@ import ShellPopUp from '../../components/shell-popup'; import { SEND_ADDRESS_AMOUNT_ROUTE, SEND_CONFIRMATION_ROUTE } from '../../routes/constants'; import { feeAmountFromTx, - feeLevelToSatsPerByte, fetchTopupFromTaxi, createTaxiTxFromTopup, imgPathMapMainnet, imgPathMapRegtest, - lbtcAssetByNetwork, taxiURL, } from '../../../application/utils'; import { formatDecimalAmount, fromSatoshi, fromSatoshiStr } from '../../utils'; @@ -30,7 +28,9 @@ import { ProxyStoreDispatch } from '../../../application/redux/proxyStore'; import { Address, createAddress } from '../../../domain/address'; import { Topup } from 'taxi-protobuf/generated/js/taxi_pb'; import { incrementChangeAddressIndex } from '../../../application/redux/actions/wallet'; -import { Account } from '../../../domain/account'; +import { Account, AccountID } from '../../../domain/account'; +import { extractErrorMessage } from '../../utils/error'; +import { AnyAction } from 'redux'; export interface ChooseFeeProps { network: Network; @@ -46,6 +46,14 @@ export interface ChooseFeeProps { utxos: UtxoInterface[]; } +interface State { + unsignedPset?: string; + feeChange?: Address; + topup?: Topup.AsObject; +} + +const initialState: State = {}; + const ChooseFeeView: React.FC = ({ network, assets, @@ -62,143 +70,67 @@ const ChooseFeeView: React.FC = ({ const history = useHistory(); const dispatch = useDispatch(); - const [feeCurrency, setFeeCurrency] = useState(lbtcAssetHash); - const [feeLevel] = useState('50'); - const [unsignedPendingTx, setUnsignedPendingTx] = useState(''); - const [errorMessage, setErrorMessage] = useState(); + const [state, setState] = useState(initialState); + const [feeAsset, setFeeAsset] = useState(lbtcAssetHash); const [loading, setLoading] = useState(false); - const [feeChange, setFeeChange] = useState
(); - const [topup, setTopup] = useState(); + const [error, setError] = useState(); const circleLoaderRef = React.useRef(null); useLottieLoader(circleLoaderRef, '/assets/animations/circle-loader.json'); - useEffect(() => { - if (!loading) updatePendingTx().catch(console.error); - }, [feeCurrency]); - - const updatePendingTx = async () => { - if (!feeCurrency) return; - setLoading(true); - setErrorMessage(undefined); - try { - if (!sendAddress) throw new Error('sendAddress is undefined'); - - const recipients = [ - { - asset: sendAsset, - value: sendAmount, - address: sendAddress.value, - }, - ]; - - if (feeCurrency === lbtcAssetByNetwork(network)) { - createTx(recipients); - } else if (taxiAssets.includes(feeCurrency)) { - await createTaxiTx(recipients); - } - } catch (err) { - console.error(err); - setErrorMessage((err as Error).message); - setFeeCurrency(undefined); - setFeeChange(undefined); - setTopup(undefined); - } finally { - setLoading(false); - } + const handleError = (err: unknown) => { + console.error(err); + setError(extractErrorMessage(err)); + setState(initialState); + setFeeAsset(undefined); }; - const createTx = (recipients: RecipientInterface[]) => { - // no taxi - setFeeChange(undefined); - const w = walletFromCoins(utxos, network); - const currentSatsPerByte = feeLevelToSatsPerByte[feeLevel]; - - if (!changeAddress) throw new Error('change address is not defined'); - - const tx: string = w.buildTx( - w.createTx(), - recipients, - greedyCoinSelector(), - () => changeAddress.value, - true, - currentSatsPerByte - ); - setUnsignedPendingTx(tx); - return; - }; + const getRecipient = () => ({ + asset: sendAsset, + value: sendAmount, + address: sendAddress!.value, + }); - const createTaxiTx = async (recipients: RecipientInterface[]) => { - if (!feeCurrency) throw new Error('feeCurrency is undefined'); + const isTaxi = () => feeAsset !== lbtcAssetHash; - let taxiTopup = topup; - if (!taxiTopup || feeCurrency !== taxiTopup.assetHash) { - taxiTopup = (await fetchTopupFromTaxi(taxiURL[network], feeCurrency)).topup; - setTopup(taxiTopup); - } - - if (!taxiTopup) { - throw new Error('Taxi topup is undefined'); - } - - let nextChangeAddr = feeChange; - if (!nextChangeAddr) { - const restored = await account.getWatchIdentity(); - const next = await restored.getNextChangeAddress(); - nextChangeAddr = createAddress(next.confidentialAddress, next.derivationPath); - setFeeChange(nextChangeAddr); + // create the pset each time the user select a different fee currency + useEffect(() => { + if (!sendAddress || !changeAddress) { + history.goBack(); // should be set in previous step } - const changeGetter = (asset: string) => { - if (asset === sendAsset) { - return changeAddress?.value; - } - return nextChangeAddr?.value; - }; - - const tx: string = createTaxiTxFromTopup( - taxiTopup, - utxos, - recipients, - greedyCoinSelector(), - changeGetter - ); - - setUnsignedPendingTx(tx); - return; - }; - - // send the transaction - const handleConfirm = async () => { - if (!feeCurrency) return; - + if (loading) return; + if (!feeAsset) return; setLoading(true); + setError(undefined); + const done = () => setLoading(false); + + const newStatePromise = isTaxi() + ? stateForTaxiPSET( + account, + feeAsset, + utxos, + getRecipient(), + changeAddress!, + network, + state.topup + ) + : stateForRegularPSET(getRecipient(), changeAddress!, utxos, network); + + newStatePromise.then(setState).catch(handleError).finally(done); + }, [feeAsset]); + + // dispatch a set of actions in order to save the pset in redux state + const handleConfirm = async () => { try { - let feeAmount: number; - if (feeCurrency === lbtcAssetByNetwork(network)) { - feeAmount = feeAmountFromTx(unsignedPendingTx); - } else { - feeAmount = topup?.assetAmount || 0; - } - - await Promise.all([ - dispatch(setPset(unsignedPendingTx)), - dispatch(setFeeAssetAndAmount(feeCurrency, feeAmount)), - ]); - - if (feeChange) { - await Promise.all([ - dispatch(setFeeChangeAddress(feeChange)), - dispatch(incrementChangeAddressIndex(account.getAccountID())), - ]); - } - + if (!feeAsset) throw new Error('fee asset not selected'); + setLoading(true); + await Promise.all(actionsFromState(state, feeAsset, account.getAccountID()).map(dispatch)); history.push({ pathname: SEND_CONFIRMATION_ROUTE, }); } catch (error: any) { - console.error(error); - setErrorMessage(error.message || error); + handleError(error); } finally { setLoading(false); } @@ -209,16 +141,15 @@ const ChooseFeeView: React.FC = ({ }; const handlePayFees = (assetHash: string) => { - if (feeCurrency !== assetHash) { - setUnsignedPendingTx(''); - setFeeCurrency(assetHash); + if (feeAsset !== assetHash) { + setFeeAsset(assetHash); } }; const getFeeCurrencyImgPath = (): string => { - let img: string = imgPathMapMainnet[feeCurrency || '']; + let img: string = imgPathMapMainnet[feeAsset || '']; if (network === 'regtest') { - img = imgPathMapRegtest[assets[feeCurrency || '']?.ticker]; + img = imgPathMapRegtest[assets[feeAsset || '']?.ticker]; } if (!img) { @@ -236,10 +167,10 @@ const ChooseFeeView: React.FC = ({ currentPage="Send" >
@@ -252,7 +183,7 @@ const ChooseFeeView: React.FC = ({ {[lbtcAssetHash, ...taxiAssets].map((assetHash) => (
- {feeCurrency && unsignedPendingTx.length > 0 && ( + {feeAsset && state.unsignedPset && ( <>
Fee: - {!taxiAssets.includes(feeCurrency) - ? `${fromSatoshiStr(feeAmountFromTx(unsignedPendingTx))} L-BTC` - : `${fromSatoshiStr(topup?.assetAmount || 0)} USDt *`} + {!isTaxi() + ? `${fromSatoshiStr(feeAmountFromTx(state.unsignedPset))} L-BTC` + : `${fromSatoshiStr(state.topup?.assetAmount || 0)} USDt *`}
- {taxiAssets.includes(feeCurrency) && ( + {taxiAssets.includes(feeAsset) && (

* Fee paid with Liquid taxi 🚕

)} )} - {errorMessage && ( -

{errorMessage}

- )} + {error &&

{error}

} {loading &&
} ); }; + +function stateForRegularPSET( + recipient: RecipientInterface, + change: Address, + utxos: UtxoInterface[], + network: Network +): Promise { + const result: State = {}; + result.unsignedPset = undefined; + result.feeChange = undefined; + const w = walletFromCoins(utxos, network); + + result.unsignedPset = w.buildTx( + w.createTx(), + [recipient], + greedyCoinSelector(), + () => change.value, + true, + 0.1 + ); + + result.topup = undefined; + return Promise.resolve(result); +} + +async function stateForTaxiPSET( + account: Account, + feeAsset: string, + utxos: UtxoInterface[], + recipient: RecipientInterface, + change: Address, + network: Network, + lastTopup?: Topup.AsObject +): Promise { + const result: State = {}; + result.unsignedPset = undefined; + result.topup = lastTopup; + + if (!lastTopup || feeAsset !== lastTopup.assetHash) { + result.topup = undefined; + result.topup = (await fetchTopupFromTaxi(taxiURL[network], feeAsset)).topup; + } + + if (!result.topup) { + throw new Error('Taxi topup should be defined for Taxi PSET'); + } + + const restored = await account.getWatchIdentity(); + const next = await restored.getNextChangeAddress(); + const feeChange = createAddress(next.confidentialAddress, next.derivationPath); + + const changeGetter = (asset: string) => { + if (asset === recipient.asset) { + return change.value; + } + + result.feeChange = feeChange; + return feeChange.value; + }; + + result.unsignedPset = createTaxiTxFromTopup( + result.topup, + utxos, + [recipient], + greedyCoinSelector(), + changeGetter + ); + + return result; +} + +function actionsFromState(state: State, feeCurrency: string, accountID: AccountID): AnyAction[] { + if (!state.unsignedPset) return []; + + const actions: AnyAction[] = []; + const feeAmount = state.topup ? state.topup.assetAmount : feeAmountFromTx(state.unsignedPset); + actions.push(setPset(state.unsignedPset)); + actions.push(setFeeAssetAndAmount(feeCurrency, feeAmount)); + + if (state.feeChange) { + actions.push(setFeeChangeAddress(state.feeChange)); + actions.push(incrementChangeAddressIndex(accountID)); + } + + return actions; +} + export default ChooseFeeView; From 941386a828b04987515e7fad840d988a445f38dc Mon Sep 17 00:00:00 2001 From: louisinger Date: Fri, 22 Oct 2021 19:27:35 +0200 Subject: [PATCH 28/52] blind with blinder --- src/application/redux/actions/transaction.ts | 5 +- .../redux/reducers/transaction-reducer.ts | 5 +- src/application/utils/address.ts | 4 - src/application/utils/transaction.ts | 89 +++++++++++++------ src/presentation/wallet/send/choose-fee.tsx | 34 +++++-- test/spend.spec.ts | 9 +- 6 files changed, 102 insertions(+), 44 deletions(-) diff --git a/src/application/redux/actions/transaction.ts b/src/application/redux/actions/transaction.ts index 4cb0938b..9b1ea108 100644 --- a/src/application/redux/actions/transaction.ts +++ b/src/application/redux/actions/transaction.ts @@ -12,6 +12,7 @@ import { Address } from '../../../domain/address'; import { TxDisplayInterface } from '../../../domain/transaction'; import { Network } from '../../../domain/network'; import { AccountID } from '../../../domain/account'; +import { UtxoInterface } from 'ldk'; export function setAsset(asset: string): AnyAction { return { type: PENDING_TX_SET_ASSET, payload: { asset } }; @@ -40,10 +41,10 @@ export function flushPendingTx(): AnyAction { return { type: PENDING_TX_FLUSH }; } -export function setPset(pset: string): AnyAction { +export function setPset(pset: string, utxos: UtxoInterface[]): AnyAction { return { type: PENDING_TX_SET_PSET, - payload: { pset }, + payload: { pset, utxos }, }; } diff --git a/src/application/redux/reducers/transaction-reducer.ts b/src/application/redux/reducers/transaction-reducer.ts index 4e41941b..1a48a107 100644 --- a/src/application/redux/reducers/transaction-reducer.ts +++ b/src/application/redux/reducers/transaction-reducer.ts @@ -1,6 +1,7 @@ import * as ACTION_TYPES from '../actions/action-types'; import { AnyAction } from 'redux'; import { Address } from '../../../domain/address'; +import { UtxoInterface } from 'ldk'; export type PendingTxStep = 'empty' | 'address-amount' | 'choose-fee' | 'confirmation'; @@ -14,6 +15,7 @@ export interface TransactionState { sendAddress?: Address; changeAddress?: Address; feeChangeAddress?: Address; + selectedUtxos?: UtxoInterface[]; } export const transactionInitState: TransactionState = { @@ -55,7 +57,7 @@ export function transactionReducer( feeChangeAddress: payload.feeChangeAddress, }; } - + case ACTION_TYPES.PENDING_TX_SET_FEE_AMOUNT_AND_ASSET: { return { ...state, @@ -73,6 +75,7 @@ export function transactionReducer( ...state, step: 'confirmation', pset: payload.pset, + selectedUtxos: payload.utxos }; } diff --git a/src/application/utils/address.ts b/src/application/utils/address.ts index 3ad39306..dfeefb36 100644 --- a/src/application/utils/address.ts +++ b/src/application/utils/address.ts @@ -1,9 +1,5 @@ import { address, networks } from 'ldk'; -export const blindingKeyFromAddress = (addr: string): string => { - return address.fromConfidential(addr).blindingKey.toString('hex'); -}; - export const isConfidentialAddress = (addr: string): boolean => { try { address.fromConfidential(addr); diff --git a/src/application/utils/transaction.ts b/src/application/utils/transaction.ts index 9a56d160..b609f5c9 100644 --- a/src/application/utils/transaction.ts +++ b/src/application/utils/transaction.ts @@ -1,4 +1,5 @@ import { + address, address as addrLDK, addToTx, BlindedOutputInterface, @@ -11,6 +12,7 @@ import { IdentityInterface, InputInterface, isBlindedOutputInterface, + isBlindedUtxo, psetToUnsignedTx, RecipientInterface, TxInterface, @@ -18,7 +20,7 @@ import { UtxoInterface, } from 'ldk'; import { confidential, networks, payments, Psbt } from 'liquidjs-lib'; -import { blindingKeyFromAddress, isConfidentialAddress, networkFromString } from './address'; +import { isConfidentialAddress, networkFromString } from './address'; import { Transfer, TxDisplayInterface, TxStatusEnum, TxType } from '../../domain/transaction'; import { Topup } from 'taxi-protobuf/generated/js/taxi_pb'; import { lbtcAssetByNetwork } from './network'; @@ -27,21 +29,57 @@ import { fetchTopupFromTaxi } from './taxi'; import { taxiURL } from './constants'; import { DataRecipient, isAddressRecipient, isDataRecipient, Recipient } from 'marina-provider'; -function outPubKeysMap(pset: string, outputAddresses: string[]): Map { - const outPubkeys: Map = new Map(); +const blindingKeyFromAddress = (addr: string): Buffer => { + return address.fromConfidential(addr).blindingKey; +}; + +function outPubKeysMap(pset: string, outputAddresses: string[]): Map { + const outPubkeys: Map = new Map(); - for (const outAddr of outputAddresses) { - const index = outputIndexFromAddress(pset, outAddr); + for (const outAddress of outputAddresses) { + const index = outputIndexFromAddress(pset, outAddress); if (index === -1) continue; - if (isConfidentialAddress(outAddr)) { - const blindingPublicKey = blindingKeyFromAddress(outAddr); - outPubkeys.set(index, blindingPublicKey); + if (isConfidentialAddress(outAddress)) { + outPubkeys.set(index, blindingKeyFromAddress(outAddress)); } } return outPubkeys; } +function inputBlindingDataMap(pset: string, utxos: UtxoInterface[]): Map { + const inputBlindingData = new Map(); + + let index = -1; + for (const input of psetToUnsignedTx(pset).ins) { + index++; + const utxo = utxos.find(u => u.txid === input.hash.toString('hex')); + if (!utxo) { + throw new Error(`blindPSET error: utxo not found '${input.hash.reverse().toString('hex')}'`) + } + if (!isBlindedUtxo(utxo)) continue; + + if (!utxo.unblindData) { + throw new Error(`blindPSET error: utxo need unblind data '${input.hash.reverse().toString('hex')}'`) + } + + inputBlindingData.set(index, utxo.unblindData) + } + + return inputBlindingData; +} + +async function blindPset( + psetBase64: string, + utxos: UtxoInterface[], + outputAddresses: string[], +) { + const outputPubKeys = outPubKeysMap(psetBase64, outputAddresses); + const inputBlindingData = inputBlindingDataMap(psetBase64, utxos); + + return (await decodePset(psetBase64).blindOutputsByIndex(inputBlindingData, outputPubKeys)).toBase64() +} + /** * Take an unsigned pset, blind it according to recipientAddresses and sign the pset using the mnemonic. * @param signerIdentity Identity using to sign the tx. should be restored. @@ -49,28 +87,29 @@ function outPubKeysMap(pset: string, outputAddresses: string[]): Map { - const outputAddresses = (await signerIdentity.getAddresses()).map((a) => a.confidentialAddress); - - const outputPubKeys = outPubKeysMap(psetBase64, outputAddresses.concat(recipientAddresses)); - const outputsToBlind = Array.from(outputPubKeys.keys()); - - const blindedPset: string = await signerIdentity.blindPset( - psetBase64, - outputsToBlind, - outputPubKeys - ); - - const signedPset: string = await signerIdentity.signPset(blindedPset); + const outputAddresses: string[] = recipientAddresses; + for (const id of identities) { + outputAddresses.push(...(await id.getAddresses()).map((a) => a.confidentialAddress)); + } + + let pset = await blindPset(psetBase64, selectedUtxos, outputAddresses); + + for (const id of identities) { + pset = await id.signPset(pset); + if (decodePset(pset).validateSignaturesOfAllInputs()) break; + } - const ptx = decodePset(signedPset); - if (!ptx.validateSignaturesOfAllInputs()) { - throw new Error('Transaction containes invalid signatures'); + const decodedPset = decodePset(pset); + if (!decodedPset.validateSignaturesOfAllInputs()) { + throw new Error('PSET is not fully signed'); } - return ptx.finalizeAllInputs().extractTransaction().toHex(); + + return decodedPset.finalizeAllInputs().extractTransaction().toHex(); } function outputIndexFromAddress(tx: string, addressToFind: string): number { diff --git a/src/presentation/wallet/send/choose-fee.tsx b/src/presentation/wallet/send/choose-fee.tsx index da5cd423..2746b449 100644 --- a/src/presentation/wallet/send/choose-fee.tsx +++ b/src/presentation/wallet/send/choose-fee.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router'; -import { greedyCoinSelector, RecipientInterface, UtxoInterface, walletFromCoins } from 'ldk'; +import { ChangeAddressFromAssetGetter, CoinSelectionResult, CoinSelector, greedyCoinSelector, RecipientInterface, UtxoInterface, walletFromCoins } from 'ldk'; import Balance from '../../components/balance'; import Button from '../../components/button'; import ShellPopUp from '../../components/shell-popup'; @@ -48,6 +48,7 @@ export interface ChooseFeeProps { interface State { unsignedPset?: string; + utxos?: UtxoInterface[]; feeChange?: Address; topup?: Topup.AsObject; } @@ -109,13 +110,18 @@ const ChooseFeeView: React.FC = ({ ? stateForTaxiPSET( account, feeAsset, - utxos, getRecipient(), changeAddress!, + utxos, network, state.topup ) - : stateForRegularPSET(getRecipient(), changeAddress!, utxos, network); + : stateForRegularPSET( + getRecipient(), + changeAddress!, + utxos, + network, + ); newStatePromise.then(setState).catch(handleError).finally(done); }, [feeAsset]); @@ -228,21 +234,30 @@ const ChooseFeeView: React.FC = ({ ); }; +const sideEffectCoinSelector = (sideEffect: (r: CoinSelectionResult) => void): CoinSelector => { + return (unspents: UtxoInterface[], outputs: RecipientInterface[], changeGetter: ChangeAddressFromAssetGetter) => { + const result = greedyCoinSelector()(unspents, outputs, changeGetter); + sideEffect(result); + return result; + } +} + function stateForRegularPSET( recipient: RecipientInterface, change: Address, utxos: UtxoInterface[], - network: Network + network: Network, ): Promise { const result: State = {}; result.unsignedPset = undefined; result.feeChange = undefined; + result.utxos = []; const w = walletFromCoins(utxos, network); result.unsignedPset = w.buildTx( w.createTx(), [recipient], - greedyCoinSelector(), + sideEffectCoinSelector(({ selectedUtxos }) => result.utxos?.push(...selectedUtxos)), () => change.value, true, 0.1 @@ -255,15 +270,16 @@ function stateForRegularPSET( async function stateForTaxiPSET( account: Account, feeAsset: string, - utxos: UtxoInterface[], recipient: RecipientInterface, change: Address, + utxos: UtxoInterface[], network: Network, lastTopup?: Topup.AsObject ): Promise { const result: State = {}; result.unsignedPset = undefined; result.topup = lastTopup; + result.utxos = []; if (!lastTopup || feeAsset !== lastTopup.assetHash) { result.topup = undefined; @@ -291,7 +307,7 @@ async function stateForTaxiPSET( result.topup, utxos, [recipient], - greedyCoinSelector(), + sideEffectCoinSelector(({ selectedUtxos }) => result.utxos?.push(...selectedUtxos)), changeGetter ); @@ -299,11 +315,11 @@ async function stateForTaxiPSET( } function actionsFromState(state: State, feeCurrency: string, accountID: AccountID): AnyAction[] { - if (!state.unsignedPset) return []; + if (!state.unsignedPset || !state.utxos) return []; const actions: AnyAction[] = []; const feeAmount = state.topup ? state.topup.assetAmount : feeAmountFromTx(state.unsignedPset); - actions.push(setPset(state.unsignedPset)); + actions.push(setPset(state.unsignedPset, state.utxos)); actions.push(setFeeAssetAndAmount(feeCurrency, feeAmount)); if (state.feeChange) { diff --git a/test/spend.spec.ts b/test/spend.spec.ts index 2223b7b7..9089f59b 100644 --- a/test/spend.spec.ts +++ b/test/spend.spec.ts @@ -1,4 +1,4 @@ -import { decodePset, fetchAndUnblindUtxos, Mnemonic, networks } from 'ldk'; +import { decodePset, fetchAndUnblindUtxos, Mnemonic, networks, UtxoInterface } from 'ldk'; import { makeRandomMnemonic } from './test.utils'; import { APIURL, broadcastTx, faucet } from './_regtest'; import { blindAndSignPset, createSendPset } from '../src/application/utils/transaction'; @@ -12,11 +12,14 @@ const RECEIVER = 'AzpofttCgtcfk1PDWytoocvMWqQnLUJfjZw6MVmSdJQtwWnovQPgqiWSRTFZmK describe('create send pset (build, blind & sign)', () => { const mnemonic: Mnemonic = makeRandomMnemonic(); + const unspents: UtxoInterface[] = [] const makeUnspents = async () => { const addr = await mnemonic.getNextAddress(); await faucet(addr.confidentialAddress, 10000); - return fetchAndUnblindUtxos([addr], APIURL); + const u = await fetchAndUnblindUtxos([addr], APIURL); + unspents.push(...u); + return u; }; const makeChangeAddressGetter = async () => { @@ -31,7 +34,7 @@ describe('create send pset (build, blind & sign)', () => { value, })); - const blindAndSign = (pset: string) => blindAndSignPset(mnemonic, pset, [RECEIVER]); + const blindAndSign = (pset: string) => blindAndSignPset(pset, unspents, [mnemonic], [RECEIVER]); test('should be able to create a regular transaction', async () => { const pset = await createSendPset( From 8772211901c8967512e9c8dbb249208c6b51f3f6 Mon Sep 17 00:00:00 2001 From: louisinger Date: Mon, 25 Oct 2021 14:15:35 +0200 Subject: [PATCH 29/52] send with multiple accounts --- .../redux/containers/end-of-flow.container.ts | 5 +- .../redux/reducers/transaction-reducer.ts | 4 +- .../redux/selectors/wallet.selector.ts | 12 +++ src/application/utils/transaction.ts | 39 ++++---- src/domain/account.ts | 6 +- src/domain/cosigner.ts | 90 ++++++++++--------- src/presentation/connect/spend.tsx | 3 +- src/presentation/cosigner/pair.tsx | 8 +- src/presentation/wallet/send/choose-fee.tsx | 29 +++--- src/presentation/wallet/send/end-of-flow.tsx | 29 +++--- .../wallet/send/payment-success.tsx | 10 ++- test/spend.spec.ts | 2 +- 12 files changed, 138 insertions(+), 99 deletions(-) diff --git a/src/application/redux/containers/end-of-flow.container.ts b/src/application/redux/containers/end-of-flow.container.ts index 7a6e4c2f..d8a380bf 100644 --- a/src/application/redux/containers/end-of-flow.container.ts +++ b/src/application/redux/containers/end-of-flow.container.ts @@ -2,13 +2,14 @@ import { connect } from 'react-redux'; import { RootReducerState } from '../../../domain/common'; import EndOfFlow, { EndOfFlowProps } from '../../../presentation/wallet/send/end-of-flow'; import { getExplorerURLSelector } from '../selectors/app.selector'; -import { selectMainAccount } from '../selectors/wallet.selector'; +import { selectAllAccounts } from '../selectors/wallet.selector'; const mapStateToProps = (state: RootReducerState): EndOfFlowProps => ({ - account: selectMainAccount(state), + accounts: selectAllAccounts(state), pset: state.transaction.pset, explorerURL: getExplorerURLSelector(state), recipientAddress: state.transaction.sendAddress?.value, + selectedUtxos: state.transaction.selectedUtxos ?? [], }); const SendEndOfFlow = connect(mapStateToProps)(EndOfFlow); diff --git a/src/application/redux/reducers/transaction-reducer.ts b/src/application/redux/reducers/transaction-reducer.ts index 1a48a107..a20f6b6a 100644 --- a/src/application/redux/reducers/transaction-reducer.ts +++ b/src/application/redux/reducers/transaction-reducer.ts @@ -57,7 +57,7 @@ export function transactionReducer( feeChangeAddress: payload.feeChangeAddress, }; } - + case ACTION_TYPES.PENDING_TX_SET_FEE_AMOUNT_AND_ASSET: { return { ...state, @@ -75,7 +75,7 @@ export function transactionReducer( ...state, step: 'confirmation', pset: payload.pset, - selectedUtxos: payload.utxos + selectedUtxos: payload.utxos, }; } diff --git a/src/application/redux/selectors/wallet.selector.ts b/src/application/redux/selectors/wallet.selector.ts index 80894e61..95cd4d92 100644 --- a/src/application/redux/selectors/wallet.selector.ts +++ b/src/application/redux/selectors/wallet.selector.ts @@ -6,6 +6,7 @@ import { MultisigAccount, MnemonicAccount, MainAccountID, + Account, } from '../../../domain/account'; import { RootReducerState } from '../../../domain/common'; import { TxDisplayInterface } from '../../../domain/transaction'; @@ -60,6 +61,17 @@ function selectRestrictedAssetAccount(state: RootReducerState): MultisigAccount ); } +export const selectAllAccounts = (state: RootReducerState): Account[] => { + const mainAccount = selectMainAccount(state); + const restrictedAssetAccount = selectRestrictedAssetAccount(state); + + if (restrictedAssetAccount) { + return [mainAccount, restrictedAssetAccount]; + } + + return [mainAccount]; +}; + export const selectAccount = (accountID: AccountID) => accountID === MainAccountID ? selectMainAccount : selectRestrictedAssetAccount; diff --git a/src/application/utils/transaction.ts b/src/application/utils/transaction.ts index b609f5c9..589b5b77 100644 --- a/src/application/utils/transaction.ts +++ b/src/application/utils/transaction.ts @@ -12,7 +12,6 @@ import { IdentityInterface, InputInterface, isBlindedOutputInterface, - isBlindedUtxo, psetToUnsignedTx, RecipientInterface, TxInterface, @@ -47,37 +46,39 @@ function outPubKeysMap(pset: string, outputAddresses: string[]): Map { +function inputBlindingDataMap( + pset: string, + utxos: UtxoInterface[] +): Map { const inputBlindingData = new Map(); let index = -1; for (const input of psetToUnsignedTx(pset).ins) { index++; - const utxo = utxos.find(u => u.txid === input.hash.toString('hex')); + const utxo = utxos.find((u) => Buffer.from(u.txid, 'hex').reverse().equals(input.hash)); if (!utxo) { - throw new Error(`blindPSET error: utxo not found '${input.hash.reverse().toString('hex')}'`) + throw new Error(`blindPSET error: utxo not found '${input.hash.reverse().toString('hex')}'`); } - if (!isBlindedUtxo(utxo)) continue; if (!utxo.unblindData) { - throw new Error(`blindPSET error: utxo need unblind data '${input.hash.reverse().toString('hex')}'`) + throw new Error( + `blindPSET error: utxo need unblind data '${input.hash.reverse().toString('hex')}'` + ); } - inputBlindingData.set(index, utxo.unblindData) + inputBlindingData.set(index, utxo.unblindData); } return inputBlindingData; } -async function blindPset( - psetBase64: string, - utxos: UtxoInterface[], - outputAddresses: string[], -) { +async function blindPset(psetBase64: string, utxos: UtxoInterface[], outputAddresses: string[]) { const outputPubKeys = outPubKeysMap(psetBase64, outputAddresses); const inputBlindingData = inputBlindingDataMap(psetBase64, utxos); - - return (await decodePset(psetBase64).blindOutputsByIndex(inputBlindingData, outputPubKeys)).toBase64() + + return ( + await decodePset(psetBase64).blindOutputsByIndex(inputBlindingData, outputPubKeys) + ).toBase64(); } /** @@ -96,12 +97,16 @@ export async function blindAndSignPset( for (const id of identities) { outputAddresses.push(...(await id.getAddresses()).map((a) => a.confidentialAddress)); } - + let pset = await blindPset(psetBase64, selectedUtxos, outputAddresses); - + for (const id of identities) { pset = await id.signPset(pset); - if (decodePset(pset).validateSignaturesOfAllInputs()) break; + try { + if (decodePset(pset).validateSignaturesOfAllInputs()) break; + } catch { + continue; + } } const decodedPset = decodePset(pset); diff --git a/src/domain/account.ts b/src/domain/account.ts index 6cf7a082..fd8f223e 100644 --- a/src/domain/account.ts +++ b/src/domain/account.ts @@ -20,7 +20,7 @@ import { restoredWatchOnlyMultisig, } from '../application/utils/restorer'; import { createAddress } from './address'; -import { HDSignerToXPub, MockedCosigner, MultisigWithCosigner } from './cosigner'; +import { MockedCosigner, MultisigWithCosigner } from './cosigner'; import { EncryptedMnemonic } from './encrypted-mnemonic'; import { MasterBlindingKey } from './master-blinding-key'; import { MasterXPub } from './master-extended-pub'; @@ -117,7 +117,7 @@ export async function create2of2MultisigAccountData( return { baseDerivationPath: signer.baseDerivationPath || DEFAULT_BASE_DERIVATION_PATH, - signerXPub: HDSignerToXPub(signer, network), + signerXPub: multisigID.getXPub(), cosignerXPubs: [cosignerXPub], requiredSignature: 2, extraData, @@ -141,7 +141,7 @@ export function createMultisigAccount( data.cosignerXPubs, data.requiredSignature, data.restorerOpts, - new MockedCosigner(data.network, data.signerXPub), + new MockedCosigner(data.network), data.network ), getWatchIdentity: () => diff --git a/src/domain/cosigner.ts b/src/domain/cosigner.ts index b3be1c9e..abf8cf54 100644 --- a/src/domain/cosigner.ts +++ b/src/domain/cosigner.ts @@ -1,22 +1,42 @@ -import { fromPublicKey, fromSeed } from 'bip32'; -import { mnemonicToSeedSync } from 'bip39'; import { + decodePset, DEFAULT_BASE_DERIVATION_PATH, - HDSignerMultisig, IdentityInterface, IdentityOpts, IdentityType, - Mnemonic, Multisig, multisigFromEsplora, MultisigOpts, - toXpub, XPub, } from 'ldk'; -import { networkFromString } from '../application/utils'; import { BlockstreamExplorerURLs, NigiriDefaultExplorerURLs } from './app'; import { Network } from './network'; +function decodeMultisigPath(path: string) { + const splitted = path.split('/'); + return { change: parseInt(splitted[0]), index: parseInt(splitted[1]) }; +} + +function addRedeemAndWitnessScriptsToInputs(pset: string, multisig: Multisig): string { + const decoded = decodePset(pset); + let inputIndex = 0; + for (const input of decoded.data.inputs) { + if (!input.witnessUtxo) continue; + const path = multisig.scriptToPath[input.witnessUtxo.script.toString('hex')]; + if (path) { + const { change, index } = decodeMultisigPath(path); + const p2ms = multisig.getMultisigAddress(change, index); + decoded.updateInput(inputIndex, { + redeemScript: Buffer.from(p2ms.redeemScript, 'hex'), + witnessScript: Buffer.from(p2ms.witnessScript, 'hex'), + }); + } + + inputIndex++; + } + return decoded.toBase64(); +} + export class MultisigWithCosigner extends Multisig implements IdentityInterface { private cosigner: Cosigner; @@ -26,73 +46,59 @@ export class MultisigWithCosigner extends Multisig implements IdentityInterface } async signPset(pset: string): Promise { - const signed = await super.signPset(pset); - return this.cosigner.signPset(signed); + const toSign = addRedeemAndWitnessScriptsToInputs(pset, this); + const signed = await super.signPset(toSign); + return this.cosigner.signPset(signed, this.getXPub()); } } export interface Cosigner { - requestXPub(signerXPub: XPub): Promise; - signPset(pset: string): Promise; -} - -export function HDSignerToXPub(signer: HDSignerMultisig, network: Network) { - const walletSeed = mnemonicToSeedSync(signer.mnemonic); - const net = networkFromString(network); - const baseNode = fromSeed(walletSeed, net).derivePath( - signer.baseDerivationPath || DEFAULT_BASE_DERIVATION_PATH - ); - return toXpub(fromPublicKey(baseNode.publicKey, baseNode.chainCode, net).toBase58()); + xPub(): Promise; + signPset(pset: string, xpub: XPub): Promise; } export class MockedCosigner implements Cosigner { - private mnemonic: Mnemonic; - private cosignerXPub: XPub; + private mnemonic = + 'sponsor envelope waste fork indicate board survey tobacco laugh cover guitar layer'; private network: Network; private esploraURL: string; - constructor(network: Network, cosignerXPub: XPub) { - this.mnemonic = new Mnemonic({ - chain: network, - type: IdentityType.Mnemonic, - opts: { - mnemonic: - 'sponsor envelope waste fork indicate board survey tobacco laugh cover guitar layer', - baseDerivationPath: DEFAULT_BASE_DERIVATION_PATH, - }, - }); + constructor(network: Network) { this.network = network; this.esploraURL = network === 'liquid' ? BlockstreamExplorerURLs.esploraURL : NigiriDefaultExplorerURLs.esploraURL; - this.cosignerXPub = cosignerXPub; } - requestXPub(_: XPub) { - return Promise.resolve(this.mnemonic.getXPub()); + xPub() { + return Promise.resolve( + 'xpub661MyMwAqRbcFgkcqS2dYiVoJLc9QEiVQLPcyG1pkVi2UTUSe8dCAjkUVqczLiamx4R9jrSj6GefRRFZyF9cfApymZm4WzazurfdaAYWqhb' + ); } - async signPset(pset: string) { - if (this.cosignerXPub === undefined) { - throw new Error('pairing is not done'); - } - + async signPset(pset: string, cosignerXPub: XPub) { const multisigID = await multisigFromEsplora( new Multisig({ chain: this.network, type: IdentityType.Multisig, opts: { requiredSignatures: 2, - cosigners: [this.cosignerXPub], + cosigners: [cosignerXPub], signer: { - mnemonic: this.mnemonic.mnemonic, + mnemonic: this.mnemonic, baseDerivationPath: DEFAULT_BASE_DERIVATION_PATH, }, }, }) )({ esploraURL: this.esploraURL, gapLimit: 20 }); - return multisigID.signPset(pset); + await multisigID.getNextAddress(); + const signed = await multisigID.signPset(pset); + + if (!decodePset(signed).validateSignaturesOfAllInputs()) { + throw new Error('Mocked cosigner: not able to sign pset'); + } + return signed; } } diff --git a/src/presentation/connect/spend.tsx b/src/presentation/connect/spend.tsx index 444b6bbf..126a026d 100644 --- a/src/presentation/connect/spend.tsx +++ b/src/presentation/connect/spend.tsx @@ -191,8 +191,9 @@ async function makeTransaction( ); const txHex = await blindAndSignPset( - mnemonic, unsignedPset, + coins, + [mnemonic], recipients .map(({ address }) => address) .concat(Object.values(changeAddresses).map(({ confidentialAddress }) => confidentialAddress)) diff --git a/src/presentation/cosigner/pair.tsx b/src/presentation/cosigner/pair.tsx index cfa390e0..629ecdd4 100644 --- a/src/presentation/cosigner/pair.tsx +++ b/src/presentation/cosigner/pair.tsx @@ -7,7 +7,7 @@ import { create2of2MultisigAccountData } from '../../domain/account'; import { CosignerExtraData } from '../../domain/wallet'; import { decrypt } from '../../application/utils'; import { EncryptedMnemonic } from '../../domain/encrypted-mnemonic'; -import { Cosigner, HDSignerToXPub, MockedCosigner } from '../../domain/cosigner'; +import { Cosigner, MockedCosigner } from '../../domain/cosigner'; import { Network } from '../../domain/network'; import { useDispatch } from 'react-redux'; import { ProxyStoreDispatch } from '../../application/redux/proxyStore'; @@ -107,15 +107,13 @@ const PairCosignerView: React.FC = ({ mnemonic: decrypt(encryptedMnemonic, values.password), baseDerivationPath: values.derivationPath, }; - const walletXPub = HDSignerToXPub(walletSignerData, network); // cosigner should be created from values.cosignerURL - const cosigner: Cosigner = new MockedCosigner(network, walletXPub); - const requestedXPub = await cosigner.requestXPub(walletXPub); + const cosigner: Cosigner = new MockedCosigner(network); const multisigAccountData = await create2of2MultisigAccountData( walletSignerData, - requestedXPub, + await cosigner.xPub(), network, { cosignerURL: values.cosignerURL }, explorerURL diff --git a/src/presentation/wallet/send/choose-fee.tsx b/src/presentation/wallet/send/choose-fee.tsx index 2746b449..db752b74 100644 --- a/src/presentation/wallet/send/choose-fee.tsx +++ b/src/presentation/wallet/send/choose-fee.tsx @@ -1,6 +1,14 @@ import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router'; -import { ChangeAddressFromAssetGetter, CoinSelectionResult, CoinSelector, greedyCoinSelector, RecipientInterface, UtxoInterface, walletFromCoins } from 'ldk'; +import { + ChangeAddressFromAssetGetter, + CoinSelectionResult, + CoinSelector, + greedyCoinSelector, + RecipientInterface, + UtxoInterface, + walletFromCoins, +} from 'ldk'; import Balance from '../../components/balance'; import Button from '../../components/button'; import ShellPopUp from '../../components/shell-popup'; @@ -116,12 +124,7 @@ const ChooseFeeView: React.FC = ({ network, state.topup ) - : stateForRegularPSET( - getRecipient(), - changeAddress!, - utxos, - network, - ); + : stateForRegularPSET(getRecipient(), changeAddress!, utxos, network); newStatePromise.then(setState).catch(handleError).finally(done); }, [feeAsset]); @@ -235,18 +238,22 @@ const ChooseFeeView: React.FC = ({ }; const sideEffectCoinSelector = (sideEffect: (r: CoinSelectionResult) => void): CoinSelector => { - return (unspents: UtxoInterface[], outputs: RecipientInterface[], changeGetter: ChangeAddressFromAssetGetter) => { + return ( + unspents: UtxoInterface[], + outputs: RecipientInterface[], + changeGetter: ChangeAddressFromAssetGetter + ) => { const result = greedyCoinSelector()(unspents, outputs, changeGetter); sideEffect(result); return result; - } -} + }; +}; function stateForRegularPSET( recipient: RecipientInterface, change: Address, utxos: UtxoInterface[], - network: Network, + network: Network ): Promise { const result: State = {}; result.unsignedPset = undefined; diff --git a/src/presentation/wallet/send/end-of-flow.tsx b/src/presentation/wallet/send/end-of-flow.tsx index 7b0d97fc..9700a61d 100644 --- a/src/presentation/wallet/send/end-of-flow.tsx +++ b/src/presentation/wallet/send/end-of-flow.tsx @@ -8,17 +8,25 @@ import { SEND_PAYMENT_ERROR_ROUTE, SEND_PAYMENT_SUCCESS_ROUTE } from '../../rout import { debounce } from 'lodash'; import { createPassword } from '../../../domain/password'; import { extractErrorMessage } from '../../utils/error'; -import { Account, MainAccountID } from '../../../domain/account'; +import { Account } from '../../../domain/account'; import { Transaction } from 'liquidjs-lib'; +import { UtxoInterface } from 'ldk'; export interface EndOfFlowProps { - account: Account; + accounts: Account[]; pset?: string; + selectedUtxos: UtxoInterface[]; explorerURL: string; recipientAddress?: string; } -const EndOfFlow: React.FC = ({ account, pset, explorerURL, recipientAddress }) => { +const EndOfFlow: React.FC = ({ + accounts, + pset, + explorerURL, + recipientAddress, + selectedUtxos, +}) => { const history = useHistory(); const [isModalUnlockOpen, showUnlockModal] = useState(true); @@ -26,27 +34,24 @@ const EndOfFlow: React.FC = ({ account, pset, explorerURL, recip const handleUnlockModalOpen = () => showUnlockModal(true); const handleUnlock = async (password: string) => { - let tx = ''; - if (!pset || !recipientAddress) return; try { + if (!pset || !recipientAddress) throw new Error('no pset to sign'); const pass = createPassword(password); - const signer = await account.getSigningIdentity(pass); - tx = await blindAndSignPset(signer, pset, [recipientAddress]); + const identities = await Promise.all(accounts.map((a) => a.getSigningIdentity(pass))); + const tx = await blindAndSignPset(pset, selectedUtxos, identities, [recipientAddress]); const txid = Transaction.fromHex(tx).getId(); - if (account.getAccountID() === MainAccountID) { - await broadcastTx(explorerURL, tx); - } + await broadcastTx(explorerURL, tx); history.push({ pathname: SEND_PAYMENT_SUCCESS_ROUTE, - state: { txid, accountID: account.getAccountID() }, + state: { txid, accountIDs: accounts.map((a) => a.getAccountID()) }, }); } catch (error: unknown) { return history.push({ pathname: SEND_PAYMENT_ERROR_ROUTE, state: { - tx: tx, + tx: '', error: extractErrorMessage(error), }, }); diff --git a/src/presentation/wallet/send/payment-success.tsx b/src/presentation/wallet/send/payment-success.tsx index c74069c8..1f682704 100644 --- a/src/presentation/wallet/send/payment-success.tsx +++ b/src/presentation/wallet/send/payment-success.tsx @@ -13,7 +13,7 @@ import { txsUpdateTask, utxosUpdateTask } from '../../../application/redux/actio interface LocationState { txid: string; - accountID: AccountID; + accountIDs: AccountID[]; } export interface PaymentSuccessProps { @@ -41,8 +41,12 @@ const PaymentSuccessView: React.FC = ({ electrsExplorerURL useEffect(() => { void (async () => { await dispatch(flushPendingTx()); - await dispatch(utxosUpdateTask(state.accountID)).catch(console.error); - await dispatch(txsUpdateTask(state.accountID)).catch(console.error); + + await Promise.all( + (state.accountIDs ?? []) + .flatMap((ID) => [utxosUpdateTask(ID), txsUpdateTask(ID)]) + .map(dispatch) + ).catch(console.error); })(); }, []); diff --git a/test/spend.spec.ts b/test/spend.spec.ts index 9089f59b..ad3967db 100644 --- a/test/spend.spec.ts +++ b/test/spend.spec.ts @@ -12,7 +12,7 @@ const RECEIVER = 'AzpofttCgtcfk1PDWytoocvMWqQnLUJfjZw6MVmSdJQtwWnovQPgqiWSRTFZmK describe('create send pset (build, blind & sign)', () => { const mnemonic: Mnemonic = makeRandomMnemonic(); - const unspents: UtxoInterface[] = [] + const unspents: UtxoInterface[] = []; const makeUnspents = async () => { const addr = await mnemonic.getNextAddress(); From d0d57c7710549dd40e9d0b37fe9f441be6fe7286 Mon Sep 17 00:00:00 2001 From: louisinger Date: Mon, 25 Oct 2021 14:25:16 +0200 Subject: [PATCH 30/52] fix blindPSET with several identities --- src/application/utils/transaction.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/application/utils/transaction.ts b/src/application/utils/transaction.ts index 589b5b77..ed891900 100644 --- a/src/application/utils/transaction.ts +++ b/src/application/utils/transaction.ts @@ -60,13 +60,9 @@ function inputBlindingDataMap( throw new Error(`blindPSET error: utxo not found '${input.hash.reverse().toString('hex')}'`); } - if (!utxo.unblindData) { - throw new Error( - `blindPSET error: utxo need unblind data '${input.hash.reverse().toString('hex')}'` - ); + if (utxo.unblindData) { + inputBlindingData.set(index, utxo.unblindData); } - - inputBlindingData.set(index, utxo.unblindData); } return inputBlindingData; From cafaed5f3ed726a624c7890f290649622e7ba9c1 Mon Sep 17 00:00:00 2001 From: louisinger Date: Mon, 25 Oct 2021 15:30:11 +0200 Subject: [PATCH 31/52] multiple providers implementation --- src/content/broker.ts | 8 +++- src/content/coinos/coinosBroker.ts | 41 +++++++++++++++++++ src/content/content-script.ts | 5 ++- src/content/{ => marina}/marinaBroker.ts | 34 +++++++-------- src/domain/message.ts | 1 + src/inject/coinOS/provider.ts | 14 +++++++ src/inject/inject-script.ts | 7 +++- src/inject/{ => marina}/marinaEventHandler.ts | 4 +- src/inject/{marina.ts => marina/provider.ts} | 4 +- src/inject/proxy.ts | 7 ++++ src/presentation/connect/popupBroker.ts | 3 +- src/presentation/connect/popupWindowProxy.ts | 6 +++ 12 files changed, 108 insertions(+), 26 deletions(-) create mode 100644 src/content/coinos/coinosBroker.ts rename src/content/{ => marina}/marinaBroker.ts (89%) create mode 100644 src/inject/coinOS/provider.ts rename src/inject/{ => marina}/marinaEventHandler.ts (94%) rename src/inject/{marina.ts => marina/provider.ts} (97%) diff --git a/src/content/broker.ts b/src/content/broker.ts index 46b8f922..be7caaed 100644 --- a/src/content/broker.ts +++ b/src/content/broker.ts @@ -15,12 +15,15 @@ export type BrokerOption = (broker: Broker) => void; export default class Broker { protected store?: BrokerProxyStore = undefined; protected backgroundScriptPort: browser.Runtime.Port; + protected providerName: string; - constructor(options: BrokerOption[] = []) { + constructor(name: string, options: BrokerOption[] = []) { this.backgroundScriptPort = browser.runtime.connect(); for (const opt of options) { opt(this); } + + this.providerName = name; } start(handler: MessageHandler) { @@ -29,6 +32,7 @@ export default class Broker { 'message', (event: MessageEvent) => { if (!isMessageEvent(event)) return; + if (event.data.provider !== this.providerName) return; // handler should reject and resolve ResponseMessage. handler(event.data) @@ -76,5 +80,5 @@ export default class Broker { // custom type guard for MessageEvent function isMessageEvent(event: MessageEvent): event is MessageEvent { - return event.source === window && event.data && event.data.id && event.data.name; + return event.source === window && event.data && event.data.id && event.data.name && event.data.provider; } diff --git a/src/content/coinos/coinosBroker.ts b/src/content/coinos/coinosBroker.ts new file mode 100644 index 00000000..c3b82214 --- /dev/null +++ b/src/content/coinos/coinosBroker.ts @@ -0,0 +1,41 @@ +import { selectUtxos } from "../../application/redux/selectors/wallet.selector"; +import { RestrictedAssetAccountID } from "../../domain/account"; +import { MessageHandler, newErrorResponseMessage, newSuccessResponseMessage, RequestMessage } from "../../domain/message"; +import CoinosProvider from "../../inject/coinOS/provider"; +import Broker, { BrokerOption } from "../broker"; +import MarinaBroker from "../marina/marinaBroker"; + +export default class CoinosBroker extends Broker { + static async Start() { + const broker = new CoinosBroker([await MarinaBroker.WithProxyStore()]); + broker.start(); + } + + private constructor(opts: BrokerOption[]) { + super(CoinosProvider.PROVIDER_NAME, opts); + } + + start() { + super.start(this.messageHandler); + } + + private messageHandler: MessageHandler = async ({ id, name, params }: RequestMessage) => { + if (!this.store) throw new Error('proxy store is not set up in allowance broker'); + const successMsg = (data?: any) => newSuccessResponseMessage(id, data); + + try { + switch (name) { + case CoinosProvider.prototype.getCoins.name: { + const utxos = selectUtxos(RestrictedAssetAccountID)(this.store.getState()); + return successMsg(utxos); + } + + default: + return newErrorResponseMessage(id, new Error('Method not implemented.')); + } + } catch (err) { + if (err instanceof Error) return newErrorResponseMessage(id, err); + else throw err; + } + } +} \ No newline at end of file diff --git a/src/content/content-script.ts b/src/content/content-script.ts index 8becfd5f..36084a02 100644 --- a/src/content/content-script.ts +++ b/src/content/content-script.ts @@ -1,6 +1,7 @@ import browser from 'webextension-polyfill'; -import MarinaBroker from './marinaBroker'; +import MarinaBroker from './marina/marinaBroker'; +import CoinosBroker from './coinos/coinosBroker'; // start the broker + inject the inject-script.js script startContentScript().catch(console.error); @@ -10,6 +11,8 @@ async function startContentScript() { if (doctypeCheck() && suffixCheck() && documentElementCheck()) { const currentHostname = window.location.hostname; await MarinaBroker.Start(currentHostname); + await CoinosBroker.Start(); + injectScript(browser.runtime.getURL('inject-script.js')); } } diff --git a/src/content/marinaBroker.ts b/src/content/marina/marinaBroker.ts similarity index 89% rename from src/content/marinaBroker.ts rename to src/content/marina/marinaBroker.ts index 6f4011a4..1b93748c 100644 --- a/src/content/marinaBroker.ts +++ b/src/content/marina/marinaBroker.ts @@ -1,14 +1,14 @@ -import { stringify } from '../application/utils/browser-storage-converters'; -import { compareCacheForEvents, newCacheFromState, newStoreCache, StoreCache } from './store-cache'; -import Broker, { BrokerOption } from './broker'; +import { stringify } from '../../application/utils/browser-storage-converters'; +import { compareCacheForEvents, newCacheFromState, newStoreCache, StoreCache } from '../store-cache'; +import Broker, { BrokerOption } from '../broker'; import { MessageHandler, newErrorResponseMessage, newSuccessResponseMessage, RequestMessage, -} from '../domain/message'; -import Marina from '../inject/marina'; -import { RootReducerState } from '../domain/common'; +} from '../../domain/message'; +import Marina from '../../inject/marina/provider'; +import { RootReducerState } from '../../domain/common'; import { disableWebsite, flushMsg, @@ -17,24 +17,24 @@ import { setMsg, setTx, setTxData, -} from '../application/redux/actions/connect'; +} from '../../application/redux/actions/connect'; import { selectMainAccount, selectTransactions, selectUtxos, -} from '../application/redux/selectors/wallet.selector'; +} from '../../application/redux/selectors/wallet.selector'; import { incrementAddressIndex, incrementChangeAddressIndex, -} from '../application/redux/actions/wallet'; -import { lbtcAssetByNetwork, sortRecipients } from '../application/utils'; -import { selectBalances } from '../application/redux/selectors/balance.selector'; -import { assetGetterFromIAssets } from '../domain/assets'; +} from '../../application/redux/actions/wallet'; +import { lbtcAssetByNetwork, sortRecipients } from '../../application/utils'; +import { selectBalances } from '../../application/redux/selectors/balance.selector'; +import { assetGetterFromIAssets } from '../../domain/assets'; import { Balance, Recipient } from 'marina-provider'; -import { SignTransactionPopupResponse } from '../presentation/connect/sign-pset'; -import { SpendPopupResponse } from '../presentation/connect/spend'; -import { SignMessagePopupResponse } from '../presentation/connect/sign-msg'; -import { MainAccountID } from '../domain/account'; +import { SignTransactionPopupResponse } from '../../presentation/connect/sign-pset'; +import { SpendPopupResponse } from '../../presentation/connect/spend'; +import { SignMessagePopupResponse } from '../../presentation/connect/sign-msg'; +import { MainAccountID } from '../../domain/account'; export default class MarinaBroker extends Broker { private static NotSetUpError = new Error('proxy store and/or cache are not set up'); @@ -47,7 +47,7 @@ export default class MarinaBroker extends Broker { } private constructor(hostname = '', brokerOpts?: BrokerOption[]) { - super(brokerOpts); + super(Marina.PROVIDER_NAME, brokerOpts); this.hostname = hostname; this.cache = newStoreCache(); this.subscribeToStoreEvents(); diff --git a/src/domain/message.ts b/src/domain/message.ts index 47e2658d..2a7af46c 100644 --- a/src/domain/message.ts +++ b/src/domain/message.ts @@ -4,6 +4,7 @@ export interface RequestMessage { id: string; name: string; params?: Array; + provider: string; } // the message received by the inject script diff --git a/src/inject/coinOS/provider.ts b/src/inject/coinOS/provider.ts new file mode 100644 index 00000000..22793496 --- /dev/null +++ b/src/inject/coinOS/provider.ts @@ -0,0 +1,14 @@ +import { UtxoInterface } from "ldk"; +import WindowProxy from "../proxy"; + +export default class CoinosProvider extends WindowProxy { + static PROVIDER_NAME = 'coinos'; + + constructor() { + super(CoinosProvider.PROVIDER_NAME); + } + + async getCoins(): Promise { + return this.proxy(this.getCoins.name, []); + } +} \ No newline at end of file diff --git a/src/inject/inject-script.ts b/src/inject/inject-script.ts index 0940acf6..2ef686eb 100644 --- a/src/inject/inject-script.ts +++ b/src/inject/inject-script.ts @@ -1,8 +1,13 @@ // this is an inject script, "injected" in web pages // this script set up window.marina provider -import Marina from './marina'; +import CoinosProvider from './coinOS/provider'; +import Marina from './marina/provider'; const marina = new Marina(); (window as Record)[Marina.PROVIDER_NAME] = marina; window.dispatchEvent(new Event(`${Marina.PROVIDER_NAME}#initialized`)); + +const coinos = new CoinosProvider(); +(window as Record)[CoinosProvider.PROVIDER_NAME] = coinos; +window.dispatchEvent(new Event(`${CoinosProvider.PROVIDER_NAME}#initialized`)); diff --git a/src/inject/marinaEventHandler.ts b/src/inject/marina/marinaEventHandler.ts similarity index 94% rename from src/inject/marinaEventHandler.ts rename to src/inject/marina/marinaEventHandler.ts index 24b1f59a..6b86176f 100644 --- a/src/inject/marinaEventHandler.ts +++ b/src/inject/marina/marinaEventHandler.ts @@ -1,6 +1,6 @@ import { MarinaEventType } from 'marina-provider'; -import { parse } from '../application/utils/browser-storage-converters'; -import { makeid } from './proxy'; +import { parse } from '../../application/utils/browser-storage-converters'; +import { makeid } from '../proxy'; type EventListenerID = string; diff --git a/src/inject/marina.ts b/src/inject/marina/provider.ts similarity index 97% rename from src/inject/marina.ts rename to src/inject/marina/provider.ts index 430bee28..886aaa10 100644 --- a/src/inject/marina.ts +++ b/src/inject/marina/provider.ts @@ -11,7 +11,7 @@ import { Recipient, } from 'marina-provider'; import MarinaEventHandler from './marinaEventHandler'; -import WindowProxy from './proxy'; +import WindowProxy from '../proxy'; export default class Marina extends WindowProxy implements MarinaProvider { static PROVIDER_NAME = 'marina'; @@ -19,7 +19,7 @@ export default class Marina extends WindowProxy implements MarinaProvider { private eventHandler: MarinaEventHandler; constructor() { - super(); + super(Marina.PROVIDER_NAME); this.eventHandler = new MarinaEventHandler(); } diff --git a/src/inject/proxy.ts b/src/inject/proxy.ts index 50d9bc69..bfcab88a 100644 --- a/src/inject/proxy.ts +++ b/src/inject/proxy.ts @@ -1,6 +1,12 @@ import { parse } from '../application/utils/browser-storage-converters'; export default class WindowProxy { + protected providerName: string; + + constructor(providerName: string) { + this.providerName = providerName; + } + proxy(name: string, params: any[] = []): Promise { return new Promise((resolve, reject) => { const id = makeid(16); @@ -31,6 +37,7 @@ export default class WindowProxy { id, name, params, + provider: this.providerName, }, window.location.origin ); diff --git a/src/presentation/connect/popupBroker.ts b/src/presentation/connect/popupBroker.ts index 8f6efd52..a5625bfb 100644 --- a/src/presentation/connect/popupBroker.ts +++ b/src/presentation/connect/popupBroker.ts @@ -5,12 +5,13 @@ import { newSuccessResponseMessage, RequestMessage, } from '../../domain/message'; +import PopupWindowProxy from './popupWindowProxy'; export const POPUP_RESPONSE = 'POPUP_RESPONSE'; export default class PopupBroker extends Broker { static Start() { - const broker = new PopupBroker(); + const broker = new PopupBroker(PopupWindowProxy.PROVIDER_NAME); broker.start(); } diff --git a/src/presentation/connect/popupWindowProxy.ts b/src/presentation/connect/popupWindowProxy.ts index 56166a35..08f57ad4 100644 --- a/src/presentation/connect/popupWindowProxy.ts +++ b/src/presentation/connect/popupWindowProxy.ts @@ -3,6 +3,12 @@ import WindowProxy from '../../inject/proxy'; import { POPUP_RESPONSE } from './popupBroker'; export default class PopupWindowProxy extends WindowProxy { + static PROVIDER_NAME = 'connect'; + + constructor() { + super(PopupWindowProxy.PROVIDER_NAME); + } + sendResponse(message: PopupResponseMessage): Promise { return this.proxy(POPUP_RESPONSE, [message]); } From 15865412371f2a3b6da042a299e8dedce4326030 Mon Sep 17 00:00:00 2001 From: louisinger Date: Mon, 25 Oct 2021 17:29:58 +0200 Subject: [PATCH 32/52] allow UI (connect popup) --- package.json | 1 + src/application/redux/actions/action-types.ts | 4 + src/application/redux/actions/allowance.ts | 23 +++ .../redux/reducers/allowance-reducer.ts | 28 ++++ .../redux/reducers/connect-data-reducer.ts | 12 ++ src/application/redux/reducers/index.ts | 7 + .../redux/selectors/wallet.selector.ts | 2 +- src/application/utils/transaction.ts | 5 +- src/content/broker.ts | 4 +- src/content/coinos/coinosBroker.ts | 45 ++++-- src/content/marina/marinaBroker.ts | 7 +- src/domain/common.ts | 2 + src/domain/connect.ts | 5 +- src/domain/cosigner.ts | 42 ++++++ src/domain/message.ts | 10 +- src/inject/coinOS/provider.ts | 10 +- src/presentation/connect/allow-coin.tsx | 134 ++++++++++++++++++ src/presentation/routes/constants.ts | 2 + src/presentation/routes/index.tsx | 3 + 19 files changed, 327 insertions(+), 19 deletions(-) create mode 100644 src/application/redux/actions/allowance.ts create mode 100644 src/application/redux/reducers/allowance-reducer.ts create mode 100644 src/presentation/connect/allow-coin.tsx diff --git a/package.json b/package.json index e8bf1c32..2e70df22 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "formik": "^2.2.6", "google-protobuf": "^3.15.8", "ldk": "^0.3.14", + "liquidjs-lib": "^5.2.2", "lodash.debounce": "^4.0.8", "lottie-web": "^5.7.8", "marina-provider": "^1.4.3", diff --git a/src/application/redux/actions/action-types.ts b/src/application/redux/actions/action-types.ts index cd1e09a1..4f5bb0c7 100644 --- a/src/application/redux/actions/action-types.ts +++ b/src/application/redux/actions/action-types.ts @@ -49,6 +49,7 @@ export const SET_MSG = 'SET_MSG'; export const FLUSH_MSG = 'FLUSH_MSG'; export const SELECT_HOSTNAME = 'SELECT_HOSTNAME'; export const FLUSH_SELECTED_HOSTNAME = 'FLUSH_SELECTED_HOSTNAME'; +export const SET_ALLOW_COIN = 'SET_ALLOW_COIN'; export const RESET_CONNECT = 'RESET_CONNECT'; // Taxi @@ -68,3 +69,6 @@ export const RESET = 'RESET'; // Updater export const PUSH_UPDATER_TASK = 'PUSH_UPDATER_TASK'; export const POP_UPDATER_TASK = 'POP_UPDATER_TASK'; + +// Allowance +export const ALLOW_COIN = 'ALLOW_COIN'; diff --git a/src/application/redux/actions/allowance.ts b/src/application/redux/actions/allowance.ts new file mode 100644 index 00000000..5f3a5219 --- /dev/null +++ b/src/application/redux/actions/allowance.ts @@ -0,0 +1,23 @@ +import { Outpoint } from 'ldk'; +import { ActionWithPayload } from '../../../domain/common'; +import { ALLOW_COIN, SET_ALLOW_COIN } from './action-types'; + +export function allowCoin(txid: string, vout: number): ActionWithPayload { + return { + type: ALLOW_COIN, + payload: { + txid, + vout, + }, + }; +} + +export function setAllowCoinInConnectData(txid: string, vout: number): ActionWithPayload { + return { + type: SET_ALLOW_COIN, + payload: { + txid, + vout, + }, + }; +} diff --git a/src/application/redux/reducers/allowance-reducer.ts b/src/application/redux/reducers/allowance-reducer.ts new file mode 100644 index 00000000..93c36903 --- /dev/null +++ b/src/application/redux/reducers/allowance-reducer.ts @@ -0,0 +1,28 @@ +import { Outpoint } from 'ldk'; +import { AnyAction } from 'redux'; +import { ALLOW_COIN } from '../actions/action-types'; + +export interface AllowanceState { + allowed: Outpoint[]; +} + +export const allowanceInitState: AllowanceState = { + allowed: [], +}; + +export function allowanceReducer( + state: AllowanceState = allowanceInitState, + { type, payload }: AnyAction +): AllowanceState { + switch (type) { + case ALLOW_COIN: { + return { + ...state, + allowed: state.allowed.concat([{ txid: payload.txid, vout: payload.vout }]), + }; + } + + default: + return state; + } +} diff --git a/src/application/redux/reducers/connect-data-reducer.ts b/src/application/redux/reducers/connect-data-reducer.ts index e6a7805e..bd8319e3 100644 --- a/src/application/redux/reducers/connect-data-reducer.ts +++ b/src/application/redux/reducers/connect-data-reducer.ts @@ -71,6 +71,18 @@ export function connectDataReducer( }; } + case ACTION_TYPES.SET_ALLOW_COIN: { + return { + ...state, + allowance: { + allowCoin: { + txid: payload.txid, + vout: payload.vout, + }, + }, + }; + } + default: return state; } diff --git a/src/application/redux/reducers/index.ts b/src/application/redux/reducers/index.ts index 52d7e2c4..3c87aac0 100644 --- a/src/application/redux/reducers/index.ts +++ b/src/application/redux/reducers/index.ts @@ -16,6 +16,7 @@ import { appReducer, appInitState } from './app-reducer'; import { walletInitState, walletReducer } from './wallet-reducer'; import { connectDataReducer, connectDataInitState } from './connect-data-reducer'; import { updaterReducer } from './updater-reducer'; +import { allowanceInitState, allowanceReducer, AllowanceState } from './allowance-reducer'; const browserLocalStorage: Storage = { getItem: async (key: string) => { @@ -108,6 +109,12 @@ const marinaReducer = combineReducers({ initialState: connectDataInitState, }), updater: updaterReducer, + allowance: persist({ + reducer: allowanceReducer, + key: 'allowance', + version: 0, + initialState: allowanceInitState, + }), }); export default marinaReducer; diff --git a/src/application/redux/selectors/wallet.selector.ts b/src/application/redux/selectors/wallet.selector.ts index 95cd4d92..3b06312c 100644 --- a/src/application/redux/selectors/wallet.selector.ts +++ b/src/application/redux/selectors/wallet.selector.ts @@ -52,7 +52,7 @@ export function selectMainAccount(state: RootReducerState): MnemonicAccount { return createMnemonicAccount(state.wallet.mainAccount, state.app.network); } -function selectRestrictedAssetAccount(state: RootReducerState): MultisigAccount | undefined { +export function selectRestrictedAssetAccount(state: RootReducerState): MultisigAccount | undefined { if (!state.wallet.restrictedAssetAccount) return undefined; return createMultisigAccount( diff --git a/src/application/utils/transaction.ts b/src/application/utils/transaction.ts index ed891900..07913219 100644 --- a/src/application/utils/transaction.ts +++ b/src/application/utils/transaction.ts @@ -51,11 +51,14 @@ function inputBlindingDataMap( utxos: UtxoInterface[] ): Map { const inputBlindingData = new Map(); + const txidToBuffer = function (txid: string) { + return Buffer.from(txid, 'hex').reverse(); + }; let index = -1; for (const input of psetToUnsignedTx(pset).ins) { index++; - const utxo = utxos.find((u) => Buffer.from(u.txid, 'hex').reverse().equals(input.hash)); + const utxo = utxos.find((u) => txidToBuffer(u.txid).equals(input.hash)); if (!utxo) { throw new Error(`blindPSET error: utxo not found '${input.hash.reverse().toString('hex')}'`); } diff --git a/src/content/broker.ts b/src/content/broker.ts index be7caaed..91b7688a 100644 --- a/src/content/broker.ts +++ b/src/content/broker.ts @@ -80,5 +80,7 @@ export default class Broker { // custom type guard for MessageEvent function isMessageEvent(event: MessageEvent): event is MessageEvent { - return event.source === window && event.data && event.data.id && event.data.name && event.data.provider; + return ( + event.source === window && event.data && event.data.id && event.data.name && event.data.provider + ); } diff --git a/src/content/coinos/coinosBroker.ts b/src/content/coinos/coinosBroker.ts index c3b82214..cc4e3e79 100644 --- a/src/content/coinos/coinosBroker.ts +++ b/src/content/coinos/coinosBroker.ts @@ -1,9 +1,15 @@ -import { selectUtxos } from "../../application/redux/selectors/wallet.selector"; -import { RestrictedAssetAccountID } from "../../domain/account"; -import { MessageHandler, newErrorResponseMessage, newSuccessResponseMessage, RequestMessage } from "../../domain/message"; -import CoinosProvider from "../../inject/coinOS/provider"; -import Broker, { BrokerOption } from "../broker"; -import MarinaBroker from "../marina/marinaBroker"; +import { setAllowCoinInConnectData } from '../../application/redux/actions/allowance'; +import { selectUtxos } from '../../application/redux/selectors/wallet.selector'; +import { RestrictedAssetAccountID } from '../../domain/account'; +import { + MessageHandler, + newErrorResponseMessage, + newSuccessResponseMessage, + RequestMessage, +} from '../../domain/message'; +import CoinosProvider from '../../inject/coinOS/provider'; +import Broker, { BrokerOption } from '../broker'; +import MarinaBroker from '../marina/marinaBroker'; export default class CoinosBroker extends Broker { static async Start() { @@ -29,7 +35,28 @@ export default class CoinosBroker extends Broker { const utxos = selectUtxos(RestrictedAssetAccountID)(this.store.getState()); return successMsg(utxos); } - + + case CoinosProvider.prototype.allowCoin.name: { + if (!params || params.length < 2) throw new Error('invalid params'); + const txid = params[0]; + const vout = params[1]; + + await this.store.dispatchAsync(setAllowCoinInConnectData(txid, vout)); + + const utxos = selectUtxos(RestrictedAssetAccountID)(this.store.getState()); + const selectedCoin = utxos.find((u) => u.txid === txid && u.vout === vout); + if (!selectedCoin) { + throw new Error(`utxo ${txid}:${vout} is not found.`); + } + + const result = await this.openAndWaitPopup('allow-coin'); + if (!result) { + throw new Error('user rejected the allowance'); + } + + return successMsg(result); + } + default: return newErrorResponseMessage(id, new Error('Method not implemented.')); } @@ -37,5 +64,5 @@ export default class CoinosBroker extends Broker { if (err instanceof Error) return newErrorResponseMessage(id, err); else throw err; } - } -} \ No newline at end of file + }; +} diff --git a/src/content/marina/marinaBroker.ts b/src/content/marina/marinaBroker.ts index 1b93748c..413d5b31 100644 --- a/src/content/marina/marinaBroker.ts +++ b/src/content/marina/marinaBroker.ts @@ -1,5 +1,10 @@ import { stringify } from '../../application/utils/browser-storage-converters'; -import { compareCacheForEvents, newCacheFromState, newStoreCache, StoreCache } from '../store-cache'; +import { + compareCacheForEvents, + newCacheFromState, + newStoreCache, + StoreCache, +} from '../store-cache'; import Broker, { BrokerOption } from '../broker'; import { MessageHandler, diff --git a/src/domain/common.ts b/src/domain/common.ts index f349d6e7..e24600e1 100644 --- a/src/domain/common.ts +++ b/src/domain/common.ts @@ -7,6 +7,7 @@ import { Action } from 'redux'; import { TaxiState } from '../application/redux/reducers/taxi-reducer'; import { IAssets } from './assets'; import { UpdaterState } from '../application/redux/reducers/updater-reducer'; +import { AllowanceState } from '../application/redux/reducers/allowance-reducer'; export interface RootReducerState { app: IApp; @@ -17,6 +18,7 @@ export interface RootReducerState { connect: ConnectData; taxi: TaxiState; updater: UpdaterState; + allowance: AllowanceState; } export interface ActionWithPayload extends Action { diff --git a/src/domain/connect.ts b/src/domain/connect.ts index bddd1e6f..cd5a87e8 100644 --- a/src/domain/connect.ts +++ b/src/domain/connect.ts @@ -1,5 +1,5 @@ import { Network } from './network'; -import { RecipientInterface } from 'ldk'; +import { Outpoint, RecipientInterface } from 'ldk'; import { DataRecipient } from 'marina-provider'; export type ConnectData = { @@ -16,6 +16,9 @@ export type ConnectData = { hostname?: string; message?: string; }; + allowance?: { + allowCoin: Outpoint; + }; }; export function newEmptyConnectData(): ConnectData { diff --git a/src/domain/cosigner.ts b/src/domain/cosigner.ts index abf8cf54..5f983028 100644 --- a/src/domain/cosigner.ts +++ b/src/domain/cosigner.ts @@ -9,6 +9,7 @@ import { MultisigOpts, XPub, } from 'ldk'; +import { ECPair, Transaction } from 'liquidjs-lib'; import { BlockstreamExplorerURLs, NigiriDefaultExplorerURLs } from './app'; import { Network } from './network'; @@ -45,16 +46,53 @@ export class MultisigWithCosigner extends Multisig implements IdentityInterface this.cosigner = cosigner; } + // this is used instead of super.signPset in case of the input must be signed with SIGHASH_NONE + SIGHASH_ANYONE_CAN_PAY (allowance) + private async signWithSighashNone(psetBase64: string): Promise { + const pset = decodePset(psetBase64); + const signInputPromises: Promise[] = []; + + for (let index = 0; index < pset.data.inputs.length; index++) { + const input = pset.data.inputs[index]; + if (input.witnessUtxo) { + const derivationPath = this.scriptToPath[input.witnessUtxo.script.toString('hex')]; + + if (derivationPath) { + // if there is an address generated for the input script: build the signing key pair. + const privKey = this.baseNode.derivePath(derivationPath).privateKey; + if (!privKey) throw new Error('signing private key is undefined'); + const signingKeyPair = ECPair.fromPrivateKey(privKey); + // add the promise to array + signInputPromises.push( + pset.signInputAsync(index, signingKeyPair, [ + Transaction.SIGHASH_NONE + Transaction.SIGHASH_ANYONECANPAY, + ]) + ); + } + } + } + // wait that all signing promise resolved + await Promise.all(signInputPromises); + // return the signed pset, base64 encoded. + return pset.toBase64(); + } + async signPset(pset: string): Promise { const toSign = addRedeemAndWitnessScriptsToInputs(pset, this); const signed = await super.signPset(toSign); return this.cosigner.signPset(signed, this.getXPub()); } + + async allow(pset: string): Promise { + const toSign = addRedeemAndWitnessScriptsToInputs(pset, this); + const signed = await this.signWithSighashNone(toSign); + return this.cosigner.allow(signed); + } } export interface Cosigner { xPub(): Promise; signPset(pset: string, xpub: XPub): Promise; + allow(pset: string): Promise; } export class MockedCosigner implements Cosigner { @@ -101,4 +139,8 @@ export class MockedCosigner implements Cosigner { } return signed; } + + allow(pset: string) { + return Promise.resolve(); + } } diff --git a/src/domain/message.ts b/src/domain/message.ts index 2a7af46c..12fd1928 100644 --- a/src/domain/message.ts +++ b/src/domain/message.ts @@ -15,10 +15,16 @@ export interface ResponseMessage { } // basically the name of the connect/* files -export type PopupName = 'enable' | 'sign-msg' | 'sign-pset' | 'spend'; +export type PopupName = 'enable' | 'sign-msg' | 'sign-pset' | 'spend' | 'allow-coin'; export function isPopupName(name: any): name is PopupName { - return name === 'enable' || name === 'sign-msg' || name === 'sign-pset' || name === 'spend'; + return ( + name === 'enable' || + name === 'sign-msg' || + name === 'sign-pset' || + name === 'spend' || + name === 'allow-coin' + ); } export function isResponseMessage(message: unknown): message is ResponseMessage { diff --git a/src/inject/coinOS/provider.ts b/src/inject/coinOS/provider.ts index 22793496..3568450a 100644 --- a/src/inject/coinOS/provider.ts +++ b/src/inject/coinOS/provider.ts @@ -1,5 +1,5 @@ -import { UtxoInterface } from "ldk"; -import WindowProxy from "../proxy"; +import { UtxoInterface } from 'ldk'; +import WindowProxy from '../proxy'; export default class CoinosProvider extends WindowProxy { static PROVIDER_NAME = 'coinos'; @@ -11,4 +11,8 @@ export default class CoinosProvider extends WindowProxy { async getCoins(): Promise { return this.proxy(this.getCoins.name, []); } -} \ No newline at end of file + + async allowCoin(txid: string, vout: number) { + return this.proxy(this.allowCoin.name, [txid, vout]); + } +} diff --git a/src/presentation/connect/allow-coin.tsx b/src/presentation/connect/allow-coin.tsx new file mode 100644 index 00000000..2c9a5eb6 --- /dev/null +++ b/src/presentation/connect/allow-coin.tsx @@ -0,0 +1,134 @@ +import React, { useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + connectWithConnectData, + WithConnectDataProps, +} from '../../application/redux/containers/with-connect-data.container'; +import { ProxyStoreDispatch } from '../../application/redux/proxyStore'; +import { RootReducerState } from '../../domain/common'; +import Button from '../components/button'; +import ShellConnectPopup from '../components/shell-connect-popup'; +import PopupWindowProxy from './popupWindowProxy'; +import { debounce } from 'lodash'; +import { MultisigWithCosigner } from '../../domain/cosigner'; +import { UtxoInterface } from 'ldk'; +import { Network } from '../../domain/network'; +import { Psbt, Transaction } from 'liquidjs-lib'; +import { networkFromString } from '../../application/utils'; +import { + selectRestrictedAssetAccount, + selectUtxos, +} from '../../application/redux/selectors/wallet.selector'; +import { RestrictedAssetAccountID } from '../../domain/account'; +import ModalUnlock from '../components/modal-unlock'; +import { extractErrorMessage } from '../utils/error'; +import { allowCoin } from '../../application/redux/actions/allowance'; +import browser from 'webextension-polyfill'; + +async function createAndSendAllowCoinPset( + identity: MultisigWithCosigner, + utxo: UtxoInterface, + network: Network +) { + const pset = new Psbt({ network: networkFromString(network) }); + + pset.addInput({ + hash: utxo.txid, + index: utxo.vout, + witnessUtxo: utxo.prevout, + sighashType: Transaction.SIGHASH_NONE + Transaction.SIGHASH_ANYONECANPAY, + }); + + return identity.allow(pset.toBase64()); +} + +const AllowCoinView: React.FC = ({ connectData }) => { + const network = useSelector((state: RootReducerState) => state.app.network); + const electrs = useSelector( + (state: RootReducerState) => state.app.explorerByNetwork[state.app.network].electrsURL + ); + const restrictedAssetAccount = useSelector(selectRestrictedAssetAccount); + const utxos = useSelector(selectUtxos(RestrictedAssetAccountID)); + + const [unlock, setUnlock] = useState(false); + const [error, setError] = useState(); + const dispatch = useDispatch(); + const popupWindowProxy = new PopupWindowProxy(); + + const handleReject = async () => { + await popupWindowProxy.sendResponse({ data: false }); + window.close(); + }; + + const openPasswordModal = () => { + setError(undefined); + setUnlock(true); + }; + + const handleAllow = async (password: string) => { + if (!connectData.allowance?.allowCoin) throw new Error('no coin has been selected'); + if (!restrictedAssetAccount) + throw new Error('multisig account is undefined, u maybe need to pair with a cosigner'); + try { + const { txid, vout } = connectData.allowance.allowCoin; + const utxo = utxos.find((u) => u.txid === txid && u.vout === vout); + if (!utxo) + throw new Error('the requested coin is not owned by your restricted asset account'); + + const id = await restrictedAssetAccount.getSigningIdentity(password); + await createAndSendAllowCoinPset(id, utxo, network); + await dispatch(allowCoin(utxo.txid, utxo.vout)); + await popupWindowProxy.sendResponse({ data: true }); + window.close(); + } catch (err) { + setError(extractErrorMessage(err)); + } finally { + setUnlock(false); + } + }; + + const debouncedHandleAllow = useRef( + debounce(handleAllow, 2000, { leading: true, trailing: false }) + ).current; + + const handleOpenExplorer = () => { + browser.tabs + .create({ + url: `${electrs}/tx/${connectData.allowance?.allowCoin.txid}`, + active: false, + }) + .catch((err) => setError(extractErrorMessage(err))); + }; + + return ( + +

Allow

+ +

Allow website to spend a coin

+ + + {error &&

{error}

} + +
+ + +
+ setUnlock(false)} + handleUnlock={debouncedHandleAllow} + /> +
+ ); +}; + +export default connectWithConnectData(AllowCoinView); diff --git a/src/presentation/routes/constants.ts b/src/presentation/routes/constants.ts index c3ca8a9d..300472c7 100644 --- a/src/presentation/routes/constants.ts +++ b/src/presentation/routes/constants.ts @@ -7,6 +7,7 @@ const CONNECT_ENABLE_ROUTE = makeConnectRoute('enable'); const CONNECT_SPEND_ROUTE = makeConnectRoute('spend'); const CONNECT_SIGN_PSET_ROUTE = makeConnectRoute('sign-pset'); const CONNECT_SIGN_MSG_ROUTE = makeConnectRoute('sign-msg'); +const CONNECT_ALLOW_COIN_ROUTE = makeConnectRoute('allow-coin'); // Onboarding const INITIALIZE_WELCOME_ROUTE = '/initialize/welcome'; @@ -61,6 +62,7 @@ export { CONNECT_SPEND_ROUTE, CONNECT_SIGN_PSET_ROUTE, CONNECT_SIGN_MSG_ROUTE, + CONNECT_ALLOW_COIN_ROUTE, // Onboarding INITIALIZE_WELCOME_ROUTE, INITIALIZE_CREATE_PASSWORD_ROUTE, diff --git a/src/presentation/routes/index.tsx b/src/presentation/routes/index.tsx index 069e825d..f7e2075e 100644 --- a/src/presentation/routes/index.tsx +++ b/src/presentation/routes/index.tsx @@ -40,6 +40,7 @@ import { PAIR_COSIGNER_ROUTE, SETTINGS_COSIGNERS_ROUTE, PAIR_SUCCESS_COSIGNER_ROUTE, + CONNECT_ALLOW_COIN_ROUTE, } from './constants'; // Connect @@ -47,6 +48,7 @@ import ConnectEnableView from '../connect/enable'; import ConnectSpend from '../connect/spend'; import ConnectSignTransaction from '../connect/sign-pset'; import ConnectSignMsg from '../connect/sign-msg'; +import ConnectAllowCoin from '../connect/allow-coin'; // Onboarding import Welcome from '../onboarding/welcome'; @@ -134,6 +136,7 @@ const Routes: React.FC = () => { + From 03312f936f3554276a73e2d2e912f24d0f221c6a Mon Sep 17 00:00:00 2001 From: louisinger Date: Mon, 25 Oct 2021 17:45:58 +0200 Subject: [PATCH 33/52] allow remove cosigner URL in pairing form --- src/presentation/cosigner/pair.tsx | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/presentation/cosigner/pair.tsx b/src/presentation/cosigner/pair.tsx index 629ecdd4..644a03a7 100644 --- a/src/presentation/cosigner/pair.tsx +++ b/src/presentation/cosigner/pair.tsx @@ -21,7 +21,6 @@ interface OptInFormProps { } interface OptInFormValues { - cosignerURL: string; password: string; derivationPath: string; } @@ -33,15 +32,6 @@ const optInForm = (props: FormikProps) => { return (
-

Cosigner URL

- - {touchedAndError('cosignerURL') &&
{errors.cosignerURL}
} -

Password

) => { const OptInFormikForm = withFormik({ validationSchema: Yup.object().shape({ - cosignerURL: Yup.string().required().url('invalid URL'), password: Yup.string().required(), derivationPath: Yup.string() .required() @@ -115,7 +104,7 @@ const PairCosignerView: React.FC = ({ walletSignerData, await cosigner.xPub(), network, - { cosignerURL: values.cosignerURL }, + { cosignerURL: 'http://cosigner.URL' }, explorerURL ); From 5cc6b69780b5e4e7aa878cf986fb06f6b087ef86 Mon Sep 17 00:00:00 2001 From: louisinger Date: Wed, 27 Oct 2021 11:57:35 +0200 Subject: [PATCH 34/52] allow coin better params + returns the sighash none pset --- src/application/redux/actions/action-types.ts | 2 +- src/application/redux/actions/allowance.ts | 18 ++-- .../redux/reducers/connect-data-reducer.ts | 7 +- src/content/coinos/coinosBroker.ts | 24 +++-- src/domain/address.ts | 36 ++++---- src/domain/connect.ts | 9 +- src/domain/cosigner.ts | 6 +- src/inject/coinOS/provider.ts | 7 +- src/presentation/connect/allow-coin.tsx | 91 ++++++++++--------- 9 files changed, 102 insertions(+), 98 deletions(-) diff --git a/src/application/redux/actions/action-types.ts b/src/application/redux/actions/action-types.ts index 4f5bb0c7..788dcc08 100644 --- a/src/application/redux/actions/action-types.ts +++ b/src/application/redux/actions/action-types.ts @@ -49,7 +49,7 @@ export const SET_MSG = 'SET_MSG'; export const FLUSH_MSG = 'FLUSH_MSG'; export const SELECT_HOSTNAME = 'SELECT_HOSTNAME'; export const FLUSH_SELECTED_HOSTNAME = 'FLUSH_SELECTED_HOSTNAME'; -export const SET_ALLOW_COIN = 'SET_ALLOW_COIN'; +export const SET_APPROVE_REQUEST_PARAM = 'SET_APPROVE_REQUEST_PARAM'; export const RESET_CONNECT = 'RESET_CONNECT'; // Taxi diff --git a/src/application/redux/actions/allowance.ts b/src/application/redux/actions/allowance.ts index 5f3a5219..cef8eb39 100644 --- a/src/application/redux/actions/allowance.ts +++ b/src/application/redux/actions/allowance.ts @@ -1,23 +1,25 @@ import { Outpoint } from 'ldk'; import { ActionWithPayload } from '../../../domain/common'; -import { ALLOW_COIN, SET_ALLOW_COIN } from './action-types'; +import { AssetAmount } from '../../../domain/connect'; +import { ALLOW_COIN, SET_APPROVE_REQUEST_PARAM } from './action-types'; -export function allowCoin(txid: string, vout: number): ActionWithPayload { +export function addAllowedCoin(utxo: Outpoint): ActionWithPayload { return { type: ALLOW_COIN, payload: { - txid, - vout, + txid: utxo.txid, + vout: utxo.vout, }, }; } -export function setAllowCoinInConnectData(txid: string, vout: number): ActionWithPayload { +export function setApproveParams( + assetAmounts: AssetAmount[] +): ActionWithPayload<{ assetAmounts: AssetAmount[] }> { return { - type: SET_ALLOW_COIN, + type: SET_APPROVE_REQUEST_PARAM, payload: { - txid, - vout, + assetAmounts, }, }; } diff --git a/src/application/redux/reducers/connect-data-reducer.ts b/src/application/redux/reducers/connect-data-reducer.ts index bd8319e3..c08acb9d 100644 --- a/src/application/redux/reducers/connect-data-reducer.ts +++ b/src/application/redux/reducers/connect-data-reducer.ts @@ -71,14 +71,11 @@ export function connectDataReducer( }; } - case ACTION_TYPES.SET_ALLOW_COIN: { + case ACTION_TYPES.SET_APPROVE_REQUEST_PARAM: { return { ...state, allowance: { - allowCoin: { - txid: payload.txid, - vout: payload.vout, - }, + requestParam: payload.assetAmounts, }, }; } diff --git a/src/content/coinos/coinosBroker.ts b/src/content/coinos/coinosBroker.ts index cc4e3e79..065a9d25 100644 --- a/src/content/coinos/coinosBroker.ts +++ b/src/content/coinos/coinosBroker.ts @@ -1,6 +1,7 @@ -import { setAllowCoinInConnectData } from '../../application/redux/actions/allowance'; +import { setApproveParams } from '../../application/redux/actions/allowance'; import { selectUtxos } from '../../application/redux/selectors/wallet.selector'; import { RestrictedAssetAccountID } from '../../domain/account'; +import { AssetAmount } from '../../domain/connect'; import { MessageHandler, newErrorResponseMessage, @@ -37,19 +38,12 @@ export default class CoinosBroker extends Broker { } case CoinosProvider.prototype.allowCoin.name: { - if (!params || params.length < 2) throw new Error('invalid params'); - const txid = params[0]; - const vout = params[1]; + if (!params || params.length !== 1) throw new Error('invalid params'); + const requestParams: AssetAmount[] = params[0].filter(isAssetAmount); + if (requestParams.length <= 0) throw new Error('invalid params'); - await this.store.dispatchAsync(setAllowCoinInConnectData(txid, vout)); - - const utxos = selectUtxos(RestrictedAssetAccountID)(this.store.getState()); - const selectedCoin = utxos.find((u) => u.txid === txid && u.vout === vout); - if (!selectedCoin) { - throw new Error(`utxo ${txid}:${vout} is not found.`); - } - - const result = await this.openAndWaitPopup('allow-coin'); + await this.store.dispatchAsync(setApproveParams(requestParams)); + const result = await this.openAndWaitPopup('allow-coin'); if (!result) { throw new Error('user rejected the allowance'); } @@ -66,3 +60,7 @@ export default class CoinosBroker extends Broker { } }; } + +function isAssetAmount(assetAmount: any): assetAmount is AssetAmount { + return assetAmount.asset && assetAmount.amount; +} diff --git a/src/domain/address.ts b/src/domain/address.ts index eb54ee0f..d2459bbd 100644 --- a/src/domain/address.ts +++ b/src/domain/address.ts @@ -12,25 +12,21 @@ export function createAddress( address: Address['value'], derivationPath?: Address['derivationPath'] ): Address { - try { - // Non Confidential - if (address.startsWith('ert') || address.startsWith('ex')) { - addressLDK.fromBech32(address); - return { - derivationPath: derivationPath, - value: address, - }; - } else { - // Confidential - const { blindingKey, unconfidentialAddress } = addressLDK.fromConfidential(address); - return { - value: address, - blindingKey: blindingKey, - derivationPath: derivationPath, - unconfidentialAddress: unconfidentialAddress, - }; - } - } catch (err) { - throw new Error(err.message); + // Non Confidential + if (address.startsWith('ert') || address.startsWith('ex')) { + addressLDK.fromBech32(address); + return { + derivationPath: derivationPath, + value: address, + }; + } else { + // Confidential + const { blindingKey, unconfidentialAddress } = addressLDK.fromConfidential(address); + return { + value: address, + blindingKey: blindingKey, + derivationPath: derivationPath, + unconfidentialAddress: unconfidentialAddress, + }; } } diff --git a/src/domain/connect.ts b/src/domain/connect.ts index cd5a87e8..17476bb3 100644 --- a/src/domain/connect.ts +++ b/src/domain/connect.ts @@ -1,7 +1,12 @@ import { Network } from './network'; -import { Outpoint, RecipientInterface } from 'ldk'; +import { RecipientInterface } from 'ldk'; import { DataRecipient } from 'marina-provider'; +export interface AssetAmount { + asset: string; + amount: number; +} + export type ConnectData = { enabledSites: Record; hostnameSelected: string; @@ -17,7 +22,7 @@ export type ConnectData = { message?: string; }; allowance?: { - allowCoin: Outpoint; + requestParam: AssetAmount[]; }; }; diff --git a/src/domain/cosigner.ts b/src/domain/cosigner.ts index 5f983028..495f1203 100644 --- a/src/domain/cosigner.ts +++ b/src/domain/cosigner.ts @@ -82,17 +82,15 @@ export class MultisigWithCosigner extends Multisig implements IdentityInterface return this.cosigner.signPset(signed, this.getXPub()); } - async allow(pset: string): Promise { + async allow(pset: string): Promise { const toSign = addRedeemAndWitnessScriptsToInputs(pset, this); - const signed = await this.signWithSighashNone(toSign); - return this.cosigner.allow(signed); + return this.signWithSighashNone(toSign); } } export interface Cosigner { xPub(): Promise; signPset(pset: string, xpub: XPub): Promise; - allow(pset: string): Promise; } export class MockedCosigner implements Cosigner { diff --git a/src/inject/coinOS/provider.ts b/src/inject/coinOS/provider.ts index 3568450a..d3164230 100644 --- a/src/inject/coinOS/provider.ts +++ b/src/inject/coinOS/provider.ts @@ -1,4 +1,5 @@ import { UtxoInterface } from 'ldk'; +import { AssetAmount } from '../../domain/connect'; import WindowProxy from '../proxy'; export default class CoinosProvider extends WindowProxy { @@ -8,11 +9,13 @@ export default class CoinosProvider extends WindowProxy { super(CoinosProvider.PROVIDER_NAME); } + // returns the list of unspents owned by the restricted asset account async getCoins(): Promise { return this.proxy(this.getCoins.name, []); } - async allowCoin(txid: string, vout: number) { - return this.proxy(this.allowCoin.name, [txid, vout]); + // returns a signed pset with input = (txid, vout) (signed with SIGHASH_NONE) + async allowCoin(toAllow: AssetAmount[]): Promise { + return this.proxy(this.allowCoin.name, [toAllow]); } } diff --git a/src/presentation/connect/allow-coin.tsx b/src/presentation/connect/allow-coin.tsx index 2c9a5eb6..0596c9ab 100644 --- a/src/presentation/connect/allow-coin.tsx +++ b/src/presentation/connect/allow-coin.tsx @@ -11,7 +11,7 @@ import ShellConnectPopup from '../components/shell-connect-popup'; import PopupWindowProxy from './popupWindowProxy'; import { debounce } from 'lodash'; import { MultisigWithCosigner } from '../../domain/cosigner'; -import { UtxoInterface } from 'ldk'; +import { greedyCoinSelector, UtxoInterface } from 'ldk'; import { Network } from '../../domain/network'; import { Psbt, Transaction } from 'liquidjs-lib'; import { networkFromString } from '../../application/utils'; @@ -22,41 +22,48 @@ import { import { RestrictedAssetAccountID } from '../../domain/account'; import ModalUnlock from '../components/modal-unlock'; import { extractErrorMessage } from '../utils/error'; -import { allowCoin } from '../../application/redux/actions/allowance'; -import browser from 'webextension-polyfill'; - -async function createAndSendAllowCoinPset( +import { addAllowedCoin } from '../../application/redux/actions/allowance'; +import { assetGetterFromIAssets } from '../../domain/assets'; +import { fromSatoshi } from '../utils'; + +/** + * All a set of utxos to be spent later. + * use SIGHASH_NONE to sign the inputs + * @param identity the signer + * @param utxos the coins to approve + */ +async function createAllowCoinsPset( identity: MultisigWithCosigner, - utxo: UtxoInterface, + utxos: UtxoInterface[], network: Network -) { +): Promise { const pset = new Psbt({ network: networkFromString(network) }); - pset.addInput({ - hash: utxo.txid, - index: utxo.vout, - witnessUtxo: utxo.prevout, - sighashType: Transaction.SIGHASH_NONE + Transaction.SIGHASH_ANYONECANPAY, - }); + for (const utxo of utxos) { + pset.addInput({ + hash: utxo.txid, + index: utxo.vout, + witnessUtxo: utxo.prevout, + sighashType: Transaction.SIGHASH_NONE + Transaction.SIGHASH_ANYONECANPAY, + }); + } return identity.allow(pset.toBase64()); } const AllowCoinView: React.FC = ({ connectData }) => { const network = useSelector((state: RootReducerState) => state.app.network); - const electrs = useSelector( - (state: RootReducerState) => state.app.explorerByNetwork[state.app.network].electrsURL - ); const restrictedAssetAccount = useSelector(selectRestrictedAssetAccount); const utxos = useSelector(selectUtxos(RestrictedAssetAccountID)); + const getAsset = useSelector((state: RootReducerState) => assetGetterFromIAssets(state.assets)); const [unlock, setUnlock] = useState(false); const [error, setError] = useState(); const dispatch = useDispatch(); - const popupWindowProxy = new PopupWindowProxy(); + const popupWindowProxy = new PopupWindowProxy(); const handleReject = async () => { - await popupWindowProxy.sendResponse({ data: false }); + await popupWindowProxy.sendResponse({ data: '' }); window.close(); }; @@ -66,19 +73,25 @@ const AllowCoinView: React.FC = ({ connectData }) => { }; const handleAllow = async (password: string) => { - if (!connectData.allowance?.allowCoin) throw new Error('no coin has been selected'); + if (!connectData.allowance?.requestParam) throw new Error('no coin to allow'); if (!restrictedAssetAccount) throw new Error('multisig account is undefined, u maybe need to pair with a cosigner'); - try { - const { txid, vout } = connectData.allowance.allowCoin; - const utxo = utxos.find((u) => u.txid === txid && u.vout === vout); - if (!utxo) - throw new Error('the requested coin is not owned by your restricted asset account'); + try { const id = await restrictedAssetAccount.getSigningIdentity(password); - await createAndSendAllowCoinPset(id, utxo, network); - await dispatch(allowCoin(utxo.txid, utxo.vout)); - await popupWindowProxy.sendResponse({ data: true }); + const changeAddress = (await id.getNextChangeAddress()).confidentialAddress; + + const { selectedUtxos } = greedyCoinSelector()( + utxos, + connectData.allowance.requestParam.map((p) => ({ ...p, value: p.amount, address: '' })), + () => changeAddress + ); + + const allowPset = await createAllowCoinsPset(id, selectedUtxos, network); + + await Promise.all(selectedUtxos.map(addAllowedCoin).map(dispatch)); + await popupWindowProxy.sendResponse({ data: allowPset }); + window.close(); } catch (err) { setError(extractErrorMessage(err)); @@ -91,15 +104,6 @@ const AllowCoinView: React.FC = ({ connectData }) => { debounce(handleAllow, 2000, { leading: true, trailing: false }) ).current; - const handleOpenExplorer = () => { - browser.tabs - .create({ - url: `${electrs}/tx/${connectData.allowance?.allowCoin.txid}`, - active: false, - }) - .catch((err) => setError(extractErrorMessage(err))); - }; - return ( = ({ connectData }) => { >

Allow

-

Allow website to spend a coin

- - - {error &&

{error}

} - +

Allow website to spend:

+
+ {error &&

{error}

} + {connectData.allowance?.requestParam && + connectData.allowance.requestParam.map(({ asset, amount }) => ( +

{`${fromSatoshi(amount, getAsset(asset).precision)} ${getAsset(asset).ticker}`}

+ ))} +
+ + ); +}; + +interface OnboardingFormProps { + onSubmit: (values: { password: string; makeSecurityAccount: boolean }) => Promise; +} + +const OnboardingForm = withFormik({ + mapPropsToValues: (): OnboardingFormValues => ({ + confirmPassword: '', + password: '', + acceptTerms: false, + makeSecurityAccount: false, + }), + + validationSchema: Yup.object().shape({ + password: Yup.string() + .required('Please input password') + .min(8, 'Password is too short - should be 8 chars minimum.'), + confirmPassword: Yup.string() + .required('Please confirm password') + .min(8, 'Password is too short - should be 8 chars minimum.') + .oneOf([Yup.ref('password'), null], 'Passwords must match'), + acceptTerms: Yup.bool().oneOf([true], 'Accepting Terms & Conditions is required'), + }), + + handleSubmit: (values, { props }) => { + props.onSubmit(values).catch(console.error); + }, + + displayName: 'WalletCreateForm', +})(OnboardingFormView); + +export default OnboardingForm; diff --git a/src/presentation/onboarding/wallet-create/index.tsx b/src/presentation/onboarding/wallet-create/index.tsx index e6d62051..b7bd448c 100644 --- a/src/presentation/onboarding/wallet-create/index.tsx +++ b/src/presentation/onboarding/wallet-create/index.tsx @@ -1,166 +1,37 @@ +import { generateMnemonic } from 'bip39'; import React from 'react'; -import { RouteComponentProps, useHistory } from 'react-router-dom'; -import { FormikProps, withFormik } from 'formik'; -import * as Yup from 'yup'; -import cx from 'classnames'; -import Button from '../../components/button'; -import Shell from '../../components/shell'; -import { INITIALIZE_SEED_PHRASE_ROUTE, SETTINGS_TERMS_ROUTE } from '../../routes/constants'; import { useDispatch } from 'react-redux'; +import { useHistory } from 'react-router'; import { setPasswordAndOnboardingMnemonic } from '../../../application/redux/actions/onboarding'; -import { generateMnemonic } from 'bip39'; -import { ProxyStoreDispatch } from '../../../application/redux/proxyStore'; - -interface WalletCreateFormValues { - password: string; - confirmPassword: string; - acceptTerms: boolean; -} - -const WalletCreateForm = (props: FormikProps) => { - const { values, touched, errors, isSubmitting, handleChange, handleBlur, handleSubmit } = props; - - return ( -
-
- - {errors.password && touched.password && ( -

{errors.password}

- )} -
- -
- - {errors.confirmPassword && touched.confirmPassword && ( -

{errors.confirmPassword}

- )} -
- -
- - {errors.acceptTerms && touched.acceptTerms && ( -

{errors.acceptTerms}

- )} -
- - -
- ); -}; - -interface WalletCreateFormProps { - dispatch: ProxyStoreDispatch; - history: RouteComponentProps['history']; -} - -const WalletCreateEnhancedForm = withFormik({ - mapPropsToValues: (): WalletCreateFormValues => ({ - confirmPassword: '', - password: '', - acceptTerms: false, - }), - validationSchema: Yup.object().shape({ - password: Yup.string() - .required('Please input password') - .min(8, 'Password is too short - should be 8 chars minimum.'), - confirmPassword: Yup.string() - .required('Please confirm password') - .min(8, 'Password is too short - should be 8 chars minimum.') - .oneOf([Yup.ref('password'), null], 'Passwords must match'), - acceptTerms: Yup.bool().oneOf([true], 'Accepting Terms & Conditions is required'), - }), - - handleSubmit: (values, { props }) => { - props - .dispatch(setPasswordAndOnboardingMnemonic(values.password, generateMnemonic())) - .catch(console.error); - props.history.push(INITIALIZE_SEED_PHRASE_ROUTE); - }, - - displayName: 'WalletCreateForm', -})(WalletCreateForm); +import { ProxyStoreDispatch } from '../../../application/redux/proxyStore'; +import Shell from '../../components/shell'; +import { INITIALIZE_SEED_PHRASE_ROUTE } from '../../routes/constants'; +import OnboardingForm from '../onboarding-form'; -const WalletCreate: React.FC = () => { +const WalletCreate: React.FC = () => { const dispatch = useDispatch(); const history = useHistory(); + const onSubmit = async ({ + password, + makeSecurityAccount, + }: { + password: string; + makeSecurityAccount: boolean; + }) => { + await dispatch( + setPasswordAndOnboardingMnemonic(password, generateMnemonic(), makeSecurityAccount) + ); + history.push(INITIALIZE_SEED_PHRASE_ROUTE); + }; + return (

Create password

- +
); }; -const OpenTerms: React.FC = () => { - const history = useHistory(); - - const handleClick = (e: any) => { - e.preventDefault(); - - history.push({ - pathname: SETTINGS_TERMS_ROUTE, - state: { isFullScreen: true }, - }); - }; - - return ( - /* eslint-disable-next-line jsx-a11y/anchor-is-valid */ - - terms of service - - ); -}; - export default WalletCreate; diff --git a/src/presentation/onboarding/wallet-restore/index.tsx b/src/presentation/onboarding/wallet-restore/index.tsx index 29961a3d..9c726912 100644 --- a/src/presentation/onboarding/wallet-restore/index.tsx +++ b/src/presentation/onboarding/wallet-restore/index.tsx @@ -1,167 +1,39 @@ -import React from 'react'; -import { useHistory, RouteComponentProps } from 'react-router-dom'; -import cx from 'classnames'; -import { withFormik, FormikProps } from 'formik'; -import * as Yup from 'yup'; -import Button from '../../components/button'; +import React, { useState } from 'react'; +import { useHistory } from 'react-router-dom'; import Shell from '../../components/shell'; -import { IError } from '../../../domain/common'; import { INITIALIZE_END_OF_FLOW_ROUTE } from '../../routes/constants'; import { setPasswordAndOnboardingMnemonic } from '../../../application/redux/actions/onboarding'; import { useDispatch } from 'react-redux'; import { ProxyStoreDispatch } from '../../../application/redux/proxyStore'; import { setVerified } from '../../../application/redux/actions/wallet'; +import { MnemonicField } from './mnemonic-field'; +import OnboardingForm from '../onboarding-form'; -interface WalletRestoreFormValues { - mnemonic: string; - password: string; - confirmPassword: string; - ctxErrors?: Record; -} - -interface WalletRestoreFormProps { - dispatch: ProxyStoreDispatch; - history: RouteComponentProps['history']; -} - -const WalletRestoreForm = (props: FormikProps) => { - const { values, touched, errors, isSubmitting, handleChange, handleBlur, handleSubmit } = props; - - return ( -
-
-