From b4094363cc87c1cebf5980f22c586dfdd60f5d28 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 21 Nov 2025 17:37:11 -0300 Subject: [PATCH 01/16] [REF] TSS-1 --- babel.config.js | 1 + index.js | 5 +- ios/Podfile.lock | 51 ++ metro.config.js | 29 +- package.json | 6 +- patches/@bitgo+sdk-lib-mpc+10.8.1.patch | 781 ++++++++++++++++++ patches/bitcore-tss+11.3.7.patch | 151 ++++ patches/bitcore-wallet-client+11.3.6.patch | 161 ++++ scripts/allowed-url-prefixes.js | 1 + shim.js | 8 + shims/silence-dkls-web.js | 394 +++++++++ src/Root.tsx | 2 +- src/dkls/DklsWorker.tsx | 775 +++++++++++++++++ src/lib/bwc.ts | 10 +- .../RequestEncryptPasswordToggle.tsx | 2 +- .../screens/CreateEncryptionPassword.tsx | 2 +- .../wallet/screens/WalletSettings.tsx | 2 +- src/store/transforms/transforms.ts | 16 + src/store/wallet/effects/create/create.ts | 4 +- .../effects/join-multisig/join-multisig.ts | 2 +- src/store/wallet/utils/wallet.ts | 38 +- src/store/wallet/wallet.models.ts | 17 +- src/utils/wallet-hardware.ts | 4 +- yarn.lock | 416 +++++++--- 24 files changed, 2762 insertions(+), 116 deletions(-) create mode 100644 patches/@bitgo+sdk-lib-mpc+10.8.1.patch create mode 100644 patches/bitcore-tss+11.3.7.patch create mode 100644 patches/bitcore-wallet-client+11.3.6.patch create mode 100644 shims/silence-dkls-web.js create mode 100644 src/dkls/DklsWorker.tsx diff --git a/babel.config.js b/babel.config.js index 053b16f533..7214153c63 100644 --- a/babel.config.js +++ b/babel.config.js @@ -3,6 +3,7 @@ const {NODE_ENV} = process.env; const prod = NODE_ENV === 'production'; const plugins = [ + 'babel-plugin-transform-import-meta', '@babel/plugin-proposal-export-namespace-from', '@babel/plugin-transform-shorthand-properties', '@babel/plugin-transform-arrow-functions', diff --git a/index.js b/index.js index 56158c0e7e..885cf76ddb 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ import 'react-native-get-random-values'; // must import before @ethersproject/shims +import 'react-native-quick-crypto'; import '@ethersproject/shims'; -import 'fast-text-encoding'; +// import 'fast-text-encoding'; import './shim'; import '@walletconnect/react-native-compat'; import {AppRegistry, Alert, StatusBar, Appearance} from 'react-native'; @@ -38,6 +39,7 @@ import { } from './src/contexts'; import {BitPayDarkTheme, BitPayLightTheme} from './src/themes/bitpay'; import {useAppSelector} from './src/utils/hooks'; +import { DklsWorkerHost } from './src/dkls/DklsWorker'; const makeErrorHandler = store => (e, isFatal) => { if (isFatal) { @@ -152,6 +154,7 @@ const AppWrapper = () => { + diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 03bc091ea7..6801f35f87 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -69,6 +69,7 @@ PODS: - Mixpanel-swift (= 4.1.3) - React-Core - MultiplatformBleAdapter (0.2.0) + - OpenSSL-Universal (3.3.3001) - RCT-Folly (2024.11.18.00): - boost - DoubleConversion @@ -108,6 +109,7 @@ PODS: - React-RCTText (= 0.82.0) - React-RCTVibration (= 0.82.0) - React-callinvoker (0.82.0) + - React-Codegen (0.1.0) - React-Core (0.82.0): - boost - DoubleConversion @@ -2033,6 +2035,36 @@ PODS: - Yoga - react-native-print (0.11.0): - React-Core + - react-native-quick-crypto (0.7.17): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - OpenSSL-Universal + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - react-native-randombytes (3.6.1): - React-Core - react-native-render-html (6.3.4): @@ -2219,6 +2251,13 @@ PODS: - React-RCTText - react-native-user-agent (2.3.1): - React + - react-native-webassembly (0.3.3): + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-Codegen + - React-Core + - ReactCommon/turbomodule/core - react-native-webview (13.16.0): - boost - DoubleConversion @@ -3380,6 +3419,7 @@ DEPENDENCIES: - react-native-pager-view (from `../node_modules/react-native-pager-view`) - react-native-passkey (from `../node_modules/react-native-passkey`) - react-native-print (from `../node_modules/react-native-print`) + - react-native-quick-crypto (from `../node_modules/react-native-quick-crypto`) - react-native-randombytes (from `../node_modules/react-native-randombytes`) - react-native-render-html (from `../node_modules/react-native-render-html`) - react-native-restart (from `../node_modules/react-native-restart`) @@ -3388,6 +3428,7 @@ DEPENDENCIES: - "react-native-slider (from `../node_modules/@react-native-community/slider`)" - react-native-text-input-mask (from `../node_modules/react-native-text-input-mask`) - react-native-user-agent (from `../node_modules/react-native-user-agent`) + - react-native-webassembly (from `../node_modules/react-native-webassembly`) - react-native-webview (from `../node_modules/react-native-webview`) - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`) @@ -3457,6 +3498,8 @@ SPEC REPOS: - libwebp - Mixpanel-swift - MultiplatformBleAdapter + - OpenSSL-Universal + - React-Codegen - SDWebImage - SDWebImageWebPCoder - SocketRocket @@ -3579,6 +3622,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-passkey" react-native-print: :path: "../node_modules/react-native-print" + react-native-quick-crypto: + :path: "../node_modules/react-native-quick-crypto" react-native-randombytes: :path: "../node_modules/react-native-randombytes" react-native-render-html: @@ -3595,6 +3640,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-text-input-mask" react-native-user-agent: :path: "../node_modules/react-native-user-agent" + react-native-webassembly: + :path: "../node_modules/react-native-webassembly" react-native-webview: :path: "../node_modules/react-native-webview" React-NativeModulesApple: @@ -3729,12 +3776,14 @@ SPEC CHECKSUMS: Mixpanel-swift: d7c7c6a2f7c65f735af3cb4746ed61a5aab0e551 MixpanelReactNative: 07c808338a1eb69b0c9077668e3acfb0b2dd6689 MultiplatformBleAdapter: b1fddd0d499b96b607e00f0faa8e60648343dc1d + OpenSSL-Universal: 6082b0bf950e5636fe0d78def171184e2b3899c2 RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f RCTDeprecation: 22bf66112da540a7d40e536366ddd8557934fca1 RCTRequired: a0ed4dc41b35f79fbb6d8ba320e06882a8c792cf RCTTypeSafety: 59a046ff1e602409a86b89fcd6edff367a5b14af React: ade831e2e38887292c2c7d40f2f4098826a9dda4 React-callinvoker: fb097304922c5da47152147a5fb0712713438575 + React-Codegen: 4b8b4817cea7a54b83851d4c1f91f79aa73de30a React-Core: 2f7181fccf31a895720bb0668ac9f67985d6a4a1 React-CoreModules: 3f7a8f9d28ba287fc07240c5bc53aa4d5e23450a React-cxxreact: dca5689d4332bbf71495302103bb24f73fa1de00 @@ -3777,6 +3826,7 @@ SPEC CHECKSUMS: react-native-pager-view: e17f0602f115f6a6a8953e58f2182dbe8eeb0bd5 react-native-passkey: b85aaca917594d530521f484f20fa829bcdcd0f8 react-native-print: f704aef52d931bfce6d1d84351dbb5232d7ecb89 + react-native-quick-crypto: 96d796b74a7b1d266950d0f631b57818659e138e react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846 react-native-render-html: 984dfe2294163d04bf5fe25d7c9f122e60e05ebe react-native-restart: 7595693413fe3ca15893702f2c8306c62a708162 @@ -3785,6 +3835,7 @@ SPEC CHECKSUMS: react-native-slider: 30cea7008de785564de2f4fd064f2deb38614a4a react-native-text-input-mask: 36a546b378fadd2efe1b7484a859d34bc2c80395 react-native-user-agent: a90a1e839b99801baad67a73dd6f361a52aa3cf1 + react-native-webassembly: bdd67f75a6145cbfdb3ab8de4ee0381f89e6b140 react-native-webview: 8b9097e270a99ee8798449f191a7ea27c790fa1c React-NativeModulesApple: 1b4d9722d8df62e881684abadf320e7a8fa1b7f6 React-oscompat: 80ca388c4831481cd03a6b45ecfc82739ca9a95e diff --git a/metro.config.js b/metro.config.js index 1341640bf1..90827156e3 100644 --- a/metro.config.js +++ b/metro.config.js @@ -1,10 +1,20 @@ -const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); const path = require('path'); const { resolver: {sourceExts, assetExts}, } = getDefaultConfig(); +const SHIM_PATH = path.resolve(__dirname, 'shims/silence-dkls-web.js'); +const REAL_SILENCE_PATH = path.resolve( + __dirname, + 'node_modules/@silencelaboratories/dkls-wasm-ll-web/dkls-wasm-ll-web.js' +); +const SILENCE_WASM_PATH = path.resolve( + __dirname, + 'node_modules/@silencelaboratories/dkls-wasm-ll-web/dkls-wasm-ll-web_bg.wasm' +); + const ALIASES = { tslib: path.resolve(__dirname, 'node_modules/tslib/tslib.es6.js'), }; @@ -22,6 +32,13 @@ const config = { resolver: { assetExts: assetExts.filter(ext => ext !== 'svg'), sourceExts: [...sourceExts, 'svg'], + alias: { + crypto: require.resolve('react-native-quick-crypto'), + '@silencelaboratories/dkls-wasm-ll-web': SHIM_PATH, + '@@silence-original': REAL_SILENCE_PATH, + '@@silence-wasm': SILENCE_WASM_PATH, + ...ALIASES, + }, }, }; @@ -54,7 +71,17 @@ config.resolver.resolveRequest = (context, moduleName, platform) => { if (moduleName === 'rpc-websockets') { moduleName = 'rpc-websockets/dist/index.browser.mjs'; } + + if ( + moduleName === '@silencelaboratories/dkls-wasm-ll-web' || + moduleName.startsWith('@silencelaboratories/dkls-wasm-ll-web/') + ) { + moduleName = SHIM_PATH; + } + if (moduleName === '@@silence-original') moduleName = REAL_SILENCE_PATH; + if (moduleName === '@@silence-wasm') moduleName = SILENCE_WASM_PATH; + return context.resolveRequest( context, ALIASES[moduleName] ?? moduleName, diff --git a/package.json b/package.json index 103c037476..5f7ed46971 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "@solana/sysvars": "3.0.2", "big-integer": "1.6.51", "bitauth": "0.4.1", - "bitcore-wallet-client": "10.10.15", + "bitcore-wallet-client": "11.3.6", "bs58": "6.0.0", "buffer": "4.9.2", "countries-list": "2.6.1", @@ -151,6 +151,7 @@ "react-native-prompt-android": "1.1.0", "react-native-qrcode-svg": "6.2.0", "react-native-quick-actions": "0.3.13", + "react-native-quick-crypto": "0.7.17", "react-native-randombytes": "3.6.1", "react-native-rate": "1.2.12", "react-native-reanimated": "4.1.3", @@ -172,6 +173,7 @@ "react-native-user-agent": "2.3.1", "react-native-uuid": "2.0.1", "react-native-vision-camera": "4.7.2", + "react-native-webassembly": "0.3.3", "react-native-webview": "13.16.0", "react-native-worklets": "0.6.1", "react-navigation-backhandler": "2.0.3", @@ -190,6 +192,7 @@ "safe-json-utils": "1.1.1", "stream-browserify": "1.0.0", "styled-components": "5.3.3", + "text-encoding": "0.7.0", "url": "0.10.3", "yup": "0.32.11" }, @@ -242,6 +245,7 @@ "@types/styled-components-react-native": "5.2.1", "babel-jest": "29.6.3", "babel-loader": "9.1.2", + "babel-plugin-transform-import-meta": "2.3.3", "babel-plugin-transform-remove-console": "6.9.4", "dotenv": "16.0.1", "eslint": "8.48.0", diff --git a/patches/@bitgo+sdk-lib-mpc+10.8.1.patch b/patches/@bitgo+sdk-lib-mpc+10.8.1.patch new file mode 100644 index 0000000000..c071f6b0e5 --- /dev/null +++ b/patches/@bitgo+sdk-lib-mpc+10.8.1.patch @@ -0,0 +1,781 @@ +diff --git a/node_modules/@bitgo/sdk-lib-mpc/dist/src/tss/ecdsa-dkls/dkg.js b/node_modules/@bitgo/sdk-lib-mpc/dist/src/tss/ecdsa-dkls/dkg.js +index d4075db..a5da83c 100644 +--- a/node_modules/@bitgo/sdk-lib-mpc/dist/src/tss/ecdsa-dkls/dkg.js ++++ b/node_modules/@bitgo/sdk-lib-mpc/dist/src/tss/ecdsa-dkls/dkg.js +@@ -51,7 +51,9 @@ class Dkg { + } + async loadDklsWasm() { + if (!this.dklsWasm) { +- this.dklsWasm = await Promise.resolve().then(() => __importStar(require('@silencelaboratories/dkls-wasm-ll-node'))); ++ const shim = await import('@silencelaboratories/dkls-wasm-ll-web'); ++ await shim.default(); ++ this.dklsWasm = shim; + } + } + getDklsWasm() { +@@ -60,12 +62,13 @@ class Dkg { + } + return this.dklsWasm; + } +- _restoreSession() { ++ async _restoreSession() { + if (!this.dkgSession) { +- this.dkgSession = this.getDklsWasm().KeygenSession.fromBytes(this.dkgSessionBytes); ++ this.dkgSession = await this.getDklsWasm().KeygenSession.fromBytes(this.dkgSessionBytes); + } + } +- _createDKLsRetrofitKeyShare() { ++ async _createDKLsRetrofitKeyShare() { ++ + if (this.retrofitData) { + if (!this.retrofitData.xShare.y || !this.retrofitData.xShare.chaincode || !this.retrofitData.xShare.x) { + throw Error('xShare must have a public key, private share value, and a chaincode.'); +@@ -75,6 +78,7 @@ class Dkg { + xiList.push(Array.from((0, util_1.bigIntToBufferBE)(BigInt(i + 1), 32))); + } + const secp256k1 = new curves_1.Secp256k1Curve(); ++ + const dklsKeyShare = { + total_parties: this.n, + threshold: this.t, +@@ -93,14 +97,17 @@ class Dkg { + big_s_list: new Array(this.n).fill(Array.from((0, util_1.bigIntToBufferBE)(secp256k1.basePointMult(BigInt('0x' + this.retrofitData.xShare.x))))), + x_i_list: this.retrofitData.xiList ? this.retrofitData.xiList : xiList, + }; +- this.dklsKeyShareRetrofitObject = this.getDklsWasm().Keyshare.fromBytes((0, cbor_x_1.encode)(dklsKeyShare)); ++ ++ this.dklsKeyShareRetrofitObject = await this.getDklsWasm().Keyshare.fromBytes((0, cbor_x_1.encode)(dklsKeyShare)); + } + } +- _deserializeState() { ++ async _deserializeState() { + if (!this.dkgSession) { + throw Error('Session not intialized'); + } +- const round = (0, cbor_x_1.decode)(this.dkgSession.toBytes()).round; ++ const sessionBytes = await this.dkgSession.toBytes(); ++ const round = (0, cbor_x_1.decode)(sessionBytes).round; ++ + switch (round) { + case 'WaitMsg1': + this.dkgState = types_1.DkgState.Round1; +@@ -132,38 +139,43 @@ class Dkg { + if (this.dkgState != types_1.DkgState.Uninitialized) { + throw Error('DKG session already initialized'); + } +- if (typeof window !== 'undefined' && +- /* checks for electron processes */ +- !window.process && +- !window.process?.['type']) { +- /* This is only needed for browsers/web because it uses fetch to resolve the wasm asset for the web */ +- const initDkls = await Promise.resolve().then(() => __importStar(require('@silencelaboratories/dkls-wasm-ll-web'))); +- await initDkls.default(); +- } +- this._createDKLsRetrofitKeyShare(); +- if (this.seed && this.seed.length !== 32) { +- throw Error(`Seed should be 32 bytes, got ${this.seed.length}.`); +- } ++ await this._createDKLsRetrofitKeyShare(); ++ + const { KeygenSession } = this.getDklsWasm(); ++ + if (this.dklsKeyShareRetrofitObject) { + this.dkgSession = this.seed +- ? KeygenSession.initKeyRotation(this.dklsKeyShareRetrofitObject, new Uint8Array(this.seed)) +- : KeygenSession.initKeyRotation(this.dklsKeyShareRetrofitObject); +- } +- else { ++ ? await KeygenSession.initKeyRotation(this.dklsKeyShareRetrofitObject, new Uint8Array(this.seed)) ++ : await KeygenSession.initKeyRotation(this.dklsKeyShareRetrofitObject); ++ } else { + this.dkgSession = this.seed + ? new KeygenSession(this.n, this.t, this.partyIdx, new Uint8Array(this.seed)) + : new KeygenSession(this.n, this.t, this.partyIdx); +- } +- try { +- const payload = this.dkgSession.createFirstMessage().payload; +- this.dkgSessionBytes = this.dkgSession.toBytes(); +- this._deserializeState(); ++ } ++ try { ++ const firstMsg = await this.dkgSession.createFirstMessage(); ++ ++ let payload; ++ try { ++ payload = await firstMsg.payload(); ++ } catch (e1) { ++ try { ++ payload = firstMsg.payload(); ++ } catch (e2) { ++ payload = firstMsg.payload; ++ } ++ } ++ ++ try { await firstMsg.free?.(); } catch {} ++ ++ this.dkgSessionBytes = await this.dkgSession.toBytes(); ++ await this._deserializeState(); ++ + return { + payload: payload, + from: this.partyIdx, + }; +- } ++ } + catch (e) { + throw Error(`Error while creating the first message from party ${this.partyIdx}: ${e}`); + } +@@ -189,86 +201,205 @@ class Dkg { + const encodedKeyShare = (0, cbor_x_1.encode)(reducedKeyShare); + return encodedKeyShare; + } +- handleIncomingMessages(messagesForIthRound) { ++ async handleIncomingMessages(messagesForIthRound) { + let nextRoundMessages = []; + let nextRoundDeserializedMessages = { broadcastMessages: [], p2pMessages: [] }; +- this._restoreSession(); +- if (!this.dkgSession) { +- throw Error('Session not initialized'); +- } +- const { Message } = this.getDklsWasm(); ++ + try { +- if (this.dkgState === types_1.DkgState.Round3) { +- const commitmentsUnsorted = messagesForIthRound.p2pMessages +- .map((m) => { +- return { from: m.from, commitment: m.commitment }; +- }) +- .concat([{ from: this.partyIdx, commitment: this.chainCodeCommitment }]); +- const commitmentsSorted = commitmentsUnsorted +- .sort((a, b) => { +- return a.from - b.from; +- }) +- .map((c) => c.commitment); +- nextRoundMessages = this.dkgSession.handleMessages(messagesForIthRound.broadcastMessages +- .map((m) => new Message(m.payload, m.from, undefined)) +- .concat(messagesForIthRound.p2pMessages.map((m) => new Message(m.payload, m.from, m.to))), commitmentsSorted); ++ await this._restoreSession(); ++ ++ if (!this.dkgSession) { ++ throw Error('Session not initialized'); + } +- else { +- nextRoundMessages = this.dkgSession.handleMessages(messagesForIthRound.broadcastMessages +- .map((m) => new Message(m.payload, m.from, undefined)) +- .concat(messagesForIthRound.p2pMessages.map((m) => new Message(m.payload, m.from, m.to))), undefined); ++ ++ const { Message } = this.getDklsWasm(); ++ ++ const broadcastInstances = []; ++ for (const m of messagesForIthRound.broadcastMessages) { ++ const msg = new Message(m.payload, m.from, undefined); ++ ++ if (msg._ready) { ++ await msg._ready; ++ } ++ ++ broadcastInstances.push(msg); ++ } ++ ++ const p2pInstances = []; ++ for (const m of messagesForIthRound.p2pMessages) { ++ const msg = new Message(m.payload, m.from, m.to); ++ ++ if (msg._ready) { ++ await msg._ready; ++ } ++ ++ p2pInstances.push(msg); ++ } ++ ++ const allMessages = broadcastInstances.concat(p2pInstances); ++ ++ if (this.dkgState === types_1.DkgState.Round3) { ++ if (!this.dkgSession) { ++ throw new Error('No hay sesión DKG en Round 3'); ++ } ++ ++ if (!this.chainCodeCommitment) { ++ throw new Error('Missing own chainCodeCommitment in Round 3'); ++ } ++ ++ const missingCommitments = messagesForIthRound.p2pMessages.filter(m => !m.commitment); ++ if (missingCommitments.length > 0) { ++ throw new Error(`Missing commitments from parties: ${missingCommitments.map(m => m.from).join(', ')}`); ++ } ++ ++ const commitments = []; ++ ++ for (let partyId = 0; partyId < this.n; partyId++) { ++ if (partyId === this.partyIdx) { ++ commitments.push(this.chainCodeCommitment); ++ } else { ++ const msg = messagesForIthRound.p2pMessages.find(m => m.from === partyId); ++ if (!msg || !msg.commitment) { ++ throw new Error(`Missing commitment from party ${partyId}`); ++ } ++ commitments.push(msg.commitment); ++ } ++ } ++ ++ nextRoundMessages = await this.dkgSession.handleMessages( ++ allMessages, ++ commitments ++ ); ++ ++ } else { ++ nextRoundMessages = await this.dkgSession.handleMessages( ++ allMessages, ++ undefined ++ ); + } ++ + if (this.dkgState === types_1.DkgState.Round4) { +- this.dkgKeyShare = this.dkgSession.keyshare(); +- this.keyShareBuff = Buffer.from(this.dkgKeyShare.toBytes()); +- this.dkgKeyShare.free(); +- if (this.dklsKeyShareRetrofitObject) { +- this.dklsKeyShareRetrofitObject.free(); ++ this.dkgKeyShare = await this.dkgSession.keyshare(); ++ ++ let keyShareBytes; ++ if (typeof this.dkgKeyShare.toBytes === 'function') { ++ keyShareBytes = await this.dkgKeyShare.toBytes(); ++ } else if (this.dkgKeyShare instanceof Uint8Array) { ++ keyShareBytes = this.dkgKeyShare; ++ } else if (Array.isArray(this.dkgKeyShare)) { ++ keyShareBytes = new Uint8Array(this.dkgKeyShare); ++ } else { ++ throw new Error('Cannot get bytes from keyshare'); ++ } ++ ++ this.keyShareBuff = Buffer.from(keyShareBytes); ++ ++ if (typeof this.dkgKeyShare.free === 'function') { ++ await this.dkgKeyShare.free(); + } ++ if (this.dklsKeyShareRetrofitObject?.free) { ++ await this.dklsKeyShareRetrofitObject.free(); ++ } ++ + this.dkgState = types_1.DkgState.Complete; + return { broadcastMessages: [], p2pMessages: [] }; ++ } else { ++ await this._deserializeState(); + } +- else { +- // Update round data. +- this._deserializeState(); +- } ++ + if (this.dkgState === types_1.DkgState.Round2) { +- this.chainCodeCommitment = this.dkgSession.calculateChainCodeCommitment(); ++ const commitmentResult = this.dkgSession.calculateChainCodeCommitment(); ++ ++ if (commitmentResult && typeof commitmentResult.then === 'function') { ++ this.chainCodeCommitment = await commitmentResult; ++ } else { ++ this.chainCodeCommitment = commitmentResult; ++ } ++ ++ if (!(this.chainCodeCommitment instanceof Uint8Array)) { ++ this.chainCodeCommitment = new Uint8Array(this.chainCodeCommitment); ++ } ++ } ++ ++ const p2pMessagesWithPayloads = []; ++ for (const m of nextRoundMessages) { ++ let to_id_value; ++ if (typeof m.to_id === 'function') { ++ const result = m.to_id(); ++ to_id_value = (result && typeof result.then === 'function') ? await result : result; ++ } else { ++ to_id_value = m.to_id; ++ } ++ ++ if (to_id_value === undefined) continue; ++ ++ let payload; ++ if (typeof m.payload === 'function') { ++ const result = m.payload(); ++ payload = (result && typeof result.then === 'function') ? await result : result; ++ } else { ++ payload = m.payload; ++ } ++ ++ let from_id; ++ if (typeof m.from_id === 'function') { ++ const result = m.from_id(); ++ from_id = (result && typeof result.then === 'function') ? await result : result; ++ } else { ++ from_id = m.from_id; ++ } ++ ++ p2pMessagesWithPayloads.push({ ++ payload: new Uint8Array(payload), ++ from: from_id, ++ to: to_id_value, ++ commitment: this.chainCodeCommitment, ++ }); ++ } ++ ++ const broadcastMessagesWithPayloads = []; ++ for (const m of nextRoundMessages) { ++ const to_id_value = typeof m.to_id === 'function' ? await m.to_id() : m.to_id; ++ if (to_id_value !== undefined) continue; ++ ++ const payload = await m.payload(); ++ const from_id = typeof m.from_id === 'function' ? await m.from_id() : m.from_id; ++ ++ broadcastMessagesWithPayloads.push({ ++ payload: new Uint8Array(payload), ++ from: from_id, ++ }); + } ++ + nextRoundDeserializedMessages = { +- p2pMessages: nextRoundMessages +- .filter((m) => m.to_id !== undefined) +- .map((m) => { +- const p2pReturn = { +- payload: m.payload, +- from: m.from_id, +- to: m.to_id, +- commitment: this.chainCodeCommitment, +- }; +- return p2pReturn; +- }), +- broadcastMessages: nextRoundMessages +- .filter((m) => m.to_id === undefined) +- .map((m) => { +- const broadcastReturn = { +- payload: m.payload, +- from: m.from_id, +- }; +- return broadcastReturn; +- }), ++ p2pMessages: p2pMessagesWithPayloads, ++ broadcastMessages: broadcastMessagesWithPayloads, + }; +- } +- catch (e) { +- throw Error(`Error while creating messages from party ${this.partyIdx}, round ${this.dkgState}: ${e}`); +- } +- finally { +- nextRoundMessages.forEach((m) => m.free()); +- // Session is freed when keyshare is called. +- if (this.dkgState !== types_1.DkgState.Complete) { +- this.dkgSessionBytes = this.dkgSession.toBytes(); +- this.dkgSession = undefined; ++ ++ } catch (e) { ++ throw Error(`Error while creating messages from party ${this.partyIdx}, round ${this.dkgState}: ${e.message}`); ++ } finally { ++ for (const m of nextRoundMessages) { ++ if (m && typeof m.free === 'function') { ++ try { ++ await m.free(); ++ } catch (err) { ++ } ++ } ++ } ++ ++ if (this.dkgState !== types_1.DkgState.Round4 && ++ this.dkgState !== types_1.DkgState.Complete) { ++ try { ++ this.dkgSessionBytes = await this.dkgSession.toBytes(); ++ } catch (serError) { ++ if (this.dkgState !== types_1.DkgState.Round4) { ++ throw serError; ++ } ++ } + } + } ++ + return nextRoundDeserializedMessages; + } + /** +@@ -313,7 +444,7 @@ class Dkg { + if (sessionData.keyShareBuff) { + dkg.keyShareBuff = sessionData.keyShareBuff; + } +- dkg._restoreSession(); ++ await dkg._restoreSession(); + return dkg; + } + } +diff --git a/node_modules/@bitgo/sdk-lib-mpc/dist/src/tss/ecdsa-dkls/dsg.js b/node_modules/@bitgo/sdk-lib-mpc/dist/src/tss/ecdsa-dkls/dsg.js +index 42b0316..32798a3 100644 +--- a/node_modules/@bitgo/sdk-lib-mpc/dist/src/tss/ecdsa-dkls/dsg.js ++++ b/node_modules/@bitgo/sdk-lib-mpc/dist/src/tss/ecdsa-dkls/dsg.js +@@ -45,27 +45,34 @@ class Dsg { + this.messageHash = messageHash; + this.dklsWasm = dklsWasm ?? null; + } +- _restoreSession() { ++ ++ async _restoreSession() { + if (!this.dsgSession) { +- this.dsgSession = this.getDklsWasm().SignSessionOTVariant.fromBytes(this.dsgSessionBytes); ++ this.dsgSession = await this.getDklsWasm().SignSessionOTVariant.fromBytes(this.dsgSessionBytes); + } + } +- _deserializeState() { ++ ++ async _deserializeState() { + if (!this.dsgSession) { + throw Error('Session not intialized'); + } +- const round = (0, cbor_x_1.decode)(this.dsgSession.toBytes()).round; +- switch (round) { +- case 'WaitMsg1': ++ const sessionBytes = await this.dsgSession.toBytes(); ++ const round = (0, cbor_x_1.decode)(sessionBytes).round; ++ ++ switch (true) { ++ case round === 'WaitMsg1': + this.dsgState = types_1.DsgState.Round1; + break; +- case 'WaitMsg2': ++ case round === 'WaitMsg2': + this.dsgState = types_1.DsgState.Round2; + break; +- case 'WaitMsg3': ++ case round === 'WaitMsg3': + this.dsgState = types_1.DsgState.Round3; + break; +- case 'Ended': ++ case 'WaitMsg4' in round: ++ this.dsgState = types_1.DsgState.Round4; ++ break; ++ case round === 'Ended': + this.dsgState = types_1.DsgState.Complete; + break; + default: +@@ -73,17 +80,22 @@ class Dsg { + throw Error(`Invalid State: ${round}`); + } + } ++ + async loadDklsWasm() { + if (!this.dklsWasm) { +- this.dklsWasm = await Promise.resolve().then(() => __importStar(require('@silencelaboratories/dkls-wasm-ll-node'))); ++ const shim = await Promise.resolve().then(() => __importStar(require('@silencelaboratories/dkls-wasm-ll-web'))); ++ await shim.default(); ++ this.dklsWasm = shim; + } + } ++ + getDklsWasm() { + if (!this.dklsWasm) { + throw Error('DKLS wasm not loaded'); + } + return this.dklsWasm; + } ++ + /** + * Returns the current DSG session as a base64 string. + * @returns {string} - base64 string of the current DSG session +@@ -91,6 +103,7 @@ class Dsg { + getSession() { + return Buffer.from(this.dsgSessionBytes).toString('base64'); + } ++ + /** + * Sets the DSG session from a base64 string. + * @param {string} session - base64 string of the DSG session +@@ -120,6 +133,7 @@ class Dsg { + } + this.dsgSessionBytes = sessionBytes; + } ++ + async init() { + if (this.dsgState !== types_1.DsgState.Uninitialized) { + throw Error('DSG session already initialized'); +@@ -127,140 +141,221 @@ class Dsg { + if (!this.dklsWasm) { + await this.loadDklsWasm(); + } +- if (typeof window !== 'undefined' && +- /* checks for electron processes */ +- !window.process && +- !window.process?.['type']) { +- /* This is only needed for browsers/web because it uses fetch to resolve the wasm asset for the web */ +- const initDkls = await Promise.resolve().then(() => __importStar(require('@silencelaboratories/dkls-wasm-ll-web'))); +- await initDkls.default(); +- } ++ + const { Keyshare, SignSessionOTVariant } = this.getDklsWasm(); +- const keyShare = Keyshare.fromBytes(this.keyShareBytes); +- if (keyShare.partyId !== this.partyIdx) { +- throw Error(`Party index: ${this.partyIdx} does not match key share partyId: ${keyShare.partyId} `); ++ const keyShare = await Keyshare.fromBytes(this.keyShareBytes); ++ ++ const keySharePartyId = await keyShare.partyId(); ++ ++ if (keySharePartyId !== this.partyIdx) { ++ throw Error(`Party index: ${this.partyIdx} does not match key share partyId: ${keySharePartyId} `); + } ++ + this.dsgSession = new SignSessionOTVariant(keyShare, this.derivationPath); ++ + try { +- const payload = this.dsgSession.createFirstMessage().payload; +- this._deserializeState(); +- this.dsgSessionBytes = this.dsgSession.toBytes(); ++ const firstMsg = await this.dsgSession.createFirstMessage(); ++ ++ const payload = await firstMsg.payload(); ++ ++ try { await firstMsg.free?.(); } catch {} ++ ++ await this._deserializeState(); ++ this.dsgSessionBytes = await this.dsgSession.toBytes(); + this.dsgSession = undefined; ++ + return { + payload: payload, + from: this.partyIdx, + }; + } + catch (e) { +- throw Error(`Error while creating the first message from party ${this.partyIdx}: ${e}`); ++ throw Error(`Error while creating the first message from party ${this.partyIdx}: ${e.message}`); + } + } ++ + get signature() { + if (!this._signature) { + throw Error('Can not request signature. Signature not produced yet.'); + } + return this._signature; + } ++ + /** + * Ends the DSG session by freeing any heap allocations from wasm. Note that the session is freed if a signature is produced. + */ +- endSession() { ++ async endSession() { + if (this._signature) { + new Error('Session already ended because combined signature was produced.'); + } + if (this.dsgSession) { +- this.dsgSession.free(); ++ if (typeof this.dsgSession.free === 'function') { ++ await this.dsgSession.free(); ++ } + } + this.dsgState = types_1.DsgState.Uninitialized; + } ++ + /** + * Proccesses incoming messages to this party in the DKLs DSG protocol and + * produces messages from this party to other parties for the next round. + * @param messagesForIthRound - messages to process the current round + * @returns {DeserializedMessages} - messages to send to other parties for the next round + */ +- handleIncomingMessages(messagesForIthRound) { ++ async handleIncomingMessages(messagesForIthRound) { + let nextRoundMessages = []; + let nextRoundDeserializedMessages = { broadcastMessages: [], p2pMessages: [] }; +- this._restoreSession(); +- if (!this.dsgSession) { +- throw Error('Session not initialized'); +- } +- const { Message } = this.getDklsWasm(); ++ + try { ++ await this._restoreSession(); ++ ++ if (!this.dsgSession) { ++ throw Error('Session not initialized'); ++ } ++ ++ const { Message } = this.getDklsWasm(); ++ + if (this.dsgState === types_1.DsgState.Round4) { + this.dsgState = types_1.DsgState.Complete; +- const combineResult = this.dsgSession.combine(messagesForIthRound.broadcastMessages.map((m) => new Message(m.payload, m.from, undefined))); ++ ++ const messages = []; ++ for (const m of messagesForIthRound.broadcastMessages) { ++ const msg = new Message(m.payload, m.from, undefined); ++ ++ if (msg._ready) { ++ await msg._ready; ++ } ++ ++ messages.push(msg); ++ } ++ ++ const combineResult = await this.dsgSession.combine(messages); ++ + this._signature = { +- R: combineResult[0], +- S: combineResult[1], ++ R: Buffer.from(combineResult[0]), ++ S: Buffer.from(combineResult[1]), + }; ++ + return { broadcastMessages: [], p2pMessages: [] }; + } +- else { +- nextRoundMessages = this.dsgSession.handleMessages(messagesForIthRound.broadcastMessages +- .map((m) => new Message(m.payload, m.from, undefined)) +- .concat(messagesForIthRound.p2pMessages.map((m) => new Message(m.payload, m.from, m.to)))); ++ ++ const broadcastInstances = []; ++ for (const m of messagesForIthRound.broadcastMessages) { ++ const msg = new Message(m.payload, m.from, undefined); ++ ++ if (msg._ready) { ++ await msg._ready; ++ } ++ ++ broadcastInstances.push(msg); + } ++ ++ const p2pInstances = []; ++ for (const m of messagesForIthRound.p2pMessages) { ++ const msg = new Message(m.payload, m.from, m.to); ++ ++ if (msg._ready) { ++ await msg._ready; ++ } ++ ++ p2pInstances.push(msg); ++ } ++ ++ const allMessages = broadcastInstances.concat(p2pInstances); ++ ++ nextRoundMessages = await this.dsgSession.handleMessages(allMessages); ++ + if (this.dsgState === types_1.DsgState.Round3) { +- nextRoundMessages = [this.dsgSession.lastMessage(this.messageHash)]; ++ nextRoundMessages = [await this.dsgSession.lastMessage(this.messageHash)]; + this.dsgState = types_1.DsgState.Round4; ++ ++ const payload = await nextRoundMessages[0].payload(); ++ const from_id = await nextRoundMessages[0].from_id(); ++ ++ const sessionBytes = await this.dsgSession.toBytes(); ++ const signatureR = (0, cbor_x_1.decode)(sessionBytes).round.WaitMsg4.r; ++ + return { + broadcastMessages: [ + { +- payload: nextRoundMessages[0].payload, +- from: nextRoundMessages[0].from_id, +- signatureR: (0, cbor_x_1.decode)(this.dsgSession.toBytes()).round.WaitMsg4.r, ++ payload: new Uint8Array(payload), ++ from: from_id, ++ signatureR: signatureR, + }, + ], + p2pMessages: [], + }; +- } +- else { ++ } else { + // Update round data. +- this._deserializeState(); ++ await this._deserializeState(); ++ } ++ ++ const p2pMessagesWithPayloads = []; ++ for (const m of nextRoundMessages) { ++ const to_id_value = await m.to_id?.(); ++ ++ if (to_id_value === undefined) continue; ++ ++ const payload = await m.payload(); ++ const from_id = await m.from_id(); ++ ++ p2pMessagesWithPayloads.push({ ++ payload: new Uint8Array(payload), ++ from: from_id, ++ to: to_id_value, ++ }); + } ++ ++ const broadcastMessagesWithPayloads = []; ++ for (const m of nextRoundMessages) { ++ const to_id_value = await m.to_id?.(); ++ if (to_id_value !== undefined) continue; ++ ++ const payload = await m.payload(); ++ const from_id = await m.from_id(); ++ ++ broadcastMessagesWithPayloads.push({ ++ payload: new Uint8Array(payload), ++ from: from_id, ++ }); ++ } ++ + nextRoundDeserializedMessages = { +- p2pMessages: nextRoundMessages +- .filter((m) => m.to_id !== undefined) +- .map((m) => { +- if (m.to_id === undefined) { +- throw Error('Invalid P2P message, missing to_id.'); +- } +- const p2pReturn = { +- payload: m.payload, +- from: m.from_id, +- to: m.to_id, +- }; +- return p2pReturn; +- }), +- broadcastMessages: nextRoundMessages +- .filter((m) => m.to_id === undefined) +- .map((m) => { +- const broadcastReturn = { +- payload: m.payload, +- from: m.from_id, +- }; +- return broadcastReturn; +- }), ++ p2pMessages: p2pMessagesWithPayloads, ++ broadcastMessages: broadcastMessagesWithPayloads, + }; ++ + } + catch (e) { + if (e.message.startsWith('Abort the protocol and ban')) { + throw Error('Signing aborted. Please stop all transaction signing from this wallet and contact support@bitgo.com.'); + } +- throw Error(`Error while creating messages from party ${this.partyIdx}, round ${this.dsgState}: ${e}`); ++ throw Error(`Error while creating messages from party ${this.partyIdx}, round ${this.dsgState}: ${e.message}`); + } + finally { +- nextRoundMessages.forEach((m) => m.free()); ++ for (const m of nextRoundMessages) { ++ if (m && typeof m.free === 'function') { ++ try { ++ await m.free(); ++ } catch (err) { ++ } ++ } ++ } ++ + // Session is freed when combine is called. + if (this.dsgState !== types_1.DsgState.Complete) { +- this.dsgSessionBytes = this.dsgSession.toBytes(); ++ try { ++ this.dsgSessionBytes = await this.dsgSession.toBytes(); ++ } catch (serError) { ++ if (this.dsgState !== types_1.DsgState.Round4) { ++ throw serError; ++ } ++ } + this.dsgSession = undefined; + } + } ++ + return nextRoundDeserializedMessages; + } + } + exports.Dsg = Dsg; +\ No newline at end of file +-//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZHNnLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL3Rzcy9lY2RzYS1ka2xzL2RzZy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFDQSxtQ0FBa0g7QUFDbEgsbUNBQWdDO0FBUWhDLE1BQWEsR0FBRztJQVdkLFlBQ0UsUUFBZ0IsRUFDaEIsUUFBZ0IsRUFDaEIsY0FBc0IsRUFDdEIsV0FBbUIsRUFDbkIsUUFBd0I7UUFSaEIsYUFBUSxHQUFhLGdCQUFRLENBQUMsYUFBYSxDQUFDO1FBVXBELElBQUksQ0FBQyxRQUFRLEdBQUcsUUFBUSxDQUFDO1FBQ3pCLElBQUksQ0FBQyxhQUFhLEdBQUcsUUFBUSxDQUFDO1FBQzlCLElBQUksQ0FBQyxjQUFjLEdBQUcsY0FBYyxDQUFDO1FBQ3JDLElBQUksQ0FBQyxXQUFXLEdBQUcsV0FBVyxDQUFDO1FBQy9CLElBQUksQ0FBQyxRQUFRLEdBQUcsUUFBUSxJQUFJLElBQUksQ0FBQztJQUNuQyxDQUFDO0lBRU8sZUFBZTtRQUNyQixJQUFJLENBQUMsSUFBSSxDQUFDLFVBQVUsRUFBRSxDQUFDO1lBQ3JCLElBQUksQ0FBQyxVQUFVLEdBQUcsSUFBSSxDQUFDLFdBQVcsRUFBRSxDQUFDLG9CQUFvQixDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUMsZUFBZSxDQUFDLENBQUM7UUFDNUYsQ0FBQztJQUNILENBQUM7SUFFTyxpQkFBaUI7UUFDdkIsSUFBSSxDQUFDLElBQUksQ0FBQyxVQUFVLEVBQUUsQ0FBQztZQUNyQixNQUFNLEtBQUssQ0FBQyx3QkFBd0IsQ0FBQyxDQUFDO1FBQ3hDLENBQUM7UUFDRCxNQUFNLEtBQUssR0FBRyxJQUFBLGVBQU0sRUFBQyxJQUFJLENBQUMsVUFBVSxDQUFDLE9BQU8sRUFBRSxDQUFDLENBQUMsS0FBSyxDQUFDO1FBQ3RELFFBQVEsS0FBSyxFQUFFLENBQUM7WUFDZCxLQUFLLFVBQVU7Z0JBQ2IsSUFBSSxDQUFDLFFBQVEsR0FBRyxnQkFBUSxDQUFDLE1BQU0sQ0FBQztnQkFDaEMsTUFBTTtZQUNSLEtBQUssVUFBVTtnQkFDYixJQUFJLENBQUMsUUFBUSxHQUFHLGdCQUFRLENBQUMsTUFBTSxDQUFDO2dCQUNoQyxNQUFNO1lBQ1IsS0FBSyxVQUFVO2dCQUNiLElBQUksQ0FBQyxRQUFRLEdBQUcsZ0JBQVEsQ0FBQyxNQUFNLENBQUM7Z0JBQ2hDLE1BQU07WUFDUixLQUFLLE9BQU87Z0JBQ1YsSUFBSSxDQUFDLFFBQVEsR0FBRyxnQkFBUSxDQUFDLFFBQVEsQ0FBQztnQkFDbEMsTUFBTTtZQUNSO2dCQUNFLElBQUksQ0FBQyxRQUFRLEdBQUcsZ0JBQVEsQ0FBQyxZQUFZLENBQUM7Z0JBQ3RDLE1BQU0sS0FBSyxDQUFDLGtCQUFrQixLQUFLLEVBQUUsQ0FBQyxDQUFDO1FBQzNDLENBQUM7SUFDSCxDQUFDO0lBRU8sS0FBSyxDQUFDLFlBQVk7UUFDeEIsSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLEVBQUUsQ0FBQztZQUNuQixJQUFJLENBQUMsUUFBUSxHQUFHLHdEQUFhLHdDQUF3QyxHQUFDLENBQUM7UUFDekUsQ0FBQztJQUNILENBQUM7SUFFTyxXQUFXO1FBQ2pCLElBQUksQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFLENBQUM7WUFDbkIsTUFBTSxLQUFLLENBQUMsc0JBQXNCLENBQUMsQ0FBQztRQUN0QyxDQUFDO1FBRUQsT0FBTyxJQUFJLENBQUMsUUFBUSxDQUFDO0lBQ3ZCLENBQUM7SUFFRDs7O09BR0c7SUFDSCxVQUFVO1FBQ1IsT0FBTyxNQUFNLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxlQUFlLENBQUMsQ0FBQyxRQUFRLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDOUQsQ0FBQztJQUVEOzs7T0FHRztJQUNILEtBQUssQ0FBQyxVQUFVLENBQUMsT0FBZTtRQUM5QixJQUFJLENBQUMsVUFBVSxHQUFHLFNBQVMsQ0FBQztRQUM1QixJQUFJLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxDQUFDO1lBQ25CLE1BQU0sSUFBSSxDQUFDLFlBQVksRUFBRSxDQUFDO1FBQzVCLENBQUM7UUFDRCxNQUFNLFlBQVksR0FBRyxJQUFJLFVBQVUsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLE9BQU8sRUFBRSxRQUFRLENBQUMsQ0FBQyxDQUFDO1FBQ3BFLE1BQU0sS0FBSyxHQUFHLElBQUEsZUFBTSxFQUFDLFlBQVksQ0FBQyxDQUFDLEtBQUssQ0FBQztRQUN6QyxRQUFRLElBQUksRUFBRSxDQUFDO1lBQ2IsS0FBSyxLQUFLLEtBQUssVUFBVTtnQkFDdkIsSUFBSSxDQUFDLFFBQVEsR0FBRyxnQkFBUSxDQUFDLE1BQU0sQ0FBQztnQkFDaEMsTUFBTTtZQUNSLEtBQUssS0FBSyxLQUFLLFVBQVU7Z0JBQ3ZCLElBQUksQ0FBQyxRQUFRLEdBQUcsZ0JBQVEsQ0FBQyxNQUFNLENBQUM7Z0JBQ2hDLE1BQU07WUFDUixLQUFLLEtBQUssS0FBSyxVQUFVO2dCQUN2QixJQUFJLENBQUMsUUFBUSxHQUFHLGdCQUFRLENBQUMsTUFBTSxDQUFDO2dCQUNoQyxNQUFNO1lBQ1IsS0FBSyxVQUFVLElBQUksS0FBSztnQkFDdEIsSUFBSSxDQUFDLFFBQVEsR0FBRyxnQkFBUSxDQUFDLE1BQU0sQ0FBQztnQkFDaEMsTUFBTTtZQUNSO2dCQUNFLE1BQU0sS0FBSyxDQUFDLGtCQUFrQixLQUFLLEVBQUUsQ0FBQyxDQUFDO1FBQzNDLENBQUM7UUFDRCxJQUFJLENBQUMsZUFBZSxHQUFHLFlBQVksQ0FBQztJQUN0QyxDQUFDO0lBRUQsS0FBSyxDQUFDLElBQUk7UUFDUixJQUFJLElBQUksQ0FBQyxRQUFRLEtBQUssZ0JBQVEsQ0FBQyxhQUFhLEVBQUUsQ0FBQztZQUM3QyxNQUFNLEtBQUssQ0FBQyxpQ0FBaUMsQ0FBQyxDQUFDO1FBQ2pELENBQUM7UUFDRCxJQUFJLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxDQUFDO1lBQ25CLE1BQU0sSUFBSSxDQUFDLFlBQVksRUFBRSxDQUFDO1FBQzVCLENBQUM7UUFDRCxJQUNFLE9BQU8sTUFBTSxLQUFLLFdBQVc7WUFDN0IsbUNBQW1DO1lBQ25DLENBQUMsTUFBTSxDQUFDLE9BQU87WUFDZixDQUFDLE1BQU0sQ0FBQyxPQUFPLEVBQUUsQ0FBQyxNQUFNLENBQUMsRUFDekIsQ0FBQztZQUNELHNHQUFzRztZQUN0RyxNQUFNLFFBQVEsR0FBRyx3REFBYSx1Q0FBdUMsR0FBQyxDQUFDO1lBQ3ZFLE1BQU0sUUFBUSxDQUFDLE9BQU8sRUFBRSxDQUFDO1FBQzNCLENBQUM7UUFDRCxNQUFNLEVBQUUsUUFBUSxFQUFFLG9CQUFvQixFQUFFLEdBQUcsSUFBSSxDQUFDLFdBQVcsRUFBRSxDQUFDO1FBQzlELE1BQU0sUUFBUSxHQUFHLFFBQVEsQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLGFBQWEsQ0FBQyxDQUFDO1FBQ3hELElBQUksUUFBUSxDQUFDLE9BQU8sS0FBSyxJQUFJLENBQUMsUUFBUSxFQUFFLENBQUM7WUFDdkMsTUFBTSxLQUFLLENBQUMsZ0JBQWdCLElBQUksQ0FBQyxRQUFRLHNDQUFzQyxRQUFRLENBQUMsT0FBTyxHQUFHLENBQUMsQ0FBQztRQUN0RyxDQUFDO1FBQ0QsSUFBSSxDQUFDLFVBQVUsR0FBRyxJQUFJLG9CQUFvQixDQUFDLFFBQVEsRUFBRSxJQUFJLENBQUMsY0FBYyxDQUFDLENBQUM7UUFDMUUsSUFBSSxDQUFDO1lBQ0gsTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLFVBQVUsQ0FBQyxrQkFBa0IsRUFBRSxDQUFDLE9BQU8sQ0FBQztZQUM3RCxJQUFJLENBQUMsaUJBQWlCLEVBQUUsQ0FBQztZQUN6QixJQUFJLENBQUMsZUFBZSxHQUFHLElBQUksQ0FBQyxVQUFVLENBQUMsT0FBTyxFQUFFLENBQUM7WUFDakQsSUFBSSxDQUFDLFVBQVUsR0FBRyxTQUFTLENBQUM7WUFDNUIsT0FBTztnQkFDTCxPQUFPLEVBQUUsT0FBTztnQkFDaEIsSUFBSSxFQUFFLElBQUksQ0FBQyxRQUFRO2FBQ3BCLENBQUM7UUFDSixDQUFDO1FBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztZQUNYLE1BQU0sS0FBSyxDQUFDLHFEQUFxRCxJQUFJLENBQUMsUUFBUSxLQUFLLENBQUMsRUFBRSxDQUFDLENBQUM7UUFDMUYsQ0FBQztJQUNILENBQUM7SUFFRCxJQUFJLFNBQVM7UUFDWCxJQUFJLENBQUMsSUFBSSxDQUFDLFVBQVUsRUFBRSxDQUFDO1lBQ3JCLE1BQU0sS0FBSyxDQUFDLHdEQUF3RCxDQUFDLENBQUM7UUFDeEUsQ0FBQztRQUNELE9BQU8sSUFBSSxDQUFDLFVBQVUsQ0FBQztJQUN6QixDQUFDO0lBRUQ7O09BRUc7SUFDSCxVQUFVO1FBQ1IsSUFBSSxJQUFJLENBQUMsVUFBVSxFQUFFLENBQUM7WUFDcEIsSUFBSSxLQUFLLENBQUMsZ0VBQWdFLENBQUMsQ0FBQztRQUM5RSxDQUFDO1FBQ0QsSUFBSSxJQUFJLENBQUMsVUFBVSxFQUFFLENBQUM7WUFDcEIsSUFBSSxDQUFDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQztRQUN6QixDQUFDO1FBQ0QsSUFBSSxDQUFDLFFBQVEsR0FBRyxnQkFBUSxDQUFDLGFBQWEsQ0FBQztJQUN6QyxDQUFDO0lBRUQ7Ozs7O09BS0c7SUFDSCxzQkFBc0IsQ0FBQyxtQkFBeUM7UUFDOUQsSUFBSSxpQkFBaUIsR0FBYyxFQUFFLENBQUM7UUFDdEMsSUFBSSw2QkFBNkIsR0FBeUIsRUFBRSxpQkFBaUIsRUFBRSxFQUFFLEVBQUUsV0FBVyxFQUFFLEVBQUUsRUFBRSxDQUFDO1FBQ3JHLElBQUksQ0FBQyxlQUFlLEVBQUUsQ0FBQztRQUN2QixJQUFJLENBQUMsSUFBSSxDQUFDLFVBQVUsRUFBRSxDQUFDO1lBQ3JCLE1BQU0sS0FBSyxDQUFDLHlCQUF5QixDQUFDLENBQUM7UUFDekMsQ0FBQztRQUNELE1BQU0sRUFBRSxPQUFPLEVBQUUsR0FBRyxJQUFJLENBQUMsV0FBVyxFQUFFLENBQUM7UUFDdkMsSUFBSSxDQUFDO1lBQ0gsSUFBSSxJQUFJLENBQUMsUUFBUSxLQUFLLGdCQUFRLENBQUMsTUFBTSxFQUFFLENBQUM7Z0JBQ3RDLElBQUksQ0FBQyxRQUFRLEdBQUcsZ0JBQVEsQ0FBQyxRQUFRLENBQUM7Z0JBQ2xDLE1BQU0sYUFBYSxHQUFHLElBQUksQ0FBQyxVQUFVLENBQUMsT0FBTyxDQUMzQyxtQkFBbUIsQ0FBQyxpQkFBaUIsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLElBQUksT0FBTyxDQUFDLENBQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDLElBQUksRUFBRSxTQUFTLENBQUMsQ0FBQyxDQUM1RixDQUFDO2dCQUNGLElBQUksQ0FBQyxVQUFVLEdBQUc7b0JBQ2hCLENBQUMsRUFBRSxhQUFhLENBQUMsQ0FBQyxDQUFDO29CQUNuQixDQUFDLEVBQUUsYUFBYSxDQUFDLENBQUMsQ0FBQztpQkFDcEIsQ0FBQztnQkFDRixPQUFPLEVBQUUsaUJBQWlCLEVBQUUsRUFBRSxFQUFFLFdBQVcsRUFBRSxFQUFFLEVBQUUsQ0FBQztZQUNwRCxDQUFDO2lCQUFNLENBQUM7Z0JBQ04saUJBQWlCLEdBQUcsSUFBSSxDQUFDLFVBQVUsQ0FBQyxjQUFjLENBQ2hELG1CQUFtQixDQUFDLGlCQUFpQjtxQkFDbEMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxJQUFJLE9BQU8sQ0FBQyxDQUFDLENBQUMsT0FBTyxFQUFFLENBQUMsQ0FBQyxJQUFJLEVBQUUsU0FBUyxDQUFDLENBQUM7cUJBQ3JELE1BQU0sQ0FBQyxtQkFBbUIsQ0FBQyxXQUFXLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxJQUFJLE9BQU8sQ0FBQyxDQUFDLENBQUMsT0FBTyxFQUFFLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FDNUYsQ0FBQztZQUNKLENBQUM7WUFDRCxJQUFJLElBQUksQ0FBQyxRQUFRLEtBQUssZ0JBQVEsQ0FBQyxNQUFNLEVBQUUsQ0FBQztnQkFDdEMsaUJBQWlCLEdBQUcsQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLFdBQVcsQ0FBQyxJQUFJLENBQUMsV0FBVyxDQUFDLENBQUMsQ0FBQztnQkFDcEUsSUFBSSxDQUFDLFFBQVEsR0FBRyxnQkFBUSxDQUFDLE1BQU0sQ0FBQztnQkFDaEMsT0FBTztvQkFDTCxpQkFBaUIsRUFBRTt3QkFDakI7NEJBQ0UsT0FBTyxFQUFFLGlCQUFpQixDQUFDLENBQUMsQ0FBQyxDQUFDLE9BQU87NEJBQ3JDLElBQUksRUFBRSxpQkFBaUIsQ0FBQyxDQUFDLENBQUMsQ0FBQyxPQUFPOzRCQUNsQyxVQUFVLEVBQUUsSUFBQSxlQUFNLEVBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDLEtBQUssQ0FBQyxRQUFRLENBQUMsQ0FBQzt5QkFDL0Q7cUJBQ0Y7b0JBQ0QsV0FBVyxFQUFFLEVBQUU7aUJBQ2hCLENBQUM7WUFDSixDQUFDO2lCQUFNLENBQUM7Z0JBQ04scUJBQXFCO2dCQUNyQixJQUFJLENBQUMsaUJBQWlCLEVBQUUsQ0FBQztZQUMzQixDQUFDO1lBQ0QsNkJBQTZCLEdBQUc7Z0JBQzlCLFdBQVcsRUFBRSxpQkFBaUI7cUJBQzNCLE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDLEtBQUssS0FBSyxTQUFTLENBQUM7cUJBQ3BDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxFQUFFO29CQUNULElBQUksQ0FBQyxDQUFDLEtBQUssS0FBSyxTQUFTLEVBQUUsQ0FBQzt3QkFDMUIsTUFBTSxLQUFLLENBQUMscUNBQXFDLENBQUMsQ0FBQztvQkFDckQsQ0FBQztvQkFDRCxNQUFNLFNBQVMsR0FBRzt3QkFDaEIsT0FBTyxFQUFFLENBQUMsQ0FBQyxPQUFPO3dCQUNsQixJQUFJLEVBQUUsQ0FBQyxDQUFDLE9BQU87d0JBQ2YsRUFBRSxFQUFFLENBQUMsQ0FBQyxLQUFLO3FCQUNaLENBQUM7b0JBQ0YsT0FBTyxTQUFTLENBQUM7Z0JBQ25CLENBQUMsQ0FBQztnQkFDSixpQkFBaUIsRUFBRSxpQkFBaUI7cUJBQ2pDLE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDLEtBQUssS0FBSyxTQUFTLENBQUM7cUJBQ3BDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxFQUFFO29CQUNULE1BQU0sZUFBZSxHQUFHO3dCQUN0QixPQUFPLEVBQUUsQ0FBQyxDQUFDLE9BQU87d0JBQ2xCLElBQUksRUFBRSxDQUFDLENBQUMsT0FBTztxQkFDaEIsQ0FBQztvQkFDRixPQUFPLGVBQWUsQ0FBQztnQkFDekIsQ0FBQyxDQUFDO2FBQ0wsQ0FBQztRQUNKLENBQUM7UUFBQyxPQUFPLENBQUMsRUFBRSxDQUFDO1lBQ1gsSUFBSSxDQUFDLENBQUMsT0FBTyxDQUFDLFVBQVUsQ0FBQyw0QkFBNEIsQ0FBQyxFQUFFLENBQUM7Z0JBQ3ZELE1BQU0sS0FBSyxDQUNULHNHQUFzRyxDQUN2RyxDQUFDO1lBQ0osQ0FBQztZQUNELE1BQU0sS0FBSyxDQUFDLDRDQUE0QyxJQUFJLENBQUMsUUFBUSxXQUFXLElBQUksQ0FBQyxRQUFRLEtBQUssQ0FBQyxFQUFFLENBQUMsQ0FBQztRQUN6RyxDQUFDO2dCQUFTLENBQUM7WUFDVCxpQkFBaUIsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDO1lBQzNDLDJDQUEyQztZQUMzQyxJQUFJLElBQUksQ0FBQyxRQUFRLEtBQUssZ0JBQVEsQ0FBQyxRQUFRLEVBQUUsQ0FBQztnQkFDeEMsSUFBSSxDQUFDLGVBQWUsR0FBRyxJQUFJLENBQUMsVUFBVSxDQUFDLE9BQU8sRUFBRSxDQUFDO2dCQUNqRCxJQUFJLENBQUMsVUFBVSxHQUFHLFNBQVMsQ0FBQztZQUM5QixDQUFDO1FBQ0gsQ0FBQztRQUNELE9BQU8sNkJBQTZCLENBQUM7SUFDdkMsQ0FBQztDQUNGO0FBOVBELGtCQThQQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IE1lc3NhZ2UsIFNpZ25TZXNzaW9uT1RWYXJpYW50IH0gZnJvbSAnQHNpbGVuY2VsYWJvcmF0b3JpZXMvZGtscy13YXNtLWxsLW5vZGUnO1xuaW1wb3J0IHsgRGVzZXJpYWxpemVkQnJvYWRjYXN0TWVzc2FnZSwgRGVzZXJpYWxpemVkRGtsc1NpZ25hdHVyZSwgRGVzZXJpYWxpemVkTWVzc2FnZXMsIERzZ1N0YXRlIH0gZnJvbSAnLi90eXBlcyc7XG5pbXBvcnQgeyBkZWNvZGUgfSBmcm9tICdjYm9yLXgnO1xuXG50eXBlIE5vZGVXYXNtZXIgPSB0eXBlb2YgaW1wb3J0KCdAc2lsZW5jZWxhYm9yYXRvcmllcy9ka2xzLXdhc20tbGwtbm9kZScpO1xudHlwZSBXZWJXYXNtZXIgPSB0eXBlb2YgaW1wb3J0KCdAc2lsZW5jZWxhYm9yYXRvcmllcy9ka2xzLXdhc20tbGwtd2ViJyk7XG50eXBlIEJ1bmRsZXJXYXNtZXIgPSB0eXBlb2YgaW1wb3J0KCdAc2lsZW5jZWxhYm9yYXRvcmllcy9ka2xzLXdhc20tbGwtYnVuZGxlcicpO1xuXG50eXBlIERrbHNXYXNtID0gTm9kZVdhc21lciB8IFdlYldhc21lciB8IEJ1bmRsZXJXYXNtZXI7XG5cbmV4cG9ydCBjbGFzcyBEc2cge1xuICBwcm90ZWN0ZWQgZHNnU2Vzc2lvbjogU2lnblNlc3Npb25PVFZhcmlhbnQgfCB1bmRlZmluZWQ7XG4gIHByb3RlY3RlZCBkc2dTZXNzaW9uQnl0ZXM6IFVpbnQ4QXJyYXk7XG4gIHByaXZhdGUgX3NpZ25hdHVyZTogRGVzZXJpYWxpemVkRGtsc1NpZ25hdHVyZSB8IHVuZGVmaW5lZDtcbiAgcHJvdGVjdGVkIGtleVNoYXJlQnl0ZXM6IEJ1ZmZlcjtcbiAgcHJvdGVjdGVkIG1lc3NhZ2VIYXNoOiBCdWZmZXI7XG4gIHByb3RlY3RlZCBkZXJpdmF0aW9uUGF0aDogc3RyaW5nO1xuICBwcm90ZWN0ZWQgcGFydHlJZHg6IG51bWJlcjtcbiAgcHJvdGVjdGVkIGRzZ1N0YXRlOiBEc2dTdGF0ZSA9IERzZ1N0YXRlLlVuaW5pdGlhbGl6ZWQ7XG4gIHByb3RlY3RlZCBka2xzV2FzbTogRGtsc1dhc20gfCBudWxsO1xuXG4gIGNvbnN0cnVjdG9yKFxuICAgIGtleVNoYXJlOiBCdWZmZXIsXG4gICAgcGFydHlJZHg6IG51bWJlcixcbiAgICBkZXJpdmF0aW9uUGF0aDogc3RyaW5nLFxuICAgIG1lc3NhZ2VIYXNoOiBCdWZmZXIsXG4gICAgZGtsc1dhc20/OiBCdW5kbGVyV2FzbWVyXG4gICkge1xuICAgIHRoaXMucGFydHlJZHggPSBwYXJ0eUlkeDtcbiAgICB0aGlzLmtleVNoYXJlQnl0ZXMgPSBrZXlTaGFyZTtcbiAgICB0aGlzLmRlcml2YXRpb25QYXRoID0gZGVyaXZhdGlvblBhdGg7XG4gICAgdGhpcy5tZXNzYWdlSGFzaCA9IG1lc3NhZ2VIYXNoO1xuICAgIHRoaXMuZGtsc1dhc20gPSBka2xzV2FzbSA/PyBudWxsO1xuICB9XG5cbiAgcHJpdmF0ZSBfcmVzdG9yZVNlc3Npb24oKSB7XG4gICAgaWYgKCF0aGlzLmRzZ1Nlc3Npb24pIHtcbiAgICAgIHRoaXMuZHNnU2Vzc2lvbiA9IHRoaXMuZ2V0RGtsc1dhc20oKS5TaWduU2Vzc2lvbk9UVmFyaWFudC5mcm9tQnl0ZXModGhpcy5kc2dTZXNzaW9uQnl0ZXMpO1xuICAgIH1cbiAgfVxuXG4gIHByaXZhdGUgX2Rlc2VyaWFsaXplU3RhdGUoKSB7XG4gICAgaWYgKCF0aGlzLmRzZ1Nlc3Npb24pIHtcbiAgICAgIHRocm93IEVycm9yKCdTZXNzaW9uIG5vdCBpbnRpYWxpemVkJyk7XG4gICAgfVxuICAgIGNvbnN0IHJvdW5kID0gZGVjb2RlKHRoaXMuZHNnU2Vzc2lvbi50b0J5dGVzKCkpLnJvdW5kO1xuICAgIHN3aXRjaCAocm91bmQpIHtcbiAgICAgIGNhc2UgJ1dhaXRNc2cxJzpcbiAgICAgICAgdGhpcy5kc2dTdGF0ZSA9IERzZ1N0YXRlLlJvdW5kMTtcbiAgICAgICAgYnJlYWs7XG4gICAgICBjYXNlICdXYWl0TXNnMic6XG4gICAgICAgIHRoaXMuZHNnU3RhdGUgPSBEc2dTdGF0ZS5Sb3VuZDI7XG4gICAgICAgIGJyZWFrO1xuICAgICAgY2FzZSAnV2FpdE1zZzMnOlxuICAgICAgICB0aGlzLmRzZ1N0YXRlID0gRHNnU3RhdGUuUm91bmQzO1xuICAgICAgICBicmVhaztcbiAgICAgIGNhc2UgJ0VuZGVkJzpcbiAgICAgICAgdGhpcy5kc2dTdGF0ZSA9IERzZ1N0YXRlLkNvbXBsZXRlO1xuICAgICAgICBicmVhaztcbiAgICAgIGRlZmF1bHQ6XG4gICAgICAgIHRoaXMuZHNnU3RhdGUgPSBEc2dTdGF0ZS5JbnZhbGlkU3RhdGU7XG4gICAgICAgIHRocm93IEVycm9yKGBJbnZhbGlkIFN0YXRlOiAke3JvdW5kfWApO1xuICAgIH1cbiAgfVxuXG4gIHByaXZhdGUgYXN5bmMgbG9hZERrbHNXYXNtKCk6IFByb21pc2U8dm9pZD4ge1xuICAgIGlmICghdGhpcy5ka2xzV2FzbSkge1xuICAgICAgdGhpcy5ka2xzV2FzbSA9IGF3YWl0IGltcG9ydCgnQHNpbGVuY2VsYWJvcmF0b3JpZXMvZGtscy13YXNtLWxsLW5vZGUnKTtcbiAgICB9XG4gIH1cblxuICBwcml2YXRlIGdldERrbHNXYXNtKCkge1xuICAgIGlmICghdGhpcy5ka2xzV2FzbSkge1xuICAgICAgdGhyb3cgRXJyb3IoJ0RLTFMgd2FzbSBub3QgbG9hZGVkJyk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHRoaXMuZGtsc1dhc207XG4gIH1cblxuICAvKipcbiAgICogUmV0dXJucyB0aGUgY3VycmVudCBEU0cgc2Vzc2lvbiBhcyBhIGJhc2U2NCBzdHJpbmcuXG4gICAqIEByZXR1cm5zIHtzdHJpbmd9IC0gYmFzZTY0IHN0cmluZyBvZiB0aGUgY3VycmVudCBEU0cgc2Vzc2lvblxuICAgKi9cbiAgZ2V0U2Vzc2lvbigpOiBzdHJpbmcge1xuICAgIHJldHVybiBCdWZmZXIuZnJvbSh0aGlzLmRzZ1Nlc3Npb25CeXRlcykudG9TdHJpbmcoJ2Jhc2U2NCcpO1xuICB9XG5cbiAgLyoqXG4gICAqIFNldHMgdGhlIERTRyBzZXNzaW9uIGZyb20gYSBiYXNlNjQgc3RyaW5nLlxuICAgKiBAcGFyYW0ge3N0cmluZ30gc2Vzc2lvbiAtIGJhc2U2NCBzdHJpbmcgb2YgdGhlIERTRyBzZXNzaW9uXG4gICAqL1xuICBhc3luYyBzZXRTZXNzaW9uKHNlc3Npb246IHN0cmluZyk6IFByb21pc2U8dm9pZD4ge1xuICAgIHRoaXMuZHNnU2Vzc2lvbiA9IHVuZGVmaW5lZDtcbiAgICBpZiAoIXRoaXMuZGtsc1dhc20pIHtcbiAgICAgIGF3YWl0IHRoaXMubG9hZERrbHNXYXNtKCk7XG4gICAgfVxuICAgIGNvbnN0IHNlc3Npb25CeXRlcyA9IG5ldyBVaW50OEFycmF5KEJ1ZmZlci5mcm9tKHNlc3Npb24sICdiYXNlNjQnKSk7XG4gICAgY29uc3Qgcm91bmQgPSBkZWNvZGUoc2Vzc2lvbkJ5dGVzKS5yb3VuZDtcbiAgICBzd2l0Y2ggKHRydWUpIHtcbiAgICAgIGNhc2Ugcm91bmQgPT09ICdXYWl0TXNnMSc6XG4gICAgICAgIHRoaXMuZHNnU3RhdGUgPSBEc2dTdGF0ZS5Sb3VuZDE7XG4gICAgICAgIGJyZWFrO1xuICAgICAgY2FzZSByb3VuZCA9PT0gJ1dhaXRNc2cyJzpcbiAgICAgICAgdGhpcy5kc2dTdGF0ZSA9IERzZ1N0YXRlLlJvdW5kMjtcbiAgICAgICAgYnJlYWs7XG4gICAgICBjYXNlIHJvdW5kID09PSAnV2FpdE1zZzMnOlxuICAgICAgICB0aGlzLmRzZ1N0YXRlID0gRHNnU3RhdGUuUm91bmQzO1xuICAgICAgICBicmVhaztcbiAgICAgIGNhc2UgJ1dhaXRNc2c0JyBpbiByb3VuZDpcbiAgICAgICAgdGhpcy5kc2dTdGF0ZSA9IERzZ1N0YXRlLlJvdW5kNDtcbiAgICAgICAgYnJlYWs7XG4gICAgICBkZWZhdWx0OlxuICAgICAgICB0aHJvdyBFcnJvcihgSW52YWxpZCBTdGF0ZTogJHtyb3VuZH1gKTtcbiAgICB9XG4gICAgdGhpcy5kc2dTZXNzaW9uQnl0ZXMgPSBzZXNzaW9uQnl0ZXM7XG4gIH1cblxuICBhc3luYyBpbml0KCk6IFByb21pc2U8RGVzZXJpYWxpemVkQnJvYWRjYXN0TWVzc2FnZT4ge1xuICAgIGlmICh0aGlzLmRzZ1N0YXRlICE9PSBEc2dTdGF0ZS5VbmluaXRpYWxpemVkKSB7XG4gICAgICB0aHJvdyBFcnJvcignRFNHIHNlc3Npb24gYWxyZWFkeSBpbml0aWFsaXplZCcpO1xuICAgIH1cbiAgICBpZiAoIXRoaXMuZGtsc1dhc20pIHtcbiAgICAgIGF3YWl0IHRoaXMubG9hZERrbHNXYXNtKCk7XG4gICAgfVxuICAgIGlmIChcbiAgICAgIHR5cGVvZiB3aW5kb3cgIT09ICd1bmRlZmluZWQnICYmXG4gICAgICAvKiBjaGVja3MgZm9yIGVsZWN0cm9uIHByb2Nlc3NlcyAqL1xuICAgICAgIXdpbmRvdy5wcm9jZXNzICYmXG4gICAgICAhd2luZG93LnByb2Nlc3M/LlsndHlwZSddXG4gICAgKSB7XG4gICAgICAvKiBUaGlzIGlzIG9ubHkgbmVlZGVkIGZvciBicm93c2Vycy93ZWIgYmVjYXVzZSBpdCB1c2VzIGZldGNoIHRvIHJlc29sdmUgdGhlIHdhc20gYXNzZXQgZm9yIHRoZSB3ZWIgKi9cbiAgICAgIGNvbnN0IGluaXREa2xzID0gYXdhaXQgaW1wb3J0KCdAc2lsZW5jZWxhYm9yYXRvcmllcy9ka2xzLXdhc20tbGwtd2ViJyk7XG4gICAgICBhd2FpdCBpbml0RGtscy5kZWZhdWx0KCk7XG4gICAgfVxuICAgIGNvbnN0IHsgS2V5c2hhcmUsIFNpZ25TZXNzaW9uT1RWYXJpYW50IH0gPSB0aGlzLmdldERrbHNXYXNtKCk7XG4gICAgY29uc3Qga2V5U2hhcmUgPSBLZXlzaGFyZS5mcm9tQnl0ZXModGhpcy5rZXlTaGFyZUJ5dGVzKTtcbiAgICBpZiAoa2V5U2hhcmUucGFydHlJZCAhPT0gdGhpcy5wYXJ0eUlkeCkge1xuICAgICAgdGhyb3cgRXJyb3IoYFBhcnR5IGluZGV4OiAke3RoaXMucGFydHlJZHh9IGRvZXMgbm90IG1hdGNoIGtleSBzaGFyZSBwYXJ0eUlkOiAke2tleVNoYXJlLnBhcnR5SWR9IGApO1xuICAgIH1cbiAgICB0aGlzLmRzZ1Nlc3Npb24gPSBuZXcgU2lnblNlc3Npb25PVFZhcmlhbnQoa2V5U2hhcmUsIHRoaXMuZGVyaXZhdGlvblBhdGgpO1xuICAgIHRyeSB7XG4gICAgICBjb25zdCBwYXlsb2FkID0gdGhpcy5kc2dTZXNzaW9uLmNyZWF0ZUZpcnN0TWVzc2FnZSgpLnBheWxvYWQ7XG4gICAgICB0aGlzLl9kZXNlcmlhbGl6ZVN0YXRlKCk7XG4gICAgICB0aGlzLmRzZ1Nlc3Npb25CeXRlcyA9IHRoaXMuZHNnU2Vzc2lvbi50b0J5dGVzKCk7XG4gICAgICB0aGlzLmRzZ1Nlc3Npb24gPSB1bmRlZmluZWQ7XG4gICAgICByZXR1cm4ge1xuICAgICAgICBwYXlsb2FkOiBwYXlsb2FkLFxuICAgICAgICBmcm9tOiB0aGlzLnBhcnR5SWR4LFxuICAgICAgfTtcbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICB0aHJvdyBFcnJvcihgRXJyb3Igd2hpbGUgY3JlYXRpbmcgdGhlIGZpcnN0IG1lc3NhZ2UgZnJvbSBwYXJ0eSAke3RoaXMucGFydHlJZHh9OiAke2V9YCk7XG4gICAgfVxuICB9XG5cbiAgZ2V0IHNpZ25hdHVyZSgpOiBEZXNlcmlhbGl6ZWREa2xzU2lnbmF0dXJlIHtcbiAgICBpZiAoIXRoaXMuX3NpZ25hdHVyZSkge1xuICAgICAgdGhyb3cgRXJyb3IoJ0NhbiBub3QgcmVxdWVzdCBzaWduYXR1cmUuIFNpZ25hdHVyZSBub3QgcHJvZHVjZWQgeWV0LicpO1xuICAgIH1cbiAgICByZXR1cm4gdGhpcy5fc2lnbmF0dXJlO1xuICB9XG5cbiAgLyoqXG4gICAqIEVuZHMgdGhlIERTRyBzZXNzaW9uIGJ5IGZyZWVpbmcgYW55IGhlYXAgYWxsb2NhdGlvbnMgZnJvbSB3YXNtLiBOb3RlIHRoYXQgdGhlIHNlc3Npb24gaXMgZnJlZWQgaWYgYSBzaWduYXR1cmUgaXMgcHJvZHVjZWQuXG4gICAqL1xuICBlbmRTZXNzaW9uKCk6IHZvaWQge1xuICAgIGlmICh0aGlzLl9zaWduYXR1cmUpIHtcbiAgICAgIG5ldyBFcnJvcignU2Vzc2lvbiBhbHJlYWR5IGVuZGVkIGJlY2F1c2UgY29tYmluZWQgc2lnbmF0dXJlIHdhcyBwcm9kdWNlZC4nKTtcbiAgICB9XG4gICAgaWYgKHRoaXMuZHNnU2Vzc2lvbikge1xuICAgICAgdGhpcy5kc2dTZXNzaW9uLmZyZWUoKTtcbiAgICB9XG4gICAgdGhpcy5kc2dTdGF0ZSA9IERzZ1N0YXRlLlVuaW5pdGlhbGl6ZWQ7XG4gIH1cblxuICAvKipcbiAgICogUHJvY2Nlc3NlcyBpbmNvbWluZyBtZXNzYWdlcyB0byB0aGlzIHBhcnR5IGluIHRoZSBES0xzIERTRyBwcm90b2NvbCBhbmRcbiAgICogcHJvZHVjZXMgbWVzc2FnZXMgZnJvbSB0aGlzIHBhcnR5IHRvIG90aGVyIHBhcnRpZXMgZm9yIHRoZSBuZXh0IHJvdW5kLlxuICAgKiBAcGFyYW0gbWVzc2FnZXNGb3JJdGhSb3VuZCAtIG1lc3NhZ2VzIHRvIHByb2Nlc3MgdGhlIGN1cnJlbnQgcm91bmRcbiAgICogQHJldHVybnMge0Rlc2VyaWFsaXplZE1lc3NhZ2VzfSAtIG1lc3NhZ2VzIHRvIHNlbmQgdG8gb3RoZXIgcGFydGllcyBmb3IgdGhlIG5leHQgcm91bmRcbiAgICovXG4gIGhhbmRsZUluY29taW5nTWVzc2FnZXMobWVzc2FnZXNGb3JJdGhSb3VuZDogRGVzZXJpYWxpemVkTWVzc2FnZXMpOiBEZXNlcmlhbGl6ZWRNZXNzYWdlcyB7XG4gICAgbGV0IG5leHRSb3VuZE1lc3NhZ2VzOiBNZXNzYWdlW10gPSBbXTtcbiAgICBsZXQgbmV4dFJvdW5kRGVzZXJpYWxpemVkTWVzc2FnZXM6IERlc2VyaWFsaXplZE1lc3NhZ2VzID0geyBicm9hZGNhc3RNZXNzYWdlczogW10sIHAycE1lc3NhZ2VzOiBbXSB9O1xuICAgIHRoaXMuX3Jlc3RvcmVTZXNzaW9uKCk7XG4gICAgaWYgKCF0aGlzLmRzZ1Nlc3Npb24pIHtcbiAgICAgIHRocm93IEVycm9yKCdTZXNzaW9uIG5vdCBpbml0aWFsaXplZCcpO1xuICAgIH1cbiAgICBjb25zdCB7IE1lc3NhZ2UgfSA9IHRoaXMuZ2V0RGtsc1dhc20oKTtcbiAgICB0cnkge1xuICAgICAgaWYgKHRoaXMuZHNnU3RhdGUgPT09IERzZ1N0YXRlLlJvdW5kNCkge1xuICAgICAgICB0aGlzLmRzZ1N0YXRlID0gRHNnU3RhdGUuQ29tcGxldGU7XG4gICAgICAgIGNvbnN0IGNvbWJpbmVSZXN1bHQgPSB0aGlzLmRzZ1Nlc3Npb24uY29tYmluZShcbiAgICAgICAgICBtZXNzYWdlc0Zvckl0aFJvdW5kLmJyb2FkY2FzdE1lc3NhZ2VzLm1hcCgobSkgPT4gbmV3IE1lc3NhZ2UobS5wYXlsb2FkLCBtLmZyb20sIHVuZGVmaW5lZCkpXG4gICAgICAgICk7XG4gICAgICAgIHRoaXMuX3NpZ25hdHVyZSA9IHtcbiAgICAgICAgICBSOiBjb21iaW5lUmVzdWx0WzBdLFxuICAgICAgICAgIFM6IGNvbWJpbmVSZXN1bHRbMV0sXG4gICAgICAgIH07XG4gICAgICAgIHJldHVybiB7IGJyb2FkY2FzdE1lc3NhZ2VzOiBbXSwgcDJwTWVzc2FnZXM6IFtdIH07XG4gICAgICB9IGVsc2Uge1xuICAgICAgICBuZXh0Um91bmRNZXNzYWdlcyA9IHRoaXMuZHNnU2Vzc2lvbi5oYW5kbGVNZXNzYWdlcyhcbiAgICAgICAgICBtZXNzYWdlc0Zvckl0aFJvdW5kLmJyb2FkY2FzdE1lc3NhZ2VzXG4gICAgICAgICAgICAubWFwKChtKSA9PiBuZXcgTWVzc2FnZShtLnBheWxvYWQsIG0uZnJvbSwgdW5kZWZpbmVkKSlcbiAgICAgICAgICAgIC5jb25jYXQobWVzc2FnZXNGb3JJdGhSb3VuZC5wMnBNZXNzYWdlcy5tYXAoKG0pID0+IG5ldyBNZXNzYWdlKG0ucGF5bG9hZCwgbS5mcm9tLCBtLnRvKSkpXG4gICAgICAgICk7XG4gICAgICB9XG4gICAgICBpZiAodGhpcy5kc2dTdGF0ZSA9PT0gRHNnU3RhdGUuUm91bmQzKSB7XG4gICAgICAgIG5leHRSb3VuZE1lc3NhZ2VzID0gW3RoaXMuZHNnU2Vzc2lvbi5sYXN0TWVzc2FnZSh0aGlzLm1lc3NhZ2VIYXNoKV07XG4gICAgICAgIHRoaXMuZHNnU3RhdGUgPSBEc2dTdGF0ZS5Sb3VuZDQ7XG4gICAgICAgIHJldHVybiB7XG4gICAgICAgICAgYnJvYWRjYXN0TWVzc2FnZXM6IFtcbiAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgcGF5bG9hZDogbmV4dFJvdW5kTWVzc2FnZXNbMF0ucGF5bG9hZCxcbiAgICAgICAgICAgICAgZnJvbTogbmV4dFJvdW5kTWVzc2FnZXNbMF0uZnJvbV9pZCxcbiAgICAgICAgICAgICAgc2lnbmF0dXJlUjogZGVjb2RlKHRoaXMuZHNnU2Vzc2lvbi50b0J5dGVzKCkpLnJvdW5kLldhaXRNc2c0LnIsXG4gICAgICAgICAgICB9LFxuICAgICAgICAgIF0sXG4gICAgICAgICAgcDJwTWVzc2FnZXM6IFtdLFxuICAgICAgICB9O1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgLy8gVXBkYXRlIHJvdW5kIGRhdGEuXG4gICAgICAgIHRoaXMuX2Rlc2VyaWFsaXplU3RhdGUoKTtcbiAgICAgIH1cbiAgICAgIG5leHRSb3VuZERlc2VyaWFsaXplZE1lc3NhZ2VzID0ge1xuICAgICAgICBwMnBNZXNzYWdlczogbmV4dFJvdW5kTWVzc2FnZXNcbiAgICAgICAgICAuZmlsdGVyKChtKSA9PiBtLnRvX2lkICE9PSB1bmRlZmluZWQpXG4gICAgICAgICAgLm1hcCgobSkgPT4ge1xuICAgICAgICAgICAgaWYgKG0udG9faWQgPT09IHVuZGVmaW5lZCkge1xuICAgICAgICAgICAgICB0aHJvdyBFcnJvcignSW52YWxpZCBQMlAgbWVzc2FnZSwgbWlzc2luZyB0b19pZC4nKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGNvbnN0IHAycFJldHVybiA9IHtcbiAgICAgICAgICAgICAgcGF5bG9hZDogbS5wYXlsb2FkLFxuICAgICAgICAgICAgICBmcm9tOiBtLmZyb21faWQsXG4gICAgICAgICAgICAgIHRvOiBtLnRvX2lkLFxuICAgICAgICAgICAgfTtcbiAgICAgICAgICAgIHJldHVybiBwMnBSZXR1cm47XG4gICAgICAgICAgfSksXG4gICAgICAgIGJyb2FkY2FzdE1lc3NhZ2VzOiBuZXh0Um91bmRNZXNzYWdlc1xuICAgICAgICAgIC5maWx0ZXIoKG0pID0+IG0udG9faWQgPT09IHVuZGVmaW5lZClcbiAgICAgICAgICAubWFwKChtKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBicm9hZGNhc3RSZXR1cm4gPSB7XG4gICAgICAgICAgICAgIHBheWxvYWQ6IG0ucGF5bG9hZCxcbiAgICAgICAgICAgICAgZnJvbTogbS5mcm9tX2lkLFxuICAgICAgICAgICAgfTtcbiAgICAgICAgICAgIHJldHVybiBicm9hZGNhc3RSZXR1cm47XG4gICAgICAgICAgfSksXG4gICAgICB9O1xuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgIGlmIChlLm1lc3NhZ2Uuc3RhcnRzV2l0aCgnQWJvcnQgdGhlIHByb3RvY29sIGFuZCBiYW4nKSkge1xuICAgICAgICB0aHJvdyBFcnJvcihcbiAgICAgICAgICAnU2lnbmluZyBhYm9ydGVkLiBQbGVhc2Ugc3RvcCBhbGwgdHJhbnNhY3Rpb24gc2lnbmluZyBmcm9tIHRoaXMgd2FsbGV0IGFuZCBjb250YWN0IHN1cHBvcnRAYml0Z28uY29tLidcbiAgICAgICAgKTtcbiAgICAgIH1cbiAgICAgIHRocm93IEVycm9yKGBFcnJvciB3aGlsZSBjcmVhdGluZyBtZXNzYWdlcyBmcm9tIHBhcnR5ICR7dGhpcy5wYXJ0eUlkeH0sIHJvdW5kICR7dGhpcy5kc2dTdGF0ZX06ICR7ZX1gKTtcbiAgICB9IGZpbmFsbHkge1xuICAgICAgbmV4dFJvdW5kTWVzc2FnZXMuZm9yRWFjaCgobSkgPT4gbS5mcmVlKCkpO1xuICAgICAgLy8gU2Vzc2lvbiBpcyBmcmVlZCB3aGVuIGNvbWJpbmUgaXMgY2FsbGVkLlxuICAgICAgaWYgKHRoaXMuZHNnU3RhdGUgIT09IERzZ1N0YXRlLkNvbXBsZXRlKSB7XG4gICAgICAgIHRoaXMuZHNnU2Vzc2lvbkJ5dGVzID0gdGhpcy5kc2dTZXNzaW9uLnRvQnl0ZXMoKTtcbiAgICAgICAgdGhpcy5kc2dTZXNzaW9uID0gdW5kZWZpbmVkO1xuICAgICAgfVxuICAgIH1cbiAgICByZXR1cm4gbmV4dFJvdW5kRGVzZXJpYWxpemVkTWVzc2FnZXM7XG4gIH1cbn1cbiJdfQ== +\ No newline at end of file diff --git a/patches/bitcore-tss+11.3.7.patch b/patches/bitcore-tss+11.3.7.patch new file mode 100644 index 0000000000..73807cf23d --- /dev/null +++ b/patches/bitcore-tss+11.3.7.patch @@ -0,0 +1,151 @@ +diff --git a/node_modules/bitcore-tss/ecdsa/keygen.js b/node_modules/bitcore-tss/ecdsa/keygen.js +index cb9eccb..ce8ad2d 100644 +--- a/node_modules/bitcore-tss/ecdsa/keygen.js ++++ b/node_modules/bitcore-tss/ecdsa/keygen.js +@@ -153,7 +153,7 @@ class KeyGen { + * @param {Array} prevRoundMessages + * @returns {{ round: number, partyId: number, publicKey: string, p2pMessages: object[], broadcastMessage: object[] }} + */ +- nextRound(prevRoundMessages) { ++ async nextRound(prevRoundMessages) { + $.checkState(this.#round > 0, 'initJoin must be called before participating in the rounds'); + $.checkState(this.#round < 5, 'Signing rounds are over'); + $.checkArgument(Array.isArray(prevRoundMessages), 'prevRoundMessages must be an array'); +@@ -163,17 +163,40 @@ class KeyGen { + + const prevRoundIncomingMsgs = DklsComms.decryptAndVerifyIncomingMessages(prevRoundMessages, this.#authKey); + +- const thisRoundMessages = DklsTypes.serializeMessages( +- this.#dkg.handleIncomingMessages(DklsTypes.deserializeMessages(prevRoundIncomingMsgs)) +- ); +- +- const signedMessage = DklsComms.encryptAndAuthOutgoingMessages( +- thisRoundMessages, +- prevRoundMessages.map(m => ({ partyId: m.partyId, publicKey: m.publicKey })), +- this.#authKey +- ); +- +- return this._formatMessage(signedMessage); ++ let deserializedMessages; ++ try { ++ deserializedMessages = DklsTypes.deserializeMessages(prevRoundIncomingMsgs); ++ } catch (deserializeError) { ++ throw deserializeError; ++ } ++ ++ let incomingMessages; ++ try { ++ incomingMessages = await this.#dkg.handleIncomingMessages(deserializedMessages); ++ } catch (handleError) { ++ throw handleError; ++ } ++ ++ let thisRoundMessages; ++ try { ++ thisRoundMessages = await DklsTypes.serializeMessages(incomingMessages); ++ } catch (serializeError) { ++ throw serializeError; ++ } ++ ++ let signedMessage; ++ try { ++ signedMessage = DklsComms.encryptAndAuthOutgoingMessages( ++ thisRoundMessages, ++ prevRoundMessages.map(m => ({ partyId: m.partyId, publicKey: m.publicKey })), ++ this.#authKey ++ ); ++ } catch (encryptError) { ++ throw encryptError; ++ } ++ ++ const finalMessage = this._formatMessage(signedMessage); ++ return finalMessage; + } + + /** +diff --git a/node_modules/bitcore-tss/ecdsa/sign.js b/node_modules/bitcore-tss/ecdsa/sign.js +index a7d48d1..72eb551 100644 +--- a/node_modules/bitcore-tss/ecdsa/sign.js ++++ b/node_modules/bitcore-tss/ecdsa/sign.js +@@ -156,7 +156,6 @@ class Sign { + [], + this.#authKey + ); +- + return this._formatMessage(signedMessage); + } + +@@ -166,23 +165,24 @@ class Sign { + * @param {Array} prevRoundMessages + * @returns {{ round: number, partyId: number, publicKey: string, p2pMessages: Object[], broadcastMessages: Object[] }} + */ +- nextRound(prevRoundMessages) { ++ async nextRound(prevRoundMessages) { + $.checkState(this.#round > 0, 'initJoin must be called before participating in the rounds'); + $.checkState(this.#round < 5, 'Signing rounds are over'); + $.checkArgument(Array.isArray(prevRoundMessages), 'prevRoundMessages must be an array'); + $.checkArgument(prevRoundMessages.length === this.#minSigners - 1, 'Not ready to proceed to the next round'); + $.checkArgument(prevRoundMessages.every(msg => msg.round === this.#round - 1), 'All messages must be from the previous round'); +- $.checkArgument(prevRoundMessages.every(msg => msg.partyId !== this.#partyId), 'Messages must not be from the yourself'); +- +- let prevRndMsg = DklsComms.decryptAndVerifyIncomingMessages(prevRoundMessages, this.#authKey); +- prevRndMsg = DklsTypes.deserializeMessages(prevRndMsg); +- +- const thisRoundMsg = this.#dsg.handleIncomingMessages(prevRndMsg); +- const thisRoundMessage = DklsTypes.serializeMessages(thisRoundMsg); +- +- const partyPubKeys = prevRoundMessages.map(m => ({ partyId: m.partyId, publicKey: m.publicKey })); ++ $.checkArgument(prevRoundMessages.every(msg => msg.partyId !== this.#partyId), 'Messages must not be from yourself'); ++ ++ const prevRndMsg = DklsComms.decryptAndVerifyIncomingMessages(prevRoundMessages, this.#authKey); ++ const deserializedMessages = DklsTypes.deserializeMessages(prevRndMsg); ++ const incomingMessages = await this.#dsg.handleIncomingMessages(deserializedMessages); ++ const thisRoundMessages = await DklsTypes.serializeMessages(incomingMessages); ++ const partyPubKeys = prevRoundMessages.map(m => ({ ++ partyId: m.partyId, ++ publicKey: m.publicKey ++ })); + const signedMessage = DklsComms.encryptAndAuthOutgoingMessages( +- thisRoundMessage, ++ thisRoundMessages, + partyPubKeys, + this.#authKey + ); +diff --git a/node_modules/bitcore-tss/ecies/ecies.js b/node_modules/bitcore-tss/ecies/ecies.js +index 120b696..346e386 100644 +--- a/node_modules/bitcore-tss/ecies/ecies.js ++++ b/node_modules/bitcore-tss/ecies/ecies.js +@@ -16,10 +16,11 @@ function KDF(privateKey, publicKey) { + const KB = publicKey.point; + const P = KB.mul(r); + const S = P.getX(); +- const Sbuf = S.toBuffer({ size: 32 }); +- const kEkM = Hash.sha512(Sbuf); +- const kE = kEkM.subarray(0, 32); +- const kM = kEkM.subarray(32, 64); ++ const Sbuf = Buffer.from(S.toBuffer({ size: 32 })); ++ const kEkM = Buffer.from(Hash.sha512(Sbuf)); ++ // In RN, subarray may return Uint8Array; force real Buffers: ++ const kE = Buffer.from(kEkM.subarray(0, 32)); ++ const kM = Buffer.from(kEkM.subarray(32, 64)); + return [kE, kM]; + }; + +@@ -127,13 +128,13 @@ function decrypt({ payload, privateKey, publicKey, opts = {} }) { + offset += pub.length; + } + +- const ivbuf = payload.subarray(offset, offset + 16); +- const cipherText = payload.subarray(offset + 16, payload.length - tagLength); +- const tag = payload.subarray(payload.length - tagLength, payload.length); ++ const ivbuf = Buffer.from(payload.subarray(offset, offset + 16)); ++ const cipherText = Buffer.from(payload.subarray(offset + 16, payload.length - tagLength)); ++ const tag = Buffer.from(payload.subarray(payload.length - tagLength, payload.length)); + + const [kE, kM] = KDF(privateKey, publicKey); + +- const tag2 = Hash.sha256hmac(cipherText, kM).subarray(0, tagLength); ++ const tag2 = Buffer.from(Hash.sha256hmac(cipherText, kM)).slice(0, tagLength); + if (tag2.compare(tag) !== 0) { + throw new Error('Invalid checksum'); + } diff --git a/patches/bitcore-wallet-client+11.3.6.patch b/patches/bitcore-wallet-client+11.3.6.patch new file mode 100644 index 0000000000..1e9aaf1529 --- /dev/null +++ b/patches/bitcore-wallet-client+11.3.6.patch @@ -0,0 +1,161 @@ +diff --git a/node_modules/bitcore-wallet-client/ts_build/src/lib/api.js b/node_modules/bitcore-wallet-client/ts_build/src/lib/api.js +index ca96d75..1ab4ef8 100644 +--- a/node_modules/bitcore-wallet-client/ts_build/src/lib/api.js ++++ b/node_modules/bitcore-wallet-client/ts_build/src/lib/api.js +@@ -49,6 +49,7 @@ const common_1 = require("./common"); + const credentials_1 = require("./credentials"); + const errors_1 = require("./errors"); + const key_1 = require("./key"); ++const tsskey_1 = require("./tsskey"); + const log_1 = __importDefault(require("./log")); + const paypro_1 = require("./paypro"); + const payproV2_1 = require("./payproV2"); +@@ -799,6 +800,7 @@ class API extends events_1.EventEmitter { + }; + const { body: res } = await this.request.post('/v2/wallets/', args); + const walletId = res.walletId; ++ + c.addWalletInfo(walletId, walletName, m, n, copayerName, { + useNativeSegwit: opts.useNativeSegwit, + segwitVersion: opts.segwitVersion, +@@ -2821,6 +2823,7 @@ exports.API = API; + API.PayProV2 = payproV2_1.PayProV2; + API.PayPro = paypro_1.PayPro; + API.Key = key_1.Key; ++API.TssKey = tsskey_1.TssKey; + API.Verifier = verifier_1.Verifier; + API.Core = CWC; + API.Utils = common_1.Utils; +diff --git a/node_modules/bitcore-wallet-client/ts_build/src/lib/common/encryption.js b/node_modules/bitcore-wallet-client/ts_build/src/lib/common/encryption.js +index ffb28f7..9193a78 100644 +--- a/node_modules/bitcore-wallet-client/ts_build/src/lib/common/encryption.js ++++ b/node_modules/bitcore-wallet-client/ts_build/src/lib/common/encryption.js +@@ -7,10 +7,18 @@ exports.Encryption = void 0; + const crypto_1 = __importDefault(require("crypto")); + const PBKDF2_ITERATIONS = 1000; + const DEFAULT_KEY_SIZE = 256; +-const ALGORITHM = ks => `aes-${ks || DEFAULT_KEY_SIZE}-ccm`; ++const ALGORITHM = ks => `aes-${ks || DEFAULT_KEY_SIZE}-gcm`; + const AUTH_TAG_LENGTH = 16; + const SALT_LENGTH = 16; +-const MAX_IV_LENGTH = 13; ++const IV_LENGTH = 12; ++const toBuf = (x) => { ++ if (Buffer.isBuffer(x)) return x; ++ if (x instanceof Uint8Array) return Buffer.from(x); ++ if (x instanceof ArrayBuffer) return Buffer.from(new Uint8Array(x)); ++ if (x == null) return Buffer.alloc(0); ++ return Buffer.from(String(x)); ++ }; ++ + class EncryptionClass { + _optimizeIv(length, iv) { + let L = 2; +@@ -25,18 +33,18 @@ class EncryptionClass { + } + _baseEncrypt(data, key) { + const buf = Buffer.isBuffer(data) ? data : Buffer.from(typeof data === 'string' ? data : JSON.stringify(data), 'utf8'); +- const iv = this._optimizeIv(buf.length, crypto_1.default.randomBytes(MAX_IV_LENGTH)); +- const cipher = crypto_1.default.createCipheriv(ALGORITHM(key.length * 8), key, iv, { authTagLength: AUTH_TAG_LENGTH, plaintextLength: buf.length }); +- let encrypted = cipher.update(buf); +- encrypted = Buffer.concat([encrypted, cipher.final()]); ++ const iv = crypto_1.default.randomBytes(IV_LENGTH); ++ const cipher = crypto_1.default.createCipheriv(ALGORITHM(key.length * 8), key, iv, { authTagLength: AUTH_TAG_LENGTH }); ++ let encrypted = toBuf(cipher.update(buf)); ++ encrypted = Buffer.concat([encrypted, toBuf(cipher.final())]); + return { + iv: iv.toString('base64'), + v: 1, + ts: AUTH_TAG_LENGTH * 8, +- mode: 'ccm', ++ mode: 'gcm', + adata: '', + cipher: 'aes', +- ct: Buffer.concat([encrypted, cipher.getAuthTag()]).toString('base64') ++ ct: Buffer.concat([encrypted, toBuf(cipher.getAuthTag())]).toString('base64') + }; + } + encryptWithKey(data, key) { +diff --git a/node_modules/bitcore-wallet-client/ts_build/src/lib/credentials.js b/node_modules/bitcore-wallet-client/ts_build/src/lib/credentials.js +index c076dcd..c792064 100644 +--- a/node_modules/bitcore-wallet-client/ts_build/src/lib/credentials.js ++++ b/node_modules/bitcore-wallet-client/ts_build/src/lib/credentials.js +@@ -48,7 +48,7 @@ class Credentials { + const entropySource = crypto_wallet_core_1.BitcoreLib.crypto.Hash.sha256(priv.toBuffer()).toString('hex'); + const b = Buffer.from(entropySource, 'hex'); + const b2 = crypto_wallet_core_1.BitcoreLib.crypto.Hash.sha256hmac(b, Buffer.from(prefix)); +- x.personalEncryptingKey = b2.subarray(0, 16).toString('base64'); ++ x.personalEncryptingKey = Buffer.from(b2.subarray(0, 16)).toString('base64'); + x.copayerId = common_1.Utils.xPubToCopayerId(x.chain, x.xPubKey); + x.publicKeyRing = [ + { +diff --git a/node_modules/bitcore-wallet-client/ts_build/src/lib/key.js b/node_modules/bitcore-wallet-client/ts_build/src/lib/key.js +index 1fa419a..95acbb7 100644 +--- a/node_modules/bitcore-wallet-client/ts_build/src/lib/key.js ++++ b/node_modules/bitcore-wallet-client/ts_build/src/lib/key.js +@@ -95,6 +95,7 @@ class Key { + this.use44forMultisig = opts.useLegacyPurpose; + this.compliantDerivation = !opts.nonCompliantDerivation; + let x = opts.seedData; ++ + switch (opts.seedType) { + case 'new': + if (opts.language && !wordsForLang[opts.language]) +diff --git a/node_modules/bitcore-wallet-client/ts_build/src/lib/tsssign.js b/node_modules/bitcore-wallet-client/ts_build/src/lib/tsssign.js +index f0e05d8..570e212 100644 +--- a/node_modules/bitcore-wallet-client/ts_build/src/lib/tsssign.js ++++ b/node_modules/bitcore-wallet-client/ts_build/src/lib/tsssign.js +@@ -50,7 +50,12 @@ class TssSign extends events_1.EventEmitter { + $.checkArgument(!messageHash || Buffer.isBuffer(messageHash), 'messageHash must be a Buffer'); + $.checkArgument(!message || Buffer.isBuffer(message) || typeof message === 'string', 'message must be a string or Buffer'); + $.checkArgument(id == null || typeof id === 'string', 'id must be a string or not provided'); +- $.checkArgument(password || __classPrivateFieldGet(this, _TssSign_tssKey, "f").keychain.privateKeyShare, 'password is required to decrypt the TSS private key share'); ++ ++ $.checkArgument( ++ __classPrivateFieldGet(this, _TssSign_tssKey, "f").keychain.privateKeyShare, ++ 'Key shares are required for signing. Make sure backupKeyShare was enabled during key generation.' ++ ); ++ + if (!messageHash && typeof message === 'string') { + if (encoding === 'hex') { + message = message.startsWith('0x') ? message.slice(2) : message; +@@ -58,8 +63,12 @@ class TssSign extends events_1.EventEmitter { + message = Buffer.from(message, encoding); + messageHash = crypto_wallet_core_1.BitcoreLib.crypto.Hash.sha256(message); + } ++ ++ const keychain = __classPrivateFieldGet(this, _TssSign_tssKey, "f").get(password).keychain ++ if( keychain.privateKeyShare.data )keychain.privateKeyShare = Buffer.from(keychain.privateKeyShare.data); ++ + __classPrivateFieldSet(this, _TssSign_sign, new bitcore_tss_1.ECDSA.Sign({ +- keychain: __classPrivateFieldGet(this, _TssSign_tssKey, "f").get(password).keychain, ++ keychain:keychain, + partyId: __classPrivateFieldGet(this, _TssSign_tssKey, "f").metadata.partyId, + m: __classPrivateFieldGet(this, _TssSign_tssKey, "f").metadata.m, + n: __classPrivateFieldGet(this, _TssSign_tssKey, "f").metadata.n, +@@ -67,10 +76,12 @@ class TssSign extends events_1.EventEmitter { + messageHash, + authKey: __classPrivateFieldGet(this, _TssSign_credentials, "f").requestPrivKey + }), "f"); ++ + this.id = id || crypto_wallet_core_1.BitcoreLib.crypto.Hash.sha256(messageHash).toString('hex'); + const msg = await __classPrivateFieldGet(this, _TssSign_sign, "f").initJoin(); + const m = __classPrivateFieldGet(this, _TssSign_tssKey, "f").metadata.m; + await __classPrivateFieldGet(this, _TssSign_request, "f").post('/v1/tss/sign/' + this.id, { message: msg, m }); ++ + return this; + } + exportSession() { +@@ -83,9 +94,11 @@ class TssSign extends events_1.EventEmitter { + const { session } = params; + const [id, sigSession] = session.split(':'); + this.id = id; ++ const keychain = __classPrivateFieldGet(this, _TssSign_tssKey, "f").keychain ++ if( keychain.privateKeyShare.data )keychain.privateKeyShare = Buffer.from(keychain.privateKeyShare.data); + __classPrivateFieldSet(this, _TssSign_sign, await bitcore_tss_1.ECDSA.Sign.restore({ + session: sigSession, +- keychain: __classPrivateFieldGet(this, _TssSign_tssKey, "f").keychain, ++ keychain: keychain, + authKey: __classPrivateFieldGet(this, _TssSign_credentials, "f").requestPrivKey + }), "f"); + return this; diff --git a/scripts/allowed-url-prefixes.js b/scripts/allowed-url-prefixes.js index bf4f172604..209731bb6f 100644 --- a/scripts/allowed-url-prefixes.js +++ b/scripts/allowed-url-prefixes.js @@ -30,6 +30,7 @@ const developmentOnlyAllowedUrlPrefixes = : []; const allowedUrlPrefixes = [ + 'https://unpkg.com/@silencelaboratories/dkls-wasm-ll-web@latest/', 'https://bitpay.com/', 'https://test.bitpay.com/', 'https://staging.bitpay.com/', diff --git a/shim.js b/shim.js index 9d9dc2e3f2..6401284b0c 100644 --- a/shim.js +++ b/shim.js @@ -1,5 +1,13 @@ import { Crypto } from '@peculiar/webcrypto'; import { install as installEd25519 } from '@solana/webcrypto-ed25519-polyfill'; +import * as RNWasm from 'react-native-webassembly'; + +global.self ||= global; +global.globalThis ||= global; +global.window ||= global; +global.WebAssembly = RNWasm; +try { if (global.process) global.process.browser = true; } catch {} + if (typeof __dirname === 'undefined') { global.__dirname = '/'; diff --git a/shims/silence-dkls-web.js b/shims/silence-dkls-web.js new file mode 100644 index 0000000000..482edbc5a7 --- /dev/null +++ b/shims/silence-dkls-web.js @@ -0,0 +1,394 @@ +import { callWorker } from '../src/dkls/DklsWorker'; + +const preview = (v) => { + try { return JSON.stringify(v).slice(0, 200); } catch { return String(v).slice(0,200); } +}; + +export default async function init() { + console.log('[DKLS shim] init()'); + const r = await callWorker({ type: 'init' }); + console.log('[DKLS shim] init() ok ->', r); +} + +async function toHandleJSON(m) { + if (!m) return m; + if (m && typeof m._id === 'number' && !m._ready) { + return { _id: m._id }; + } + + if (m._ready && typeof m._ready.then === 'function') { + try { + await m._ready; + } catch (e) { + } + } + if (typeof m._objId === 'number') { + return { _id: m._objId }; + } + if (m._proxy && typeof m._proxy._id === 'number') { + return { _id: m._proxy._id }; + } + + if (m && typeof m === 'object' && 'payload' in m && 'from_id' in m) { + const payload = m.payload instanceof Uint8Array + ? m.payload + : Array.isArray(m.payload) + ? new Uint8Array(m.payload) + : new Uint8Array(Object.values(m.payload || {})); + + return { + payload: Array.from(payload), + from_id: m.from_id, + to_id: m.to_id !== undefined ? m.to_id : undefined + }; + } + return m; +} + +async function normalizeMsgsForBridge(msgs) { + if (!Array.isArray(msgs)) { + return msgs; + } + const normalized = await Promise.all(msgs.map(async (m, idx) => { + const result = await toHandleJSON(m); + return result; + })); + return normalized; +} + +function wrap(objId) { + return { + _id: objId, + call(method, ...args) { + return callWorker({ type: 'call', objId, method, args }); + }, + free() { + return callWorker({ type: 'free', objId }); + }, + }; +} + +function wrapAs(Cls, objId) { + const o = Object.create(Cls.prototype); + o._ready = Promise.resolve((o._proxy = wrap(objId))); + o._objId = objId; + o.toJSON = function(){ return { _id: this._objId ?? (this._proxy && this._proxy._id) }; }; + return o; +} + +function toU8(x) { + if (x == null) return undefined; + if (x instanceof Uint8Array) return x; + if (Array.isArray(x)) return new Uint8Array(x); + + if (typeof x === "object") { + const keys = Object.keys(x); + const isArrayLike = keys.length > 0 && keys.every((k, i) => k === String(i)); + + if (isArrayLike) { + return new Uint8Array(Object.values(x)); + } + + throw new Error( + "toU8() requires Uint8Array or Array, got object with keys: [" + + keys.slice(0, 5).join(', ') + + (keys.length > 5 ? ', ...' : '') + + "]" + ); + } + + throw new Error("toU8() requires Uint8Array or Array"); +} + +export class KeygenSession { + constructor(participants, threshold, party_id, seed) { + const seedArray = seed instanceof Uint8Array ? Array.from(seed) : seed; + this._ready = callWorker({ + type: 'construct', + className: 'KeygenSession', + args: [participants, threshold, party_id, seedArray], + }).then(({ objId }) => (this._proxy = wrap(objId))); + } + + static async fromBytes(bytes) { + const bytesArray = bytes instanceof Uint8Array ? Array.from(bytes) : bytes; + const { objId } = await callWorker({ + type: 'staticConstruct', + className: 'KeygenSession', + method: 'fromBytes', + args: [bytesArray], + }); + return wrapAs(KeygenSession, objId); + } + + static async initKeyRotation(keyshare, seed) { + const { objId } = await callWorker({ + type: 'staticConstruct', + className: 'KeygenSession', + method: 'initKeyRotation', + args: seed ? [keyshare, toU8(seed)] : [keyshare], + }); + return wrapAs(KeygenSession, objId); + } + + async toBytes() { + await this._ready; + const r = await this._proxy.call('toBytes'); + return toU8(r); + } + + async createFirstMessage() { + await this._ready; + const res = await this._proxy.call('createFirstMessage'); + return (res && res.objId) ? wrapAs(Message, res.objId) : res; + } + + async handleMessages(msgs, commitments, seed) { + await this._ready; + + const safeMsgs = await normalizeMsgsForBridge(msgs); + + let safeCommitments = commitments; + if (commitments && Array.isArray(commitments)) { + safeCommitments = commitments.map(c => c instanceof Uint8Array ? Array.from(c) : c); + } + + const res = await this._proxy.call('handleMessages', safeMsgs, safeCommitments, seed); + + if (Array.isArray(res) && res.length && res[0]?.objId !== undefined) { + return res.map(r => wrapAs(Message, r.objId)); + } + return res; + } + + async keyshare() { + await this._ready; + const res = await this._proxy.call('keyshare'); + if (res && res.objId) { + return new Keyshare(res.objId); + } + if (res && typeof res.__wbg_ptr === 'number') { + return new Keyshare(res.__wbg_ptr); + } + return res; + } + + async calculateChainCodeCommitment() { + await this._ready; + const res = await this._proxy.call('calculateChainCodeCommitment'); + return res instanceof Uint8Array ? res : new Uint8Array(res || []); + } +} + +export class SignSession { + constructor(keyshare, chain_path, seed) { + const seedArray = seed instanceof Uint8Array ? Array.from(seed) : seed; + this._ready = callWorker({ + type: 'construct', + className: 'SignSession', + args: [keyshare, chain_path, seedArray], + }).then(({ objId }) => (this._proxy = wrap(objId))); + } + + async createFirstMessage() { + await this._ready; + const res = await this._proxy.call('createFirstMessage'); + return (res && res.objId) ? wrapAs(Message, res.objId) : res; + } + + async handleMessages(msgs, commitments, seed) { + await this._ready; + const safeMsgs = await normalizeMsgsForBridge(msgs); + + let safeCommitments = commitments; + if (commitments && Array.isArray(commitments)) { + safeCommitments = commitments.map(c => c instanceof Uint8Array ? Array.from(c) : c); + } + + const res = await this._proxy.call('handleMessages', safeMsgs, safeCommitments, seed); + + if (Array.isArray(res) && res.length && res[0]?.objId !== undefined) { + return res.map(r => wrapAs(Message, r.objId)); + } + return res; + } + + async lastMessage(message_hash) { + await this._ready; + const r = await this._proxy.call('lastMessage', message_hash); + return r; + } + + async combine(msgs) { + await this._ready; + const r = await this._proxy.call('combine', msgs); + return r; + } +} + +export class Message { + constructor(payload, from_id, to_id) { + const u8 = payload instanceof Uint8Array + ? payload + : Array.isArray(payload) + ? new Uint8Array(payload) + : new Uint8Array(payload || []); + + this._ready = callWorker({ + type: 'construct', + className: 'Message', + args: [Array.from(u8), from_id, to_id], + }).then(({ objId }) => { + this._proxy = wrap(objId); + this._objId = objId; + this.toJSON = () => { + const id = this._objId ?? (this._proxy && this._proxy._id); + return { _id: id }; + }; + return this._proxy; + }); + + this._isFreed = false; + } + + async payload() { + await this._ready; + const p = await this._proxy.call('payload'); + return p instanceof Uint8Array ? p : new Uint8Array(p || []); + } + + async from_id() { await this._ready; return this._proxy.call('from_id'); } + async to_id() { await this._ready; return this._proxy.call('to_id'); } + async free() { + await this._ready; + if (this._isFreed) return; + this._isFreed = true; + return this._proxy.free(); + } +} + +export class Keyshare { + constructor(objId) { + if (typeof objId === 'number') { + this._ready = Promise.resolve((this._proxy = wrap(objId))); + this._objId = objId; + } else { + this._ready = Promise.reject(new Error('Keyshare should be obtained from KeygenSession.keyshare()')); + } + } + + static async fromBytes(bytes) { + const bytesArray = bytes instanceof Uint8Array ? Array.from(bytes) : bytes; + const { objId } = await callWorker({ + type: 'staticConstruct', + className: 'Keyshare', + method: 'fromBytes', + args: [bytesArray], + }); + return wrapAs(Keyshare, objId); + } + + async partyId() { + await this._ready; + return this._proxy.call('partyId'); + } + + async toBytes() { + await this._ready; + try { + const r = await this._proxy.call('toBytes'); + return toU8(r); + } catch (error) { + throw error; + } + } + + async free() { + await this._ready; + return this._proxy.free(); + } +} + +export class SignSessionOTVariant { + constructor(keyshare, chain_path, seed) { + let keyshareHandle; + if (typeof keyshare === 'object' && keyshare !== null) { + keyshareHandle = keyshare._objId || (keyshare._proxy && keyshare._proxy._id) || keyshare; + } else { + throw new Error('Invalid keyshare object'); + } + + const seedArray = seed instanceof Uint8Array ? Array.from(seed) : seed; + + const args = seedArray + ? [{ _id: keyshareHandle }, chain_path, seedArray] + : [{ _id: keyshareHandle }, chain_path]; + + this._ready = callWorker({ + type: 'construct', + className: 'SignSessionOTVariant', + args: args, + }).then(({ objId }) => { + this._proxy = wrap(objId); + this._objId = objId; + return this._proxy; + }); + } + + static async fromBytes(bytes) { + const bytesArray = bytes instanceof Uint8Array ? Array.from(bytes) : bytes; + const { objId } = await callWorker({ + type: 'staticConstruct', + className: 'SignSessionOTVariant', + method: 'fromBytes', + args: [bytesArray], + }); + return wrapAs(SignSessionOTVariant, objId); + } + + async toBytes() { + await this._ready; + const r = await this._proxy.call('toBytes'); + return toU8(r); + } + + async createFirstMessage() { + await this._ready; + const res = await this._proxy.call('createFirstMessage'); + return (res && res.objId) ? wrapAs(Message, res.objId) : res; + } + + async handleMessages(msgs) { + await this._ready; + const safeMsgs = await normalizeMsgsForBridge(msgs); + const res = await this._proxy.call('handleMessages', safeMsgs); + + if (Array.isArray(res) && res.length && res[0]?.objId !== undefined) { + return res.map(r => wrapAs(Message, r.objId)); + } + return res; + } + + async lastMessage(message_hash) { + await this._ready; + const messageHashArray = message_hash instanceof Uint8Array ? Array.from(message_hash) : message_hash; + const res = await this._proxy.call('lastMessage', messageHashArray); + return (res && res.objId) ? wrapAs(Message, res.objId) : res; + } + + async combine(msgs) { + await this._ready; + const safeMsgs = await normalizeMsgsForBridge(msgs); + const r = await this._proxy.call('combine', safeMsgs); + return r; + } + + async free() { + await this._ready; + return this._proxy.free(); + } +} + +try { + module.exports = { __esModule: true, default: init, KeygenSession, SignSession, SignSessionOTVariant, Message, Keyshare }; +} catch {} \ No newline at end of file diff --git a/src/Root.tsx b/src/Root.tsx index d4b770de89..0078925758 100644 --- a/src/Root.tsx +++ b/src/Root.tsx @@ -152,7 +152,7 @@ import { getBaseEVMAccountCreationCoinsAndTokens, getBaseSVMAccountCreationCoinsAndTokens, } from './constants/currencies'; -import Logger from 'bitcore-wallet-client/ts_build/lib/log'; +import Logger from 'bitcore-wallet-client/ts_build/src/lib/log'; import {BwcProvider} from './lib/bwc'; import {isNarrowHeight} from './components/styled/Containers'; import {useOngoingProcess} from './contexts'; diff --git a/src/dkls/DklsWorker.tsx b/src/dkls/DklsWorker.tsx new file mode 100644 index 0000000000..6ffb88f053 --- /dev/null +++ b/src/dkls/DklsWorker.tsx @@ -0,0 +1,775 @@ +import React, {useRef, useEffect, useState} from 'react'; +import {View, StyleSheet} from 'react-native'; +import {WebView, WebViewMessageEvent} from 'react-native-webview'; + +const WORKER_LOGS_ENABLED = false; + +type Pending = { + resolve: (v: any) => void; + reject: (e: any) => void; +}; + +let _post: ((data: any) => void) | null = null; +let _isReady = false; +let _readyResolve: (() => void) | null = null; +const _readyPromise: Promise = new Promise(res => (_readyResolve = res)); +const _outQueue: string[] = []; +const pendings = new Map(); +let seq = 1; + +const processArgs = (args: any): any => { + if (!Array.isArray(args)) return args; + + return args.map(arg => { + if (arg instanceof Uint8Array) { + console.log( + '[DKLS shim] processArgs: Uint8Array → Array, len=', + arg.length, + ); + return Array.from(arg); + } + if (Array.isArray(arg)) { + return processArgs(arg); + } + if (arg && typeof arg === 'object') { + const processed = {}; + for (const key in arg) { + // @ts-ignore + processed[key] = + arg[key] instanceof Uint8Array ? Array.from(arg[key]) : arg[key]; + } + return processed; + } + return arg; + }); +}; + +export async function callWorker(msg: any): Promise { + const id = ++seq; + const processedMsg = { + ...msg, + args: msg.args ? processArgs(msg.args) : undefined, + }; + const payload = JSON.stringify({id, ...processedMsg}); + + const t0 = Date.now(); + if (WORKER_LOGS_ENABLED) + console.log('[Dkls RN→WV] send', { + id, + type: msg?.type, + className: msg?.className, + method: msg?.method, + t0, + preview: payload.slice(0, 300), + }); + + if (!_isReady) { + if (WORKER_LOGS_ENABLED) console.log('[Dkls RN] waiting _readyPromise…'); + await _readyPromise; + } + + return new Promise((resolve, reject) => { + pendings.set(id, { + resolve: v => { + const dt = Date.now() - t0; + if (WORKER_LOGS_ENABLED) + console.log('[Dkls RN←WV] ok', { + id, + dt, + preview: JSON.stringify(v).slice(0, 300), + }); + resolve(v); + }, + reject: e => { + const dt = Date.now() - t0; + if (WORKER_LOGS_ENABLED) + console.log('[Dkls RN←WV] ERR', {id, dt, err: String(e)}); + reject(e); + }, + }); + if (_post) _post(payload); + else { + if (WORKER_LOGS_ENABLED) + console.log('[Dkls RN] _post not ready, queueing', {id}); + _outQueue.push(payload); + } + }); +} + +export const DklsWorkerHost = () => { + const [bootHtml] = useState(() => { + return ` + + + + + + + + + + + `; + }); + + const ref = useRef(null); + + const onMessage = (ev: WebViewMessageEvent) => { + try { + const raw = ev.nativeEvent.data; + const trimmed = raw?.length > 500 ? raw.slice(0, 500) + '…' : raw; + if (WORKER_LOGS_ENABLED) console.log('[Dkls WV→RN] raw', trimmed); + + const {id, ok, result} = JSON.parse(raw); + + if (typeof id === 'number' && id < -2) { + if (WORKER_LOGS_ENABLED) console.log('[DKLS LOG] internal log', result); + return; + } + + if ( + id === -1 && + ok && + (result === 'BOOTED' || result === 'BOOTED_LATE') + ) { + if (!_isReady) { + _isReady = true; + _readyResolve?.(); + _readyResolve = null; + if (_outQueue.length && _post) { + for (const p of _outQueue.splice(0)) _post(p); + } + } + return; + } + + const p = pendings.get(id); + if (!p) { + if (WORKER_LOGS_ENABLED) console.log('[Dkls RN] no pending for id', id); + return; + } + pendings.delete(id); + if (ok) p.resolve(result); + else p.reject(result); + } catch (e) { + if (WORKER_LOGS_ENABLED) + console.log( + '[DKLS HOST] onMessage parse error', + e, + 'raw-start=', + (ev.nativeEvent.data || '').slice(0, 200), + ); + } + }; + + useEffect(() => { + _post = data => { + ref.current?.postMessage(data); + }; + if (_outQueue.length && _post) { + for (const p of _outQueue.splice(0)) _post(p); + } + return () => { + _post = null; + }; + }, []); + + return ( + + req.url === 'about:blank'} + setSupportMultipleWindows={false} + javaScriptEnabled + onMessage={onMessage} + domStorageEnabled={false} + allowFileAccess={false} + thirdPartyCookiesEnabled={false} + sharedCookiesEnabled={false} + incognito + mixedContentMode="never" + onLoad={() => console.log('[DKLS HOST] webview onLoad')} + onError={e => console.log('[DKLS HOST] onError', e?.nativeEvent)} + onHttpError={e => + console.log('[DKLS HOST] onHttpError', e?.nativeEvent) + } + /> + + ); +}; + +const styles = StyleSheet.create({ + hidden: {width: 0, height: 0, overflow: 'hidden'}, +}); diff --git a/src/lib/bwc.ts b/src/lib/bwc.ts index b5711dd722..e44883344d 100644 --- a/src/lib/bwc.ts +++ b/src/lib/bwc.ts @@ -1,5 +1,5 @@ import BWC from 'bitcore-wallet-client'; -import {Constants} from 'bitcore-wallet-client/ts_build/lib/common'; +import {Constants} from 'bitcore-wallet-client/ts_build/src/lib/common'; import {PAYPRO_TRUSTED_KEYS} from '@env'; import { APP_NAME, @@ -58,6 +58,10 @@ export class BwcProvider { return BWC.Key; } + public getTssKey() { + return BWC.TssKey; + } + public upgradeCredentialsV1(x: any) { return BWC.upgradeCredentialsV1(x); } @@ -70,6 +74,10 @@ export class BwcProvider { return new BWC.Key(opts); } + public createTssKey(opts: KeyOpts) { + return new BWC.TssKey(opts); + } + public getBitcore() { return BWC.Bitcore; } diff --git a/src/navigation/wallet/components/RequestEncryptPasswordToggle.tsx b/src/navigation/wallet/components/RequestEncryptPasswordToggle.tsx index 461863bc67..cb0f141f43 100644 --- a/src/navigation/wallet/components/RequestEncryptPasswordToggle.tsx +++ b/src/navigation/wallet/components/RequestEncryptPasswordToggle.tsx @@ -15,7 +15,7 @@ import { sleep, } from '../../../utils/helper-methods'; import {useTranslation} from 'react-i18next'; -import {Constants} from 'bitcore-wallet-client/ts_build/lib/common'; +import {Constants} from 'bitcore-wallet-client/ts_build/src/lib/common'; import {checkPrivateKeyEncrypted} from '../../../store/wallet/utils/wallet'; import {useAppDispatch} from '../../../utils/hooks'; diff --git a/src/navigation/wallet/screens/CreateEncryptionPassword.tsx b/src/navigation/wallet/screens/CreateEncryptionPassword.tsx index a16f18993a..15179f3dcf 100644 --- a/src/navigation/wallet/screens/CreateEncryptionPassword.tsx +++ b/src/navigation/wallet/screens/CreateEncryptionPassword.tsx @@ -20,7 +20,7 @@ import { } from '../../../store/app/app.actions'; import {TextInput} from 'react-native'; import {useTranslation} from 'react-i18next'; -import {Constants} from 'bitcore-wallet-client/ts_build/lib/common'; +import {Constants} from 'bitcore-wallet-client/ts_build/src/lib/common'; import {checkPrivateKeyEncrypted} from '../../../store/wallet/utils/wallet'; const EncryptPasswordContainer = styled.SafeAreaView` diff --git a/src/navigation/wallet/screens/WalletSettings.tsx b/src/navigation/wallet/screens/WalletSettings.tsx index c89230a44b..33e7089650 100644 --- a/src/navigation/wallet/screens/WalletSettings.tsx +++ b/src/navigation/wallet/screens/WalletSettings.tsx @@ -49,7 +49,7 @@ import { import {useTranslation} from 'react-i18next'; import {IsVMChain} from '../../../store/wallet/utils/currency'; import {TouchableOpacity} from '@components/base/TouchableOpacity'; -import {Constants} from 'bitcore-wallet-client/ts_build/lib/common'; +import {Constants} from 'bitcore-wallet-client/ts_build/src/lib/common'; const WalletSettingsContainer = styled.SafeAreaView` flex: 1; diff --git a/src/store/transforms/transforms.ts b/src/store/transforms/transforms.ts index eb659b31e5..c1aae5e2bd 100644 --- a/src/store/transforms/transforms.ts +++ b/src/store/transforms/transforms.ts @@ -81,6 +81,22 @@ export const bootstrapKey = (key: Key, id: string) => { return key; } else if (key.hardwareSource) { return key; + } else if (key.properties?.metadata) { + try { + const TssKey = BWCProvider.getTssKey(); + const tssKey = new TssKey(key.properties); + + const _key = merge(key, { + methods: tssKey, + }); + + const successLog = `bindTssKey - ${id}`; + initLogs.add(LogActions.info(successLog)); + return _key; + } catch (err: unknown) { + const errorLog = `Failed to bindTssKey - ${id} - ${getErrorString(err)}`; + initLogs.add(LogActions.persistLog(LogActions.error(errorLog))); + } } else { try { const _key = merge(key, { diff --git a/src/store/wallet/effects/create/create.ts b/src/store/wallet/effects/create/create.ts index 174e851709..c8fd0aa1ee 100644 --- a/src/store/wallet/effects/create/create.ts +++ b/src/store/wallet/effects/create/create.ts @@ -4,7 +4,7 @@ import { SupportedChains, } from '../../../../constants/currencies'; import {Effect} from '../../../index'; -import {Credentials} from 'bitcore-wallet-client/ts_build/lib/credentials'; +import {Credentials} from 'bitcore-wallet-client/ts_build/src/lib/credentials'; import {BwcProvider} from '../../../../lib/bwc'; import merge from 'lodash.merge'; import { @@ -18,7 +18,7 @@ import { successCreateKey, successUpdateKey, } from '../../wallet.actions'; -import API from 'bitcore-wallet-client/ts_build'; +import API from 'bitcore-wallet-client/ts_build/src'; import {Key, KeyMethods, KeyOptions, Token, Wallet} from '../../wallet.models'; import {Network} from '../../../../constants'; import {BitpaySupportedTokenOptsByAddress} from '../../../../constants/tokens'; diff --git a/src/store/wallet/effects/join-multisig/join-multisig.ts b/src/store/wallet/effects/join-multisig/join-multisig.ts index b4c47a9ccd..dc9dea8dae 100644 --- a/src/store/wallet/effects/join-multisig/join-multisig.ts +++ b/src/store/wallet/effects/join-multisig/join-multisig.ts @@ -7,7 +7,7 @@ import { mapAbbreviationAndName, } from '../../utils/wallet'; import {successCreateKey, successAddWallet} from '../../wallet.actions'; -import API from 'bitcore-wallet-client/ts_build'; +import API from 'bitcore-wallet-client/ts_build/src'; import {Key, KeyMethods, KeyOptions, Wallet} from '../../wallet.models'; import { subscribePushNotifications, diff --git a/src/store/wallet/utils/wallet.ts b/src/store/wallet/utils/wallet.ts index 18521cfbb6..f816a16099 100644 --- a/src/store/wallet/utils/wallet.ts +++ b/src/store/wallet/utils/wallet.ts @@ -9,7 +9,7 @@ import { WalletObj, } from '../wallet.models'; import {Rates} from '../../rate/rate.models'; -import {Credentials} from 'bitcore-wallet-client/ts_build/lib/credentials'; +import {Credentials} from 'bitcore-wallet-client/ts_build/src/lib/credentials'; import { BitpaySupportedCoins, BitpaySupportedMaticTokens, @@ -135,6 +135,9 @@ export const buildWalletObj = ( hardwareData = {}, singleAddress, receiveAddress, + isTssWallet = false, + tssKeyId, + tssPartyId, }: Credentials & { balance?: WalletBalance; tokens?: any; @@ -158,6 +161,9 @@ export const buildWalletObj = ( }; singleAddress: boolean; receiveAddress?: string; + isTssWallet?: boolean; + tssKeyId?: string; + tssPartyId?: number; }, tokenOptsByAddress?: {[key in string]: Token}, ): WalletObj => { @@ -212,6 +218,36 @@ export const buildWalletObj = ( hardwareData, singleAddress, receiveAddress, + isTssWallet, + tssKeyId, + tssPartyId, + }; +}; + +export const buildTssKeyObj = ({ + tssKey, + wallets, + keyName, +}: { + tssKey: any; + wallets: Wallet[]; + keyName?: string; +}): Key => { + const cleanProperties = tssKey.toObj(); + delete cleanProperties.privateKeyShare; + return { + id: tssKey.id, + wallets, + properties: cleanProperties, + methods: tssKey, + totalBalance: 0, + totalBalanceLastDay: 0, + isPrivKeyEncrypted: tssKey.isPrivKeyEncrypted(), + backupComplete: true, + keyName: + keyName || `TSS Key (${tssKey.metadata.m}-of-${tssKey.metadata.n})`, + hideKeyBalance: false, + isReadOnly: false, }; }; diff --git a/src/store/wallet/wallet.models.ts b/src/store/wallet/wallet.models.ts index f6274337bb..7c38d95440 100644 --- a/src/store/wallet/wallet.models.ts +++ b/src/store/wallet/wallet.models.ts @@ -1,6 +1,6 @@ -import API from 'bitcore-wallet-client/ts_build'; +import API from 'bitcore-wallet-client/ts_build/src'; import {ReactElement} from 'react'; -import {Credentials} from 'bitcore-wallet-client/ts_build/lib/credentials'; +import {Credentials} from 'bitcore-wallet-client/ts_build/src/lib/credentials'; import {RootState} from '../index'; import {Invoice} from '../shop/shop.models'; import {Network} from '../../constants'; @@ -48,6 +48,12 @@ export interface KeyProperties { xPrivKeyEncrypted?: string; xPrivKeyEDDSAEncrypted?: string; mnemonicEncrypted?: string; + metadata?: { + chain: string; + network: string; + m: string; + n: string; + }; } export interface Key { @@ -149,6 +155,13 @@ export interface WalletObj { */ accountPath?: string; }; + isTssWallet?: boolean; + tssKeyId?: string; + tssPartyId?: number; + tssThreshold?: { + m: number; + n: number; + }; } export interface KeyOptions { diff --git a/src/utils/wallet-hardware.ts b/src/utils/wallet-hardware.ts index 6b067675dc..1a8071a3c6 100644 --- a/src/utils/wallet-hardware.ts +++ b/src/utils/wallet-hardware.ts @@ -1,5 +1,5 @@ -import {Credentials} from 'bitcore-wallet-client/ts_build/lib/credentials'; -import {Constants} from 'bitcore-wallet-client/ts_build/lib/common'; +import {Credentials} from 'bitcore-wallet-client/ts_build/src/lib/credentials'; +import {Constants} from 'bitcore-wallet-client/ts_build/src/lib/common'; import {BwcProvider} from '../lib/bwc'; import {Network} from '../constants'; diff --git a/yarn.lock b/yarn.lock index 726ce79832..e781efbb2e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1251,11 +1251,69 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@bitgo/sdk-lib-mpc@^10.1.2": + version "10.8.1" + resolved "https://registry.yarnpkg.com/@bitgo/sdk-lib-mpc/-/sdk-lib-mpc-10.8.1.tgz#494bd3d1476d31f1e200033163811464d4a0af7c" + integrity sha512-FUHqupDQWiozu298T5Ocv9SVKvNr+0PSov/lfQubUYKeDsj2LBIUzpmKJAJqbXmo/Ok/X6QqWz0DUrHNuPZCxg== + dependencies: + "@noble/curves" "1.8.1" + "@silencelaboratories/dkls-wasm-ll-node" "1.2.0-pre.4" + "@silencelaboratories/dkls-wasm-ll-web" "1.2.0-pre.4" + "@types/superagent" "4.1.15" + "@wasmer/wasi" "^1.2.2" + bigint-crypto-utils "3.1.4" + bigint-mod-arith "3.1.2" + cbor-x "1.5.9" + fp-ts "2.16.2" + io-ts "npm:@bitgo-forks/io-ts@2.1.4" + libsodium-wrappers-sumo "^0.7.9" + openpgp "5.11.3" + paillier-bigint "3.3.0" + secp256k1 "5.0.1" + "@braze/react-native-sdk@16.1.0": version "16.1.0" resolved "https://registry.yarnpkg.com/@braze/react-native-sdk/-/react-native-sdk-16.1.0.tgz#d66c838a4eb38d2c5390d3dc4168767d7808a55a" integrity sha512-IUoL+36KELKk4w6tlNdYjJ2fi0QBs2HT+yhYsupwfntvbbKPdSqlKrXU0e/xAyDv34qACcIxl2AYxeeD7Vhptg== +"@cbor-extract/cbor-extract-darwin-arm64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz#8d65cb861a99622e1b4a268e2d522d2ec6137338" + integrity sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w== + +"@cbor-extract/cbor-extract-darwin-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz#9fbec199c888c5ec485a1839f4fad0485ab6c40a" + integrity sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w== + +"@cbor-extract/cbor-extract-linux-arm64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz#bf77e0db4a1d2200a5aa072e02210d5043e953ae" + integrity sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ== + +"@cbor-extract/cbor-extract-linux-arm@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz#491335037eb8533ed8e21b139c59f6df04e39709" + integrity sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q== + +"@cbor-extract/cbor-extract-linux-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz#672574485ccd24759bf8fb8eab9dbca517d35b97" + integrity sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw== + +"@cbor-extract/cbor-extract-win32-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz#4b3f07af047f984c082de34b116e765cb9af975f" + integrity sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w== + +"@craftzdog/react-native-buffer@^6.0.5": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@craftzdog/react-native-buffer/-/react-native-buffer-6.1.1.tgz#46afcb6b539a9a3e4dc836ebceb1a9b46f374056" + integrity sha512-YXJ0Jr4V+Hk2CZXpQw0A0NJeuiW2Rv6rAAutJCZ2k/JG13vLsppUibkJ8exSMxODtH9yJUrLiR96rilG3pFZ4Q== + dependencies: + ieee754 "^1.2.1" + react-native-quick-base64 "^2.2.2" + "@egjs/hammerjs@^2.0.17": version "2.0.17" resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124" @@ -3114,6 +3172,13 @@ dependencies: "@noble/hashes" "1.7.0" +"@noble/curves@1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.8.1.tgz#19bc3970e205c99e4bdb1c64a4785706bce497ff" + integrity sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ== + dependencies: + "@noble/hashes" "1.7.1" + "@noble/curves@1.9.1": version "1.9.1" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.1.tgz#9654a0bc6c13420ae252ddcf975eaf0f58f0a35c" @@ -3155,6 +3220,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.0.tgz#5d9e33af2c7d04fee35de1519b80c958b2e35e39" integrity sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w== +"@noble/hashes@1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.1.tgz#5738f6d765710921e7a751e00c20ae091ed8db0f" + integrity sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ== + "@noble/hashes@1.8.0", "@noble/hashes@^1.0.0", "@noble/hashes@^1.2.0", "@noble/hashes@^1.4.0", "@noble/hashes@^1.5.0", "@noble/hashes@~1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" @@ -3817,6 +3887,16 @@ resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== +"@silencelaboratories/dkls-wasm-ll-node@1.2.0-pre.4": + version "1.2.0-pre.4" + resolved "https://registry.yarnpkg.com/@silencelaboratories/dkls-wasm-ll-node/-/dkls-wasm-ll-node-1.2.0-pre.4.tgz#0f8f9a802d564bb2b36561ac770493d63af146e1" + integrity sha512-KWHR/6SCa67mrYVPbhNjzoYEKadhQ5cL3UPI4UgtVZEk/Fc5yB0AaYUX3DuWHskxQTvj0mF2shYcZe9OubkvnQ== + +"@silencelaboratories/dkls-wasm-ll-web@1.2.0-pre.4": + version "1.2.0-pre.4" + resolved "https://registry.yarnpkg.com/@silencelaboratories/dkls-wasm-ll-web/-/dkls-wasm-ll-web-1.2.0-pre.4.tgz#a6ca7f951c0c2cc682c7390ae7ba12c1b055fcad" + integrity sha512-RDyGVX6nyABPchnucl4IOV78LWzXBV9QucRiitRNONo3pfO4z375T00lI/wPiId13wXb8YNkB1Ej90hBNUK25A== + "@sinclair/typebox@^0.24.1": version "0.24.51" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" @@ -4937,6 +5017,11 @@ dependencies: "@types/node" "*" +"@types/cookiejar@*": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" + integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== + "@types/crypto-js@^4.2.2": version "4.2.2" resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.2.2.tgz#771c4a768d94eb5922cc202a3009558204df0cea" @@ -5310,6 +5395,14 @@ "@types/react" "*" csstype "^3.0.2" +"@types/superagent@4.1.15": + version "4.1.15" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.15.tgz#63297de457eba5e2bc502a7609426c4cceab434a" + integrity sha512-mu/N4uvfDN2zVQQ5AYJI/g4qxn2bHB6521t1UuH09ShNWjebTqN0ZFuYK9uYjcgmI0dTQEs+Owi1EO6U0OkOZQ== + dependencies: + "@types/cookiejar" "*" + "@types/node" "*" + "@types/symlink-or-copy@^1.2.0": version "1.2.2" resolved "https://registry.yarnpkg.com/@types/symlink-or-copy/-/symlink-or-copy-1.2.2.tgz#51b1c00b516a5774ada5d611e65eb123f988ef8d" @@ -5680,6 +5773,11 @@ "@walletconnect/window-getters" "^1.0.1" tslib "1.14.1" +"@wasmer/wasi@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@wasmer/wasi/-/wasi-1.2.2.tgz#46689b568100ad3923fb1bee0205ccb01f9c5b80" + integrity sha512-39ZB3gefOVhBmkhf7Ta79RRSV/emIV8LhdvcWhP/MOZEjMmtzoZWMzt7phdKj8CUXOze+AwbvGK60lKaKldn1w== + "@webgpu/types@0.1.21": version "0.1.21" resolved "https://registry.yarnpkg.com/@webgpu/types/-/types-0.1.21.tgz#b181202daec30d66ccd67264de23814cfd176d3a" @@ -5811,16 +5909,6 @@ ajv-keywords@^5.1.0: dependencies: fast-deep-equal "^3.1.3" -ajv@6.12.0: - version "6.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7" - integrity sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - ajv@^6.12.3, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -6055,6 +6143,16 @@ asn1.js@^4.10.1: inherits "^2.0.1" minimalistic-assert "^1.0.0" +asn1.js@^5.0.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" + integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + safer-buffer "^2.1.0" + asn1@~0.2.3: version "0.2.6" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" @@ -6119,13 +6217,6 @@ async@0.9.2: resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" integrity sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw== -async@^2.5.0: - version "2.6.4" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" - integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== - dependencies: - lodash "^4.17.14" - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -6383,6 +6474,14 @@ babel-plugin-transform-flow-enums@^0.0.2: dependencies: "@babel/plugin-syntax-flow" "^7.12.1" +babel-plugin-transform-import-meta@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-import-meta/-/babel-plugin-transform-import-meta-2.3.3.tgz#863de841f7df37e2bf39a057572a24e4f65f3c51" + integrity sha512-bbh30qz1m6ZU1ybJoNOhA2zaDvmeXMnGNBMVMDOJ1Fni4+wMBoy/j7MTRVmqAUCIcy54/rEnr9VEBsfcgbpm3Q== + dependencies: + "@babel/template" "^7.25.9" + tslib "^2.8.1" + babel-plugin-transform-remove-console@6.9.4: version "6.9.4" resolved "https://registry.yarnpkg.com/babel-plugin-transform-remove-console/-/babel-plugin-transform-remove-console-6.9.4.tgz#b980360c067384e24b357a588d807d3c83527780" @@ -6563,16 +6662,33 @@ big-integer@1.6.x, big-integer@^1.6.48: resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - bigi@^1.1.0, bigi@^1.2.0: version "1.4.2" resolved "https://registry.yarnpkg.com/bigi/-/bigi-1.4.2.tgz#9c665a95f88b8b08fc05cfd731f561859d725825" integrity sha512-ddkU+dFIuEIW8lE7ZwdIAf2UPoM90eaprg5m3YXAVVTmKlqV/9BX4A2M8BOK2yOq6/VgZFVhK6QAxJebhlbhzw== +bigint-crypto-utils@3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/bigint-crypto-utils/-/bigint-crypto-utils-3.1.4.tgz#b00aa00eb792b14f2f71ead916105c17aac98a4c" + integrity sha512-niSkvARUEe8MiAiH+zKXPkgXzlvGDbOqXL3JDevWaA1TrPhUGSCgV+iedm8qMEBQwvSlMMn8GpSuoUjvsm2QfQ== + dependencies: + bigint-mod-arith "^3.1.0" + +bigint-crypto-utils@^3.0.17: + version "3.3.0" + resolved "https://registry.yarnpkg.com/bigint-crypto-utils/-/bigint-crypto-utils-3.3.0.tgz#72ad00ae91062cf07f2b1def9594006c279c1d77" + integrity sha512-jOTSb+drvEDxEq6OuUybOAv/xxoh3cuYRUIPyu8sSHQNKM303UQ2R1DAo45o1AkcIXw6fzbaFI1+xGGdaXs2lg== + +bigint-mod-arith@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bigint-mod-arith/-/bigint-mod-arith-3.1.2.tgz#658e416bc593a463d97b59766226d0a3021a76b1" + integrity sha512-nx8J8bBeiRR+NlsROFH9jHswW5HO8mgfOSqW0AmjicMMvaONDa8AO+5ViKDUUNytBPWiwfvZP4/Bj4Y3lUfvgQ== + +bigint-mod-arith@^3.1.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/bigint-mod-arith/-/bigint-mod-arith-3.3.1.tgz#8ed33dc9f7886e552a7d47c239e051836e74cfa8" + integrity sha512-pX/cYW3dCa87Jrzv6DAr8ivbbJRzEX5yGhdt8IutnX/PCIXfpx+mabWNK/M8qqh+zQ0J3thftUBHW0ByuUlG0w== + bignumber.js@^9.0.0, bignumber.js@^9.0.2, bignumber.js@^9.1.1, bignumber.js@^9.1.2: version "9.3.0" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.0.tgz#bdba7e2a4c1a2eba08290e8dcad4f36393c92acd" @@ -6669,10 +6785,10 @@ bitcoinjs-lib@^5.2.0: varuint-bitcoin "^1.0.4" wif "^2.0.1" -bitcore-lib-cash@^10.10.5: - version "10.10.5" - resolved "https://registry.yarnpkg.com/bitcore-lib-cash/-/bitcore-lib-cash-10.10.5.tgz#f7bac28354e33d641a5ea729704b01a4c0101bda" - integrity sha512-GOzIdDgRmKAO+77vz3Ou5oLnw1UtC/ygLrKIAkCVtGwNvjV/1FmHY5/SO3Sb9GKzFvy8D1NRKXypb90fIczOXg== +bitcore-lib-cash@^11.3.7: + version "11.3.7" + resolved "https://registry.yarnpkg.com/bitcore-lib-cash/-/bitcore-lib-cash-11.3.7.tgz#f78ba226b4b588e2136846dbeff3d4a1f2faa1ac" + integrity sha512-29tduOOCku6xwwVHRkY/wnN5zdQx06xiOYDJuTPkiZMSU7AmAgqcfFuZOQDhplaplek5RD2XIDWC3xibY4l68w== dependencies: bn.js "=4.11.8" bs58 "^4.0.1" @@ -6681,10 +6797,10 @@ bitcore-lib-cash@^10.10.5: inherits "=2.0.1" lodash "^4.17.20" -bitcore-lib-doge@^10.10.5: - version "10.10.5" - resolved "https://registry.yarnpkg.com/bitcore-lib-doge/-/bitcore-lib-doge-10.10.5.tgz#2a3213638aa3c21b7343879f4531411c3a3475eb" - integrity sha512-3CQe3nWJaBdUCY2Y4vtaADahIFK/te5A85osc7np8CaAizKBxMLgHCtsqfca5zp42OTKHfqi+ssUSXh7QYTD4Q== +bitcore-lib-doge@^11.3.7: + version "11.3.7" + resolved "https://registry.yarnpkg.com/bitcore-lib-doge/-/bitcore-lib-doge-11.3.7.tgz#ac3dfc885380b68ed92273bd328ac51a70635d5c" + integrity sha512-PtIIHBjtD9zuI1Ch/tXBO1398WPh8uThCITeWQS3oJkTC5JJTp9KZFuNaGmbClhCRf9O1S0HXqdbKlNsauvK4g== dependencies: bn.js "=4.11.8" bs58 "^4.0.1" @@ -6694,10 +6810,10 @@ bitcore-lib-doge@^10.10.5: lodash "^4.17.20" scryptsy "2.1.0" -bitcore-lib-ltc@^10.10.5: - version "10.10.5" - resolved "https://registry.yarnpkg.com/bitcore-lib-ltc/-/bitcore-lib-ltc-10.10.5.tgz#061fbe568c6ce94afe0e3e87e731e874f6b2d4b9" - integrity sha512-TQ3I0G3YORr1Sae0no/2TFPYXChgcQbHoFhXP5VWK26SOYIFt2gHDIF1ef5KRunXaqBsJlVDbWYqEQu7fr/j3w== +bitcore-lib-ltc@^11.3.7: + version "11.3.7" + resolved "https://registry.yarnpkg.com/bitcore-lib-ltc/-/bitcore-lib-ltc-11.3.7.tgz#81ab21314cc1aa1e614167e3ce6fa6a7fe8d9ca9" + integrity sha512-PZt9Hae7mlT8AMxZZcOo11eVYFbmFPi3U7kCovHwSMUaeBa8TSeL/145d0B9YSmVkkyA4BrTIqSUQwwbh8KoAg== dependencies: bech32 "=2.0.0" bn.js "=4.11.8" @@ -6708,10 +6824,10 @@ bitcore-lib-ltc@^10.10.5: lodash "^4.17.20" scryptsy "2.1.0" -bitcore-lib@^10.10.7: - version "10.10.7" - resolved "https://registry.yarnpkg.com/bitcore-lib/-/bitcore-lib-10.10.7.tgz#60a3620320ce3b15c81da3eb3a8893bc4513ec41" - integrity sha512-HOqooxVUpFdTUcIENjxVkSp3TUgdVMEAQVAonpWH0pF/CmDhTSz57gm0FNQ5OOtazjkQKR9Ui4Ab4iyrAQb0Iw== +bitcore-lib@^11.3.7: + version "11.3.7" + resolved "https://registry.yarnpkg.com/bitcore-lib/-/bitcore-lib-11.3.7.tgz#b4c0ae852e8733f3d6c942b9ddef017c45335b79" + integrity sha512-Smibdh/fOqVU88XJN/MEYxsqCi55bs8aDBf1V7hslZl+0L3oUbXAt3McLBDrKY02RmYwjYTZ81TDfmHxcMwvgg== dependencies: bech32 "=2.0.0" bn.js "=4.11.8" @@ -6721,31 +6837,38 @@ bitcore-lib@^10.10.7: inherits "=2.0.1" lodash "^4.17.20" -bitcore-mnemonic@^10.10.7: - version "10.10.7" - resolved "https://registry.yarnpkg.com/bitcore-mnemonic/-/bitcore-mnemonic-10.10.7.tgz#8f931211fb5cd9d5fafa6535dc56feeb0e0152bc" - integrity sha512-PxkAHqcluyK2j0Tjl8pdzo0MR+hG0QMmaEvkIHhnbg2Zcyqw/Vz/87CME3CwzqCBowNfRHQpAjKv0P+oDzFy9A== +bitcore-mnemonic@^11.3.6: + version "11.3.7" + resolved "https://registry.yarnpkg.com/bitcore-mnemonic/-/bitcore-mnemonic-11.3.7.tgz#1ac233c50bb45e20a8ba926f259af551825800dd" + integrity sha512-fyjjrkfDkUnjZ4l3he6y7Cyit9NKbVGbWGRlpCefyGGZUxXf3wkbEeQwa2H7iQhJqVyyEmpecL6XmZvudRSkcA== dependencies: - bitcore-lib "^10.10.7" + bitcore-lib "^11.3.7" unorm "^1.4.1" -bitcore-wallet-client@10.10.15: - version "10.10.15" - resolved "https://registry.yarnpkg.com/bitcore-wallet-client/-/bitcore-wallet-client-10.10.15.tgz#d1f0e91ce2d04f383328542f547288df02c839b8" - integrity sha512-U1NFDqfzslk7lzX287f1GRIpGEd772I8JgFsGqGeWLc17wXy8SI7G0B6AtQxZdZU//L/0TXIUHWpGu5TOZB7aw== +bitcore-tss@^11.3.6: + version "11.3.7" + resolved "https://registry.yarnpkg.com/bitcore-tss/-/bitcore-tss-11.3.7.tgz#6dfe7ccf2ccd58634865c68a11ba73f995b6693e" + integrity sha512-NnPy+pSEz10VNcgVfmjbhGUX/tzHP1OY9By7JhTC+yHETVL+W1U7x6VT7BRFPtLHmVteMQh6iUgAtQZwF5TfWw== + dependencies: + "@bitgo/sdk-lib-mpc" "^10.1.2" + bitcore-lib "^11.3.7" + +bitcore-wallet-client@11.3.6: + version "11.3.6" + resolved "https://registry.yarnpkg.com/bitcore-wallet-client/-/bitcore-wallet-client-11.3.6.tgz#cae255f1571044a1d50f56ba23329fe25cc750fc" + integrity sha512-9nvNVU1VN7LHiiidSoA3B7YdZ6LNqzIjW+JmqIU4AQPDESWNWzDPLk0aWcF4Ioj5qpXtvIx0zlAXejgNE2q/JQ== dependencies: - ajv "6.12.0" async "0.9.2" bip38 "1.4.0" - bitcore-mnemonic "^10.10.7" - crypto-wallet-core "^10.10.15" + bitcore-mnemonic "^11.3.6" + bitcore-tss "^11.3.6" + crypto-wallet-core "^11.3.6" json-stable-stringify "1.0.1" preconditions "2.2.3" - sjcl "1.0.3" - source-map-loader "0.2.4" - source-map-support "0.5.19" + sjcl "1.0.8" superagent "5.2.2" typescript "5.7.3" + uuid "^11.1.0" bl@^4.0.3, bl@^4.1.0: version "4.1.0" @@ -7224,6 +7347,27 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== +cbor-extract@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cbor-extract/-/cbor-extract-2.2.0.tgz#cee78e630cbeae3918d1e2e58e0cebaf3a3be840" + integrity sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA== + dependencies: + node-gyp-build-optional-packages "5.1.1" + optionalDependencies: + "@cbor-extract/cbor-extract-darwin-arm64" "2.2.0" + "@cbor-extract/cbor-extract-darwin-x64" "2.2.0" + "@cbor-extract/cbor-extract-linux-arm" "2.2.0" + "@cbor-extract/cbor-extract-linux-arm64" "2.2.0" + "@cbor-extract/cbor-extract-linux-x64" "2.2.0" + "@cbor-extract/cbor-extract-win32-x64" "2.2.0" + +cbor-x@1.5.9: + version "1.5.9" + resolved "https://registry.yarnpkg.com/cbor-x/-/cbor-x-1.5.9.tgz#ed6b2afcd7884bdd697674bfb7332c1473a13ecf" + integrity sha512-OEI5rEu3MeR0WWNUXuIGkxmbXVhABP+VtgAXzm48c9ulkrsvxshjjk94XSOGphyAKeNGLPfAxxzEtgQ6rEVpYQ== + optionalDependencies: + cbor-extract "^2.2.0" + chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -7946,20 +8090,20 @@ crypto-js@3.1.9-1: resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-3.1.9-1.tgz#fda19e761fc077e01ffbfdc6e9fdfc59e8806cd8" integrity sha512-W93aKztssqf29OvUlqfikzGyYbD1rpkXvGP9IQ1JchLY3bxaLXZSWYbwrtib2vk8DobrDzX7PIXcDWHp0B6Ymw== -crypto-wallet-core@^10.10.15: - version "10.10.15" - resolved "https://registry.yarnpkg.com/crypto-wallet-core/-/crypto-wallet-core-10.10.15.tgz#85481d4195558a293ec25e6bd1c74bd0d59b0bf8" - integrity sha512-4TpOXmbFL6OSRh31w1+6qVlIZ9ttuL/HAQI6FaGjzQIDT0qY4PoWqu5X0L8dLDKz6CWY9tlr0ad1z+W/T4yl2g== +crypto-wallet-core@^11.3.6: + version "11.3.7" + resolved "https://registry.yarnpkg.com/crypto-wallet-core/-/crypto-wallet-core-11.3.7.tgz#b0182bc1f934895891c1aec17f06370799994f8c" + integrity sha512-gyjCSAU41TIFPBuzZ8XOi1Fxyl4BJAJePGKEshXpMuySSACSRFe7/y67UntzUFNYqoY1OH/9jWSJ3XQQDBsUiw== dependencies: "@solana-program/compute-budget" "^0.7.0" "@solana-program/memo" "^0.7.0" "@solana-program/system" "^0.7.0" "@solana-program/token" "^0.5.1" "@solana/kit" "^2.1.0" - bitcore-lib "^10.10.7" - bitcore-lib-cash "^10.10.5" - bitcore-lib-doge "^10.10.5" - bitcore-lib-ltc "^10.10.5" + bitcore-lib "^11.3.7" + bitcore-lib-cash "^11.3.7" + bitcore-lib-doge "^11.3.7" + bitcore-lib-ltc "^11.3.7" ed25519-hd-key "^1.3.0" ethers "6.13.5" info "0.0.6-beta.0" @@ -8315,6 +8459,11 @@ detect-libc@^2.0.0, detect-libc@^2.0.2: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8" integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA== +detect-libc@^2.0.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -8604,11 +8753,6 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - emotion-theming@^10.0.19: version "10.3.0" resolved "https://registry.yarnpkg.com/emotion-theming/-/emotion-theming-10.3.0.tgz#7f84d7099581d7ffe808aab5cd870e30843db72a" @@ -9875,6 +10019,11 @@ forwarded@0.2.0: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== +fp-ts@2.16.2: + version "2.16.2" + resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.16.2.tgz#7faa90f6fc2e8cf84c711d2c4e606afe2be9e342" + integrity sha512-CkqAjnIKFqvo3sCyoBTqgJvF+bHrSik584S9nhTjtBESLx26cbtVMR/T9a6ApChOcSDAaM3JydDmWDUn4EEXng== + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -10794,6 +10943,11 @@ invariant@2, invariant@2.2.4, invariant@^2.1.0, invariant@^2.2.2, invariant@^2.2 dependencies: loose-envify "^1.0.0" +"io-ts@npm:@bitgo-forks/io-ts@2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@bitgo-forks/io-ts/-/io-ts-2.1.4.tgz#a7431bb5473c5d5f9a94de8f8b058e189a298423" + integrity sha512-jCt3WPfDM+wM0SJMGJkY0TS6JmaQ78ATAYtsppJYJfts8geOS/N/UftwAROXwv6azKAMz8uo163t6dWWwfsYug== + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -12318,7 +12472,7 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== -json5@^1.0.1, json5@^1.0.2: +json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== @@ -12545,6 +12699,18 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +libsodium-sumo@^0.7.15: + version "0.7.15" + resolved "https://registry.yarnpkg.com/libsodium-sumo/-/libsodium-sumo-0.7.15.tgz#91c1d863fe3fbce6d6b9db1aadaa622733a1d007" + integrity sha512-5tPmqPmq8T8Nikpm1Nqj0hBHvsLFCXvdhBFV7SGOitQPZAA6jso8XoL0r4L7vmfKXr486fiQInvErHtEvizFMw== + +libsodium-wrappers-sumo@^0.7.9: + version "0.7.15" + resolved "https://registry.yarnpkg.com/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.15.tgz#0ef2a99b4b17e8385aa7e6850593660dbaf5fb40" + integrity sha512-aSWY8wKDZh5TC7rMvEdTHoyppVq/1dTSAeAR7H6pzd6QRT3vQWcT5pGwCotLcpPEOLXX6VvqihSPkpEhYAjANA== + dependencies: + libsodium-sumo "^0.7.15" + lighthouse-logger@^1.0.0: version "1.4.2" resolved "https://registry.yarnpkg.com/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz#aef90f9e97cd81db367c7634292ee22079280aaa" @@ -12565,15 +12731,6 @@ linkify-it@^2.0.0: dependencies: uc.micro "^1.0.1" -loader-utils@^1.1.0: - version "1.4.2" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" - integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^1.0.1" - locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -12656,7 +12813,7 @@ lodash.uniqby@4.7.0: resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" integrity sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww== -lodash@4.x, lodash@^4.0.1, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4: +lodash@4.x, lodash@^4.0.1, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -13448,6 +13605,11 @@ nanoclone@^0.2.1: resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== +nanoid@3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" + integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== + nanoid@^3.3.1, nanoid@^3.3.11: version "3.3.11" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" @@ -13559,6 +13721,13 @@ node-fetch@^2.7.0: dependencies: whatwg-url "^5.0.0" +node-gyp-build-optional-packages@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz#52b143b9dd77b7669073cbfe39e3f4118bfc603c" + integrity sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw== + dependencies: + detect-libc "^2.0.1" + node-gyp-build@^4.2.0, node-gyp-build@^4.3.0: version "4.8.4" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" @@ -13848,6 +14017,13 @@ opencollective-postinstall@^2.0.3: resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q== +openpgp@5.11.3: + version "5.11.3" + resolved "https://registry.yarnpkg.com/openpgp/-/openpgp-5.11.3.tgz#a2532aa973f1f6413556eaf328b97a6955b1d8a3" + integrity sha512-jXOPfIteBUQ2zSmRG4+Y6PNntIIDEAvoM/lOYCnvpXAByJEruzrHQZWE/0CGOKHbubwUuty2HoPHsqBzyKHOpA== + dependencies: + asn1.js "^5.0.0" + optionator@^0.9.3: version "0.9.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -13978,6 +14154,13 @@ package-json-from-dist@^1.0.0: resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== +paillier-bigint@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/paillier-bigint/-/paillier-bigint-3.3.0.tgz#25afb724fdce8359625d3eea93bf80cb6024065a" + integrity sha512-Aa8a75dODYOGxLYQhi1Y0Xsi0Vbl+5gzPvaVfxuCA/zT8CK/keXv5CA2Ddn5AV9VxmTkpIEdYs40hv1rkFcODg== + dependencies: + bigint-crypto-utils "^3.0.17" + papaparse@5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.3.2.tgz#d1abed498a0ee299f103130a6109720404fbd467" @@ -15150,6 +15333,22 @@ react-native-quick-actions@0.3.13: resolved "https://registry.yarnpkg.com/react-native-quick-actions/-/react-native-quick-actions-0.3.13.tgz#74431b0b30e98ac896e44cc1024c38864cfe2bbc" integrity sha512-Vz13a0+NV0mzCh/29tNt0qDzWPh8i2srTQW8eCSzGFDArnVm1COTOhTD0FY0hWHlxRY0ahvX+BlezTDvsyAuMA== +react-native-quick-base64@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/react-native-quick-base64/-/react-native-quick-base64-2.2.2.tgz#52005a0b455b04acc1c6ff3eb8fa220401656aae" + integrity sha512-WLHSifHLoamr2kF00Gov0W9ud6CfPshe1rmqWTquVIi9c62qxOaJCFVDrXFZhEBU8B8PvGLVuOlVKH78yhY0Fg== + +react-native-quick-crypto@0.7.17: + version "0.7.17" + resolved "https://registry.yarnpkg.com/react-native-quick-crypto/-/react-native-quick-crypto-0.7.17.tgz#702d8ce232c3d4ba45dd1255529758bd9be470a1" + integrity sha512-cJzp6oA/dM1lujt+Rwtn46Mgcs3w9F/0oQvNz1jcADc/AXktveAOUTzzKrDMxyg6YPziCYnoqMDzHBo6OLSU1g== + dependencies: + "@craftzdog/react-native-buffer" "^6.0.5" + events "^3.3.0" + readable-stream "^4.5.2" + string_decoder "^1.3.0" + util "^0.12.5" + react-native-randombytes@3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/react-native-randombytes/-/react-native-randombytes-3.6.1.tgz#cac578093b5ca38e3e085becffdc6cbcf6f0d654" @@ -15324,6 +15523,14 @@ react-native-vision-camera@4.7.2: resolved "https://registry.yarnpkg.com/react-native-vision-camera/-/react-native-vision-camera-4.7.2.tgz#e4bd8f3d893a3f5fa9d8224ce28b8ef00d171bc1" integrity sha512-C+5PvlSunN6I4aYplSask+v3jfhgduZumIVw6H6lG+Afpf8boIcG3uFSsSfVgj+hxI7fx6qM6bsciEhzgxEUYg== +react-native-webassembly@0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/react-native-webassembly/-/react-native-webassembly-0.3.3.tgz#46ca583e5c193055803e9a5bfb4e98d172c30482" + integrity sha512-wC8r/C6bHcWsNZmzES6vhMX1bao3JtG2FIVC9Un5kTn9EKQBN63qGVGMaD7ppe/vkzD8aPTfj9MKjsid4m7VRA== + dependencies: + buffer "^6.0.3" + nanoid "3.3.4" + react-native-webview@13.16.0: version "13.16.0" resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-13.16.0.tgz#c995148f944a7eaf12389f0e6d5c6f5e6a775686" @@ -15566,6 +15773,17 @@ readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.5, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@^4.5.2: + version "4.7.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.7.0.tgz#cedbd8a1146c13dfff8dab14068028d58c15ac91" + integrity sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + readable-stream@~1.0.26, readable-stream@~1.0.26-4: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" @@ -16213,6 +16431,15 @@ secp256k1@3.7.1: nan "^2.14.0" safe-buffer "^5.1.2" +secp256k1@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-5.0.1.tgz#dc2c86187d48ff2da756f0f7e96417ee03c414b1" + integrity sha512-lDFs9AAIaWP9UCdtWrotXWWF9t8PWgQDcxqgAnpM9rMqxb3Oaq2J0thzPVSxBwdJgyQtkU/sYtFtbM1RSt/iYA== + dependencies: + elliptic "^6.5.7" + node-addon-api "^5.0.0" + node-gyp-build "^4.2.0" + secp256k1@^4.0.1: version "4.0.4" resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.4.tgz#58f0bfe1830fe777d9ca1ffc7574962a8189f8ab" @@ -16530,12 +16757,7 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== -sjcl@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sjcl/-/sjcl-1.0.3.tgz#4ed486498ed6b742b5d4a21902268116f054a709" - integrity sha512-d47gakvT49vvhMG+9ujYvK+/gh7s1Hon9IBJ8ci9RutycnflVgDhF6DjnID2DfxVkTxUrsOlxbVnolLFrstunQ== - -sjcl@^1.0.3: +sjcl@1.0.8, sjcl@^1.0.3: version "1.0.8" resolved "https://registry.yarnpkg.com/sjcl/-/sjcl-1.0.8.tgz#f2ec8d7dc1f0f21b069b8914a41a8f236b0e252a" integrity sha512-LzIjEQ0S0DpIgnxMEayM1rq9aGwGRG4OnZhCdjx7glTaJtf4zRfpg87ImfjSJjoW9vKpagd82McDOwbRT5kQKQ== @@ -16621,14 +16843,6 @@ source-map-js@^1.0.1: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== -source-map-loader@0.2.4: - version "0.2.4" - resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-0.2.4.tgz#c18b0dc6e23bf66f6792437557c569a11e072271" - integrity sha512-OU6UJUty+i2JDpTItnizPrlpOIBLmQbWMuBg9q5bVtnHACqw1tn9nNwqJLbv0/00JjnJb/Ee5g5WS5vrRv7zIQ== - dependencies: - async "^2.5.0" - loader-utils "^1.1.0" - source-map-resolve@^0.5.0: version "0.5.3" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" @@ -16648,14 +16862,6 @@ source-map-support@0.5.13: buffer-from "^1.0.0" source-map "^0.6.0" -source-map-support@0.5.19: - version "0.5.19" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - source-map-support@^0.5.16, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" @@ -16935,7 +17141,7 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -string_decoder@^1.1.1: +string_decoder@^1.1.1, string_decoder@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -17256,6 +17462,11 @@ text-encoding-utf-8@^1.0.2: resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== +text-encoding@0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.7.0.tgz#f895e836e45990624086601798ea98e8f36ee643" + integrity sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA== + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -17903,6 +18114,11 @@ uuid@3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== +uuid@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" + integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== + uuid@^3.3.2, uuid@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" From 638bc4f7f936d42c853ecd4c8662f9cbcca105ab Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 4 Dec 2025 15:49:33 -0300 Subject: [PATCH 02/16] [FEAT] tss Create Flow - Join Flow - Send --- assets/img/add-black.svg | 3 + assets/img/add-grey.svg | 3 + assets/img/backup-keyshare.svg | 159 +++ assets/img/check-dark.svg | 3 + assets/img/clock-blue.svg | 3 + assets/img/clock-light-blue.svg | 3 + assets/img/qr-code-black.svg | 3 + assets/img/qr-code-grey.svg | 3 + assets/img/shared-success.svg | 118 +++ package.json | 2 +- patches/@bitgo+sdk-lib-mpc+10.8.1.patch | 3 +- patches/bitcore-tss+11.3.7.patch | 16 +- patches/bitcore-wallet-client+11.3.6.patch | 161 --- patches/bitcore-wallet-client+11.4.6.patch | 130 +++ src/Root.tsx | 6 +- src/components/list/CurrencySelectionRow.tsx | 327 +----- .../modal/transact-menu/TransactMenu.tsx | 4 +- src/components/styled/Containers.tsx | 1 + src/lib/bwc.ts | 4 + .../coinbase/screens/CoinbaseAccount.tsx | 5 +- .../tabs/contacts/screens/ContactsAdd.tsx | 329 +------ .../tabs/contacts/screens/ContactsDetails.tsx | 1 + src/navigation/wallet/WalletGroup.tsx | 54 +- .../wallet/components/FileOrText.tsx | 68 +- .../wallet/components/OptionsSheet.tsx | 61 +- .../wallet/components/ReceiveAddress.tsx | 2 +- .../wallet/components/RecoveryPhrase.tsx | 88 +- .../wallet/components/TSSProgressTracker.tsx | 425 ++++++++ .../wallet/screens/AccountDetails.tsx | 9 +- .../wallet/screens/AddingOptions.tsx | 94 +- .../wallet/screens/BackupSharedKey.tsx | 172 ++++ .../wallet/screens/CreateMultisig.tsx | 104 +- .../wallet/screens/CreationOptions.tsx | 27 +- .../wallet/screens/CurrencySelection.tsx | 872 ++++------------ src/navigation/wallet/screens/DeleteKey.tsx | 4 +- .../wallet/screens/GlobalSelect.tsx | 4 +- .../wallet/screens/InviteCosigners.tsx | 878 +++++++++++++++++ .../wallet/screens/JoinTSSWallet.tsx | 757 ++++++++++++++ src/navigation/wallet/screens/KeyOverview.tsx | 36 +- src/navigation/wallet/screens/KeySettings.tsx | 19 +- .../wallet/screens/MultisigOptions.tsx | 194 +++- src/navigation/wallet/screens/PaperWallet.tsx | 3 +- .../screens/TransactionProposalDetails.tsx | 178 +++- .../wallet/screens/send/confirm/Confirm.tsx | 136 ++- .../screens/wallet-settings/Addresses.tsx | 2 +- .../wallet-settings/ExportTSSWallet.tsx | 386 ++++++++ .../zenledger/screens/ZenLedgerImport.tsx | 15 +- src/store/transforms/transforms.ts | 32 +- .../create-multisig/create-multisig.ts | 929 +++++++++++++++++- src/store/wallet/effects/create/create.ts | 4 +- src/store/wallet/effects/import/import.ts | 71 ++ .../effects/join-multisig/join-multisig.ts | 4 +- src/store/wallet/effects/send/send.ts | 117 ++- src/store/wallet/effects/status/status.ts | 6 +- .../effects/transactions/transactions.ts | 2 +- src/store/wallet/effects/tss-send/tss-send.ts | 319 ++++++ src/store/wallet/utils/wallet.ts | 17 +- src/store/wallet/wallet.actions.ts | 12 + src/store/wallet/wallet.models.ts | 76 ++ src/store/wallet/wallet.reducer.ts | 16 +- src/store/wallet/wallet.types.ts | 20 +- src/utils/helper-methods.ts | 6 +- yarn.lock | 83 +- 63 files changed, 5857 insertions(+), 1732 deletions(-) create mode 100644 assets/img/add-black.svg create mode 100644 assets/img/add-grey.svg create mode 100644 assets/img/backup-keyshare.svg create mode 100644 assets/img/check-dark.svg create mode 100644 assets/img/clock-blue.svg create mode 100644 assets/img/clock-light-blue.svg create mode 100644 assets/img/qr-code-black.svg create mode 100644 assets/img/qr-code-grey.svg create mode 100644 assets/img/shared-success.svg delete mode 100644 patches/bitcore-wallet-client+11.3.6.patch create mode 100644 patches/bitcore-wallet-client+11.4.6.patch create mode 100644 src/navigation/wallet/components/TSSProgressTracker.tsx create mode 100644 src/navigation/wallet/screens/BackupSharedKey.tsx create mode 100644 src/navigation/wallet/screens/InviteCosigners.tsx create mode 100644 src/navigation/wallet/screens/JoinTSSWallet.tsx create mode 100644 src/navigation/wallet/screens/wallet-settings/ExportTSSWallet.tsx create mode 100644 src/store/wallet/effects/tss-send/tss-send.ts diff --git a/assets/img/add-black.svg b/assets/img/add-black.svg new file mode 100644 index 0000000000..8ca554860a --- /dev/null +++ b/assets/img/add-black.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/add-grey.svg b/assets/img/add-grey.svg new file mode 100644 index 0000000000..e28086d395 --- /dev/null +++ b/assets/img/add-grey.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/backup-keyshare.svg b/assets/img/backup-keyshare.svg new file mode 100644 index 0000000000..0b75890853 --- /dev/null +++ b/assets/img/backup-keyshare.svg @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/check-dark.svg b/assets/img/check-dark.svg new file mode 100644 index 0000000000..ec222a3c7d --- /dev/null +++ b/assets/img/check-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/clock-blue.svg b/assets/img/clock-blue.svg new file mode 100644 index 0000000000..5969243ded --- /dev/null +++ b/assets/img/clock-blue.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/clock-light-blue.svg b/assets/img/clock-light-blue.svg new file mode 100644 index 0000000000..7490239208 --- /dev/null +++ b/assets/img/clock-light-blue.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/qr-code-black.svg b/assets/img/qr-code-black.svg new file mode 100644 index 0000000000..b740f573c6 --- /dev/null +++ b/assets/img/qr-code-black.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/qr-code-grey.svg b/assets/img/qr-code-grey.svg new file mode 100644 index 0000000000..fc9c4c8de4 --- /dev/null +++ b/assets/img/qr-code-grey.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/shared-success.svg b/assets/img/shared-success.svg new file mode 100644 index 0000000000..45cc467520 --- /dev/null +++ b/assets/img/shared-success.svg @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package.json b/package.json index 5f7ed46971..3debcdbf36 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "@solana/sysvars": "3.0.2", "big-integer": "1.6.51", "bitauth": "0.4.1", - "bitcore-wallet-client": "11.3.6", + "bitcore-wallet-client": "11.4.6", "bs58": "6.0.0", "buffer": "4.9.2", "countries-list": "2.6.1", diff --git a/patches/@bitgo+sdk-lib-mpc+10.8.1.patch b/patches/@bitgo+sdk-lib-mpc+10.8.1.patch index c071f6b0e5..0b0fb54fc6 100644 --- a/patches/@bitgo+sdk-lib-mpc+10.8.1.patch +++ b/patches/@bitgo+sdk-lib-mpc+10.8.1.patch @@ -405,7 +405,7 @@ index d4075db..a5da83c 100644 } } diff --git a/node_modules/@bitgo/sdk-lib-mpc/dist/src/tss/ecdsa-dkls/dsg.js b/node_modules/@bitgo/sdk-lib-mpc/dist/src/tss/ecdsa-dkls/dsg.js -index 42b0316..32798a3 100644 +index 42b0316..6be39e6 100644 --- a/node_modules/@bitgo/sdk-lib-mpc/dist/src/tss/ecdsa-dkls/dsg.js +++ b/node_modules/@bitgo/sdk-lib-mpc/dist/src/tss/ecdsa-dkls/dsg.js @@ -45,27 +45,34 @@ class Dsg { @@ -776,6 +776,5 @@ index 42b0316..32798a3 100644 } } exports.Dsg = Dsg; -\ No newline at end of file -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZHNnLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL3Rzcy9lY2RzYS1ka2xzL2RzZy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFDQSxtQ0FBa0g7QUFDbEgsbUNBQWdDO0FBUWhDLE1BQWEsR0FBRztJQVdkLFlBQ0UsUUFBZ0IsRUFDaEIsUUFBZ0IsRUFDaEIsY0FBc0IsRUFDdEIsV0FBbUIsRUFDbkIsUUFBd0I7UUFSaEIsYUFBUSxHQUFhLGdCQUFRLENBQUMsYUFBYSxDQUFDO1FBVXBELElBQUksQ0FBQyxRQUFRLEdBQUcsUUFBUSxDQUFDO1FBQ3pCLElBQUksQ0FBQyxhQUFhLEdBQUcsUUFBUSxDQUFDO1FBQzlCLElBQUksQ0FBQyxjQUFjLEdBQUcsY0FBYyxDQUFDO1FBQ3JDLElBQUksQ0FBQyxXQUFXLEdBQUcsV0FBVyxDQUFDO1FBQy9CLElBQUksQ0FBQyxRQUFRLEdBQUcsUUFBUSxJQUFJLElBQUksQ0FBQztJQUNuQyxDQUFDO0lBRU8sZUFBZTtRQUNyQixJQUFJLENBQUMsSUFBSSxDQUFDLFVBQVUsRUFBRSxDQUFDO1lBQ3JCLElBQUksQ0FBQyxVQUFVLEdBQUcsSUFBSSxDQUFDLFdBQVcsRUFBRSxDQUFDLG9CQUFvQixDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUMsZUFBZSxDQUFDLENBQUM7UUFDNUYsQ0FBQztJQUNILENBQUM7SUFFTyxpQkFBaUI7UUFDdkIsSUFBSSxDQUFDLElBQUksQ0FBQyxVQUFVLEVBQUUsQ0FBQztZQUNyQixNQUFNLEtBQUssQ0FBQyx3QkFBd0IsQ0FBQyxDQUFDO1FBQ3hDLENBQUM7UUFDRCxNQUFNLEtBQUssR0FBRyxJQUFBLGVBQU0sRUFBQyxJQUFJLENBQUMsVUFBVSxDQUFDLE9BQU8sRUFBRSxDQUFDLENBQUMsS0FBSyxDQUFDO1FBQ3RELFFBQVEsS0FBSyxFQUFFLENBQUM7WUFDZCxLQUFLLFVBQVU7Z0JBQ2IsSUFBSSxDQUFDLFFBQVEsR0FBRyxnQkFBUSxDQUFDLE1BQU0sQ0FBQztnQkFDaEMsTUFBTTtZQUNSLEtBQUssVUFBVTtnQkFDYixJQUFJLENBQUMsUUFBUSxHQUFHLGdCQUFRLENBQUMsTUFBTSxDQUFDO2dCQUNoQyxNQUFNO1lBQ1IsS0FBSyxVQUFVO2dCQUNiLElBQUksQ0FBQyxRQUFRLEdBQUcsZ0JBQVEsQ0FBQyxNQUFNLENBQUM7Z0JBQ2hDLE1BQU07WUFDUixLQUFLLE9BQU87Z0JBQ1YsSUFBSSxDQUFDLFFBQVEsR0FBRyxnQkFBUSxDQUFDLFFBQVEsQ0FBQztnQkFDbEMsTUFBTTtZQUNSO2dCQUNFLElBQUksQ0FBQyxRQUFRLEdBQUcsZ0JBQVEsQ0FBQyxZQUFZLENBQUM7Z0JBQ3RDLE1BQU0sS0FBSyxDQUFDLGtCQUFrQixLQUFLLEVBQUUsQ0FBQyxDQUFDO1FBQzNDLENBQUM7SUFDSCxDQUFDO0lBRU8sS0FBSyxDQUFDLFlBQVk7UUFDeEIsSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLEVBQUUsQ0FBQztZQUNuQixJQUFJLENBQUMsUUFBUSxHQUFHLHdEQUFhLHdDQUF3QyxHQUFDLENBQUM7UUFDekUsQ0FBQztJQUNILENBQUM7SUFFTyxXQUFXO1FBQ2pCLElBQUksQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFLENBQUM7WUFDbkIsTUFBTSxLQUFLLENBQUMsc0JBQXNCLENBQUMsQ0FBQztRQUN0QyxDQUFDO1FBRUQsT0FBTyxJQUFJLENBQUMsUUFBUSxDQUFDO0lBQ3ZCLENBQUM7SUFFRDs7O09BR0c7SUFDSCxVQUFVO1FBQ1IsT0FBTyxNQUFNLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxlQUFlLENBQUMsQ0FBQyxRQUFRLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDOUQsQ0FBQztJQUVEOzs7T0FHRztJQUNILEtBQUssQ0FBQyxVQUFVLENBQUMsT0FBZTtRQUM5QixJQUFJLENBQUMsVUFBVSxHQUFHLFNBQVMsQ0FBQztRQUM1QixJQUFJLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxDQUFDO1lBQ25CLE1BQU0sSUFBSSxDQUFDLFlBQVksRUFBRSxDQUFDO1FBQzVCLENBQUM7UUFDRCxNQUFNLFlBQVksR0FBRyxJQUFJLFVBQVUsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLE9BQU8sRUFBRSxRQUFRLENBQUMsQ0FBQyxDQUFDO1FBQ3BFLE1BQU0sS0FBSyxHQUFHLElBQUEsZUFBTSxFQUFDLFlBQVksQ0FBQyxDQUFDLEtBQUssQ0FBQztRQUN6QyxRQUFRLElBQUksRUFBRSxDQUFDO1lBQ2IsS0FBSyxLQUFLLEtBQUssVUFBVTtnQkFDdkIsSUFBSSxDQUFDLFFBQVEsR0FBRyxnQkFBUSxDQUFDLE1BQU0sQ0FBQztnQkFDaEMsTUFBTTtZQUNSLEtBQUssS0FBSyxLQUFLLFVBQVU7Z0JBQ3ZCLElBQUksQ0FBQyxRQUFRLEdBQUcsZ0JBQVEsQ0FBQyxNQUFNLENBQUM7Z0JBQ2hDLE1BQU07WUFDUixLQUFLLEtBQUssS0FBSyxVQUFVO2dCQUN2QixJQUFJLENBQUMsUUFBUSxHQUFHLGdCQUFRLENBQUMsTUFBTSxDQUFDO2dCQUNoQyxNQUFNO1lBQ1IsS0FBSyxVQUFVLElBQUksS0FBSztnQkFDdEIsSUFBSSxDQUFDLFFBQVEsR0FBRyxnQkFBUSxDQUFDLE1BQU0sQ0FBQztnQkFDaEMsTUFBTTtZQUNSO2dCQUNFLE1BQU0sS0FBSyxDQUFDLGtCQUFrQixLQUFLLEVBQUUsQ0FBQyxDQUFDO1FBQzNDLENBQUM7UUFDRCxJQUFJLENBQUMsZUFBZSxHQUFHLFlBQVksQ0FBQztJQUN0QyxDQUFDO0lBRUQsS0FBSyxDQUFDLElBQUk7UUFDUixJQUFJLElBQUksQ0FBQyxRQUFRLEtBQUssZ0JBQVEsQ0FBQyxhQUFhLEVBQUUsQ0FBQztZQUM3QyxNQUFNLEtBQUssQ0FBQyxpQ0FBaUMsQ0FBQyxDQUFDO1FBQ2pELENBQUM7UUFDRCxJQUFJLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxDQUFDO1lBQ25CLE1BQU0sSUFBSSxDQUFDLFlBQVksRUFBRSxDQUFDO1FBQzVCLENBQUM7UUFDRCxJQUNFLE9BQU8sTUFBTSxLQUFLLFdBQVc7WUFDN0IsbUNBQW1DO1lBQ25DLENBQUMsTUFBTSxDQUFDLE9BQU87WUFDZixDQUFDLE1BQU0sQ0FBQyxPQUFPLEVBQUUsQ0FBQyxNQUFNLENBQUMsRUFDekIsQ0FBQztZQUNELHNHQUFzRztZQUN0RyxNQUFNLFFBQVEsR0FBRyx3REFBYSx1Q0FBdUMsR0FBQyxDQUFDO1lBQ3ZFLE1BQU0sUUFBUSxDQUFDLE9BQU8sRUFBRSxDQUFDO1FBQzNCLENBQUM7UUFDRCxNQUFNLEVBQUUsUUFBUSxFQUFFLG9CQUFvQixFQUFFLEdBQUcsSUFBSSxDQUFDLFdBQVcsRUFBRSxDQUFDO1FBQzlELE1BQU0sUUFBUSxHQUFHLFFBQVEsQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLGFBQWEsQ0FBQyxDQUFDO1FBQ3hELElBQUksUUFBUSxDQUFDLE9BQU8sS0FBSyxJQUFJLENBQUMsUUFBUSxFQUFFLENBQUM7WUFDdkMsTUFBTSxLQUFLLENBQUMsZ0JBQWdCLElBQUksQ0FBQyxRQUFRLHNDQUFzQyxRQUFRLENBQUMsT0FBTyxHQUFHLENBQUMsQ0FBQztRQUN0RyxDQUFDO1FBQ0QsSUFBSSxDQUFDLFVBQVUsR0FBRyxJQUFJLG9CQUFvQixDQUFDLFFBQVEsRUFBRSxJQUFJLENBQUMsY0FBYyxDQUFDLENBQUM7UUFDMUUsSUFBSSxDQUFDO1lBQ0gsTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLFVBQVUsQ0FBQyxrQkFBa0IsRUFBRSxDQUFDLE9BQU8sQ0FBQztZQUM3RCxJQUFJLENBQUMsaUJBQWlCLEVBQUUsQ0FBQztZQUN6QixJQUFJLENBQUMsZUFBZSxHQUFHLElBQUksQ0FBQyxVQUFVLENBQUMsT0FBTyxFQUFFLENBQUM7WUFDakQsSUFBSSxDQUFDLFVBQVUsR0FBRyxTQUFTLENBQUM7WUFDNUIsT0FBTztnQkFDTCxPQUFPLEVBQUUsT0FBTztnQkFDaEIsSUFBSSxFQUFFLElBQUksQ0FBQyxRQUFRO2FBQ3BCLENBQUM7UUFDSixDQUFDO1FBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztZQUNYLE1BQU0sS0FBSyxDQUFDLHFEQUFxRCxJQUFJLENBQUMsUUFBUSxLQUFLLENBQUMsRUFBRSxDQUFDLENBQUM7UUFDMUYsQ0FBQztJQUNILENBQUM7SUFFRCxJQUFJLFNBQVM7UUFDWCxJQUFJLENBQUMsSUFBSSxDQUFDLFVBQVUsRUFBRSxDQUFDO1lBQ3JCLE1BQU0sS0FBSyxDQUFDLHdEQUF3RCxDQUFDLENBQUM7UUFDeEUsQ0FBQztRQUNELE9BQU8sSUFBSSxDQUFDLFVBQVUsQ0FBQztJQUN6QixDQUFDO0lBRUQ7O09BRUc7SUFDSCxVQUFVO1FBQ1IsSUFBSSxJQUFJLENBQUMsVUFBVSxFQUFFLENBQUM7WUFDcEIsSUFBSSxLQUFLLENBQUMsZ0VBQWdFLENBQUMsQ0FBQztRQUM5RSxDQUFDO1FBQ0QsSUFBSSxJQUFJLENBQUMsVUFBVSxFQUFFLENBQUM7WUFDcEIsSUFBSSxDQUFDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQztRQUN6QixDQUFDO1FBQ0QsSUFBSSxDQUFDLFFBQVEsR0FBRyxnQkFBUSxDQUFDLGFBQWEsQ0FBQztJQUN6QyxDQUFDO0lBRUQ7Ozs7O09BS0c7SUFDSCxzQkFBc0IsQ0FBQyxtQkFBeUM7UUFDOUQsSUFBSSxpQkFBaUIsR0FBYyxFQUFFLENBQUM7UUFDdEMsSUFBSSw2QkFBNkIsR0FBeUIsRUFBRSxpQkFBaUIsRUFBRSxFQUFFLEVBQUUsV0FBVyxFQUFFLEVBQUUsRUFBRSxDQUFDO1FBQ3JHLElBQUksQ0FBQyxlQUFlLEVBQUUsQ0FBQztRQUN2QixJQUFJLENBQUMsSUFBSSxDQUFDLFVBQVUsRUFBRSxDQUFDO1lBQ3JCLE1BQU0sS0FBSyxDQUFDLHlCQUF5QixDQUFDLENBQUM7UUFDekMsQ0FBQztRQUNELE1BQU0sRUFBRSxPQUFPLEVBQUUsR0FBRyxJQUFJLENBQUMsV0FBVyxFQUFFLENBQUM7UUFDdkMsSUFBSSxDQUFDO1lBQ0gsSUFBSSxJQUFJLENBQUMsUUFBUSxLQUFLLGdCQUFRLENBQUMsTUFBTSxFQUFFLENBQUM7Z0JBQ3RDLElBQUksQ0FBQyxRQUFRLEdBQUcsZ0JBQVEsQ0FBQyxRQUFRLENBQUM7Z0JBQ2xDLE1BQU0sYUFBYSxHQUFHLElBQUksQ0FBQyxVQUFVLENBQUMsT0FBTyxDQUMzQyxtQkFBbUIsQ0FBQyxpQkFBaUIsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLElBQUksT0FBTyxDQUFDLENBQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDLElBQUksRUFBRSxTQUFTLENBQUMsQ0FBQyxDQUM1RixDQUFDO2dCQUNGLElBQUksQ0FBQyxVQUFVLEdBQUc7b0JBQ2hCLENBQUMsRUFBRSxhQUFhLENBQUMsQ0FBQyxDQUFDO29CQUNuQixDQUFDLEVBQUUsYUFBYSxDQUFDLENBQUMsQ0FBQztpQkFDcEIsQ0FBQztnQkFDRixPQUFPLEVBQUUsaUJBQWlCLEVBQUUsRUFBRSxFQUFFLFdBQVcsRUFBRSxFQUFFLEVBQUUsQ0FBQztZQUNwRCxDQUFDO2lCQUFNLENBQUM7Z0JBQ04saUJBQWlCLEdBQUcsSUFBSSxDQUFDLFVBQVUsQ0FBQyxjQUFjLENBQ2hELG1CQUFtQixDQUFDLGlCQUFpQjtxQkFDbEMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxJQUFJLE9BQU8sQ0FBQyxDQUFDLENBQUMsT0FBTyxFQUFFLENBQUMsQ0FBQyxJQUFJLEVBQUUsU0FBUyxDQUFDLENBQUM7cUJBQ3JELE1BQU0sQ0FBQyxtQkFBbUIsQ0FBQyxXQUFXLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxJQUFJLE9BQU8sQ0FBQyxDQUFDLENBQUMsT0FBTyxFQUFFLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FDNUYsQ0FBQztZQUNKLENBQUM7WUFDRCxJQUFJLElBQUksQ0FBQyxRQUFRLEtBQUssZ0JBQVEsQ0FBQyxNQUFNLEVBQUUsQ0FBQztnQkFDdEMsaUJBQWlCLEdBQUcsQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLFdBQVcsQ0FBQyxJQUFJLENBQUMsV0FBVyxDQUFDLENBQUMsQ0FBQztnQkFDcEUsSUFBSSxDQUFDLFFBQVEsR0FBRyxnQkFBUSxDQUFDLE1BQU0sQ0FBQztnQkFDaEMsT0FBTztvQkFDTCxpQkFBaUIsRUFBRTt3QkFDakI7NEJBQ0UsT0FBTyxFQUFFLGlCQUFpQixDQUFDLENBQUMsQ0FBQyxDQUFDLE9BQU87NEJBQ3JDLElBQUksRUFBRSxpQkFBaUIsQ0FBQyxDQUFDLENBQUMsQ0FBQyxPQUFPOzRCQUNsQyxVQUFVLEVBQUUsSUFBQSxlQUFNLEVBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDLEtBQUssQ0FBQyxRQUFRLENBQUMsQ0FBQzt5QkFDL0Q7cUJBQ0Y7b0JBQ0QsV0FBVyxFQUFFLEVBQUU7aUJBQ2hCLENBQUM7WUFDSixDQUFDO2lCQUFNLENBQUM7Z0JBQ04scUJBQXFCO2dCQUNyQixJQUFJLENBQUMsaUJBQWlCLEVBQUUsQ0FBQztZQUMzQixDQUFDO1lBQ0QsNkJBQTZCLEdBQUc7Z0JBQzlCLFdBQVcsRUFBRSxpQkFBaUI7cUJBQzNCLE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDLEtBQUssS0FBSyxTQUFTLENBQUM7cUJBQ3BDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxFQUFFO29CQUNULElBQUksQ0FBQyxDQUFDLEtBQUssS0FBSyxTQUFTLEVBQUUsQ0FBQzt3QkFDMUIsTUFBTSxLQUFLLENBQUMscUNBQXFDLENBQUMsQ0FBQztvQkFDckQsQ0FBQztvQkFDRCxNQUFNLFNBQVMsR0FBRzt3QkFDaEIsT0FBTyxFQUFFLENBQUMsQ0FBQyxPQUFPO3dCQUNsQixJQUFJLEVBQUUsQ0FBQyxDQUFDLE9BQU87d0JBQ2YsRUFBRSxFQUFFLENBQUMsQ0FBQyxLQUFLO3FCQUNaLENBQUM7b0JBQ0YsT0FBTyxTQUFTLENBQUM7Z0JBQ25CLENBQUMsQ0FBQztnQkFDSixpQkFBaUIsRUFBRSxpQkFBaUI7cUJBQ2pDLE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDLEtBQUssS0FBSyxTQUFTLENBQUM7cUJBQ3BDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxFQUFFO29CQUNULE1BQU0sZUFBZSxHQUFHO3dCQUN0QixPQUFPLEVBQUUsQ0FBQyxDQUFDLE9BQU87d0JBQ2xCLElBQUksRUFBRSxDQUFDLENBQUMsT0FBTztxQkFDaEIsQ0FBQztvQkFDRixPQUFPLGVBQWUsQ0FBQztnQkFDekIsQ0FBQyxDQUFDO2FBQ0wsQ0FBQztRQUNKLENBQUM7UUFBQyxPQUFPLENBQUMsRUFBRSxDQUFDO1lBQ1gsSUFBSSxDQUFDLENBQUMsT0FBTyxDQUFDLFVBQVUsQ0FBQyw0QkFBNEIsQ0FBQyxFQUFFLENBQUM7Z0JBQ3ZELE1BQU0sS0FBSyxDQUNULHNHQUFzRyxDQUN2RyxDQUFDO1lBQ0osQ0FBQztZQUNELE1BQU0sS0FBSyxDQUFDLDRDQUE0QyxJQUFJLENBQUMsUUFBUSxXQUFXLElBQUksQ0FBQyxRQUFRLEtBQUssQ0FBQyxFQUFFLENBQUMsQ0FBQztRQUN6RyxDQUFDO2dCQUFTLENBQUM7WUFDVCxpQkFBaUIsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDO1lBQzNDLDJDQUEyQztZQUMzQyxJQUFJLElBQUksQ0FBQyxRQUFRLEtBQUssZ0JBQVEsQ0FBQyxRQUFRLEVBQUUsQ0FBQztnQkFDeEMsSUFBSSxDQUFDLGVBQWUsR0FBRyxJQUFJLENBQUMsVUFBVSxDQUFDLE9BQU8sRUFBRSxDQUFDO2dCQUNqRCxJQUFJLENBQUMsVUFBVSxHQUFHLFNBQVMsQ0FBQztZQUM5QixDQUFDO1FBQ0gsQ0FBQztRQUNELE9BQU8sNkJBQTZCLENBQUM7SUFDdkMsQ0FBQztDQUNGO0FBOVBELGtCQThQQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IE1lc3NhZ2UsIFNpZ25TZXNzaW9uT1RWYXJpYW50IH0gZnJvbSAnQHNpbGVuY2VsYWJvcmF0b3JpZXMvZGtscy13YXNtLWxsLW5vZGUnO1xuaW1wb3J0IHsgRGVzZXJpYWxpemVkQnJvYWRjYXN0TWVzc2FnZSwgRGVzZXJpYWxpemVkRGtsc1NpZ25hdHVyZSwgRGVzZXJpYWxpemVkTWVzc2FnZXMsIERzZ1N0YXRlIH0gZnJvbSAnLi90eXBlcyc7XG5pbXBvcnQgeyBkZWNvZGUgfSBmcm9tICdjYm9yLXgnO1xuXG50eXBlIE5vZGVXYXNtZXIgPSB0eXBlb2YgaW1wb3J0KCdAc2lsZW5jZWxhYm9yYXRvcmllcy9ka2xzLXdhc20tbGwtbm9kZScpO1xudHlwZSBXZWJXYXNtZXIgPSB0eXBlb2YgaW1wb3J0KCdAc2lsZW5jZWxhYm9yYXRvcmllcy9ka2xzLXdhc20tbGwtd2ViJyk7XG50eXBlIEJ1bmRsZXJXYXNtZXIgPSB0eXBlb2YgaW1wb3J0KCdAc2lsZW5jZWxhYm9yYXRvcmllcy9ka2xzLXdhc20tbGwtYnVuZGxlcicpO1xuXG50eXBlIERrbHNXYXNtID0gTm9kZVdhc21lciB8IFdlYldhc21lciB8IEJ1bmRsZXJXYXNtZXI7XG5cbmV4cG9ydCBjbGFzcyBEc2cge1xuICBwcm90ZWN0ZWQgZHNnU2Vzc2lvbjogU2lnblNlc3Npb25PVFZhcmlhbnQgfCB1bmRlZmluZWQ7XG4gIHByb3RlY3RlZCBkc2dTZXNzaW9uQnl0ZXM6IFVpbnQ4QXJyYXk7XG4gIHByaXZhdGUgX3NpZ25hdHVyZTogRGVzZXJpYWxpemVkRGtsc1NpZ25hdHVyZSB8IHVuZGVmaW5lZDtcbiAgcHJvdGVjdGVkIGtleVNoYXJlQnl0ZXM6IEJ1ZmZlcjtcbiAgcHJvdGVjdGVkIG1lc3NhZ2VIYXNoOiBCdWZmZXI7XG4gIHByb3RlY3RlZCBkZXJpdmF0aW9uUGF0aDogc3RyaW5nO1xuICBwcm90ZWN0ZWQgcGFydHlJZHg6IG51bWJlcjtcbiAgcHJvdGVjdGVkIGRzZ1N0YXRlOiBEc2dTdGF0ZSA9IERzZ1N0YXRlLlVuaW5pdGlhbGl6ZWQ7XG4gIHByb3RlY3RlZCBka2xzV2FzbTogRGtsc1dhc20gfCBudWxsO1xuXG4gIGNvbnN0cnVjdG9yKFxuICAgIGtleVNoYXJlOiBCdWZmZXIsXG4gICAgcGFydHlJZHg6IG51bWJlcixcbiAgICBkZXJpdmF0aW9uUGF0aDogc3RyaW5nLFxuICAgIG1lc3NhZ2VIYXNoOiBCdWZmZXIsXG4gICAgZGtsc1dhc20/OiBCdW5kbGVyV2FzbWVyXG4gICkge1xuICAgIHRoaXMucGFydHlJZHggPSBwYXJ0eUlkeDtcbiAgICB0aGlzLmtleVNoYXJlQnl0ZXMgPSBrZXlTaGFyZTtcbiAgICB0aGlzLmRlcml2YXRpb25QYXRoID0gZGVyaXZhdGlvblBhdGg7XG4gICAgdGhpcy5tZXNzYWdlSGFzaCA9IG1lc3NhZ2VIYXNoO1xuICAgIHRoaXMuZGtsc1dhc20gPSBka2xzV2FzbSA/PyBudWxsO1xuICB9XG5cbiAgcHJpdmF0ZSBfcmVzdG9yZVNlc3Npb24oKSB7XG4gICAgaWYgKCF0aGlzLmRzZ1Nlc3Npb24pIHtcbiAgICAgIHRoaXMuZHNnU2Vzc2lvbiA9IHRoaXMuZ2V0RGtsc1dhc20oKS5TaWduU2Vzc2lvbk9UVmFyaWFudC5mcm9tQnl0ZXModGhpcy5kc2dTZXNzaW9uQnl0ZXMpO1xuICAgIH1cbiAgfVxuXG4gIHByaXZhdGUgX2Rlc2VyaWFsaXplU3RhdGUoKSB7XG4gICAgaWYgKCF0aGlzLmRzZ1Nlc3Npb24pIHtcbiAgICAgIHRocm93IEVycm9yKCdTZXNzaW9uIG5vdCBpbnRpYWxpemVkJyk7XG4gICAgfVxuICAgIGNvbnN0IHJvdW5kID0gZGVjb2RlKHRoaXMuZHNnU2Vzc2lvbi50b0J5dGVzKCkpLnJvdW5kO1xuICAgIHN3aXRjaCAocm91bmQpIHtcbiAgICAgIGNhc2UgJ1dhaXRNc2cxJzpcbiAgICAgICAgdGhpcy5kc2dTdGF0ZSA9IERzZ1N0YXRlLlJvdW5kMTtcbiAgICAgICAgYnJlYWs7XG4gICAgICBjYXNlICdXYWl0TXNnMic6XG4gICAgICAgIHRoaXMuZHNnU3RhdGUgPSBEc2dTdGF0ZS5Sb3VuZDI7XG4gICAgICAgIGJyZWFrO1xuICAgICAgY2FzZSAnV2FpdE1zZzMnOlxuICAgICAgICB0aGlzLmRzZ1N0YXRlID0gRHNnU3RhdGUuUm91bmQzO1xuICAgICAgICBicmVhaztcbiAgICAgIGNhc2UgJ0VuZGVkJzpcbiAgICAgICAgdGhpcy5kc2dTdGF0ZSA9IERzZ1N0YXRlLkNvbXBsZXRlO1xuICAgICAgICBicmVhaztcbiAgICAgIGRlZmF1bHQ6XG4gICAgICAgIHRoaXMuZHNnU3RhdGUgPSBEc2dTdGF0ZS5JbnZhbGlkU3RhdGU7XG4gICAgICAgIHRocm93IEVycm9yKGBJbnZhbGlkIFN0YXRlOiAke3JvdW5kfWApO1xuICAgIH1cbiAgfVxuXG4gIHByaXZhdGUgYXN5bmMgbG9hZERrbHNXYXNtKCk6IFByb21pc2U8dm9pZD4ge1xuICAgIGlmICghdGhpcy5ka2xzV2FzbSkge1xuICAgICAgdGhpcy5ka2xzV2FzbSA9IGF3YWl0IGltcG9ydCgnQHNpbGVuY2VsYWJvcmF0b3JpZXMvZGtscy13YXNtLWxsLW5vZGUnKTtcbiAgICB9XG4gIH1cblxuICBwcml2YXRlIGdldERrbHNXYXNtKCkge1xuICAgIGlmICghdGhpcy5ka2xzV2FzbSkge1xuICAgICAgdGhyb3cgRXJyb3IoJ0RLTFMgd2FzbSBub3QgbG9hZGVkJyk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHRoaXMuZGtsc1dhc207XG4gIH1cblxuICAvKipcbiAgICogUmV0dXJucyB0aGUgY3VycmVudCBEU0cgc2Vzc2lvbiBhcyBhIGJhc2U2NCBzdHJpbmcuXG4gICAqIEByZXR1cm5zIHtzdHJpbmd9IC0gYmFzZTY0IHN0cmluZyBvZiB0aGUgY3VycmVudCBEU0cgc2Vzc2lvblxuICAgKi9cbiAgZ2V0U2Vzc2lvbigpOiBzdHJpbmcge1xuICAgIHJldHVybiBCdWZmZXIuZnJvbSh0aGlzLmRzZ1Nlc3Npb25CeXRlcykudG9TdHJpbmcoJ2Jhc2U2NCcpO1xuICB9XG5cbiAgLyoqXG4gICAqIFNldHMgdGhlIERTRyBzZXNzaW9uIGZyb20gYSBiYXNlNjQgc3RyaW5nLlxuICAgKiBAcGFyYW0ge3N0cmluZ30gc2Vzc2lvbiAtIGJhc2U2NCBzdHJpbmcgb2YgdGhlIERTRyBzZXNzaW9uXG4gICAqL1xuICBhc3luYyBzZXRTZXNzaW9uKHNlc3Npb246IHN0cmluZyk6IFByb21pc2U8dm9pZD4ge1xuICAgIHRoaXMuZHNnU2Vzc2lvbiA9IHVuZGVmaW5lZDtcbiAgICBpZiAoIXRoaXMuZGtsc1dhc20pIHtcbiAgICAgIGF3YWl0IHRoaXMubG9hZERrbHNXYXNtKCk7XG4gICAgfVxuICAgIGNvbnN0IHNlc3Npb25CeXRlcyA9IG5ldyBVaW50OEFycmF5KEJ1ZmZlci5mcm9tKHNlc3Npb24sICdiYXNlNjQnKSk7XG4gICAgY29uc3Qgcm91bmQgPSBkZWNvZGUoc2Vzc2lvbkJ5dGVzKS5yb3VuZDtcbiAgICBzd2l0Y2ggKHRydWUpIHtcbiAgICAgIGNhc2Ugcm91bmQgPT09ICdXYWl0TXNnMSc6XG4gICAgICAgIHRoaXMuZHNnU3RhdGUgPSBEc2dTdGF0ZS5Sb3VuZDE7XG4gICAgICAgIGJyZWFrO1xuICAgICAgY2FzZSByb3VuZCA9PT0gJ1dhaXRNc2cyJzpcbiAgICAgICAgdGhpcy5kc2dTdGF0ZSA9IERzZ1N0YXRlLlJvdW5kMjtcbiAgICAgICAgYnJlYWs7XG4gICAgICBjYXNlIHJvdW5kID09PSAnV2FpdE1zZzMnOlxuICAgICAgICB0aGlzLmRzZ1N0YXRlID0gRHNnU3RhdGUuUm91bmQzO1xuICAgICAgICBicmVhaztcbiAgICAgIGNhc2UgJ1dhaXRNc2c0JyBpbiByb3VuZDpcbiAgICAgICAgdGhpcy5kc2dTdGF0ZSA9IERzZ1N0YXRlLlJvdW5kNDtcbiAgICAgICAgYnJlYWs7XG4gICAgICBkZWZhdWx0OlxuICAgICAgICB0aHJvdyBFcnJvcihgSW52YWxpZCBTdGF0ZTogJHtyb3VuZH1gKTtcbiAgICB9XG4gICAgdGhpcy5kc2dTZXNzaW9uQnl0ZXMgPSBzZXNzaW9uQnl0ZXM7XG4gIH1cblxuICBhc3luYyBpbml0KCk6IFByb21pc2U8RGVzZXJpYWxpemVkQnJvYWRjYXN0TWVzc2FnZT4ge1xuICAgIGlmICh0aGlzLmRzZ1N0YXRlICE9PSBEc2dTdGF0ZS5VbmluaXRpYWxpemVkKSB7XG4gICAgICB0aHJvdyBFcnJvcignRFNHIHNlc3Npb24gYWxyZWFkeSBpbml0aWFsaXplZCcpO1xuICAgIH1cbiAgICBpZiAoIXRoaXMuZGtsc1dhc20pIHtcbiAgICAgIGF3YWl0IHRoaXMubG9hZERrbHNXYXNtKCk7XG4gICAgfVxuICAgIGlmIChcbiAgICAgIHR5cGVvZiB3aW5kb3cgIT09ICd1bmRlZmluZWQnICYmXG4gICAgICAvKiBjaGVja3MgZm9yIGVsZWN0cm9uIHByb2Nlc3NlcyAqL1xuICAgICAgIXdpbmRvdy5wcm9jZXNzICYmXG4gICAgICAhd2luZG93LnByb2Nlc3M/LlsndHlwZSddXG4gICAgKSB7XG4gICAgICAvKiBUaGlzIGlzIG9ubHkgbmVlZGVkIGZvciBicm93c2Vycy93ZWIgYmVjYXVzZSBpdCB1c2VzIGZldGNoIHRvIHJlc29sdmUgdGhlIHdhc20gYXNzZXQgZm9yIHRoZSB3ZWIgKi9cbiAgICAgIGNvbnN0IGluaXREa2xzID0gYXdhaXQgaW1wb3J0KCdAc2lsZW5jZWxhYm9yYXRvcmllcy9ka2xzLXdhc20tbGwtd2ViJyk7XG4gICAgICBhd2FpdCBpbml0RGtscy5kZWZhdWx0KCk7XG4gICAgfVxuICAgIGNvbnN0IHsgS2V5c2hhcmUsIFNpZ25TZXNzaW9uT1RWYXJpYW50IH0gPSB0aGlzLmdldERrbHNXYXNtKCk7XG4gICAgY29uc3Qga2V5U2hhcmUgPSBLZXlzaGFyZS5mcm9tQnl0ZXModGhpcy5rZXlTaGFyZUJ5dGVzKTtcbiAgICBpZiAoa2V5U2hhcmUucGFydHlJZCAhPT0gdGhpcy5wYXJ0eUlkeCkge1xuICAgICAgdGhyb3cgRXJyb3IoYFBhcnR5IGluZGV4OiAke3RoaXMucGFydHlJZHh9IGRvZXMgbm90IG1hdGNoIGtleSBzaGFyZSBwYXJ0eUlkOiAke2tleVNoYXJlLnBhcnR5SWR9IGApO1xuICAgIH1cbiAgICB0aGlzLmRzZ1Nlc3Npb24gPSBuZXcgU2lnblNlc3Npb25PVFZhcmlhbnQoa2V5U2hhcmUsIHRoaXMuZGVyaXZhdGlvblBhdGgpO1xuICAgIHRyeSB7XG4gICAgICBjb25zdCBwYXlsb2FkID0gdGhpcy5kc2dTZXNzaW9uLmNyZWF0ZUZpcnN0TWVzc2FnZSgpLnBheWxvYWQ7XG4gICAgICB0aGlzLl9kZXNlcmlhbGl6ZVN0YXRlKCk7XG4gICAgICB0aGlzLmRzZ1Nlc3Npb25CeXRlcyA9IHRoaXMuZHNnU2Vzc2lvbi50b0J5dGVzKCk7XG4gICAgICB0aGlzLmRzZ1Nlc3Npb24gPSB1bmRlZmluZWQ7XG4gICAgICByZXR1cm4ge1xuICAgICAgICBwYXlsb2FkOiBwYXlsb2FkLFxuICAgICAgICBmcm9tOiB0aGlzLnBhcnR5SWR4LFxuICAgICAgfTtcbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICB0aHJvdyBFcnJvcihgRXJyb3Igd2hpbGUgY3JlYXRpbmcgdGhlIGZpcnN0IG1lc3NhZ2UgZnJvbSBwYXJ0eSAke3RoaXMucGFydHlJZHh9OiAke2V9YCk7XG4gICAgfVxuICB9XG5cbiAgZ2V0IHNpZ25hdHVyZSgpOiBEZXNlcmlhbGl6ZWREa2xzU2lnbmF0dXJlIHtcbiAgICBpZiAoIXRoaXMuX3NpZ25hdHVyZSkge1xuICAgICAgdGhyb3cgRXJyb3IoJ0NhbiBub3QgcmVxdWVzdCBzaWduYXR1cmUuIFNpZ25hdHVyZSBub3QgcHJvZHVjZWQgeWV0LicpO1xuICAgIH1cbiAgICByZXR1cm4gdGhpcy5fc2lnbmF0dXJlO1xuICB9XG5cbiAgLyoqXG4gICAqIEVuZHMgdGhlIERTRyBzZXNzaW9uIGJ5IGZyZWVpbmcgYW55IGhlYXAgYWxsb2NhdGlvbnMgZnJvbSB3YXNtLiBOb3RlIHRoYXQgdGhlIHNlc3Npb24gaXMgZnJlZWQgaWYgYSBzaWduYXR1cmUgaXMgcHJvZHVjZWQuXG4gICAqL1xuICBlbmRTZXNzaW9uKCk6IHZvaWQge1xuICAgIGlmICh0aGlzLl9zaWduYXR1cmUpIHtcbiAgICAgIG5ldyBFcnJvcignU2Vzc2lvbiBhbHJlYWR5IGVuZGVkIGJlY2F1c2UgY29tYmluZWQgc2lnbmF0dXJlIHdhcyBwcm9kdWNlZC4nKTtcbiAgICB9XG4gICAgaWYgKHRoaXMuZHNnU2Vzc2lvbikge1xuICAgICAgdGhpcy5kc2dTZXNzaW9uLmZyZWUoKTtcbiAgICB9XG4gICAgdGhpcy5kc2dTdGF0ZSA9IERzZ1N0YXRlLlVuaW5pdGlhbGl6ZWQ7XG4gIH1cblxuICAvKipcbiAgICogUHJvY2Nlc3NlcyBpbmNvbWluZyBtZXNzYWdlcyB0byB0aGlzIHBhcnR5IGluIHRoZSBES0xzIERTRyBwcm90b2NvbCBhbmRcbiAgICogcHJvZHVjZXMgbWVzc2FnZXMgZnJvbSB0aGlzIHBhcnR5IHRvIG90aGVyIHBhcnRpZXMgZm9yIHRoZSBuZXh0IHJvdW5kLlxuICAgKiBAcGFyYW0gbWVzc2FnZXNGb3JJdGhSb3VuZCAtIG1lc3NhZ2VzIHRvIHByb2Nlc3MgdGhlIGN1cnJlbnQgcm91bmRcbiAgICogQHJldHVybnMge0Rlc2VyaWFsaXplZE1lc3NhZ2VzfSAtIG1lc3NhZ2VzIHRvIHNlbmQgdG8gb3RoZXIgcGFydGllcyBmb3IgdGhlIG5leHQgcm91bmRcbiAgICovXG4gIGhhbmRsZUluY29taW5nTWVzc2FnZXMobWVzc2FnZXNGb3JJdGhSb3VuZDogRGVzZXJpYWxpemVkTWVzc2FnZXMpOiBEZXNlcmlhbGl6ZWRNZXNzYWdlcyB7XG4gICAgbGV0IG5leHRSb3VuZE1lc3NhZ2VzOiBNZXNzYWdlW10gPSBbXTtcbiAgICBsZXQgbmV4dFJvdW5kRGVzZXJpYWxpemVkTWVzc2FnZXM6IERlc2VyaWFsaXplZE1lc3NhZ2VzID0geyBicm9hZGNhc3RNZXNzYWdlczogW10sIHAycE1lc3NhZ2VzOiBbXSB9O1xuICAgIHRoaXMuX3Jlc3RvcmVTZXNzaW9uKCk7XG4gICAgaWYgKCF0aGlzLmRzZ1Nlc3Npb24pIHtcbiAgICAgIHRocm93IEVycm9yKCdTZXNzaW9uIG5vdCBpbml0aWFsaXplZCcpO1xuICAgIH1cbiAgICBjb25zdCB7IE1lc3NhZ2UgfSA9IHRoaXMuZ2V0RGtsc1dhc20oKTtcbiAgICB0cnkge1xuICAgICAgaWYgKHRoaXMuZHNnU3RhdGUgPT09IERzZ1N0YXRlLlJvdW5kNCkge1xuICAgICAgICB0aGlzLmRzZ1N0YXRlID0gRHNnU3RhdGUuQ29tcGxldGU7XG4gICAgICAgIGNvbnN0IGNvbWJpbmVSZXN1bHQgPSB0aGlzLmRzZ1Nlc3Npb24uY29tYmluZShcbiAgICAgICAgICBtZXNzYWdlc0Zvckl0aFJvdW5kLmJyb2FkY2FzdE1lc3NhZ2VzLm1hcCgobSkgPT4gbmV3IE1lc3NhZ2UobS5wYXlsb2FkLCBtLmZyb20sIHVuZGVmaW5lZCkpXG4gICAgICAgICk7XG4gICAgICAgIHRoaXMuX3NpZ25hdHVyZSA9IHtcbiAgICAgICAgICBSOiBjb21iaW5lUmVzdWx0WzBdLFxuICAgICAgICAgIFM6IGNvbWJpbmVSZXN1bHRbMV0sXG4gICAgICAgIH07XG4gICAgICAgIHJldHVybiB7IGJyb2FkY2FzdE1lc3NhZ2VzOiBbXSwgcDJwTWVzc2FnZXM6IFtdIH07XG4gICAgICB9IGVsc2Uge1xuICAgICAgICBuZXh0Um91bmRNZXNzYWdlcyA9IHRoaXMuZHNnU2Vzc2lvbi5oYW5kbGVNZXNzYWdlcyhcbiAgICAgICAgICBtZXNzYWdlc0Zvckl0aFJvdW5kLmJyb2FkY2FzdE1lc3NhZ2VzXG4gICAgICAgICAgICAubWFwKChtKSA9PiBuZXcgTWVzc2FnZShtLnBheWxvYWQsIG0uZnJvbSwgdW5kZWZpbmVkKSlcbiAgICAgICAgICAgIC5jb25jYXQobWVzc2FnZXNGb3JJdGhSb3VuZC5wMnBNZXNzYWdlcy5tYXAoKG0pID0+IG5ldyBNZXNzYWdlKG0ucGF5bG9hZCwgbS5mcm9tLCBtLnRvKSkpXG4gICAgICAgICk7XG4gICAgICB9XG4gICAgICBpZiAodGhpcy5kc2dTdGF0ZSA9PT0gRHNnU3RhdGUuUm91bmQzKSB7XG4gICAgICAgIG5leHRSb3VuZE1lc3NhZ2VzID0gW3RoaXMuZHNnU2Vzc2lvbi5sYXN0TWVzc2FnZSh0aGlzLm1lc3NhZ2VIYXNoKV07XG4gICAgICAgIHRoaXMuZHNnU3RhdGUgPSBEc2dTdGF0ZS5Sb3VuZDQ7XG4gICAgICAgIHJldHVybiB7XG4gICAgICAgICAgYnJvYWRjYXN0TWVzc2FnZXM6IFtcbiAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgcGF5bG9hZDogbmV4dFJvdW5kTWVzc2FnZXNbMF0ucGF5bG9hZCxcbiAgICAgICAgICAgICAgZnJvbTogbmV4dFJvdW5kTWVzc2FnZXNbMF0uZnJvbV9pZCxcbiAgICAgICAgICAgICAgc2lnbmF0dXJlUjogZGVjb2RlKHRoaXMuZHNnU2Vzc2lvbi50b0J5dGVzKCkpLnJvdW5kLldhaXRNc2c0LnIsXG4gICAgICAgICAgICB9LFxuICAgICAgICAgIF0sXG4gICAgICAgICAgcDJwTWVzc2FnZXM6IFtdLFxuICAgICAgICB9O1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgLy8gVXBkYXRlIHJvdW5kIGRhdGEuXG4gICAgICAgIHRoaXMuX2Rlc2VyaWFsaXplU3RhdGUoKTtcbiAgICAgIH1cbiAgICAgIG5leHRSb3VuZERlc2VyaWFsaXplZE1lc3NhZ2VzID0ge1xuICAgICAgICBwMnBNZXNzYWdlczogbmV4dFJvdW5kTWVzc2FnZXNcbiAgICAgICAgICAuZmlsdGVyKChtKSA9PiBtLnRvX2lkICE9PSB1bmRlZmluZWQpXG4gICAgICAgICAgLm1hcCgobSkgPT4ge1xuICAgICAgICAgICAgaWYgKG0udG9faWQgPT09IHVuZGVmaW5lZCkge1xuICAgICAgICAgICAgICB0aHJvdyBFcnJvcignSW52YWxpZCBQMlAgbWVzc2FnZSwgbWlzc2luZyB0b19pZC4nKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGNvbnN0IHAycFJldHVybiA9IHtcbiAgICAgICAgICAgICAgcGF5bG9hZDogbS5wYXlsb2FkLFxuICAgICAgICAgICAgICBmcm9tOiBtLmZyb21faWQsXG4gICAgICAgICAgICAgIHRvOiBtLnRvX2lkLFxuICAgICAgICAgICAgfTtcbiAgICAgICAgICAgIHJldHVybiBwMnBSZXR1cm47XG4gICAgICAgICAgfSksXG4gICAgICAgIGJyb2FkY2FzdE1lc3NhZ2VzOiBuZXh0Um91bmRNZXNzYWdlc1xuICAgICAgICAgIC5maWx0ZXIoKG0pID0+IG0udG9faWQgPT09IHVuZGVmaW5lZClcbiAgICAgICAgICAubWFwKChtKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBicm9hZGNhc3RSZXR1cm4gPSB7XG4gICAgICAgICAgICAgIHBheWxvYWQ6IG0ucGF5bG9hZCxcbiAgICAgICAgICAgICAgZnJvbTogbS5mcm9tX2lkLFxuICAgICAgICAgICAgfTtcbiAgICAgICAgICAgIHJldHVybiBicm9hZGNhc3RSZXR1cm47XG4gICAgICAgICAgfSksXG4gICAgICB9O1xuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgIGlmIChlLm1lc3NhZ2Uuc3RhcnRzV2l0aCgnQWJvcnQgdGhlIHByb3RvY29sIGFuZCBiYW4nKSkge1xuICAgICAgICB0aHJvdyBFcnJvcihcbiAgICAgICAgICAnU2lnbmluZyBhYm9ydGVkLiBQbGVhc2Ugc3RvcCBhbGwgdHJhbnNhY3Rpb24gc2lnbmluZyBmcm9tIHRoaXMgd2FsbGV0IGFuZCBjb250YWN0IHN1cHBvcnRAYml0Z28uY29tLidcbiAgICAgICAgKTtcbiAgICAgIH1cbiAgICAgIHRocm93IEVycm9yKGBFcnJvciB3aGlsZSBjcmVhdGluZyBtZXNzYWdlcyBmcm9tIHBhcnR5ICR7dGhpcy5wYXJ0eUlkeH0sIHJvdW5kICR7dGhpcy5kc2dTdGF0ZX06ICR7ZX1gKTtcbiAgICB9IGZpbmFsbHkge1xuICAgICAgbmV4dFJvdW5kTWVzc2FnZXMuZm9yRWFjaCgobSkgPT4gbS5mcmVlKCkpO1xuICAgICAgLy8gU2Vzc2lvbiBpcyBmcmVlZCB3aGVuIGNvbWJpbmUgaXMgY2FsbGVkLlxuICAgICAgaWYgKHRoaXMuZHNnU3RhdGUgIT09IERzZ1N0YXRlLkNvbXBsZXRlKSB7XG4gICAgICAgIHRoaXMuZHNnU2Vzc2lvbkJ5dGVzID0gdGhpcy5kc2dTZXNzaW9uLnRvQnl0ZXMoKTtcbiAgICAgICAgdGhpcy5kc2dTZXNzaW9uID0gdW5kZWZpbmVkO1xuICAgICAgfVxuICAgIH1cbiAgICByZXR1cm4gbmV4dFJvdW5kRGVzZXJpYWxpemVkTWVzc2FnZXM7XG4gIH1cbn1cbiJdfQ== \ No newline at end of file diff --git a/patches/bitcore-tss+11.3.7.patch b/patches/bitcore-tss+11.3.7.patch index 73807cf23d..37420cb68b 100644 --- a/patches/bitcore-tss+11.3.7.patch +++ b/patches/bitcore-tss+11.3.7.patch @@ -1,8 +1,16 @@ diff --git a/node_modules/bitcore-tss/ecdsa/keygen.js b/node_modules/bitcore-tss/ecdsa/keygen.js -index cb9eccb..ce8ad2d 100644 +index cb9eccb..9f33573 100644 --- a/node_modules/bitcore-tss/ecdsa/keygen.js +++ b/node_modules/bitcore-tss/ecdsa/keygen.js -@@ -153,7 +153,7 @@ class KeyGen { +@@ -137,6 +137,7 @@ class KeyGen { + $.checkState(this.#round == 0, 'initJoin must be called before the rounds '); + + const unsignedMessageR1 = await this.#dkg.initDkg(); ++ + const serializedMsg = DklsTypes.serializeBroadcastMessage(unsignedMessageR1); + const signedMessage = await DklsComms.encryptAndAuthOutgoingMessages( + { broadcastMessages: [serializedMsg], p2pMessages: [] }, +@@ -153,7 +154,7 @@ class KeyGen { * @param {Array} prevRoundMessages * @returns {{ round: number, partyId: number, publicKey: string, p2pMessages: object[], broadcastMessage: object[] }} */ @@ -11,7 +19,7 @@ index cb9eccb..ce8ad2d 100644 $.checkState(this.#round > 0, 'initJoin must be called before participating in the rounds'); $.checkState(this.#round < 5, 'Signing rounds are over'); $.checkArgument(Array.isArray(prevRoundMessages), 'prevRoundMessages must be an array'); -@@ -163,17 +163,40 @@ class KeyGen { +@@ -163,17 +164,40 @@ class KeyGen { const prevRoundIncomingMsgs = DklsComms.decryptAndVerifyIncomingMessages(prevRoundMessages, this.#authKey); @@ -64,7 +72,7 @@ index cb9eccb..ce8ad2d 100644 /** diff --git a/node_modules/bitcore-tss/ecdsa/sign.js b/node_modules/bitcore-tss/ecdsa/sign.js -index a7d48d1..72eb551 100644 +index a7d48d1..68c7c9f 100644 --- a/node_modules/bitcore-tss/ecdsa/sign.js +++ b/node_modules/bitcore-tss/ecdsa/sign.js @@ -156,7 +156,6 @@ class Sign { diff --git a/patches/bitcore-wallet-client+11.3.6.patch b/patches/bitcore-wallet-client+11.3.6.patch deleted file mode 100644 index 1e9aaf1529..0000000000 --- a/patches/bitcore-wallet-client+11.3.6.patch +++ /dev/null @@ -1,161 +0,0 @@ -diff --git a/node_modules/bitcore-wallet-client/ts_build/src/lib/api.js b/node_modules/bitcore-wallet-client/ts_build/src/lib/api.js -index ca96d75..1ab4ef8 100644 ---- a/node_modules/bitcore-wallet-client/ts_build/src/lib/api.js -+++ b/node_modules/bitcore-wallet-client/ts_build/src/lib/api.js -@@ -49,6 +49,7 @@ const common_1 = require("./common"); - const credentials_1 = require("./credentials"); - const errors_1 = require("./errors"); - const key_1 = require("./key"); -+const tsskey_1 = require("./tsskey"); - const log_1 = __importDefault(require("./log")); - const paypro_1 = require("./paypro"); - const payproV2_1 = require("./payproV2"); -@@ -799,6 +800,7 @@ class API extends events_1.EventEmitter { - }; - const { body: res } = await this.request.post('/v2/wallets/', args); - const walletId = res.walletId; -+ - c.addWalletInfo(walletId, walletName, m, n, copayerName, { - useNativeSegwit: opts.useNativeSegwit, - segwitVersion: opts.segwitVersion, -@@ -2821,6 +2823,7 @@ exports.API = API; - API.PayProV2 = payproV2_1.PayProV2; - API.PayPro = paypro_1.PayPro; - API.Key = key_1.Key; -+API.TssKey = tsskey_1.TssKey; - API.Verifier = verifier_1.Verifier; - API.Core = CWC; - API.Utils = common_1.Utils; -diff --git a/node_modules/bitcore-wallet-client/ts_build/src/lib/common/encryption.js b/node_modules/bitcore-wallet-client/ts_build/src/lib/common/encryption.js -index ffb28f7..9193a78 100644 ---- a/node_modules/bitcore-wallet-client/ts_build/src/lib/common/encryption.js -+++ b/node_modules/bitcore-wallet-client/ts_build/src/lib/common/encryption.js -@@ -7,10 +7,18 @@ exports.Encryption = void 0; - const crypto_1 = __importDefault(require("crypto")); - const PBKDF2_ITERATIONS = 1000; - const DEFAULT_KEY_SIZE = 256; --const ALGORITHM = ks => `aes-${ks || DEFAULT_KEY_SIZE}-ccm`; -+const ALGORITHM = ks => `aes-${ks || DEFAULT_KEY_SIZE}-gcm`; - const AUTH_TAG_LENGTH = 16; - const SALT_LENGTH = 16; --const MAX_IV_LENGTH = 13; -+const IV_LENGTH = 12; -+const toBuf = (x) => { -+ if (Buffer.isBuffer(x)) return x; -+ if (x instanceof Uint8Array) return Buffer.from(x); -+ if (x instanceof ArrayBuffer) return Buffer.from(new Uint8Array(x)); -+ if (x == null) return Buffer.alloc(0); -+ return Buffer.from(String(x)); -+ }; -+ - class EncryptionClass { - _optimizeIv(length, iv) { - let L = 2; -@@ -25,18 +33,18 @@ class EncryptionClass { - } - _baseEncrypt(data, key) { - const buf = Buffer.isBuffer(data) ? data : Buffer.from(typeof data === 'string' ? data : JSON.stringify(data), 'utf8'); -- const iv = this._optimizeIv(buf.length, crypto_1.default.randomBytes(MAX_IV_LENGTH)); -- const cipher = crypto_1.default.createCipheriv(ALGORITHM(key.length * 8), key, iv, { authTagLength: AUTH_TAG_LENGTH, plaintextLength: buf.length }); -- let encrypted = cipher.update(buf); -- encrypted = Buffer.concat([encrypted, cipher.final()]); -+ const iv = crypto_1.default.randomBytes(IV_LENGTH); -+ const cipher = crypto_1.default.createCipheriv(ALGORITHM(key.length * 8), key, iv, { authTagLength: AUTH_TAG_LENGTH }); -+ let encrypted = toBuf(cipher.update(buf)); -+ encrypted = Buffer.concat([encrypted, toBuf(cipher.final())]); - return { - iv: iv.toString('base64'), - v: 1, - ts: AUTH_TAG_LENGTH * 8, -- mode: 'ccm', -+ mode: 'gcm', - adata: '', - cipher: 'aes', -- ct: Buffer.concat([encrypted, cipher.getAuthTag()]).toString('base64') -+ ct: Buffer.concat([encrypted, toBuf(cipher.getAuthTag())]).toString('base64') - }; - } - encryptWithKey(data, key) { -diff --git a/node_modules/bitcore-wallet-client/ts_build/src/lib/credentials.js b/node_modules/bitcore-wallet-client/ts_build/src/lib/credentials.js -index c076dcd..c792064 100644 ---- a/node_modules/bitcore-wallet-client/ts_build/src/lib/credentials.js -+++ b/node_modules/bitcore-wallet-client/ts_build/src/lib/credentials.js -@@ -48,7 +48,7 @@ class Credentials { - const entropySource = crypto_wallet_core_1.BitcoreLib.crypto.Hash.sha256(priv.toBuffer()).toString('hex'); - const b = Buffer.from(entropySource, 'hex'); - const b2 = crypto_wallet_core_1.BitcoreLib.crypto.Hash.sha256hmac(b, Buffer.from(prefix)); -- x.personalEncryptingKey = b2.subarray(0, 16).toString('base64'); -+ x.personalEncryptingKey = Buffer.from(b2.subarray(0, 16)).toString('base64'); - x.copayerId = common_1.Utils.xPubToCopayerId(x.chain, x.xPubKey); - x.publicKeyRing = [ - { -diff --git a/node_modules/bitcore-wallet-client/ts_build/src/lib/key.js b/node_modules/bitcore-wallet-client/ts_build/src/lib/key.js -index 1fa419a..95acbb7 100644 ---- a/node_modules/bitcore-wallet-client/ts_build/src/lib/key.js -+++ b/node_modules/bitcore-wallet-client/ts_build/src/lib/key.js -@@ -95,6 +95,7 @@ class Key { - this.use44forMultisig = opts.useLegacyPurpose; - this.compliantDerivation = !opts.nonCompliantDerivation; - let x = opts.seedData; -+ - switch (opts.seedType) { - case 'new': - if (opts.language && !wordsForLang[opts.language]) -diff --git a/node_modules/bitcore-wallet-client/ts_build/src/lib/tsssign.js b/node_modules/bitcore-wallet-client/ts_build/src/lib/tsssign.js -index f0e05d8..570e212 100644 ---- a/node_modules/bitcore-wallet-client/ts_build/src/lib/tsssign.js -+++ b/node_modules/bitcore-wallet-client/ts_build/src/lib/tsssign.js -@@ -50,7 +50,12 @@ class TssSign extends events_1.EventEmitter { - $.checkArgument(!messageHash || Buffer.isBuffer(messageHash), 'messageHash must be a Buffer'); - $.checkArgument(!message || Buffer.isBuffer(message) || typeof message === 'string', 'message must be a string or Buffer'); - $.checkArgument(id == null || typeof id === 'string', 'id must be a string or not provided'); -- $.checkArgument(password || __classPrivateFieldGet(this, _TssSign_tssKey, "f").keychain.privateKeyShare, 'password is required to decrypt the TSS private key share'); -+ -+ $.checkArgument( -+ __classPrivateFieldGet(this, _TssSign_tssKey, "f").keychain.privateKeyShare, -+ 'Key shares are required for signing. Make sure backupKeyShare was enabled during key generation.' -+ ); -+ - if (!messageHash && typeof message === 'string') { - if (encoding === 'hex') { - message = message.startsWith('0x') ? message.slice(2) : message; -@@ -58,8 +63,12 @@ class TssSign extends events_1.EventEmitter { - message = Buffer.from(message, encoding); - messageHash = crypto_wallet_core_1.BitcoreLib.crypto.Hash.sha256(message); - } -+ -+ const keychain = __classPrivateFieldGet(this, _TssSign_tssKey, "f").get(password).keychain -+ if( keychain.privateKeyShare.data )keychain.privateKeyShare = Buffer.from(keychain.privateKeyShare.data); -+ - __classPrivateFieldSet(this, _TssSign_sign, new bitcore_tss_1.ECDSA.Sign({ -- keychain: __classPrivateFieldGet(this, _TssSign_tssKey, "f").get(password).keychain, -+ keychain:keychain, - partyId: __classPrivateFieldGet(this, _TssSign_tssKey, "f").metadata.partyId, - m: __classPrivateFieldGet(this, _TssSign_tssKey, "f").metadata.m, - n: __classPrivateFieldGet(this, _TssSign_tssKey, "f").metadata.n, -@@ -67,10 +76,12 @@ class TssSign extends events_1.EventEmitter { - messageHash, - authKey: __classPrivateFieldGet(this, _TssSign_credentials, "f").requestPrivKey - }), "f"); -+ - this.id = id || crypto_wallet_core_1.BitcoreLib.crypto.Hash.sha256(messageHash).toString('hex'); - const msg = await __classPrivateFieldGet(this, _TssSign_sign, "f").initJoin(); - const m = __classPrivateFieldGet(this, _TssSign_tssKey, "f").metadata.m; - await __classPrivateFieldGet(this, _TssSign_request, "f").post('/v1/tss/sign/' + this.id, { message: msg, m }); -+ - return this; - } - exportSession() { -@@ -83,9 +94,11 @@ class TssSign extends events_1.EventEmitter { - const { session } = params; - const [id, sigSession] = session.split(':'); - this.id = id; -+ const keychain = __classPrivateFieldGet(this, _TssSign_tssKey, "f").keychain -+ if( keychain.privateKeyShare.data )keychain.privateKeyShare = Buffer.from(keychain.privateKeyShare.data); - __classPrivateFieldSet(this, _TssSign_sign, await bitcore_tss_1.ECDSA.Sign.restore({ - session: sigSession, -- keychain: __classPrivateFieldGet(this, _TssSign_tssKey, "f").keychain, -+ keychain: keychain, - authKey: __classPrivateFieldGet(this, _TssSign_credentials, "f").requestPrivKey - }), "f"); - return this; diff --git a/patches/bitcore-wallet-client+11.4.6.patch b/patches/bitcore-wallet-client+11.4.6.patch new file mode 100644 index 0000000000..65443b607f --- /dev/null +++ b/patches/bitcore-wallet-client+11.4.6.patch @@ -0,0 +1,130 @@ +diff --git a/node_modules/bitcore-wallet-client/src/lib/verifier.ts b/node_modules/bitcore-wallet-client/src/lib/verifier.ts +index 443c525..93b1d56 100644 +--- a/node_modules/bitcore-wallet-client/src/lib/verifier.ts ++++ b/node_modules/bitcore-wallet-client/src/lib/verifier.ts +@@ -85,7 +85,7 @@ export class Verifier { + const uniq = []; + let error; + for (const copayer of copayers || []) { +- if (uniq[copayers.xPubKey]++) { ++ if (uniq[copayer.xPubKey]++) { + log.error('Repeated public keys in server response'); + error = true; + } +diff --git a/node_modules/bitcore-wallet-client/ts_build/src/lib/api.js b/node_modules/bitcore-wallet-client/ts_build/src/lib/api.js +index 7a27efd..d916b5f 100644 +--- a/node_modules/bitcore-wallet-client/ts_build/src/lib/api.js ++++ b/node_modules/bitcore-wallet-client/ts_build/src/lib/api.js +@@ -50,6 +50,7 @@ const common_1 = require("./common"); + const credentials_1 = require("./credentials"); + const errors_1 = require("./errors"); + const key_1 = require("./key"); ++const tsskey_1 = require("./tsskey"); + const log_1 = __importDefault(require("./log")); + const paypro_1 = require("./paypro"); + const payproV2_1 = require("./payproV2"); +@@ -2819,6 +2820,8 @@ exports.API = API; + API.PayProV2 = payproV2_1.PayProV2; + API.PayPro = paypro_1.PayPro; + API.Key = key_1.Key; ++API.TssKey = tsskey_1.TssKey; ++API.TssKeyGen = tsskey_1.TssKeyGen; + API.Verifier = verifier_1.Verifier; + API.Core = CWC; + API.Utils = common_1.Utils; +diff --git a/node_modules/bitcore-wallet-client/ts_build/src/lib/common/encryption.js b/node_modules/bitcore-wallet-client/ts_build/src/lib/common/encryption.js +index ffb28f7..92db28f 100644 +--- a/node_modules/bitcore-wallet-client/ts_build/src/lib/common/encryption.js ++++ b/node_modules/bitcore-wallet-client/ts_build/src/lib/common/encryption.js +@@ -7,10 +7,18 @@ exports.Encryption = void 0; + const crypto_1 = __importDefault(require("crypto")); + const PBKDF2_ITERATIONS = 1000; + const DEFAULT_KEY_SIZE = 256; +-const ALGORITHM = ks => `aes-${ks || DEFAULT_KEY_SIZE}-ccm`; ++const ALGORITHM = ks => `aes-${ks || DEFAULT_KEY_SIZE}-gcm`; + const AUTH_TAG_LENGTH = 16; + const SALT_LENGTH = 16; +-const MAX_IV_LENGTH = 13; ++const IV_LENGTH = 12; ++const toBuf = (x) => { ++ if (Buffer.isBuffer(x)) return x; ++ if (x instanceof Uint8Array) return Buffer.from(x); ++ if (x instanceof ArrayBuffer) return Buffer.from(new Uint8Array(x)); ++ if (x == null) return Buffer.alloc(0); ++ return Buffer.from(String(x)); ++ }; ++ + class EncryptionClass { + _optimizeIv(length, iv) { + let L = 2; +@@ -25,18 +33,18 @@ class EncryptionClass { + } + _baseEncrypt(data, key) { + const buf = Buffer.isBuffer(data) ? data : Buffer.from(typeof data === 'string' ? data : JSON.stringify(data), 'utf8'); +- const iv = this._optimizeIv(buf.length, crypto_1.default.randomBytes(MAX_IV_LENGTH)); +- const cipher = crypto_1.default.createCipheriv(ALGORITHM(key.length * 8), key, iv, { authTagLength: AUTH_TAG_LENGTH, plaintextLength: buf.length }); +- let encrypted = cipher.update(buf); +- encrypted = Buffer.concat([encrypted, cipher.final()]); ++ const iv = crypto_1.default.randomBytes(IV_LENGTH); ++ const cipher = crypto_1.default.createCipheriv(ALGORITHM(key.length * 8), key, iv, { authTagLength: AUTH_TAG_LENGTH }); ++ let encrypted = toBuf(cipher.update(buf)); ++ encrypted = Buffer.concat([encrypted, toBuf(cipher.final())]); + return { + iv: iv.toString('base64'), + v: 1, + ts: AUTH_TAG_LENGTH * 8, +- mode: 'ccm', ++ mode: 'gcm', + adata: '', + cipher: 'aes', +- ct: Buffer.concat([encrypted, cipher.getAuthTag()]).toString('base64') ++ ct: Buffer.concat([encrypted, toBuf(cipher.getAuthTag())]).toString('base64') + }; + } + encryptWithKey(data, key) { +@@ -67,8 +75,30 @@ class EncryptionClass { + const authTagLength = json.ts / 8; + const ciphertext = ct.subarray(0, ct.length - authTagLength); + const authTag = ct.subarray(ct.length - authTagLength); +- const iv = this._optimizeIv(ciphertext.length, Buffer.from(json.iv, 'base64')); +- const decipher = crypto_1.default.createDecipheriv(`${json.cipher}-${json.ks}-${json.mode}`, key, iv, { authTagLength }); ++ ++ let iv; ++ let decipher; ++ ++ if (json.mode === 'gcm') { ++ // GCM mode ++ iv = Buffer.from(json.iv, 'base64'); ++ decipher = crypto_1.default.createDecipheriv( ++ `${json.cipher}-${json.ks}-${json.mode}`, ++ key, ++ iv, ++ { authTagLength } ++ ); ++ } else { ++ // CCM mode (legacy/server) ++ iv = this._optimizeIv(ciphertext.length, Buffer.from(json.iv, 'base64')); ++ decipher = crypto_1.default.createDecipheriv( ++ `${json.cipher}-${json.ks}-${json.mode}`, ++ key, ++ iv, ++ { authTagLength, plaintextLength: ciphertext.length } ++ ); ++ } ++ + decipher.setAuthTag(authTag); + let decrypted = decipher.update(ciphertext); + decrypted = Buffer.concat([decrypted, decipher.final()]); +diff --git a/node_modules/bitcore-wallet-client/ts_build/src/lib/credentials.js b/node_modules/bitcore-wallet-client/ts_build/src/lib/credentials.js +index 43e3f5d..4767846 100644 +--- a/node_modules/bitcore-wallet-client/ts_build/src/lib/credentials.js ++++ b/node_modules/bitcore-wallet-client/ts_build/src/lib/credentials.js +@@ -49,7 +49,7 @@ class Credentials { + const entropySource = crypto_wallet_core_1.BitcoreLib.crypto.Hash.sha256(priv.toBuffer()).toString('hex'); + const b = Buffer.from(entropySource, 'hex'); + const b2 = crypto_wallet_core_1.BitcoreLib.crypto.Hash.sha256hmac(b, Buffer.from(prefix)); +- x.personalEncryptingKey = b2.subarray(0, 16).toString('base64'); ++ x.personalEncryptingKey = Buffer.from(b2.subarray(0, 16)).toString('base64'); + x.copayerId = common_1.Utils.xPubToCopayerId(x.chain, x.xPubKey); + x.publicKeyRing = [ + { diff --git a/src/Root.tsx b/src/Root.tsx index 0078925758..d6e4fbbbf4 100644 --- a/src/Root.tsx +++ b/src/Root.tsx @@ -323,6 +323,7 @@ export default () => { WalletScreens.ADDRESSES, WalletScreens.ALL_ADDRESSES, WalletScreens.COPAYERS, + WalletScreens.INVITE_COSIGNERS, WalletScreens.EXPORT_KEY, WalletScreens.EXPORT_WALLET, WalletScreens.JOIN_MULTISIG, @@ -710,7 +711,9 @@ export default () => { const walletsToFix = Object.values(keys).flatMap(key => key.wallets.filter( wallet => - !wallet.receiveAddress && wallet?.credentials?.isComplete(), + !wallet.receiveAddress && + wallet?.credentials?.isComplete() && + !wallet.pendingTssSession, ), ); if (walletsToFix.length > 0) { @@ -844,6 +847,7 @@ export default () => { if ( wallet.chain?.toLowerCase() !== 'sol' || !wallet.credentials.isComplete() || + wallet.pendingTssSession || !wallet.receiveAddress ) { continue; diff --git a/src/components/list/CurrencySelectionRow.tsx b/src/components/list/CurrencySelectionRow.tsx index 1f7dae2e76..e960cc7b27 100644 --- a/src/components/list/CurrencySelectionRow.tsx +++ b/src/components/list/CurrencySelectionRow.tsx @@ -1,28 +1,18 @@ -import React, {memo, ReactElement, useCallback} from 'react'; -import {useTranslation} from 'react-i18next'; -import {ImageRequireSource, View} from 'react-native'; +import React, {memo, useCallback} from 'react'; +import {ImageRequireSource} from 'react-native'; import styled from 'styled-components/native'; import {IS_ANDROID} from '../../constants'; import {SupportedCurrencyOption} from '../../constants/SupportedCurrencyOptions'; import {CurrencySelectionMode} from '../../navigation/wallet/screens/CurrencySelection'; -import { - LightBlack, - LuckySevens, - Slate10, - Slate30, - SlateDark, -} from '../../styles/colors'; -import { - formatCurrencyAbbreviation, - getBadgeImg, -} from '../../utils/helper-methods'; +import {LightBlack, LuckySevens, Slate30, SlateDark} from '../../styles/colors'; +import {formatCurrencyAbbreviation} from '../../utils/helper-methods'; import Checkbox from '../checkbox/Checkbox'; import {CurrencyImage} from '../currency-image/CurrencyImage'; import haptic from '../haptic-feedback/haptic'; -import NestedArrowIcon from '../nested-arrow/NestedArrow'; import {ScreenGutter} from '../styled/Containers'; -import {BaseText, H6, H7} from '../styled/Text'; +import {BaseText, H7} from '../styled/Text'; import {TouchableOpacity} from '@components/base/TouchableOpacity'; +import ChevronRightSvg from '../../../assets/img/angle-right.svg'; export type CurrencySelectionItem = Pick< SupportedCurrencyOption, @@ -35,7 +25,6 @@ export type CurrencySelectionItem = Pick< > & { chain: string; chainName?: string; - tokenAddress?: string; imgSrc?: ImageRequireSource | undefined; selected?: boolean; disabled?: boolean; @@ -43,39 +32,22 @@ export type CurrencySelectionItem = Pick< export type CurrencySelectionRowProps = { currency: CurrencySelectionItem; - tokens?: CurrencySelectionItem[]; - filterSelected?: boolean; - description?: string; hideCheckbox?: boolean; + hideChevron?: boolean; disableCheckbox?: boolean; selectionMode?: CurrencySelectionMode; - onToggle?: ( - currencyAbbreviation: string, - chain: string, - tokenAddress?: string, - ) => void; - onViewAllTokensPressed?: ( - currency: CurrencySelectionItem, - tokens: CurrencySelectionItem[], - ) => any; + onToggle?: (currencyAbbreviation: string, chain: string) => void; }; -export const CurrencySelectionRowContainer = styled.View` +const RowContainer = styled(TouchableOpacity)` border: 1px solid ${({theme}) => (theme.dark ? LightBlack : Slate30)}; border-radius: 12px; - flex-direction: column; + flex-direction: row; + align-items: center; margin: 0 ${ScreenGutter} ${ScreenGutter}; padding: 16px; `; -const FlexRow = styled(TouchableOpacity)` - flex-direction: row; -`; - -const ChainDescription = styled(H7)` - color: ${({theme}) => (theme.dark ? Slate10 : LightBlack)}; -`; - const CurrencyColumn = styled.View` justify-content: center; margin-right: 8px; @@ -97,254 +69,53 @@ const CurrencySubTitle = styled(BaseText)` font-size: 12px; `; -export const TokensHeading = styled(H7).attrs(() => ({ - medium: true, -}))` - color: ${({theme}) => (theme.dark ? Slate10 : LuckySevens)}; - font-weight: 500; - margin: 16px 0; -`; - -const TokensFooter = styled.View` +const ChevronContainer = styled.View` + justify-content: center; align-items: center; `; -const ViewAllLink = styled(H6)` - color: ${({theme}) => theme.colors.link}; - text-align: center; -`; - -interface FeeCurrencySelectionRowProps { - currency: CurrencySelectionItem; - hideCheckbox?: boolean; - disableCheckbox?: boolean; - selectionMode?: CurrencySelectionMode; - onToggle?: (currencyAbbreviation: string, chain: string) => any; -} - -export const FeeCurrencySelectionRow: React.FC = - memo(props => { - const {onToggle, currency, hideCheckbox, selectionMode, disableCheckbox} = - props; - const { - currencyAbbreviation, - currencyName, - img, - imgSrc, - badgeUri, - selected, - disabled, - chain, - } = currency; - - return ( - - !disabled && !disableCheckbox - ? onToggle?.(currencyAbbreviation, chain) - : null - }> - - - - - - {currencyName} - - - {formatCurrencyAbbreviation(currencyAbbreviation)} - - - - {!hideCheckbox && !disableCheckbox && ( - - onToggle?.(currencyAbbreviation, chain)} - /> - - )} - - ); - }); - -interface TokenSelectionRowProps { - token: CurrencySelectionItem; - hideCheckbox?: boolean; - selectionMode?: CurrencySelectionMode; - onToggle?: ( - currencyAbbreviation: string, - chain: string, - tokenAddress: string, - ) => any; - hideArrow?: boolean; - badgeUri?: string | ((props?: any) => ReactElement); -} - -export const TokenSelectionRow: React.FC = memo( - props => { - const { - token, - hideCheckbox, - selectionMode, - onToggle, - hideArrow, - badgeUri: _badgeUri, - } = props; - const badgeUri = - _badgeUri || getBadgeImg(token.currencyAbbreviation, token.chain); - const _currencyAbbreviation = formatCurrencyAbbreviation( - token.currencyAbbreviation, - ); - - return ( - - onToggle?.( - token.currencyAbbreviation, - token.chain, - token.tokenAddress!, - ) - }> - {!hideArrow ? ( - - - - ) : null} - - - - - - - {token.currencyName} - - - {_currencyAbbreviation} - - - - {!hideCheckbox ? ( - - - onToggle?.( - token.currencyAbbreviation, - token.chain, - token.tokenAddress!, - ) - } - /> - - ) : null} - - ); - }, -); - -export const DescriptionRow: React.FC = ({children}) => { - return ( - - {children} - - ); -}; - const CurrencySelectionRow: React.FC = ({ currency, - description, - tokens, - filterSelected, - hideCheckbox, - disableCheckbox, - selectionMode, onToggle, - onViewAllTokensPressed, }) => { - const {t} = useTranslation(); - const {currencyName, chainName} = currency; - const onPress = useCallback( - ( - currencyAbbreviation: string, - chain: string, - tokenAddress?: string, - ): void => { - haptic(IS_ANDROID ? 'keyboardPress' : 'impactLight'); - onToggle?.(currencyAbbreviation, chain, tokenAddress); - }, - [onToggle], - ); + const { + currencyAbbreviation, + currencyName, + img, + imgSrc, + badgeUri, + disabled, + chain, + } = currency; + + const onPress = useCallback((): void => { + if (disabled) { + return; + } + haptic(IS_ANDROID ? 'keyboardPress' : 'impactLight'); + onToggle?.(currencyAbbreviation, chain); + }, [currencyAbbreviation, chain, disabled, onToggle]); return ( - - onPress(currency.currencyAbbreviation, currency.chain)} - hideCheckbox={hideCheckbox} - selectionMode={selectionMode} - disableCheckbox={disableCheckbox} - /> - - {description ? {description} : null} - - {tokens?.length ? ( - <> - {!filterSelected ? ( - - {t('Popular {{currency}} Tokens', { - currency: chainName ? chainName : currencyName, - })} - - ) : ( - - {t('{{currency}} Tokens', { - currency: chainName ? chainName : currencyName, - })} - - )} - - {tokens.map(token => ( - { - onPress( - token.currencyAbbreviation, - token.chain, - token.tokenAddress, - ); - }} - hideCheckbox={hideCheckbox} - selectionMode={selectionMode} - /> - ))} - - {!filterSelected ? ( - - { - onViewAllTokensPressed?.(currency, tokens); - }}> - {t('View all {{currency}} tokens', {currency: t(chainName)})} - - - ) : null} - - ) : null} - + + + + + + + {currencyName} + + {formatCurrencyAbbreviation(currencyAbbreviation)} + + + + + + + ); }; diff --git a/src/components/modal/transact-menu/TransactMenu.tsx b/src/components/modal/transact-menu/TransactMenu.tsx index 4747edd9ed..7f9acf0dbe 100644 --- a/src/components/modal/transact-menu/TransactMenu.tsx +++ b/src/components/modal/transact-menu/TransactMenu.tsx @@ -128,7 +128,9 @@ const TransactModal = () => { wallet => !wallet.hideWallet && !wallet.hideWalletByAccount && - wallet.isComplete(), + wallet.isComplete() && + !wallet.pendingTssSession && + wallet.balance.sat > 0, ); const availableWalletsWithFunds = availableWallets.filter( diff --git a/src/components/styled/Containers.tsx b/src/components/styled/Containers.tsx index 3e4b909396..8ba2389c10 100644 --- a/src/components/styled/Containers.tsx +++ b/src/components/styled/Containers.tsx @@ -279,6 +279,7 @@ export const AdvancedOptionsContainer = styled.View` background-color: ${({theme}) => (theme.dark ? LightBlack : Feather)}; border-radius: 6px; margin-bottom: 20px; + margin-top: 10px; `; export const AdvancedOptionsButton = styled(TouchableOpacity)` diff --git a/src/lib/bwc.ts b/src/lib/bwc.ts index e44883344d..ec05669662 100644 --- a/src/lib/bwc.ts +++ b/src/lib/bwc.ts @@ -62,6 +62,10 @@ export class BwcProvider { return BWC.TssKey; } + public getTssKeyGen() { + return BWC.TSSKeyGen; + } + public upgradeCredentialsV1(x: any) { return BWC.upgradeCredentialsV1(x); } diff --git a/src/navigation/coinbase/screens/CoinbaseAccount.tsx b/src/navigation/coinbase/screens/CoinbaseAccount.tsx index bd28e2b355..1804cbcfca 100644 --- a/src/navigation/coinbase/screens/CoinbaseAccount.tsx +++ b/src/navigation/coinbase/screens/CoinbaseAccount.tsx @@ -399,7 +399,8 @@ const CoinbaseAccount = ({ wallet.network === 'livenet' && wallet.currencyAbbreviation === _currencyAbbreviation.toLowerCase() && wallet.chain === _chain && - wallet.isComplete(), + wallet.isComplete() && + !wallet.pendingTssSession, ); if (availableWallets.length) { @@ -545,7 +546,7 @@ const CoinbaseAccount = ({ ); if (newWallet) { if (newWallet.credentials) { - if (newWallet.isComplete()) { + if (newWallet.isComplete() && !newWallet.pendingTssSession) { if (allKeys[newWallet.keyId].backupComplete) { setSelectedWallet(newWallet); await sleep(500); diff --git a/src/navigation/tabs/contacts/screens/ContactsAdd.tsx b/src/navigation/tabs/contacts/screens/ContactsAdd.tsx index 7f0bb99329..b1d4d2d931 100644 --- a/src/navigation/tabs/contacts/screens/ContactsAdd.tsx +++ b/src/navigation/tabs/contacts/screens/ContactsAdd.tsx @@ -1,28 +1,11 @@ -import React, { - useState, - useMemo, - useCallback, - useLayoutEffect, - useEffect, -} from 'react'; -import {FlatList} from 'react-native'; +import React, {useState, useLayoutEffect, useEffect} from 'react'; import {yupResolver} from '@hookform/resolvers/yup'; import yup from '../../../../lib/yup'; import styled from 'styled-components/native'; import {Controller, useForm} from 'react-hook-form'; import Button from '../../../../components/button/Button'; import BoxInput from '../../../../components/form/BoxInput'; -import { - TextAlign, - H4, - BaseText, - HeaderTitle, -} from '../../../../components/styled/Text'; -import { - SheetContainer, - Row, - ActiveOpacity, -} from '../../../../components/styled/Containers'; +import {HeaderTitle} from '../../../../components/styled/Text'; import { IsValidEVMAddress, IsValidSVMAddress, @@ -38,34 +21,13 @@ import { } from '../../../../store/contact/contact.actions'; import SuccessIcon from '../../../../../assets/img/success.svg'; import ScanSvg from '../../../../../assets/img/onboarding/scan.svg'; -import SheetModal from '../../../../components/modal/base/sheet/SheetModal'; -import { - keyExtractor, - findContact, - getBadgeImg, - getChainFromTokenByAddressKey, -} from '../../../../utils/helper-methods'; -import {CurrencySelectionItem} from '../../../../components/list/CurrencySelectionRow'; -import NetworkSelectionRow, { - NetworkSelectionProps, -} from '../../../../components/list/NetworkSelectionRow'; -import {LightBlack, NeutralSlate, Slate} from '../../../../styles/colors'; -import WalletIcons from '../../../wallet/components/WalletIcons'; -import {BitpaySupportedTokens} from '../../../../constants/currencies'; -import {BitpaySupportedTokenOptsByAddress} from '../../../../constants/tokens'; +import {findContact} from '../../../../utils/helper-methods'; import {useAppDispatch, useAppSelector} from '../../../../utils/hooks'; import {useTranslation} from 'react-i18next'; import {ContactsScreens, ContactsGroupParamList} from '../ContactsGroup'; import {NativeStackScreenProps} from '@react-navigation/native-stack'; -import { - SupportedCurrencyOption, - SupportedChainsOptions, - SupportedTokenOptions, - SupportedCoinsOptions, -} from '../../../../constants/SupportedCurrencyOptions'; import {Analytics} from '../../../../store/analytics/analytics.effects'; import {TouchableOpacity} from '@components/base/TouchableOpacity'; -import {useTokenContext} from '../../../../contexts'; const InputContainer = styled.View<{hideInput?: boolean}>` display: ${({hideInput}) => (!hideInput ? 'flex' : 'none')}; @@ -92,44 +54,6 @@ const AddressBadge = styled.View` const ScanButtonContainer = styled(TouchableOpacity)``; -const CurrencySelectionModalContainer = styled(SheetContainer)` - padding: 15px; - min-height: 200px; -`; - -const CurrencySelectorContainer = styled.View<{hideSelector?: boolean}>` - display: ${({hideSelector}) => (!hideSelector ? 'flex' : 'none')}; - margin: 10px 0 20px 0; - position: relative; -`; - -const Label = styled(BaseText)` - font-size: 13px; - font-weight: 500; - line-height: 18px; - top: 0; - left: 1px; - margin-bottom: 3px; - color: ${({theme}) => (theme && theme.dark ? theme.colors.text : '#434d5a')}; -`; - -const CurrencyContainer = styled(TouchableOpacity)` - background: ${({theme}) => (theme.dark ? LightBlack : NeutralSlate)}; - padding: 0 20px 0 10px; - height: 55px; - border: 0.75px solid ${({theme}) => (theme.dark ? LightBlack : Slate)}; - border-top-left-radius: 4px; - border-top-right-radius: 4px; -`; - -const NetworkName = styled(BaseText)` - font-size: 16px; - font-style: normal; - font-weight: 500; - color: #9ba3ae; - text-transform: uppercase; -`; - const schema = yup.object().shape({ name: yup.string().required().trim(), email: yup.string().email().trim(), @@ -150,7 +74,6 @@ const ContactsAdd = ({ formState: {errors, dirtyFields}, } = useForm({resolver: yupResolver(schema)}); const {contact, context, onEditComplete} = route.params || {}; - const isDev = __DEV__; const contacts = useAppSelector(({CONTACT}: RootState) => CONTACT.list); const navigation = useNavigation(); @@ -158,88 +81,11 @@ const ContactsAdd = ({ const [validAddress, setValidAddress] = useState(false); const [xrpValidAddress, setXrpValidAddress] = useState(false); - const [addressValue, setAddressValue] = useState(''); - const [tokenAddressValue, setTokenAddressValue] = useState< - string | undefined - >(); + const [coinValue, setCoinValue] = useState(''); + const [chainValue, setChainValue] = useState(''); const [networkValue, setNetworkValue] = useState(''); - const [networkModalVisible, setNetworkModalVisible] = useState(false); - - const {tokenOptionsByAddress: _tokenOptionsByAddress} = useTokenContext(); - - const tokenOptionsByAddress = useAppSelector(({WALLET}: RootState) => { - return { - ...BitpaySupportedTokenOptsByAddress, - ..._tokenOptionsByAddress, - ...WALLET.customTokenOptionsByAddress, - }; - }); - - const ALL_CUSTOM_TOKENS = useMemo(() => { - return Object.entries(tokenOptionsByAddress) - .filter(([k]) => !BitpaySupportedTokens[k]) - .map(([k, {symbol, name, logoURI, address}]) => { - const chain = getChainFromTokenByAddressKey(k); - return { - id: Math.random().toString(), - coin: symbol.toLowerCase(), - currencyAbbreviation: symbol, - currencyName: name, - img: logoURI || '', - isToken: true, - chain, - badgeUri: getBadgeImg(symbol.toLowerCase(), chain), - tokenAddress: address, - } as CurrencySelectionItem; - }); - }, [tokenOptionsByAddress]); - - const SUPPORTED_TOKEN_OPTIONS = useMemo(() => { - return Object.entries(SupportedTokenOptions).map( - ([ - id, - { - img, - currencyName, - currencyAbbreviation, - chain, - isToken, - badgeUri, - tokenAddress, - }, - ]) => { - return { - id, - coin: currencyAbbreviation, - currencyAbbreviation: currencyAbbreviation, - currencyName, - img, - isToken, - chain, - badgeUri, - tokenAddress, - } as CurrencySelectionItem; - }, - ); - }, [tokenOptionsByAddress]); - - const ALL_TOKENS = useMemo( - () => [...SUPPORTED_TOKEN_OPTIONS, ...ALL_CUSTOM_TOKENS], - [ALL_CUSTOM_TOKENS], - ); - - const [selectedChain, setSelectedChain] = useState(SupportedChainsOptions[0]); - const [selectedCurrency, setSelectedCurrency] = useState< - SupportedCurrencyOption | CurrencySelectionItem - >(SupportedCoinsOptions[0]); - - const networkOptions = [ - {id: 'livenet', name: 'Livenet'}, - {id: 'testnet', name: 'Testnet'}, - ]; - useLayoutEffect(() => { navigation.setOptions({ headerTitle: () => ( @@ -252,37 +98,20 @@ const ContactsAdd = ({ const setValidValues = ( address: string, - currencyAbbreviation: string, + coin: string, network: string, chain: string, - tokenAddress: string | undefined, ) => { setValidAddress(true); setAddressValue(address); + setCoinValue(coin); + setChainValue(chain); setNetworkValue(network); - setTokenAddressValue(tokenAddress); - - _setSelectedChain(chain); - _setSelectedCurrency(currencyAbbreviation, chain, tokenAddress); - - switch (chain) { - case 'xrp': - setXrpValidAddress(true); - return; - default: - return; - } + setXrpValidAddress(chain === 'xrp'); }; - - const processAddress = ( - address?: string, - coin?: string, - network?: string, - chain?: string, - tokenAddress?: string, - ) => { + const processAddress = (address: string) => { if (address) { - const coinAndNetwork = GetCoinAndNetwork(address, undefined, chain); + const coinAndNetwork = GetCoinAndNetwork(address); if (coinAndNetwork) { const isValid = ValidateCoinAddress( address, @@ -292,13 +121,11 @@ const ContactsAdd = ({ if (isValid) { setValidValues( address, - coin || coinAndNetwork.coin, - network || coinAndNetwork.network, - chain || coinAndNetwork.coin, - tokenAddress, + coinAndNetwork.coin, + coinAndNetwork.network, + coinAndNetwork.coin, ); } else { - // try testnet const isValidTest = ValidateCoinAddress( address, coinAndNetwork.coin, @@ -307,17 +134,17 @@ const ContactsAdd = ({ if (isValidTest) { setValidValues( address, - coin || coinAndNetwork.coin, - network || 'testnet', - chain || coinAndNetwork.coin, - tokenAddress, + coinAndNetwork.coin, + 'testnet', + coinAndNetwork.coin, ); } } } else { - setNetworkValue(''); setAddressValue(''); - setTokenAddressValue(undefined); + setCoinValue(''); + setChainValue(''); + setNetworkValue(''); setValidAddress(false); setXrpValidAddress(false); } @@ -333,34 +160,9 @@ const ContactsAdd = ({ return; } - if ( - selectedCurrency.currencyAbbreviation && - selectedChain.chain && - networkValue - ) { - contact.coin = selectedCurrency.currencyAbbreviation; - contact.chain = selectedChain.chain; - contact.network = networkValue; - contact.tokenAddress = tokenAddressValue; - } else { - setError('address', { - type: 'manual', - message: t('Coin or Network invalid'), - }); - return; - } - - if ( - selectedCurrency.currencyAbbreviation === 'xrp' && - contact.destinationTag && - isNaN(contact.destinationTag) - ) { - setError('destinationTag', { - type: 'manual', - message: t('Only numbers are allowed'), - }); - return; - } + contact.coin = coinValue; + contact.chain = chainValue; + contact.network = networkValue; if (context === 'edit') { dispatch(updateContact(contact)); @@ -389,46 +191,6 @@ const ContactsAdd = ({ navigation.goBack(); }); - const _setSelectedChain = (_chain: string) => { - const _selectedChain = SupportedChainsOptions.filter( - ({chain}) => chain === _chain, - ); - setSelectedChain(_selectedChain[0]); - }; - - const _setSelectedCurrency = ( - _currencyAbbreviation: string, - _chain: string, - tokenAddress: string | undefined, - ) => { - let _selectedCurrency; - if (!tokenAddress) { - _selectedCurrency = SupportedCoinsOptions.filter( - ({currencyAbbreviation, chain}) => - currencyAbbreviation === _currencyAbbreviation && chain === _chain, - ); - setSelectedCurrency(_selectedCurrency[0]); - } else { - _selectedCurrency = ALL_TOKENS.find( - ({tokenAddress: _tokenAddress}) => - _tokenAddress?.toLowerCase() === tokenAddress.toLowerCase(), - ); - setSelectedCurrency(_selectedCurrency!); - } - }; - - const networkSelected = ({id}: NetworkSelectionProps) => { - setNetworkValue(id); - setNetworkModalVisible(false); - }; - - const renderNetworkItem = useCallback( - ({item}: {item: {id: string; name: string}}) => ( - - ), - [], - ); - const goToScan = () => { dispatch( Analytics.track('Open Scanner', { @@ -445,13 +207,7 @@ const ContactsAdd = ({ useEffect(() => { if (contact) { - processAddress( - contact.address, - contact.coin, - contact.network, - contact.chain, - contact.tokenAddress, - ); + processAddress(contact.address!!); setValue('address', contact.address!, {shouldDirty: true}); setValue('name', contact.name || ''); setValue('email', contact.email); @@ -565,49 +321,12 @@ const ContactsAdd = ({ /> - {!contact ? ( - - - { - setNetworkModalVisible(true); - }}> - - - {networkValue} - - - - - - ) : null} - - setNetworkModalVisible(false)}> - - -

{t('Select a Network')}

-
- -
-
); }; diff --git a/src/navigation/tabs/contacts/screens/ContactsDetails.tsx b/src/navigation/tabs/contacts/screens/ContactsDetails.tsx index d49f9b44d3..e1d7f8a823 100644 --- a/src/navigation/tabs/contacts/screens/ContactsDetails.tsx +++ b/src/navigation/tabs/contacts/screens/ContactsDetails.tsx @@ -170,6 +170,7 @@ const ContactsDetails = ({ !wallet.hideWalletByAccount && wallet.network === 'livenet' && wallet.isComplete() && + !wallet.pendingTssSession && wallet.currencyAbbreviation === contact.coin && wallet.balance.sat > 0, ); diff --git a/src/navigation/wallet/WalletGroup.tsx b/src/navigation/wallet/WalletGroup.tsx index 1559e864fb..d9eaf4d77a 100644 --- a/src/navigation/wallet/WalletGroup.tsx +++ b/src/navigation/wallet/WalletGroup.tsx @@ -46,6 +46,8 @@ import CreateMultisig, { } from './screens/CreateMultisig'; import JoinMultisig, {JoinMultisigParamList} from './screens/JoinMultisig'; import Copayers from './screens/Copayers'; +import InviteCosigners from './screens/InviteCosigners'; +import JoinTSSWallet from './screens/JoinTSSWallet'; import AddingOptions, {AddingOptionsParamList} from './screens/AddingOptions'; import UpdateKeyOrWalletName from './screens/UpdateKeyOrWalletName'; import RequestSpecificAmountQR from './screens/request-specific-amount/RequestSpecificAmountQR'; @@ -89,15 +91,12 @@ import BackupOnboarding, { import {Root} from '../../Root'; import {AccountRowProps} from '../../components/list/AccountListRow'; import KeyInformation from './screens/KeyInformation'; -import {IsSVMChain, IsVMChain} from '../../store/wallet/utils/currency'; -import { - AccountChainsContainer, - WIDTH, -} from '../../components/styled/Containers'; -import Blockie from '../../components/blockie/Blockie'; -import {getEVMAccountName} from '../../store/wallet/utils/wallet'; import {useAppSelector} from '../../utils/hooks'; import {RootState} from '../../store'; +import BackupSharedKeyScreen, { + BackupSharedKeyParamList, +} from './screens/BackupSharedKey'; +import ExportTSSWallet from './screens/wallet-settings/ExportTSSWallet'; interface WalletProps { Wallet: typeof Root; @@ -110,6 +109,7 @@ export type WalletGroupParamList = { AddCustomToken: AddCustomTokenParamList; BackupKey: BackupParamList; BackupOnboarding: BackupOnboardingParamList; + BackupSharedKey: BackupSharedKeyParamList; RecoveryPhrase: RecoveryPhraseParamList; VerifyPhrase: VerifyPhraseParamList; TermsOfUse: TermsOfUseParamList | undefined; @@ -149,6 +149,9 @@ export type WalletGroupParamList = { CreateMultisig: CreateMultisigParamsList; JoinMultisig: JoinMultisigParamList | undefined; Copayers: {wallet: WalletModel; status: _Credentials}; + InviteCosigners: {keyId: string}; + ShareJoinCode: {keyId: string; partyId: number; joinCode: string}; + JoinTSSWallet: {copayerName?: string}; AddingOptions: AddingOptionsParamList; RequestSpecificAmountQR: {wallet: WalletModel; requestAmount: number}; TransactionDetails: { @@ -178,6 +181,10 @@ export type WalletGroupParamList = { xPrivKey: string; }; }; + ExportTSSWallet: { + keyId: string; + context: 'createNewTSSKey' | 'joinTSSKey' | 'backupExistingTSSKey'; + }; Addresses: {wallet: WalletModel}; AllAddresses: AllAddressesParamList; PriceCharts: PriceChartsParamList; @@ -196,6 +203,7 @@ export enum WalletScreens { ADD_CUSTOM_TOKEN = 'AddCustomToken', BACKUP_KEY = 'BackupKey', BACKUP_ONBOARDING = 'BackupOnboarding', + BACKUP_SHARED_KEY = 'BackupSharedKey', RECOVERY_PHRASE = 'RecoveryPhrase', VERIFY_PHRASE = 'VerifyPhrase', TERMS_OF_USE = 'TermsOfUse', @@ -224,6 +232,8 @@ export enum WalletScreens { CREATE_MULTISIG = 'CreateMultisig', JOIN_MULTISIG = 'JoinMultisig', COPAYERS = 'Copayers', + INVITE_COSIGNERS = 'InviteCosigners', + JOIN_TSS_WALLET = 'JoinTSSWallet', ADDING_OPTIONS = 'AddingOptions', REQUEST_SPECIFIC_AMOUNT_QR = 'RequestSpecificAmountQR', EXPORT_TRANSACTION_HISTORY = 'ExportTransactionHistory', @@ -234,6 +244,7 @@ export enum WalletScreens { KEY_GLOBAL_SELECT = 'KeyGlobalSelect', WALLET_INFORMATION = 'WalletInformation', EXPORT_WALLET = 'ExportWallet', + EXPORT_TSS_WALLET = 'ExportTSSWallet', ADDRESSES = 'Addresses', ALL_ADDRESSES = 'AllAddresses', PRICE_CHARTS = 'PriceCharts', @@ -279,6 +290,13 @@ const WalletGroup = ({Wallet, theme}: WalletProps) => { name={WalletScreens.BACKUP_ONBOARDING} component={BackupOnboarding} /> + null, + }} + name={WalletScreens.BACKUP_SHARED_KEY} + component={BackupSharedKeyScreen} + /> { name={WalletScreens.COPAYERS} component={Copayers} /> + ( + {t('Invite Co-Signers')} + ), + }} + name={WalletScreens.INVITE_COSIGNERS} + component={InviteCosigners} + /> + ( + {t('Join Shared Wallet')} + ), + }} + name={WalletScreens.JOIN_TSS_WALLET} + component={JoinTSSWallet} + /> { name={WalletScreens.EXPORT_WALLET} component={ExportWallet} /> + { showErrorModal(t('Could not decrypt file, check your password')); return; } + try { + const parsed = JSON.parse(decryptBackupText); + if (parsed.isTSS) { + importTSSWallet(decryptBackupText); + return; + } + } catch {} importWallet(decryptBackupText, opts); }); @@ -191,6 +202,61 @@ const FileOrText = () => { return () => sub.remove(); }, [clearSensitive]); + const importTSSWallet = async (decryptBackupText: string) => { + try { + showOngoingProcess('IMPORTING'); + await sleep(1000); + + const key = (await dispatch( + startImportTSSFile(decryptBackupText), + )) as Key; + + hideOngoingProcess(); + await sleep(1000); + + try { + showOngoingProcess('IMPORT_SCANNING_FUNDS'); + await dispatch(startGetRates({force: true})); + await fixWalletAddresses({ + appDispatch: dispatch, + wallets: key.wallets, + }); + await dispatch( + startUpdateAllWalletStatusForKey({ + key, + force: true, + createTokenWalletWithFunds: true, + }), + ); + await sleep(1000); + await dispatch(updatePortfolioBalance()); + } catch (error) {} + + dispatch(setHomeCarouselConfig({id: key.id, show: true})); + + backupRedirect({ + context: route.params?.context, + navigation, + walletTermsAccepted, + key, + }); + + dispatch( + Analytics.track('Imported Key', { + context: route.params?.context || '', + source: 'TSSFile', + }), + ); + + hideOngoingProcess(); + } catch (e: any) { + logger.error(e.message); + hideOngoingProcess(); + await sleep(1000); + showErrorModal(e.message); + } + }; + return ( (dark ? White : Action)}; + font-weight: 600; + font-size: 16px; + line-height: 22px; + color: ${({theme: {dark}}) => (dark ? White : Black)}; + margin-bottom: 6px; `; const OptionDescriptionText = styled(BaseText)` font-style: normal; font-weight: 400; font-size: 14px; - line-height: 19px; + line-height: 20px; color: ${({theme: {dark}}) => (dark ? Slate : Black)}; margin-top: 3px; `; +const SubDescriptionContainer = styled.View` + background-color: ${({theme: {dark}}) => (dark ? '#000000' : '#F5F5F5')}; + border-radius: 8px; + padding: 12px 14px; + margin-top: 12px; +`; + +const OptionSubDescriptionText = styled(BaseText)` + font-style: normal; + font-weight: 400; + font-size: 13px; + line-height: 20px; + color: ${({theme: {dark}}) => (dark ? White : SlateDark)}; +`; + +const BoldText = styled(BaseText)` + font-weight: 700; + color: ${({theme: {dark}}) => (dark ? White : SlateDark)}; +`; + export interface Option { img?: ReactElement; imgSrc?: ImageSourcePropType; + subDescription?: string; title?: string; description?: string; onPress: () => void; @@ -71,6 +101,17 @@ interface Props extends SheetParams { paddingHorizontal?: number; } +const renderSubDescription = (text: string) => { + const parts = text.split(/(\*\*[^*]+\*\*)/); + return parts.map((part, index) => { + if (part.startsWith('**') && part.endsWith('**')) { + const boldText = part.slice(2, -2); + return {boldText}; + } + return part; + }); +}; + const OptionsSheet = ({ isVisible, closeModal, @@ -105,6 +146,7 @@ const OptionsSheet = ({ imgSrc, title: optionTitle, description, + subDescription, onPress, optionElement, }, @@ -138,6 +180,13 @@ const OptionsSheet = ({ {description} + {subDescription && ( + + + {renderSubDescription(subDescription)} + + + )} )} diff --git a/src/navigation/wallet/components/ReceiveAddress.tsx b/src/navigation/wallet/components/ReceiveAddress.tsx index 3646ab0aa4..3a03bb2719 100644 --- a/src/navigation/wallet/components/ReceiveAddress.tsx +++ b/src/navigation/wallet/components/ReceiveAddress.tsx @@ -289,7 +289,7 @@ const ReceiveAddress = ({isVisible, closeModal, wallet, context}: Props) => { }; const init = () => { - if (wallet?.isComplete()) { + if (wallet?.isComplete() && !wallet.pendingTssSession) { logger.info(`Creating address for wallet: ${wallet.id}`); createAddress(); } else { diff --git a/src/navigation/wallet/components/RecoveryPhrase.tsx b/src/navigation/wallet/components/RecoveryPhrase.tsx index 097936868a..fb35d5ae6b 100644 --- a/src/navigation/wallet/components/RecoveryPhrase.tsx +++ b/src/navigation/wallet/components/RecoveryPhrase.tsx @@ -3,7 +3,10 @@ import styled from 'styled-components/native'; import { Caution, LightBlack, + LuckySevens, NeutralSlate, + Slate10, + Slate30, SlateDark, White, } from '../../../styles/colors'; @@ -34,6 +37,7 @@ import {Controller, useForm} from 'react-hook-form'; import { BaseText, H4, + H7, ImportTitle, Paragraph, Small, @@ -60,6 +64,7 @@ import ChevronUpSvg from '../../../../assets/img/chevron-up.svg'; import Checkbox from '../../../components/checkbox/Checkbox'; import { fixWalletAddresses, + formatCurrencyAbbreviation, getAccount, getDerivationStrategy, getNetworkName, @@ -71,11 +76,13 @@ import { import {DefaultDerivationPath} from '../../../constants/defaultDerivationPath'; import {startUpdateAllWalletStatusForKey} from '../../../store/wallet/effects/status/status'; import {CurrencyImage} from '../../../components/currency-image/CurrencyImage'; -import {SupportedCurrencyOptions} from '../../../constants/SupportedCurrencyOptions'; +import { + SupportedCurrencyOption, + SupportedCurrencyOptions, +} from '../../../constants/SupportedCurrencyOptions'; import Icons from '../components/WalletIcons'; import SheetModal from '../../../components/modal/base/sheet/SheetModal'; import {AppState, FlatList, TextInput, View} from 'react-native'; -import CurrencySelectionRow from '../../../components/list/CurrencySelectionRow'; import {updatePortfolioBalance} from '../../../store/wallet/wallet.actions'; import { GetName, @@ -92,6 +99,7 @@ import { } from '../../../utils/hooks'; import {TouchableOpacity} from '@components/base/TouchableOpacity'; import {useOngoingProcess} from '../../../contexts'; +import haptic from '../../../components/haptic-feedback/haptic'; const ScrollViewContainer = styled(KeyboardAwareScrollView)` margin-top: 20px; @@ -186,6 +194,27 @@ const CtaContainer = styled(_CtaContainer)` padding: 10px 0; `; +const CurrencyColumn = styled.View` + justify-content: center; + margin-right: 8px; +`; + +const CurrencyTitleColumn = styled(CurrencyColumn)` + flex: 1 1 auto; +`; + +const CurrencyTitle = styled(H7).attrs(() => ({ + medium: true, +}))` + margin: 0; + padding: 0; +`; + +const CurrencySubTitle = styled(BaseText)` + color: ${({theme}) => (theme.dark ? LuckySevens : SlateDark)}; + font-size: 12px; +`; + const RecoveryPhrase = () => { const {t} = useTranslation(); const dispatch = useAppDispatch(); @@ -529,49 +558,44 @@ const RecoveryPhrase = () => { }; const renderItem = useCallback( - ({item}) => { - const currencySelected = ( - _currencyAbbreviation: string, - _chain: string, - ) => { - const _selectedCurrency = CurrencyOptions.filter( - currency => - currency.currencyAbbreviation === _currencyAbbreviation && - currency.chain === _chain, - ); - const currencyAbbreviation = _selectedCurrency[0].currencyAbbreviation; - const chain = _selectedCurrency[0].chain; + ({item}: {item: SupportedCurrencyOption}) => { + const {currencyAbbreviation, currencyName, img, badgeUri, chain} = item; + + const onPress = () => { + haptic(IS_ANDROID ? 'keyboardPress' : 'impactLight'); + const defaultCoin = `default${chain.toUpperCase()}`; // @ts-ignore const derivationPath = DefaultDerivationPath[defaultCoin]; - setSelectedCurrency(_selectedCurrency[0]); + + setSelectedCurrency(item); setCurrencyModalVisible(false); - const advancedOpts = { + setAdvancedOptions({ ...advancedOptions, coin: currencyAbbreviation, chain, derivationPath, - }; - setAdvancedOptions(advancedOpts); + }); }; return ( - + + + + + + {currencyName} + + {formatCurrencyAbbreviation(currencyAbbreviation)} + + + ); }, - [ - setSelectedCurrency, - setCurrencyModalVisible, - setAdvancedOptions, - advancedOptions, - recreateWallet, - setRecreateWallet, - ], + [advancedOptions], ); useEffect(() => { diff --git a/src/navigation/wallet/components/TSSProgressTracker.tsx b/src/navigation/wallet/components/TSSProgressTracker.tsx new file mode 100644 index 0000000000..3fec8f617e --- /dev/null +++ b/src/navigation/wallet/components/TSSProgressTracker.tsx @@ -0,0 +1,425 @@ +import React, {useState} from 'react'; +import {View} from 'react-native'; +import styled, {useTheme} from 'styled-components/native'; +import { + White, + Black, + SlateDark, + Slate30, + Success25, +} from '../../../styles/colors'; +import {useTranslation} from 'react-i18next'; +import { + TSSSigningStatus, + TSSSigningProgress, +} from '../../../store/wallet/wallet.models'; +import { + ActiveOpacity, + TouchableOpacity, +} from '@components/base/TouchableOpacity'; +import {GetAmTimeAgo} from '../../../store/wallet/utils/time'; +import ClockLightIcon from '../../../../assets/img/clock-blue.svg'; +import ClockDarkIcon from '../../../../assets/img/clock-light-blue.svg'; +import SuccessLightIcon from '../../../../assets/img/check-dark.svg'; +import SuccessDarkIcon from '../../../../assets/img/check.svg'; +import ChevronDownSvg from '../../../../assets/img/chevron-down.svg'; +import {BaseText, H4} from '../../../components/styled/Text'; +import SheetModal from '../../../components/modal/base/sheet/SheetModal'; + +const ProgressButton = styled(TouchableOpacity)` + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 16px; + border-radius: 12px; + border-width: 1px; + border-color: ${({theme: {dark}}) => + dark ? 'rgba(255,255,255,0.1)' : Slate30}; +`; + +const ProgressIndicator = styled.View<{status: TSSSigningStatus}>` + width: 40px; + height: 40px; + border-radius: 20px; + background-color: ${({status, theme: {dark}}) => + status === 'complete' ? (dark ? '#004D27' : Success25) : '#2240C440'}; + align-items: center; + justify-content: center; + margin-right: 12px; +`; + +const ProgressButtonText = styled(BaseText)` + font-size: 16px; + color: ${({theme}) => theme.colors.text}; +`; + +const ProgressBarContainer = styled.View` + height: 3px; + background-color: ${({theme: {dark}}) => (dark ? '#2A2A2A' : '#E5E5E5')}; + border-radius: 2px; + margin-top: 8px; + overflow: hidden; +`; + +const ProgressBarFill = styled.View<{progress: number; complete?: boolean}>` + height: 100%; + width: ${({progress}) => progress}%; + background-color: ${({complete, theme: {dark}}) => + complete ? (dark ? '#00A651' : '#2FCF6E') : '#2240C4'}; + border-radius: 2px; +`; + +const DetailsLabel = styled(BaseText)` + font-size: 14px; + color: ${({theme}) => theme.colors.description}; + margin-bottom: 8px; +`; + +const ModalContainer = styled.View` + padding: 20px; + padding-bottom: 40px; + background-color: ${({theme}) => theme.colors.background}; + border-top-left-radius: 20px; + border-top-right-radius: 20px; +`; + +const Header = styled.View` + flex-direction: row; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; +`; + +const Title = styled(H4)` + color: ${({theme}) => theme.colors.text}; +`; + +const StepsContainer = styled.View``; + +const StepRow = styled.View` + flex-direction: row; + align-items: flex-start; +`; + +const StepRail = styled.View` + width: 40px; + align-items: center; + margin-right: 12px; +`; + +const StepConnector = styled.View<{completed?: boolean; height?: number}>` + width: 2px; + height: ${({height}) => height || 20}px; + margin-top: 0px; + background-color: ${({theme: {dark}, completed}) => + completed ? (dark ? '#004D27' : Success25) : dark ? '#2A2A2A' : '#F5F5F5'}; +`; + +const StepIndicator = styled.View<{active?: boolean; completed?: boolean}>` + width: 40px; + height: 40px; + border-radius: 20px; + background-color: ${({theme: {dark}, active, completed}) => + active + ? '#2240C440' + : completed + ? dark + ? '#004D27' + : Success25 + : dark + ? '#2A2A2A' + : '#F5F5F5'}; + align-items: center; + justify-content: center; +`; + +const StepContent = styled.View` + flex: 1; + padding-bottom: 20px; +`; + +const StepNumber = styled(BaseText)` + color: ${({theme: {dark}}) => (dark ? White : Black)}; + font-size: 16px; + font-weight: 400; +`; + +const StepTitle = styled(BaseText)` + font-size: 16px; + font-weight: 400; + color: ${({theme: {dark}}) => (dark ? White : Black)}; +`; + +const StepSubtitle = styled(BaseText)` + font-size: 14px; + color: ${({theme: {dark}}) => (dark ? White : SlateDark)}; + line-height: 20px; +`; + +const StepTime = styled(BaseText)` + color: ${({theme}) => theme.colors.description}; + font-size: 12px; + margin-left: auto; +`; + +const CopayerList = styled.View` + margin-top: 0px; + margin-bottom: 0px; +`; + +const CopayerRow = styled.View` + flex-direction: row; + align-items: center; + padding: 4px 0; +`; + +const CopayerIndicator = styled.View<{signed: boolean}>` + width: 20px; + height: 20px; + border-radius: 10px; + background-color: ${({signed, theme: {dark}}) => + signed ? (dark ? '#004D27' : Success25) : '#2240C440'}; + align-items: center; + justify-content: center; + margin-right: 8px; +`; + +const CopayerName = styled(BaseText)<{signed: boolean}>` + color: ${({theme: {dark}, signed}) => + signed ? (dark ? White : Black) : dark ? White : SlateDark}; + font-size: 14px; +`; + +export interface TSSCopayer { + id: string; + name: string; + signed: boolean; +} + +interface TSSProgressTrackerProps { + status: TSSSigningStatus; + progress: TSSSigningProgress; + createdBy: string; + date: Date; + copayers: TSSCopayer[]; + isModalVisible?: boolean; + onModalVisibilityChange?: (visible: boolean) => void; +} + +const TSSProgressTracker: React.FC = ({ + status, + progress, + createdBy, + date, + copayers, + isModalVisible: externalIsVisible, + onModalVisibilityChange, +}) => { + const {t} = useTranslation(); + const theme = useTheme(); + const [internalIsVisible, setInternalIsVisible] = useState(false); + + const isModalVisible = externalIsVisible ?? internalIsVisible; + const setModalVisible = (visible: boolean) => { + if (onModalVisibilityChange) { + onModalVisibilityChange(visible); + } else { + setInternalIsVisible(visible); + } + }; + + const ClockIcon = theme.dark ? ClockDarkIcon : ClockLightIcon; + const SuccessIcon = theme.dark ? SuccessDarkIcon : SuccessLightIcon; + + const getButtonText = (): string => { + switch (status) { + case 'initializing': + return t('Waiting to initialize'); + case 'waiting_for_cosigners': + return t('Waiting for co-signers'); + case 'signature_generation': + return t('Signature Generation'); + case 'broadcasting': + return t('Broadcast Transaction'); + case 'complete': + return t('Complete'); + default: + return t('Waiting to initialize'); + } + }; + + const getProgressPercentage = (): number => { + const statusProgress: Record = { + initializing: 0, + waiting_for_cosigners: 25, + signature_generation: 50, + broadcasting: 75, + complete: 100, + error: 0, + }; + + if (status === 'signature_generation' && progress.totalRounds > 0) { + const baseProgress = 50; + const roundProgress = (progress.currentRound / progress.totalRounds) * 25; + return baseProgress + roundProgress; + } + + return statusProgress[status] || 0; + }; + + const getStepStatus = (step: number): 'pending' | 'active' | 'complete' => { + const statusOrder: TSSSigningStatus[] = [ + 'initializing', + 'waiting_for_cosigners', + 'signature_generation', + 'broadcasting', + 'complete', + ]; + + const currentIndex = statusOrder.indexOf(status); + + if (step < currentIndex) return 'complete'; + if (step === currentIndex) return 'active'; + return 'pending'; + }; + + const steps = [ + { + title: t('Proposal Created'), + subtitle: createdBy, + time: date, + }, + { + title: t('Waiting for co-signers'), + subtitle: `${copayers.filter(c => c.signed).length}/${ + copayers.length + } ${t('signed')}`, + showCopayers: true, + }, + { + title: t('Signature Generation'), + }, + { + title: t('Broadcast Transaction'), + }, + ]; + + const handleClose = () => { + if ( + status !== 'complete' && + status !== 'broadcasting' && + status !== 'signature_generation' + ) { + setModalVisible(false); + } + }; + + return ( + <> + + setModalVisible(true)}> + + {status === 'complete' ? ( + + ) : ( + + )} + + + {getButtonText()} + + + + + + + + + +
+ + {t('Transaction Progress')} + +
+ + + {steps.map((step, index) => { + const stepStatus = getStepStatus(index); + const isActive = stepStatus === 'active'; + const isComplete = stepStatus === 'complete'; + const showCopayers = + step.showCopayers && (isActive || isComplete); + + const connectorHeight = showCopayers ? 100 : 20; + + return ( + + + + + {isComplete ? ( + + ) : isActive ? ( + + ) : ( + {index + 1} + )} + + {index < steps.length - 1 && ( + + )} + + + + + {step.title} + {step.time && status !== 'initializing' && ( + + {GetAmTimeAgo(step.time.getTime())} + + )} + + {step.subtitle && ( + {step.subtitle} + )} + + {showCopayers && ( + + {copayers.map((copayer, idx) => ( + + + {copayer.signed ? ( + + ) : ( + + )} + + + {copayer.name} + + + ))} + + )} + + + + ); + })} + +
+
+ + ); +}; + +export default TSSProgressTracker; diff --git a/src/navigation/wallet/screens/AccountDetails.tsx b/src/navigation/wallet/screens/AccountDetails.tsx index 46fc3d7cc0..af18c54f75 100644 --- a/src/navigation/wallet/screens/AccountDetails.tsx +++ b/src/navigation/wallet/screens/AccountDetails.tsx @@ -467,6 +467,7 @@ const AccountDetails: React.FC = ({route}) => { .filter( sw => sw.isComplete() && + !sw.pendingTssSession && !key.wallets.some(ew => ew.id === sw.credentials.walletId), ) .map(syncWallet => { @@ -482,7 +483,11 @@ const AccountDetails: React.FC = ({route}) => { return _.merge( syncWallet, buildWalletObj( - {...syncWallet.credentials, currencyAbbreviation, currencyName}, + { + ...syncWallet.credentials, + currencyAbbreviation, + currencyName, + } as any, _tokenOptionsByAddress, ), ); @@ -1165,7 +1170,7 @@ const AccountDetails: React.FC = ({route}) => { const onPressItem = (walletId: string) => { haptic('impactLight'); const fullWalletObj = findWalletById(keyFullWalletObjs, walletId) as Wallet; - if (!fullWalletObj.isComplete()) { + if (!fullWalletObj.isComplete() && fullWalletObj.pendingTssSession) { fullWalletObj.getStatus( {network: fullWalletObj.network}, (err: any, status: Status) => { diff --git a/src/navigation/wallet/screens/AddingOptions.tsx b/src/navigation/wallet/screens/AddingOptions.tsx index 98bc29e39d..64a823edd0 100644 --- a/src/navigation/wallet/screens/AddingOptions.tsx +++ b/src/navigation/wallet/screens/AddingOptions.tsx @@ -17,7 +17,7 @@ import {Key, KeyMethods, Wallet} from '../../../store/wallet/wallet.models'; import {CommonActions, RouteProp} from '@react-navigation/core'; import {WalletGroupParamList, WalletScreens} from '../WalletGroup'; import MultisigOptions from './MultisigOptions'; -import {Option} from './CreationOptions'; +import {Option, MultisigModalType} from './CreationOptions'; import {useTranslation} from 'react-i18next'; import {useAppDispatch, useAppSelector} from '../../../utils/hooks'; import {Analytics} from '../../../store/analytics/analytics.effects'; @@ -40,6 +40,7 @@ import { import {getNavigationTabName, RootStacks} from '../../../Root'; import {useOngoingProcess} from '../../../contexts'; import {logManager} from '../../../managers/LogManager'; +import {isTSSKey} from '../../../store/wallet/effects/tss-send/tss-send'; export type AddingOptionsParamList = { key: Key; @@ -52,8 +53,11 @@ const AddingOptions: React.FC = () => { const {showOngoingProcess, hideOngoingProcess} = useOngoingProcess(); const route = useRoute>(); const {key} = route.params; - const [showMultisigOptions, setShowMultisigOptions] = useState(false); + const [multisigModalType, setMultisigModalType] = + useState(null); + const [showMultisigModal, setShowMultisigModal] = useState(false); const network = useAppSelector(({APP}) => APP.network); + useLayoutEffect(() => { navigation.setOptions({ headerTitle: () => {t('Select Wallet Type')}, @@ -61,12 +65,30 @@ const AddingOptions: React.FC = () => { }); }, [navigation, t]); - const optionList: Option[] = [ + const showErrorModal = (e: string) => { + dispatch( + showBottomNotificationModal({ + type: 'warning', + title: t('Something went wrong'), + message: e, + enableBackdropDismiss: true, + actions: [ + { + text: t('OK'), + action: () => {}, + primary: true, + }, + ], + }), + ); + }; + + const standardOptions: Option[] = [ { id: 'utxo-wallet', - title: t('UTXO Wallet'), + title: t('Single-Currency Wallet'), description: t( - 'Dedicated to a single cryptocurrency like Bitcoin, Bitcoin Cash, Litecoin, and Dogecoin. Perfect for users focusing on one specific coin', + 'Dedicated to a single cryptocurrency like Bitcoin, Bitcoin Cash, Litecoin, Dogecoin or XRP. Perfect for users focusing on one specific coin', ), cta: () => { dispatch( @@ -268,31 +290,34 @@ const AddingOptions: React.FC = () => { }, { id: 'multisig-wallet', - title: t('Multisig Wallet'), + title: t('Multisignature Wallet'), description: t( - 'Requires multiple approvals for transactions for wallets like Bitcoin, Bitcoin Cash, Litecoin, and Dogecoin. Ideal for shared funds or enhanced security', + 'Support for Bitcoin, Litecoin, Dogecoin and Bitcoin Cash networks. Each co-signer/device has a unique private key/recovery phrase, and all signatures are recorded directly on the blockchain.', ), - cta: () => setShowMultisigOptions(true), + cta: () => setShowMultisigModal(true), }, ]; - const showErrorModal = (e: string) => { - dispatch( - showBottomNotificationModal({ - type: 'warning', - title: t('Something went wrong'), - message: e, - enableBackdropDismiss: true, - actions: [ - { - text: t('OK'), - action: () => {}, - primary: true, - }, - ], - }), - ); - }; + const tssOptions: Option[] = [ + { + id: 'addMultisig', + title: t('Add Multisig Wallet'), + description: t( + 'Create a new wallet that requires multiple signatures for transactions', + ), + cta: () => setMultisigModalType('create'), + }, + { + id: 'joinMultisig', + title: t('Join Shared Wallet'), + description: t( + 'Join an existing multisig wallet using an invitation from another user', + ), + cta: () => setMultisigModalType('join'), + }, + ]; + + const optionList = isTSSKey(key) ? tssOptions : standardOptions; return ( <> @@ -315,11 +340,20 @@ const AddingOptions: React.FC = () => { ))} - + {isTSSKey(key) ? ( + setMultisigModalType(null)} + walletKey={key} + /> + ) : ( + setShowMultisigModal(false)} + /> + )} ); }; diff --git a/src/navigation/wallet/screens/BackupSharedKey.tsx b/src/navigation/wallet/screens/BackupSharedKey.tsx new file mode 100644 index 0000000000..07a57386e8 --- /dev/null +++ b/src/navigation/wallet/screens/BackupSharedKey.tsx @@ -0,0 +1,172 @@ +import React, {useLayoutEffect} from 'react'; +import styled from 'styled-components/native'; +import {H3, Paragraph, TextAlign} from '../../../components/styled/Text'; +import { + CtaContainer as _CtaContainer, + HeaderRightContainer, + ImageContainer, + TextContainer, + TitleContainer, +} from '../../../components/styled/Containers'; +import Button from '../../../components/button/Button'; +import {useAndroidBackHandler} from 'react-navigation-backhandler'; +import {useDispatch} from 'react-redux'; +import haptic from '../../../components/haptic-feedback/haptic'; +import {showBottomNotificationModal} from '../../../store/app/app.actions'; +import {WalletGroupParamList, WalletScreens} from '../WalletGroup'; +import {Key} from '../../../store/wallet/wallet.models'; +import {useNavigation} from '@react-navigation/native'; +import {CommonActions} from '@react-navigation/native'; +import {NativeStackScreenProps} from '@react-navigation/native-stack'; +import {useTranslation} from 'react-i18next'; +import {RootStacks} from '../../../Root'; +import {TabsScreens} from '../../tabs/TabsStack'; +import BackupKeyShare from '../../../../assets/img/backup-keyshare.svg'; +import Banner from '../../../components/banner/Banner'; + +type BackupSharedKeyScreenProps = NativeStackScreenProps< + WalletGroupParamList, + WalletScreens.BACKUP_SHARED_KEY +>; + +export type BackupSharedKeyParamList = { + context: 'createNewTSSKey' | 'joinTSSKey' | 'backupExistingTSSKey'; + key: Key; +}; + +const BackupContainer = styled.SafeAreaView` + flex: 1; +`; + +const ScrollViewContainer = styled.ScrollView` + padding: 0 15px; +`; + +const ContentContainer = styled.View` + align-items: center; +`; + +const CtaContainer = styled(_CtaContainer)` + padding: 10px 0; +`; + +const BackupSharedKeyScreen = ({route}: BackupSharedKeyScreenProps) => { + const {t} = useTranslation(); + const dispatch = useDispatch(); + const navigation = useNavigation(); + + const {context, key} = route.params; + + const navigateToKeyOverview = () => { + navigation.dispatch( + CommonActions.reset({ + index: 1, + routes: [ + {name: RootStacks.TABS, params: {screen: TabsScreens.HOME}}, + {name: WalletScreens.KEY_OVERVIEW, params: {id: key.id}}, + ], + }), + ); + }; + + const gotoBackup = () => { + navigation.navigate(WalletScreens.EXPORT_TSS_WALLET, { + context, + keyId: key.id, + }); + }; + + useLayoutEffect(() => { + navigation.setOptions({ + gestureEnabled: context === 'backupExistingTSSKey', + headerLeft: context === 'backupExistingTSSKey' ? undefined : () => null, + headerRight: () => + context !== 'backupExistingTSSKey' ? ( + + + + ) : null, + }); + }, [navigation, t, context]); + + useAndroidBackHandler(() => true); + + return ( + + + + + + + + +

{t('Would you like to backup your keyshare?')}

+
+
+ + + + {t( + "If you delete the BitPay app or lose your device, you'll need your keyshare file to regain access to your funds.", + )} + + + +
+ + + {}, + }} + /> + + + + + +
+
+ ); +}; + +export default BackupSharedKeyScreen; diff --git a/src/navigation/wallet/screens/CreateMultisig.tsx b/src/navigation/wallet/screens/CreateMultisig.tsx index 5c82469a8a..52bbe10907 100644 --- a/src/navigation/wallet/screens/CreateMultisig.tsx +++ b/src/navigation/wallet/screens/CreateMultisig.tsx @@ -43,6 +43,7 @@ import {WalletGroupParamList, WalletScreens} from '../WalletGroup'; import {openUrlWithInAppBrowser} from '../../../store/app/app.effects'; import { startCreateKeyMultisig, + startCreateTSSKey, addWalletMultisig, getDecryptPassword, } from '../../../store/wallet/effects'; @@ -59,12 +60,14 @@ import {Analytics} from '../../../store/analytics/analytics.effects'; import {NativeStackScreenProps} from '@react-navigation/native-stack'; import {RootStacks} from '../../../Root'; import {TabsScreens} from '../../../navigation/tabs/TabsStack'; -import {IsSegwitCoin} from '../../../store/wallet/utils/currency'; +import {IsSegwitCoin, GetName} from '../../../store/wallet/utils/currency'; import {useOngoingProcess} from '../../../contexts'; +import Banner from '../../../components/banner/Banner'; export interface CreateMultisigParamsList { + context: 'addTSSWalletMultisig' | 'addWalletMultisig'; currency: string; - key: Key; + key?: Key; } const schema = yup.object().shape({ @@ -76,8 +79,8 @@ const schema = yup.object().shape({ .positive() .integer() .min(1) - .max(3), // m - totalCopayers: yup.number().required().positive().integer().min(2).max(6), // n + .max(3), + totalCopayers: yup.number().required().positive().integer().min(2).max(6), }); export const MultisigContainer = styled.SafeAreaView` @@ -184,7 +187,7 @@ const CreateMultisig: React.FC = ({navigation, route}) => { const {t} = useTranslation(); const logger = useLogger(); const {showOngoingProcess, hideOngoingProcess} = useOngoingProcess(); - const {currency, key} = route.params; + const {currency, key, context} = route.params; const segwitSupported = IsSegwitCoin(currency); const [showOptions, setShowOptions] = useState(false); const [testnetEnabled, setTestnetEnabled] = useState(false); @@ -194,6 +197,9 @@ const CreateMultisig: React.FC = ({navigation, route}) => { networkName: 'livenet', singleAddress: false, }); + + const isTSS = context === 'addTSSWalletMultisig'; + const { control, handleSubmit, @@ -245,15 +251,71 @@ const CreateMultisig: React.FC = ({navigation, route}) => { CreateMultisigWallet(opts); }; + const CreateTSSMultisigWallet = async ( + opts: Partial, + ): Promise => { + try { + showOngoingProcess('CREATING_KEY'); + + const {key: tssKey} = await dispatch( + startCreateTSSKey({ + chain: opts.coin!, + network: opts.networkName!, + m: opts.m, + n: opts.n, + password: opts.password, + myName: opts.myName, + walletName: opts.name, + }), + ); + + hideOngoingProcess(); + + dispatch( + Analytics.track('Started TSS Wallet Creation', { + coin: currency?.toLowerCase(), + type: `${opts.m}-${opts.n}`, + }), + ); + + navigation.dispatch( + CommonActions.reset({ + index: 1, + routes: [ + { + name: RootStacks.TABS, + params: {screen: TabsScreens.HOME}, + }, + { + name: WalletScreens.INVITE_COSIGNERS, + params: { + keyId: tssKey.id, + }, + }, + ], + }), + ); + } catch (e: any) { + logger.error(e.message); + hideOngoingProcess(); + await sleep(500); + showErrorModal(e.message); + } + }; + const CreateMultisigWallet = async ( opts: Partial, ): Promise => { try { + if (isTSS) { + await CreateTSSMultisigWallet(opts); + return; + } + if (key) { if (key.isPrivKeyEncrypted) { opts.password = await dispatch(getDecryptPassword(key)); } - showOngoingProcess('ADDING_WALLET'); const wallet = (await dispatch( addWalletMultisig({ @@ -261,7 +323,6 @@ const CreateMultisig: React.FC = ({navigation, route}) => { opts, }), )) as Wallet; - dispatch( Analytics.track('Created Multisig Wallet', { coin: currency?.toLowerCase(), @@ -269,7 +330,6 @@ const CreateMultisig: React.FC = ({navigation, route}) => { addedToExistingKey: true, }), ); - wallet.getStatus( {network: wallet.network}, (err: any, status: Status) => { @@ -318,7 +378,6 @@ const CreateMultisig: React.FC = ({navigation, route}) => { const multisigKey = (await dispatch( startCreateKeyMultisig(opts), )) as Key; - dispatch( Analytics.track('Created Multisig Wallet', { coin: currency?.toLowerCase(), @@ -326,16 +385,13 @@ const CreateMultisig: React.FC = ({navigation, route}) => { addedToExistingKey: false, }), ); - dispatch( Analytics.track('Created Key', { context: 'createMultisig', coins: [currency?.toLowerCase()], }), ); - dispatch(setHomeCarouselConfig({id: multisigKey.id, show: true})); - navigation.navigate('BackupKey', { context: 'createNewMultisigKey', key: multisigKey, @@ -378,12 +434,6 @@ const CreateMultisig: React.FC = ({navigation, route}) => { return ( - - {t( - "Multisig wallets require multisig devices to set up. It takes longer to complete but it's the recommended security configuration for long term storage.", - )} - - = ({navigation, route}) => { render={({field: {value}}) => ( - {t('Total number of copayers')} + {t('Total number of co-signers')} = ({navigation, route}) => { {errors?.totalCopayers?.message} )} - + {!isTSS && ( { @@ -637,7 +687,19 @@ const CreateMultisig: React.FC = ({navigation, route}) => { )} - + )} + + {}, + }} + /> @@ -612,151 +352,56 @@ const CurrencySelection = ({route}: CurrencySelectionScreenProps) => { }); }, [navigation, t, context, headerTitle]); - const onToggle = ( - currencyAbbreviation: string, - chain?: string, - tokenAddress?: string, - ) => { + const onToggle = (currencyAbbreviation: string, chain?: string) => { + if (context === 'addWalletMultisig') { + navigation.navigate('CreateMultisig', { + currency: currencyAbbreviation.toLowerCase(), + key, + context, + }); + return; + } + + if (context === 'addTSSWalletMultisig') { + navigation.navigate('CreateMultisig', { + currency: currencyAbbreviation.toLowerCase(), + key, + context, + }); + return; + } + + if (context === 'addUtxoWallet') { + navigation.navigate('AddWallet', { + key, + currencyAbbreviation: currencyAbbreviation.toLowerCase(), + currencyName: allListItems.find( + item => item.currency.currencyAbbreviation === currencyAbbreviation, + )?.currency.currencyName, + }); + return; + } + setAllListItems(previous => previous.map(item => { + if (item.isHeader) { + return item; + } + const isCurrencyMatch = item.currency.currencyAbbreviation === currencyAbbreviation && item.currency.chain === chain; - const tokenMatch = item.tokens.find( - token => - token.currencyAbbreviation === currencyAbbreviation && - item.currency.chain === chain && - token.tokenAddress === tokenAddress, - ); - - // if multi, just toggle the selected item and rerender - if (selectionMode === 'multi') { - if (isCurrencyMatch) { - const hasSelectedTokens = item.tokens.some(token => token.selected); - - if (item.currency.selected && hasSelectedTokens) { - // do nothing - } else { - item.currency = { - ...item.currency, - selected: !item.currency.selected, - }; - } - } - - if (tokenMatch) { - // if selecting a token, make sure its chain is also selected - if (!item.currency.selected) { - item.currency = { - ...item.currency, - selected: true, - }; - } - const updatedToken = { - ...tokenMatch, - selected: !tokenMatch.selected, - }; - - // update token state - item.tokens = item.tokens.map(token => { - return token.tokenAddress === tokenAddress ? updatedToken : token; - }); - - // update popular token state - // append tokens once selected so user can see their entire selection - let appendToPopular = true; - item.popularTokens = item.popularTokens.map(token => { - if (token.tokenAddress === tokenAddress) { - appendToPopular = false; - } - - return token.tokenAddress === tokenAddress ? updatedToken : token; - }); - - if (appendToPopular) { - item.popularTokens.push(updatedToken); - } - } - } - - // if single, toggle the selected item, deselect any selected items, and rerender - if (selectionMode === 'single') { - if (isCurrencyMatch) { - item.currency = { - ...item.currency, - selected: !item.currency.selected, - }; - - // deselect any selected tokens - if (item.tokens.some(token => token.selected)) { - item.tokens = item.tokens.map(token => { - return token.selected ? {...token, selected: false} : token; - }); - } - - // deselect any selected popular tokens - if (item.popularTokens.some(token => token.selected)) { - item.popularTokens = item.popularTokens.map(token => { - return token.selected ? {...token, selected: false} : token; - }); - } - } else { - // deselect this item's currency - if (item.currency.selected) { - item.currency = { - ...item.currency, - selected: false, - }; - } - } - - if (tokenMatch) { - const updatedToken = { - ...tokenMatch, - selected: !tokenMatch.selected, - }; - - // update token state - item.tokens = item.tokens.map(token => { - if (token.tokenAddress === tokenAddress) { - return updatedToken; - } - - return token.selected ? {...token, selected: false} : token; - }); - - // update popular token state - // append tokens once selected so user can see their entire selection - let appendToPopular = true; - item.popularTokens = item.popularTokens.map(token => { - if (token.tokenAddress === tokenAddress) { - appendToPopular = false; - return updatedToken; - } - - return token.selected ? {...token, selected: false} : token; - }); - - if (appendToPopular) { - item.popularTokens.push(updatedToken); - } - } - - // if selecting a token, make sure deselect any other token selected - if ( - !tokenMatch && - !isCurrencyMatch && - item.currency.chain !== chain && - item.tokens.length > 0 - ) { - item.popularTokens = item.popularTokens.map(token => { - return token.selected ? {...token, selected: false} : token; - }); - item.tokens = item.tokens.map(token => { - return token.selected ? {...token, selected: false} : token; - }); - } + if (isCurrencyMatch) { + item.currency = { + ...item.currency, + selected: !item.currency.selected, + }; + } else if (item.currency.selected) { + item.currency = { + ...item.currency, + selected: false, + }; } return item; @@ -768,178 +413,45 @@ const CurrencySelection = ({route}: CurrencySelectionScreenProps) => { onToggleRef.current = onToggle; const memoizedOnToggle = useCallback( - (currencyAbbreviation: string, chain?: string, tokenAddress?: string) => { - onToggleRef.current(currencyAbbreviation, chain, tokenAddress); + (currencyAbbreviation: string, chain?: string) => { + onToggleRef.current(currencyAbbreviation, chain); }, [], ); - const memoizedOnViewAllPressed = useMemo(() => { - return (currency: CurrencySelectionItem) => { - const item = allListItemsRef.current.find( - i => - i.currency.currencyAbbreviation === currency.currencyAbbreviation && - i.currency.chain === currency.chain, - ); - - if (!item) { - return; - } - - // sorted selected tokens to the top for ease of use - const sortedTokens = orderBy( - item.tokens.map(token => ({...token})), - 'selected', - 'desc', - ); - - navigation.navigate(WalletScreens.CURRENCY_TOKEN_SELECTION, { - key, - currency: {...currency}, - tokens: sortedTokens, - description: item.description, - selectionMode, - onToggle: memoizedOnToggle, - }); - }; - }, [memoizedOnToggle, navigation, key, selectionMode]); + const isMultisigContext = + context === 'addWalletMultisig' || context === 'addTSSWalletMultisig'; const renderItem: ListRenderItem = useCallback( ({item}) => { + if (item.isHeader) { + return {item.headerTitle}; + } + return ( ); }, - [ - memoizedOnToggle, - memoizedOnViewAllPressed, - selectionMode, - searchVal, - selectedChainFilterOption, - ], + [memoizedOnToggle, key?.hardwareSource, isMultisigContext], ); - const filterAndSortTokens = ( - tokens: CurrencySelectionItem[], - searchVal: string, - ): CurrencySelectionItem[] => { - const filteredTokens = tokens.filter( - item => - item.currencyAbbreviation.toLowerCase().includes(searchVal) || - item.currencyName.toLowerCase().includes(searchVal) || - item.tokenAddress?.toLowerCase().includes(searchVal), - ); - return filteredTokens.sort((a, b) => { - const aStarts = a.currencyAbbreviation - .toLowerCase() - .startsWith(searchVal); - const bStarts = b.currencyAbbreviation - .toLowerCase() - .startsWith(searchVal); - if (aStarts && bStarts) { - return a.currencyAbbreviation.localeCompare(b.currencyAbbreviation); - } - if (aStarts && !bStarts) { - return -1; - } - if (!aStarts && bStarts) { - return 1; - } - return a.currencyAbbreviation.localeCompare(b.currencyAbbreviation); - }); - }; - - const filteredItems = useMemo(() => { - const _allListItems = cloneDeep(allListItems); - if (!selectedChainFilterOption && !searchVal) { - return _allListItems; - } - if (selectedChainFilterOption && !searchVal) { - return _allListItems.filter( - item => item.currency.chain === selectedChainFilterOption, - ); - } - - return _allListItems - .map(allListItem => { - if ( - selectedChainFilterOption && - selectedChainFilterOption !== allListItem.currency.chain - ) { - return null; - } - const searchValLowerCase = searchVal.toLowerCase(); - const currency = allListItem.currency; - const matchesSearch = [ - currency.currencyAbbreviation, - currency.chain, - currency.chainName, - currency.currencyName, - ].some((property: string | undefined) => - property?.toLowerCase()?.includes(searchValLowerCase), - ); - if (allListItem.tokens.length > 0) { - allListItem.tokens = filterAndSortTokens( - allListItem.tokens, - searchValLowerCase, - ); - return allListItem.tokens.length > 0 || matchesSearch - ? allListItem - : null; - } else { - return matchesSearch ? allListItem : null; - } - }) - .filter(Boolean) as CurrencySelectionListItem[]; - }, [searchVal, selectedChainFilterOption, allListItems]); - return ( - {context !== 'addWalletMultisig' && allListItems.length > 0 ? ( - - - searchVal={searchVal} - setSearchVal={setSearchVal} - searchResults={searchResults} - setSearchResults={setSearchResults} - searchFullList={allListItems} - context={context} - /> - - ) : null} - - {allListItems.length > 0 || filteredItems.length > 0 ? ( + {allListItems.length > 0 ? ( - contentContainerStyle={ - context === 'addWalletMultisig' ? {marginTop: 20} : undefined - } - data={ - !searchVal && !selectedChainFilterOption - ? allListItems - : filteredItems - } + contentContainerStyle={{marginTop: isMultisigContext ? 10 : 20}} + data={allListItems} keyExtractor={keyExtractor} renderItem={renderItem} /> - ) : ( - <> - )} + ) : null} {onCtaPress && selectedCurrencies.length > 0 ? ( { keyObj.wallets .filter( (wallet: any) => - !wallet.credentials.token && wallet.credentials.isComplete(), + !wallet.credentials.token && + wallet.credentials.isComplete() && + !wallet.pendingTssSession, ) .forEach(walletClient => { if (notificationsAccepted && brazeEid) { diff --git a/src/navigation/wallet/screens/GlobalSelect.tsx b/src/navigation/wallet/screens/GlobalSelect.tsx index ba625b6612..429860fe5b 100644 --- a/src/navigation/wallet/screens/GlobalSelect.tsx +++ b/src/navigation/wallet/screens/GlobalSelect.tsx @@ -661,7 +661,9 @@ const GlobalSelect: React.FC = ({ const filterCompleteWallets = (keys: Keys) => { return Object.fromEntries( Object.entries(keys).filter(([_, keys]) => - keys.wallets.some(wallet => wallet.isComplete()), + keys.wallets.some( + wallet => wallet.isComplete() && !wallet.pendingTssSession, + ), ), ); }; diff --git a/src/navigation/wallet/screens/InviteCosigners.tsx b/src/navigation/wallet/screens/InviteCosigners.tsx new file mode 100644 index 0000000000..1f4ed5805e --- /dev/null +++ b/src/navigation/wallet/screens/InviteCosigners.tsx @@ -0,0 +1,878 @@ +import React, {useState, useEffect} from 'react'; +import {ScrollView, TextInput, Modal, Share} from 'react-native'; +import styled from 'styled-components/native'; +import {NativeStackScreenProps} from '@react-navigation/native-stack'; +import {useTranslation} from 'react-i18next'; +import QRCode from 'react-native-qrcode-svg'; +import {WalletGroupParamList, WalletScreens} from '../WalletGroup'; +import {useAppDispatch, useAppSelector} from '../../../utils/hooks'; +import { + startTSSCeremony, + addCoSignerToTSS, +} from '../../../store/wallet/effects/create-multisig/create-multisig'; +import { + White, + SlateDark, + LightBlack, + LuckySevens, + Action, + Slate30, + Success25, + Black, + NeutralSlate, + LinkBlue, +} from '../../../styles/colors'; +import {Paragraph, BaseText} from '../../../components/styled/Text'; +import { + HeaderRightContainer, + ScreenGutter, +} from '../../../components/styled/Containers'; +import Button from '../../../components/button/Button'; +import {useLogger} from '../../../utils/hooks/useLogger'; +import {showBottomNotificationModal} from '../../../store/app/app.actions'; +import {Key, TSSCopayerInfo} from '../../../store/wallet/wallet.models'; +import AddIconBlackSvg from '../../../../assets/img/add-black.svg'; +import AddIconGreySvg from '../../../../assets/img/add-grey.svg'; +import SuccessLightIcon from '../../../../assets/img/check-dark.svg'; +import SuccessDarkIcon from '../../../../assets/img/check.svg'; +import ClockLightIcon from '../../../../assets/img/clock-blue.svg'; +import ClockDarkIcon from '../../../../assets/img/clock-light-blue.svg'; +import QrCodeSvg from '../../../../assets/img/qr-code-black.svg'; +import ShareIcon from '../../../../assets/img/share-icon.svg'; +import haptic from '../../../components/haptic-feedback/haptic'; +import {useTheme} from 'styled-components/native'; +import {sleep} from '../../../utils/helper-methods'; + +const Container = styled.SafeAreaView` + flex: 1; +`; + +const Content = styled(ScrollView)` + padding: ${ScreenGutter}; +`; + +const HeaderContainer = styled.View` + margin-bottom: 24px; +`; + +const Subtitle = styled(Paragraph)` + font-size: 16px; + font-weight: 400; + color: ${({theme: {dark}}) => (dark ? Slate30 : SlateDark)}; +`; + +const CoSignerContainerTitle = styled(Paragraph)` + color: ${({theme: {dark}}) => (dark ? Slate30 : SlateDark)}; +`; + +const CoSignerContainer = styled.View` + padding: 16px; + border-radius: 12px; + border-width: 1px; + border-color: ${({theme: {dark}}) => + dark ? 'rgba(255,255,255,0.1)' : Slate30}; + gap: 17px; +`; + +const CoSignerRow = styled.TouchableOpacity` + border-radius: 12px; +`; + +const CoSignerInfo = styled.View` + flex: 1; +`; + +const NameRow = styled.View` + flex-direction: row; + align-items: center; + justify-content: space-between; +`; + +const CoSignerName = styled(BaseText)` + color: ${({theme: {dark}}) => (dark ? Slate30 : SlateDark)}; + font-size: 16px; + font-weight: 500; + flex: 1; +`; + +const CoSignerStatus = styled(BaseText)` + color: ${SlateDark}; + font-size: 14px; + margin-top: 4px; +`; + +const AddButton = styled.View` + width: 40px; + height: 40px; + padding: 8px; + border-radius: 12px; + background-color: ${({theme: {dark}}) => (dark ? LightBlack : '#F5F5F5')}; + align-items: center; + justify-content: center; +`; + +const CheckMark = styled.View` + width: 40px; + height: 40px; + padding: 12px; + border-radius: 8px; + background-color: ${({theme: {dark}}) => (dark ? LightBlack : Success25)}; + align-items: center; + justify-content: center; +`; + +const ButtonContainer = styled.View` + padding: ${ScreenGutter}; +`; + +const ModalContainer = styled.View` + flex: 1; + background-color: ${({theme: {dark}}) => (dark ? Black : White)}; +`; + +const ModalHeader = styled.View` + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 16px ${ScreenGutter}; +`; + +const ModalTitle = styled(BaseText)` + font-size: 18px; + font-weight: 600; + color: ${({theme: {dark}}) => (dark ? White : Black)}; + text-align: center; + flex: 1; +`; + +const HeaderButton = styled.TouchableOpacity` + padding: 8px; + min-width: 60px; +`; + +const HeaderButtonText = styled(BaseText)` + font-size: 16px; + color: ${Action}; +`; + +const HeaderButtonRight = styled(HeaderButton)` + align-items: flex-end; +`; + +const PlaceholderView = styled.View` + min-width: 60px; +`; + +const ModalContent = styled.ScrollView` + flex: 1; +`; + +const TopSection = styled.View` + padding: 24px ${ScreenGutter}; +`; + +const TopSectionInputContainer = styled.View``; + +const TopSectionContainer = styled.View` + align-items: center; + border-radius: 12px; + border-width: 1px; + border-color: ${({theme: {dark}}) => + dark ? 'rgba(255,255,255,0.1)' : Slate30}; + min-height: 340px; + justify-content: center; +`; + +const InputContainer = styled.View` + padding: 16px; + width: 100%; +`; + +const InputLabel = styled(BaseText)` + font-size: 16px; + font-weight: 400; + color: ${({theme: {dark}}) => (dark ? White : Black)}; + margin-bottom: 12px; +`; + +const InputWrapper = styled.View` + flex-direction: row; + align-items: center; + border-radius: 8px; + border-width: 1px; + border-color: ${({theme: {dark}}) => (dark ? NeutralSlate : Black)}; + padding: 12px; + margin-bottom: 16px; +`; + +const StyledInput = styled(TextInput)` + flex: 1; + color: ${({theme: {dark}}) => (dark ? White : Black)}; + font-size: 16px; + padding: 0; +`; + +const ScanButton = styled.TouchableOpacity` + padding: 4px; +`; + +const StatusContainer = styled.View` + align-items: center; + justify-content: center; + flex-direction: column; + flex: 1; + width: 100%; +`; + +const StatusText = styled(BaseText)` + font-size: 16px; + font-weight: 400; + color: ${({theme: {dark}}) => (dark ? White : Black)}; + margin-top: 10px; +`; + +const StatusSubText = styled(BaseText)` + font-size: 13px; + font-weight: 400; + color: ${SlateDark}; + margin-top: 4px; +`; + +const QRSectionContainer = styled.View` + align-items: center; + border-radius: 12px; + border-width: 1px; + border-color: ${({theme: {dark}}) => + dark ? 'rgba(255,255,255,0.1)' : Slate30}; + min-height: 355px; +`; + +const QRContainer = styled.View` + align-items: center; + justify-content: center; + padding: 32px; + background-color: ${White}; + border-radius: 12px; + margin: 16px; +`; + +const ShareContainer = styled.View` + flex-direction: row; + justify-content: center; + align-items: center; + width: 100%; + padding-top: 16px; + padding-bottom: 16px; + border-top-width: 1px; + border-top-color: ${({theme: {dark}}) => + dark ? 'rgba(255,255,255,0.1)' : Slate30}; +`; + +const QRCodeWrapper = styled.View` + background-color: ${White}; + padding: 16px; + border-radius: 12px; + margin-bottom: 16px; +`; + +const ShareButton = styled.TouchableOpacity` + flex-direction: row; + align-items: center; + justify-content: center; + padding: 12px 24px; +`; + +const ShareButtonText = styled(BaseText)` + font-size: 16px; + font-weight: 500; + color: ${({theme: {dark}}) => (dark ? LinkBlue : Action)}; + margin-left: 8px; +`; + +const StepsSection = styled.View` + padding: 24px ${ScreenGutter}; +`; + +const StepsContainer = styled.View` + padding: 16px; + border-radius: 12px; + border-width: 1px; + border-color: ${({theme: {dark}}) => + dark ? 'rgba(255,255,255,0.1)' : Slate30}; +`; + +const StepsSectionTitle = styled(BaseText)` + font-size: 16px; + font-weight: 500; + color: ${({theme: {dark}}) => (dark ? White : Black)}; + margin-bottom: 12px; +`; + +const StepRow = styled.View` + flex-direction: row; + align-items: flex-start; +`; + +const StepRail = styled.View` + width: 40px; + align-items: center; + margin-right: 12px; +`; + +const StepConnector = styled.View<{completed?: boolean}>` + width: 2px; + flex-grow: 1; + margin-top: 0px; + background-color: ${({theme: {dark}, completed}) => + completed ? (dark ? '#004D27' : Success25) : dark ? '#2A2A2A' : '#F5F5F5'}; +`; + +const StepIndicator = styled.View<{active?: boolean; completed?: boolean}>` + width: 40px; + height: 40px; + border-radius: 20px; + background-color: ${({theme: {dark}, active, completed}) => + active + ? '#2240C440' + : completed + ? dark + ? '#004D27' + : Success25 + : dark + ? '#2A2A2A' + : '#F5F5F5'}; + align-items: center; + justify-content: center; +`; + +const StepContent = styled.View` + flex: 1; + padding-bottom: 20px; +`; + +const StepNumber = styled(BaseText)` + color: ${({theme: {dark}}) => (dark ? LuckySevens : Black)}; + font-size: 16px; + font-weight: 400; +`; + +const StepTitle = styled(BaseText)` + font-size: 16px; + font-weight: 400; + color: ${({theme: {dark}}) => (dark ? White : Black)}; +`; + +const StepSubtitle = styled(BaseText)` + font-size: 14px; + color: ${SlateDark}; + line-height: 20px; +`; + +const ButtonWrapper = styled.View` + width: 100%; + padding: 0 16px; + margin-top: 20px; +`; + +const AlreadySharedButton = styled.TouchableOpacity` + padding: 8px 16px; + align-items: center; +`; + +const AlreadySharedText = styled(BaseText)` + color: ${({theme: {dark}}) => (dark ? LinkBlue : Action)}; + font-size: 14px; + font-weight: 400; +`; +export interface InviteCoSignersParamsList { + keyId: string; +} + +type Props = NativeStackScreenProps< + WalletGroupParamList, + WalletScreens.INVITE_COSIGNERS +>; + +const InviteCosigners: React.FC = ({navigation, route}) => { + const dispatch = useAppDispatch(); + const {t} = useTranslation(); + const logger = useLogger(); + const theme = useTheme(); + const AddIconSvg = theme.dark ? AddIconGreySvg : AddIconBlackSvg; + const ClockIconSvg = theme.dark ? ClockDarkIcon : ClockLightIcon; + const SuccessIcon = theme.dark ? SuccessDarkIcon : SuccessLightIcon; + + const {keyId} = route.params; + const key = useAppSelector(({WALLET}) => WALLET.keys[keyId]); + const tssSession = key?.tssSession; + + const [isModalVisible, setIsModalVisible] = useState(false); + const [selectedCopayer, setSelectedCopayer] = useState( + null, + ); + const [sessionId, setSessionId] = useState(''); + + const [currentStep, setCurrentStep] = useState(1); + const [showProcessing, setShowProcessing] = useState(false); + const [pendingJoinCode, setPendingJoinCode] = useState(null); + + const [isCeremonyStarted, setIsCeremonyStarted] = useState(false); + const [isCeremonyComplete, setIsCeremonyComplete] = useState(false); + const [createdKey, setCreatedKey] = useState(null); + const [isInviteShared, setIsInviteShared] = useState(false); + + useEffect(() => { + if (pendingJoinCode && currentStep === 2) { + const timer = setTimeout(() => { + setShowProcessing(false); + }, 1500); + return () => clearTimeout(timer); + } + }, [pendingJoinCode, currentStep]); + + if (!tssSession) { + return null; + } + + const {m, n, walletName, copayers = []} = tssSession; + const allInvited = copayers.every(c => c.status === 'invited'); + + const handleOpenModal = (copayer: TSSCopayerInfo) => { + if (copayer.status === 'invited') { + setSelectedCopayer(copayer); + setPendingJoinCode(copayer.joinCode!); + setIsModalVisible(true); + } else { + setSelectedCopayer(copayer); + setSessionId(''); + setCurrentStep(1); + setShowProcessing(false); + setPendingJoinCode(null); + setIsInviteShared(false); + setIsModalVisible(true); + } + }; + + const handleCloseModal = () => { + setIsModalVisible(false); + setSelectedCopayer(null); + setSessionId(''); + setCurrentStep(1); + setShowProcessing(false); + setPendingJoinCode(null); + setIsInviteShared(false); + }; + + const handleAlreadyShared = () => { + setIsInviteShared(true); + setCurrentStep(3); + }; + + const handleScanQR = () => { + setIsModalVisible(false); + navigation.navigate(WalletScreens.SCAN, { + onScanComplete: (data: string) => { + setSessionId(data); + setIsModalVisible(true); + }, + }); + }; + + const handleShare = async () => { + if (!pendingJoinCode) return; + try { + await Share.share({ + message: pendingJoinCode, + }); + } catch (err: any) { + logger.error(`Share error: ${err.message}`); + } + }; + + const handleAddCoSigner = async () => { + if (!sessionId.trim() || !selectedCopayer) { + dispatch( + showBottomNotificationModal({ + type: 'error', + title: t('Error'), + message: t("Please enter the co-signer's Session ID"), + enableBackdropDismiss: true, + actions: [{text: t('OK'), action: () => {}, primary: true}], + }), + ); + return; + } + + setCurrentStep(2); + await sleep(1000); + + setShowProcessing(true); + await sleep(1000); + + try { + const {joinCode} = await dispatch( + addCoSignerToTSS({ + keyId, + joinerSessionId: sessionId.trim(), + partyId: selectedCopayer.partyId, + }), + ); + + setPendingJoinCode(joinCode); + } catch (err: any) { + logger.error(`[TSS] Error adding co-signer: ${err.message}`); + setCurrentStep(1); + setShowProcessing(false); + dispatch( + showBottomNotificationModal({ + type: 'error', + title: t('Error'), + message: err.message || t('Failed to add co-signer'), + enableBackdropDismiss: true, + actions: [{text: t('OK'), action: () => {}, primary: true}], + }), + ); + } + }; + + const handleStartCeremony = async () => { + setIsCeremonyStarted(true); + setIsCeremonyComplete(false); + + try { + const updatedKey = await dispatch(startTSSCeremony(keyId)); + setCreatedKey(updatedKey); + setIsCeremonyComplete(true); + } catch (err: any) { + logger.error(`[TSS] Ceremony error: ${err.message}`); + setIsCeremonyStarted(false); + dispatch( + showBottomNotificationModal({ + type: 'error', + title: t('Error'), + message: err.message || t('Failed to create wallet'), + enableBackdropDismiss: true, + actions: [{text: t('OK'), action: () => {}, primary: true}], + }), + ); + } + }; + + const getModalTitle = () => { + if (currentStep === 3) { + return t('Add Another Co-signer'); + } + return t('Invite {{name}}', {name: selectedCopayer?.name || ''}); + }; + + const renderModalTopSection = () => { + if (currentStep === 1) { + return ( + + + + {t("Co-signer's Session ID")} + + + + + + + + + + + ); + } + + if (currentStep === 2 && !showProcessing && !pendingJoinCode) { + return ( + + + + + + + {t('Session ID accepted')} + + + + ); + } + + if (currentStep === 2 && showProcessing) { + return ( + + + + + + + {t('Processing...')} + {t('Creating invite code')} + + + + ); + } + + if (currentStep === 2 && pendingJoinCode) { + return ( + <> + + + + + + + + + {t('Share Invite Code')} + + + + + + + {t('Shared! Continue to next step')} + + + + ); + } + + if (currentStep === 3 && isInviteShared) { + return ( + + + + + + + + {t('{{name}} added', {name: selectedCopayer?.name || ''})} + + + + + + + + ); + } + + return null; + }; + + const renderStepsSection = () => { + return ( + + + {t('Setting Up Your Wallet')} + + + + 1}> + {currentStep === 1 ? ( + + ) : ( + + )} + + 1} /> + + + + {t('Enter Session ID')} + {t("Enter co-signer's session ID")} + + + + + + + {isInviteShared ? ( + + ) : currentStep === 2 ? ( + + ) : ( + 2 + )} + + + + + {t('Share Invite Code')} + + {t('Share your invite code with co-signer')} + + + + + + ); + }; + + const handleCeremonyComplete = () => { + if (!createdKey) return; + + navigation.navigate(WalletScreens.BACKUP_SHARED_KEY, { + context: 'createNewTSSKey', + key: createdKey, + }); + }; + + if (isCeremonyStarted) { + return ( + + + + + + {isCeremonyComplete ? ( + <> + + + + + {t('Shared wallet has been created')} + + + ) : ( + <> + + + + {t('Creating the wallet')} + + {t('Preparing the HODL chamber')} + + + )} + + + + {isCeremonyComplete && ( + + + + )} + + + ); + } + + return ( + + + + + {t( + 'Add the co-signers below. They will need join the wallet from their device and provide you with their session ID or QR code.', + )} + + + + + + {t( + 'All participants need to keep the app open to join the wallet.', + )} + + {copayers.map(copayer => ( + handleOpenModal(copayer)}> + + + {copayer.name} + {copayer.status === 'invited' ? ( + + + + ) : ( + + + + )} + + {copayer.status === 'invited' && ( + + {t('Tap to view invitation code')} + + )} + + + ))} + + + + {allInvited && ( + + + + )} + + + + + + {getModalTitle()} + + + + + + + {renderModalTopSection()} + {renderStepsSection()} + + + + + ); +}; + +export default InviteCosigners; diff --git a/src/navigation/wallet/screens/JoinTSSWallet.tsx b/src/navigation/wallet/screens/JoinTSSWallet.tsx new file mode 100644 index 0000000000..e0e74a059e --- /dev/null +++ b/src/navigation/wallet/screens/JoinTSSWallet.tsx @@ -0,0 +1,757 @@ +import React, {useState, useEffect} from 'react'; +import {Share, TextInput} from 'react-native'; +import styled from 'styled-components/native'; +import {NativeStackScreenProps} from '@react-navigation/native-stack'; +import {useTranslation} from 'react-i18next'; +import QRCode from 'react-native-qrcode-svg'; +import {useTheme} from 'styled-components/native'; +import {WalletGroupParamList, WalletScreens} from '../WalletGroup'; +import {useAppDispatch, useAppSelector} from '../../../utils/hooks'; +import { + generateJoinerSessionId, + joinTSSWithCode, +} from '../../../store/wallet/effects/create-multisig/create-multisig'; +import {RootStacks} from '../../../Root'; +import {TabsScreens} from '../../tabs/TabsStack'; +import { + White, + SlateDark, + LuckySevens, + Action, + Black, + NeutralSlate, + Slate30, + Success25, + LinkBlue, +} from '../../../styles/colors'; +import {BaseText, H5} from '../../../components/styled/Text'; +import { + CtaContainer, + ScreenGutter, +} from '../../../components/styled/Containers'; +import Button from '../../../components/button/Button'; +import {useLogger} from '../../../utils/hooks/useLogger'; +import {showBottomNotificationModal} from '../../../store/app/app.actions'; +import {useOngoingProcess} from '../../../contexts'; +import {Key} from '../../../store/wallet/wallet.models'; +import ShareIcon from '../../../../assets/img/share-icon.svg'; +import ClockLightIcon from '../../../../assets/img/clock-blue.svg'; +import ClockDarkIcon from '../../../../assets/img/clock-light-blue.svg'; +import SuccessLightIcon from '../../../../assets/img/check-dark.svg'; +import SuccessDarkIcon from '../../../../assets/img/check.svg'; +import QrCodeLightSvg from '../../../../assets/img/qr-code-black.svg'; +import QrCodeDarkSvg from '../../../../assets/img/qr-code-grey.svg'; +import {ScanScreens} from '../../../navigation/scan/ScanGroup'; +import {removePendingJoinerSession} from '../../../store/wallet/wallet.actions'; +import {Controller, useForm} from 'react-hook-form'; +import BoxInput from '../../../components/form/BoxInput'; + +const Container = styled.SafeAreaView` + flex: 1; +`; + +const Content = styled.ScrollView` + flex: 1; +`; + +const QRSection = styled.View` + padding: 24px ${ScreenGutter}; +`; + +const QRSectionContainer = styled.View` + align-items: center; + border-radius: 12px; + border-width: 1px; + border-color: ${({theme: {dark}}) => + dark ? 'rgba(255,255,255,0.1)' : Slate30}; + min-height: 355px; +`; + +const QRContainer = styled.View` + align-items: center; + justify-content: center; + padding: 32px; + background-color: ${White}; + border-radius: 12px; + margin: 16px; +`; + +const SessionAcceptedContainer = styled.View` + align-items: center; + justify-content: center; + flex-direction: column; + flex: 1; +`; + +const SessionAcceptedText = styled(BaseText)` + font-size: 16px; + font-weight: 400; + color: ${({theme: {dark}}) => (dark ? White : Black)}; +`; + +const SessionAcceptedSubText = styled(BaseText)` + font-size: 13px; + font-weight: 400; + color: ${({theme: {dark}}) => (dark ? White : SlateDark)}; +`; + +const ShareContainer = styled.View` + flex-direction: row; + justify-content: center; + align-items: center; + width: 100%; + padding-top: 16px; + padding-bottom: 16px; + border-top-width: 1px; + border-top-color: ${({theme: {dark}}) => + dark ? 'rgba(255,255,255,0.1)' : Slate30}; +`; + +const ShareButton = styled.TouchableOpacity` + flex-direction: row; + align-items: center; + padding: 8px 12px; +`; + +const ShareButtonText = styled(BaseText)` + margin-left: 8px; + color: ${({theme: {dark}}) => (dark ? LinkBlue : Action)}; + font-size: 16px; + font-weight: 500; +`; + +const InviteCodeSection = styled.View` + padding: 0 ${ScreenGutter}; +`; + +const InviteCodeContainer = styled.View` + border-radius: 12px; + padding: 16px; + min-height: 355px; +`; + +const InviteCodeLabel = styled(BaseText)` + font-size: 16px; + font-weight: 400; + color: ${({theme: {dark}}) => (dark ? White : Black)}; + margin-bottom: 12px; +`; + +const InputWrapper = styled.View` + flex-direction: row; + align-items: center; + border-radius: 8px; + border-width: 1px; + border-color: ${({theme: {dark}}) => (dark ? NeutralSlate : Black)}; + padding: 12px; + margin-bottom: 16px; +`; + +const StyledInput = styled(TextInput)` + flex: 1; + color: ${({theme: {dark}}) => (dark ? White : Black)}; + font-size: 16px; + padding: 0; +`; + +const ScanButton = styled.TouchableOpacity` + padding: 4px; +`; + +const StepsSection = styled.View` + padding: 24px ${ScreenGutter}; +`; + +const StepsContainer = styled.View` + padding: 16px; + border-radius: 12px; + border-width: 1px; + border-color: ${({theme: {dark}}) => + dark ? 'rgba(255,255,255,0.1)' : Slate30}; +`; + +const StepsSectionTitle = styled(BaseText)` + font-size: 16px; + font-weight: 500; + color: ${({theme: {dark}}) => (dark ? White : Black)}; + margin-bottom: 12px; +`; + +const StepRow = styled.View` + flex-direction: row; + align-items: flex-start; +`; + +const StepRail = styled.View` + width: 40px; + align-items: center; + margin-right: 12px; +`; + +const StepIndicator = styled.View<{active?: boolean; completed?: boolean}>` + width: 40px; + height: 40px; + border-radius: 20px; + background-color: ${({theme: {dark}, active, completed}) => + active + ? '#2240C440' + : completed + ? dark + ? '#004D27' + : Success25 + : dark + ? '#2A2A2A' + : '#F5F5F5'}; + align-items: center; + justify-content: center; +`; + +const StepConnector = styled.View<{completed?: boolean}>` + width: 2px; + flex-grow: 1; + margin-top: 0px; + background-color: ${({theme: {dark}, completed}) => + completed ? (dark ? '#004D27' : Success25) : dark ? '#2A2A2A' : '#F5F5F5'}; +`; + +const StepContent = styled.View` + flex: 1; + padding-bottom: 20px; +`; + +const StepNumber = styled(BaseText)` + color: ${({theme: {dark}}) => (dark ? White : Black)}; + font-size: 16px; + font-weight: 400; +`; + +const StepTitle = styled(BaseText)` + font-size: 16px; + font-weight: 400; + color: ${({theme: {dark}}) => (dark ? White : Black)}; +`; + +const StepSubtitle = styled(BaseText)` + font-size: 14px; + color: ${({theme: {dark}}) => (dark ? White : SlateDark)}; + line-height: 20px; +`; + +const LoadingContainer = styled.View` + flex: 1; + align-items: center; + justify-content: center; + padding: 40px; +`; + +const LoadingText = styled(H5)` + color: ${({theme: {dark}}) => (dark ? White : SlateDark)}; +`; + +const ButtonContainer = styled.View` + padding: 16px ${ScreenGutter}; +`; + +const AlreadySharedButton = styled.TouchableOpacity` + padding: 8px 16px; + align-items: center; +`; + +const AlreadySharedText = styled(BaseText)` + color: ${({theme: {dark}}) => (dark ? LinkBlue : Action)}; + font-size: 14px; + font-weight: 400; +`; + +const InputContainer = styled.View` + margin-top: 20px; +`; + +type JoinFormValues = { + myName: string; +}; + +type Props = NativeStackScreenProps< + WalletGroupParamList, + WalletScreens.JOIN_TSS_WALLET +>; + +const JoinTSSWallet: React.FC = ({navigation, route}) => { + const dispatch = useAppDispatch(); + const {t} = useTranslation(); + const logger = useLogger(); + const theme = useTheme(); + const { + control, + handleSubmit, + formState: {errors, isValid}, + } = useForm({ + mode: 'onChange', + defaultValues: {myName: ''}, + }); + + const [showSession, setShowSession] = useState(false); + const [localCopayerName, setLocalCopayerName] = useState(''); + + const ClockIconSvg = theme.dark ? ClockDarkIcon : ClockLightIcon; + const SuccessIcon = theme.dark ? SuccessDarkIcon : SuccessLightIcon; + const QrCodeSvg = theme.dark ? QrCodeDarkSvg : QrCodeLightSvg; + const copayerName = localCopayerName; + + const pendingJoinerSession = useAppSelector( + ({WALLET}) => WALLET.pendingJoinerSession, + ); + + const [sessionId, setSessionId] = useState(''); + const [partyKey, setPartyKey] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [currentStep, setCurrentStep] = useState(0); + // const [showInviteInput, setShowInviteInput] = useState(false); + const [showWaitingCopayers, setShowWaitingCopayers] = useState(false); + const [inviteCode, setInviteCode] = useState(''); + const [isWalletReady, setIsWalletReady] = useState(false); + const [createdKey, setCreatedKey] = useState(null); + const [showProcessing, setShowProcessing] = useState(false); + + useEffect(() => { + if (sessionId && currentStep === 0) { + const timer = setTimeout(() => { + setShowProcessing(false); + setCurrentStep(1); + }, 1500); + return () => clearTimeout(timer); + } + }, [sessionId, currentStep]); + + useEffect(() => { + if (currentStep === 3) { + const timer = setTimeout(() => { + setShowWaitingCopayers(true); + }, 3000); + return () => clearTimeout(timer); + } + }, [currentStep]); + + const initializeSession = async () => { + if (pendingJoinerSession) { + logger.debug('[TSS Join] Restoring persisted session'); + setSessionId(pendingJoinerSession.sessionId); + setPartyKey(pendingJoinerSession.partyKey); + setIsLoading(false); + return; + } + await generateNewSession(); + }; + + const generateNewSession = async () => { + setIsLoading(true); + try { + const result = await dispatch( + generateJoinerSessionId({name: copayerName}), + ); + setSessionId(result.sessionId); + setPartyKey(result.partyKey); + logger.debug('[TSS Join] Session ID generated'); + } catch (err: any) { + logger.error(`[TSS Join] Error: ${err.message}`); + } finally { + setIsLoading(false); + } + }; + + const handleShare = async () => { + try { + await Share.share({ + message: sessionId, + }); + } catch (err: any) { + logger.error(`Share error: ${err.message}`); + } + }; + + const handleScanQR = () => { + navigation.navigate(ScanScreens.Root, { + onScanComplete: (data: string) => { + setInviteCode(data); + }, + }); + }; + + const handleJoin = async () => { + if (!inviteCode.trim()) { + dispatch( + showBottomNotificationModal({ + type: 'error', + title: t('Error'), + message: t('Please enter the Invite Code'), + enableBackdropDismiss: true, + actions: [{text: t('OK'), action: () => {}, primary: true}], + }), + ); + return; + } + + setCurrentStep(3); + + try { + dispatch(removePendingJoinerSession()); + + const key = await dispatch( + joinTSSWithCode({ + joinCode: inviteCode.trim(), + partyKey, + myName: copayerName, + }), + ); + + setCreatedKey(key); + setIsWalletReady(true); + } catch (err: any) { + setCurrentStep(2); + logger.error(`[TSS Join] Error: ${err.message}`); + dispatch( + showBottomNotificationModal({ + type: 'error', + title: t('Error'), + message: err.message || t('Failed to join wallet'), + enableBackdropDismiss: true, + actions: [{text: t('OK'), action: () => {}, primary: true}], + }), + ); + } + }; + const onSubmitStart = async (values: JoinFormValues) => { + const trimmedCopayerName = values.myName.trim(); + + setLocalCopayerName(trimmedCopayerName); + setShowSession(true); + setShowProcessing(true); + + try { + const result = await dispatch( + generateJoinerSessionId({name: trimmedCopayerName}), + ); + + setSessionId(result.sessionId); + setPartyKey(result.partyKey); + } catch (err: any) { + logger.error(`[TSS Join] Error: ${err.message}`); + setShowSession(false); + setShowProcessing(false); + dispatch( + showBottomNotificationModal({ + type: 'error', + title: t('Error'), + message: err.message || t('Failed to generate session'), + enableBackdropDismiss: true, + actions: [{text: t('OK'), action: () => {}, primary: true}], + }), + ); + } + }; + + const handleContinue = () => { + if (!createdKey) { + return; + } + + navigation.navigate(WalletScreens.BACKUP_SHARED_KEY, { + context: 'joinTSSKey', + key: createdKey, + }); + }; + + if (isLoading) { + return ( + + + {t('Generating Session...')} + + + ); + } + + const renderStartForm = () => { + return ( + + + ( + + )} + /> + + + + + + ); + }; + + const renderTopSection = () => { + if (currentStep === 0 && showProcessing) { + return ( + + + + + + + + {t('Processing...')} + + + {t('Creating session ID')} + + + + + ); + } + + if (currentStep === 1) { + return ( + <> + + + {sessionId ? ( + + + + ) : null} + + + + {t('Share Session ID')} + + + + + setCurrentStep(2)}> + + {t('Shared! Continue to next step')} + + + + ); + } + + if (currentStep === 2) { + // if (showInviteInput) { + return ( + + + {t("Leader's Invite Code")} + + + + + + + + + + ); + // } + + // return ( + // + // + // + // + // + // + // + // {t('Session ID accepted')} + // + // + // + // + // ); + } + + if (isWalletReady) { + return ( + + + + + + + + {t('All co-signers joined')} + + + + + ); + } + + if (showWaitingCopayers) { + return ( + + + + + + + + {t('Waiting for others to join')} + + + {t('Preparing the HODL chamber')} + + + + + ); + } + + return ( + + + + + + + + {t('Joined Shared Wallet')} + + + + + ); + }; + + const renderStepSection = () => { + return ( + + + {t('Setting Up Your Wallet')} + + + + 1}> + {currentStep === 1 ? ( + + ) : ( + + )} + + 1} /> + + + + {t('Share Session ID')} + + {t('Share your session ID with the leader')} + + + + + + + 2}> + {currentStep > 2 ? ( + + ) : currentStep === 2 ? ( + + ) : ( + 2 + )} + + 2 || isWalletReady} /> + + + + {t('Join via Invite Code')} + + {t('Scan or paste the invite code from the leader')} + + + + + + + + {isWalletReady ? ( + + ) : currentStep === 3 ? ( + + ) : ( + 3 + )} + + + + + {t('Wallet Setup')} + + {t('Waiting for other co-signers to join')} + + + + + + ); + }; + + return ( + + + {!showSession ? renderStartForm() : renderTopSection()} + {showSession ? renderStepSection() : null} + + + {isWalletReady && ( + + + + )} + + ); +}; + +export default JoinTSSWallet; diff --git a/src/navigation/wallet/screens/KeyOverview.tsx b/src/navigation/wallet/screens/KeyOverview.tsx index b791c7d8ac..14246b80d8 100644 --- a/src/navigation/wallet/screens/KeyOverview.tsx +++ b/src/navigation/wallet/screens/KeyOverview.tsx @@ -75,7 +75,7 @@ import { } from '../components/ErrorMessages'; import OptionsSheet, {Option} from '../components/OptionsSheet'; import Icons from '../components/WalletIcons'; -import {WalletGroupParamList} from '../WalletGroup'; +import {WalletGroupParamList, WalletScreens} from '../WalletGroup'; import {useAppDispatch, useAppSelector, useLogger} from '../../../utils/hooks'; import SheetModal from '../../../components/modal/base/sheet/SheetModal'; import { @@ -329,6 +329,7 @@ const KeyOverview = () => { const linkedCoinbase = useAppSelector( ({COINBASE}) => !!COINBASE.token[COINBASE_ENV], ); + const [showKeyDropdown, setShowKeyDropdown] = useState(false); const key = keys[id]; const hasMultipleKeys = @@ -524,6 +525,7 @@ const KeyOverview = () => { .filter( sw => sw.isComplete() && + !sw.pendingTssSession && !key.wallets.some(ew => ew.id === sw.credentials.walletId), ) .map(syncWallet => { @@ -539,7 +541,11 @@ const KeyOverview = () => { return _.merge( syncWallet, buildWalletObj( - {...syncWallet.credentials, currencyAbbreviation, currencyName}, + { + ...syncWallet.credentials, + currencyAbbreviation, + currencyName, + } as any, _tokenOptionsByAddress, ), ); @@ -760,7 +766,7 @@ const KeyOverview = () => { k.id === item.wallets[0].id && (!item.copayerId || k.credentials?.copayerId === item.copayerId), )!; - if (!fullWalletObj.isComplete()) { + if (!fullWalletObj.isComplete() && fullWalletObj.pendingTssSession) { fullWalletObj.getStatus( {network: fullWalletObj.network}, (err: any, status: Status) => { @@ -804,7 +810,29 @@ const KeyOverview = () => { id={item.id} accountItem={item} hideBalance={hideAllBalances} - onPress={() => onPressItem(item)} + onPress={() => { + if (key?.tssSession) { + const {status, isCreator} = key.tssSession; + if ( + isCreator && + (status === 'collecting_copayers' || + status === 'ready_to_start' || + status === 'ceremony_in_progress') + ) { + navigation.navigate(WalletScreens.INVITE_COSIGNERS, { + keyId: key.id, + }); + return; + } + + if (!isCreator && status === 'ceremony_in_progress') { + console.log( + '[TSS] Joiner has ceremony in progress - needs to reconnect', + ); + } + } + onPressItem(item); + }} /> ); }, diff --git a/src/navigation/wallet/screens/KeySettings.tsx b/src/navigation/wallet/screens/KeySettings.tsx index 9e5f8ef0b8..d31bd8d992 100644 --- a/src/navigation/wallet/screens/KeySettings.tsx +++ b/src/navigation/wallet/screens/KeySettings.tsx @@ -16,7 +16,7 @@ import { } from '../../../components/styled/Text'; import {useNavigation, useRoute} from '@react-navigation/native'; import {RouteProp} from '@react-navigation/core'; -import {WalletGroupParamList} from '../WalletGroup'; +import {WalletGroupParamList, WalletScreens} from '../WalletGroup'; import {View, ScrollView, FlatList} from 'react-native'; import {TouchableOpacity} from '@components/base/TouchableOpacity'; import styled from 'styled-components/native'; @@ -75,6 +75,7 @@ import AccountSettingsRow from '../../../components/list/AccountSettingsRow'; import {useTheme} from 'styled-components/native'; import {IsSVMChain, IsVMChain} from '../../../store/wallet/utils/currency'; import {useOngoingProcess, useTokenContext} from '../../../contexts'; +import {isTSSKey} from '../../../store/wallet/effects/tss-send/tss-send'; const WalletSettingsContainer = styled.SafeAreaView` flex: 1; @@ -221,6 +222,7 @@ const KeySettings = () => { .filter( sw => sw.isComplete() && + !sw.pendingTssSession && !_key.wallets.some(ew => ew.id === sw.credentials.walletId), ) .map(syncWallet => { @@ -236,7 +238,11 @@ const KeySettings = () => { return merge( syncWallet, buildWalletObj( - {...syncWallet.credentials, currencyAbbreviation, currencyName}, + { + ...syncWallet.credentials, + currencyAbbreviation, + currencyName, + } as any, _tokenOptionsByAddress, ), ); @@ -317,7 +323,7 @@ const KeySettings = () => { const { credentials: {walletId}, } = fullWalletObj; - if (!fullWalletObj.isComplete()) { + if (!fullWalletObj.isComplete() && fullWalletObj.pendingTssSession) { return; } navigation.navigate('WalletSettings', { @@ -395,6 +401,13 @@ const KeySettings = () => { {t('Security')} { + if (isTSSKey(_key)) { + navigation.navigate(WalletScreens.BACKUP_SHARED_KEY, { + context: 'backupExistingTSSKey', + key: _key, + }); + return; + } navigation.navigate('BackupOnboarding', { key: _key, buildEncryptModalConfig, diff --git a/src/navigation/wallet/screens/MultisigOptions.tsx b/src/navigation/wallet/screens/MultisigOptions.tsx index 18f12f668e..f34d5bf5d8 100644 --- a/src/navigation/wallet/screens/MultisigOptions.tsx +++ b/src/navigation/wallet/screens/MultisigOptions.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import {useNavigation} from '@react-navigation/native'; import {Key} from '../../../store/wallet/wallet.models'; import OptionsSheet, {Option} from '../components/OptionsSheet'; @@ -6,72 +6,174 @@ import {useThemeType} from '../../../utils/hooks/useThemeType'; import {useTranslation} from 'react-i18next'; import {useAppDispatch} from '../../../utils/hooks'; import {Analytics} from '../../../store/analytics/analytics.effects'; +import {WalletScreens} from '../../../navigation/wallet/WalletGroup'; +import {isTSSKey} from '../../../store/wallet/effects/tss-send/tss-send'; -const MultisigSharedOptionImage = { - light: require('../../../../assets/img/wallet/wallet-type/add-multisig.png'), - dark: require('../../../../assets/img/wallet/wallet-type/add-multisig-dark.png'), -}; - -const MultisigJoinOptionImage = { - light: require('../../../../assets/img/wallet/wallet-type/add-join.png'), - dark: require('../../../../assets/img/wallet/wallet-type/add-join-dark.png'), -}; +export type MultisigModalType = 'create' | 'join'; export interface MultisigOptionsProps { - setShowMultisigOptions: (value: boolean) => void; isVisible: boolean; + modalType?: MultisigModalType | null; + closeModal: () => void; walletKey?: Key; } const MultisigOptions = ({ isVisible, - setShowMultisigOptions, + modalType, + closeModal, walletKey, }: MultisigOptionsProps) => { const {t} = useTranslation(); const navigation = useNavigation(); const dispatch = useAppDispatch(); const themeType = useThemeType(); - const optionList: Option[] = [ - { - title: t('Create a Shared Wallet'), - description: t('Use more than one device to create a multisig wallet'), - onPress: () => { - dispatch( - Analytics.track('Clicked Create Multisig Wallet', { - context: walletKey ? 'AddingOptions' : 'CreationOptions', - }), - ); - navigation.navigate('CurrencySelection', { - context: 'addWalletMultisig', - key: walletKey!, - }); + + const isNonTSSKeyFlow = walletKey && !isTSSKey(walletKey) && !modalType; + + const nonTSSOptions: Option[] = useMemo( + () => [ + { + title: t('Add Multisig Wallet'), + description: t( + 'Create a new wallet that requires multiple signatures for transactions', + ), + onPress: () => { + dispatch( + Analytics.track('Clicked Create Multisig Wallet', { + context: 'AddingOptions', + }), + ); + closeModal(); + navigation.navigate('CurrencySelection', { + context: 'addWalletMultisig', + key: walletKey!, + }); + }, + }, + { + title: t('Join Shared Wallet'), + description: t( + 'Join an existing multisig wallet using an invitation from another user', + ), + onPress: () => { + dispatch( + Analytics.track('Clicked Join Multisig Wallet', { + context: 'AddingOptions', + }), + ); + closeModal(); + navigation.navigate('JoinMultisig', {key: walletKey}); + }, + }, + ], + [t, dispatch, navigation, walletKey, closeModal], + ); + + const createOptions: Option[] = useMemo( + () => [ + { + title: t('Multisignature Wallet'), + description: t( + 'Support for Bitcoin, Litecoin, Dogecoin and Bitcoin Cash networks. Each co-signer/device has a unique private key/recovery phrase, and all signatures are recorded directly on the blockchain.', + ), + onPress: () => { + dispatch( + Analytics.track('Clicked Create Multisig Wallet', { + context: walletKey ? 'AddingOptions' : 'CreationOptions', + }), + ); + closeModal(); + navigation.navigate('CurrencySelection', { + context: 'addWalletMultisig', + key: walletKey!, + }); + }, + }, + { + title: t('Threshold Signature Wallet'), + description: t( + 'Support for Ethereum (ERC-20) tokens, Bitcoin, Bitcoin Cash, Litecoin, Dogecoin, and XRP. A single private key is split into keyshares across co-signers, combining approvals into one transaction.', + ), + subDescription: t( + "All participants need to be online at the same time to create the wallet and sign transactions. This wallet **can't be imported into other crypto platforms.**", + ), + onPress: () => { + dispatch( + Analytics.track('Clicked Create TSS Wallet', { + context: walletKey ? 'AddingOptions' : 'CreationOptions', + }), + ); + closeModal(); + navigation.navigate('CurrencySelection', { + context: 'addTSSWalletMultisig', + key: walletKey!, + }); + }, + }, + ], + [t, dispatch, navigation, walletKey, closeModal], + ); + + const joinOptions: Option[] = useMemo( + () => [ + { + title: t('Multisignature Wallet'), + description: t( + 'Support for Bitcoin, Litecoin, Dogecoin and Bitcoin Cash networks. Each co-signer/device has a unique private key/recovery phrase, and all signatures are recorded directly on the blockchain.', + ), + onPress: () => { + dispatch( + Analytics.track('Clicked Join Multisig Wallet', { + context: walletKey ? 'AddingOptions' : 'CreationOptions', + }), + ); + closeModal(); + navigation.navigate('JoinMultisig', {key: walletKey}); + }, }, - imgSrc: MultisigSharedOptionImage[themeType], - }, - { - title: t('Join a Shared Wallet'), - description: t( - "Joining another user's multisig wallet requires an invitation to join", - ), - onPress: () => { - dispatch( - Analytics.track('Clicked Join Multisig Wallet', { - context: walletKey ? 'AddingOptions' : 'CreationOptions', - }), - ); - navigation.navigate('JoinMultisig', {key: walletKey}); + { + title: t('Threshold Signature Wallet'), + description: t( + 'Support for Ethereum (ERC-20) tokens, Bitcoin, Bitcoin Cash, Litecoin, Dogecoin, and XRP. A single private key is split into keyshares across co-signers, combining approvals into one transaction.', + ), + subDescription: t( + "All participants need to be online at the same time to create the wallet and sign transactions. This wallet **can't be imported into other crypto platforms.**", + ), + onPress: () => { + dispatch( + Analytics.track('Clicked Join TSS Wallet', { + context: walletKey ? 'AddingOptions' : 'CreationOptions', + }), + ); + closeModal(); + navigation.navigate(WalletScreens.JOIN_TSS_WALLET, {}); + }, }, - imgSrc: MultisigJoinOptionImage[themeType], - }, - ]; + ], + [t, dispatch, navigation, walletKey, closeModal], + ); + + const getOptions = () => { + if (isNonTSSKeyFlow) { + return nonTSSOptions; + } + return modalType === 'create' ? createOptions : joinOptions; + }; + + const getTitle = () => { + if (isNonTSSKeyFlow) { + return t('What would you like to do?'); + } + return t('What type of shared wallet?'); + }; return ( setShowMultisigOptions(false)} + title={getTitle()} + options={getOptions()} + closeModal={closeModal} /> ); }; diff --git a/src/navigation/wallet/screens/PaperWallet.tsx b/src/navigation/wallet/screens/PaperWallet.tsx index f9a318db8c..9d05c00069 100644 --- a/src/navigation/wallet/screens/PaperWallet.tsx +++ b/src/navigation/wallet/screens/PaperWallet.tsx @@ -150,7 +150,8 @@ const PaperWallet: React.FC = ({navigation, route}) => { wallet => !wallet.hideWallet && !wallet.hideWalletByAccount && - wallet.isComplete(), + wallet.isComplete() && + !wallet.pendingTssSession, ); const [walletsAvailable, setWalletsAvailable] = useState([]); diff --git a/src/navigation/wallet/screens/TransactionProposalDetails.tsx b/src/navigation/wallet/screens/TransactionProposalDetails.tsx index cf7ec83a2b..90715d27f3 100644 --- a/src/navigation/wallet/screens/TransactionProposalDetails.tsx +++ b/src/navigation/wallet/screens/TransactionProposalDetails.tsx @@ -72,6 +72,9 @@ import { Key, TransactionProposal, Wallet, + TSSSigningStatus, + TSSSigningProgress, + TSSCopayerSignStatus, } from '../../../store/wallet/wallet.models'; import { DetailColumn, @@ -87,6 +90,12 @@ import {useOngoingProcess, usePaymentSent} from '../../../contexts'; import {logManager} from '../../../managers/LogManager'; import {DeviceEventEmitter} from 'react-native'; import {DeviceEmitterEvents} from '../../../constants/device-emitter-events'; +import { + isTSSKey, + joinTSSSigningSession, + TSSSigningCallbacks, +} from '../../../store/wallet/effects/tss-send/tss-send'; +import TSSProgressTracker from '../components/TSSProgressTracker'; const TxpDetailsContainer = styled.SafeAreaView` flex: 1; @@ -123,6 +132,7 @@ const TimelineBorderLeft = styled.View<{isFirst: boolean; isLast: boolean}>` width: 1px; z-index: -1; `; + const TimelineTime = styled(H7)` color: ${({theme: {dark}}) => (dark ? White : SlateDark)}; `; @@ -229,6 +239,18 @@ const TransactionProposalDetails = () => { const {showPaymentSent, hidePaymentSent} = usePaymentSent(); const {showOngoingProcess, hideOngoingProcess} = useOngoingProcess(); + const [isTSSWallet, setIsTSSWallet] = useState(false); + const [showTSSProgressModal, setShowTSSProgressModal] = useState(false); + const [tssStatus, setTssStatus] = useState('initializing'); + const [tssProgress, setTssProgress] = useState({ + currentRound: 0, + totalRounds: 4, + status: 'pending', + }); + const [tssCopayers, setTssCopayers] = useState< + Array<{id: string; name: string; signed: boolean}> + >([]); + const title = getDetailsTitle(transaction, wallet); let {currencyAbbreviation, chain, network, tokenAddress} = wallet; currencyAbbreviation = currencyAbbreviation.toLowerCase(); @@ -241,6 +263,27 @@ const TransactionProposalDetails = () => { }); }, [navigation, title]); + useEffect(() => { + if (key) { + const isTss = isTSSKey(key); + setIsTSSWallet(isTss); + + if (isTss) { + logManager.debug(`[TxpDetails] Is TSS wallet: ${isTss}`); + const copayersList = + wallet.credentials?.publicKeyRing?.map((pkr: any, index: number) => ({ + id: pkr.copayerId || `copayer-${index}`, + name: pkr.copayerName || `Co-signer ${index + 1}`, + signed: false, + })) || []; + setTssCopayers(copayersList); + logManager.debug( + `[TxpDetails] Initialized with ${copayersList.length} copayers`, + ); + } + } + }, [key, wallet]); + const init = async () => { try { if (!transaction) { @@ -342,7 +385,6 @@ const TransactionProposalDetails = () => { setPaymentExpired(true); setRemainingTimeStr(t('Expired')); if (countDown) { - /* later */ clearInterval(countDown); } return; @@ -379,7 +421,6 @@ const TransactionProposalDetails = () => { }; const targetAmount = wallet.balance.sat - (fee + amount); setTimeout(() => { - // show refresing in wallet details view DeviceEventEmitter.emit(DeviceEmitterEvents.SET_REFRESHING, true); dispatch( waitForTargetAmountAndUpdateWallet({ @@ -481,7 +522,114 @@ const TransactionProposalDetails = () => { } }; + const tssCallbacks: TSSSigningCallbacks = { + onStatusChange: (status: TSSSigningStatus) => { + logManager.debug(`[TxpDetails TSS] Status: ${status}`); + setTssStatus(status); + }, + onProgressUpdate: (progress: TSSSigningProgress) => { + logManager.debug( + `[TxpDetails TSS] Progress: Round ${progress.currentRound}/${progress.totalRounds}`, + ); + setTssProgress(progress); + }, + onCopayerStatusChange: ( + copayerId: string, + status: TSSCopayerSignStatus, + ) => { + logManager.debug(`[TxpDetails TSS] Copayer ${copayerId} ${status}`); + setTssCopayers(prev => + prev.map(c => + c.id === copayerId ? {...c, signed: status === 'signed'} : c, + ), + ); + }, + onRoundUpdate: ( + round: number, + type: 'ready' | 'processed' | 'submitted', + ) => { + logManager.debug(`[TxpDetails TSS] Round ${round} ${type}`); + }, + onError: (error: Error) => { + logManager.error(`[TxpDetails TSS] Error: ${error.message}`); + setShowTSSProgressModal(false); + setResetSwipeButton(true); + dispatch( + showBottomNotificationModal( + CustomErrorMessage({ + errMsg: error.message, + title: t('TSS Signing Error'), + }), + ), + ); + }, + onComplete: (signature: string) => { + logManager.debug(`[TxpDetails TSS] Complete`); + }, + }; + + const joinTSSSigning = async () => { + try { + logManager.debug( + `[TxpDetails] Joining TSS signing session for txp: ${txp.id}`, + ); + setShowTSSProgressModal(true); + setTssStatus('initializing'); + + const result = await dispatch( + joinTSSSigningSession({ + key, + wallet, + txp, + callbacks: tssCallbacks, + }), + ); + + setTssStatus('complete'); + await sleep(1500); + setShowTSSProgressModal(false); + + // Update wallet status + const {fee, amount} = result as {fee: number; amount: number}; + const targetAmount = wallet.balance.sat - (fee + amount); + setTimeout(() => { + DeviceEventEmitter.emit(DeviceEmitterEvents.SET_REFRESHING, true); + dispatch( + waitForTargetAmountAndUpdateWallet({ + key, + wallet, + targetAmount, + }), + ); + }, 3000); + + showPaymentSent({ + onCloseModal, + title: lastSigner ? t('Payment Sent') : t('Payment Accepted'), + }); + + await sleep(1000); + navigation.goBack(); + } catch (err) { + logManager.error(`[TxpDetails] TSS join error: ${err}`); + setShowTSSProgressModal(false); + setResetSwipeButton(true); + + await showErrorMessage( + CustomErrorMessage({ + errMsg: BWCErrorMessage(err), + title: t('Uh oh, something went wrong'), + }), + ); + } + }; + const onSwipeComplete = async () => { + if (isTSSWallet) { + await joinTSSSigning(); + return; + } + try { await sleep(400); await dispatch( @@ -682,14 +830,25 @@ const TransactionProposalDetails = () => {
{t('DETAILS')}
-
+ {!isTSSWallet &&
} + + {isTSSWallet && ( + + )} {txp.nonce ? ( <> {t('Nonce')} - {txp.nonce} @@ -779,7 +938,6 @@ const TransactionProposalDetails = () => { {t('Created by')} - {txp.creatorName} @@ -809,8 +967,6 @@ const TransactionProposalDetails = () => { ) : null} - {/* TODO: Add Notify unconfirmed transaction row */} - {payProDetails ? ( <> @@ -873,7 +1029,13 @@ const TransactionProposalDetails = () => { (!txp.payProUrl || (payProDetails && !payproIsLoading && !paymentExpired)) ? ( diff --git a/src/navigation/wallet/screens/send/confirm/Confirm.tsx b/src/navigation/wallet/screens/send/confirm/Confirm.tsx index 3ba07c55fa..4e3f6228bd 100644 --- a/src/navigation/wallet/screens/send/confirm/Confirm.tsx +++ b/src/navigation/wallet/screens/send/confirm/Confirm.tsx @@ -21,6 +21,9 @@ import { TxDetails, Utxo, Wallet, + TSSSigningStatus, + TSSSigningProgress, + TSSCopayerSignStatus, } from '../../../../../store/wallet/wallet.models'; import SwipeButton from '../../../../../components/swipe-button/SwipeButton'; import { @@ -29,6 +32,10 @@ import { showConfirmAmountInfoSheet, startSendPayment, } from '../../../../../store/wallet/effects/send/send'; +import { + isTSSKey, + TSSSigningCallbacks, +} from '../../../../../store/wallet/effects/tss-send/tss-send'; import { formatCurrencyAbbreviation, formatFiatAmount, @@ -91,7 +98,6 @@ import { IsVMChain, } from '../../../../../store/wallet/utils/currency'; import prompt from 'react-native-prompt-android'; -import {Analytics} from '../../../../../store/analytics/analytics.effects'; import SendingToERC20Warning from '../../../components/SendingToERC20Warning'; import {HIGH_FEE_LIMIT} from '../../../../../constants/wallet'; import WarningSvg from '../../../../../../assets/img/warning.svg'; @@ -112,6 +118,10 @@ import {CommonActions} from '@react-navigation/native'; import {TabsScreens} from '../../../../tabs/TabsStack'; import {RootStacks} from '../../../../../Root'; import {useOngoingProcess, usePaymentSent} from '../../../../../contexts'; +import {logManager} from '../../../../../managers/LogManager'; +import TSSProgressTracker, { + TSSCopayer, +} from '../../../components/TSSProgressTracker'; const VerticalPadding = styled.View` padding: ${ScreenGutter} 0; @@ -195,6 +205,21 @@ const Confirm = () => { useState(null); const [confirmHardwareState, setConfirmHardwareState] = useState(null); + const [showTSSProgressModal, setShowTSSProgressModal] = useState(false); + + const [isTSSWallet, setIsTSSWallet] = useState(false); + const [tssStatus, setTssStatus] = useState('initializing'); + const [tssProgress, setTssProgress] = useState({ + currentRound: 0, + totalRounds: 4, + status: 'pending', + }); + const [tssCopayers, setTssCopayers] = useState< + Array<{id: string; name: string; signed: boolean}> + >([]); + const [tssTransactionId, setTssTransactionId] = useState< + string | undefined + >(); const { fee: _fee, @@ -224,6 +249,25 @@ const Confirm = () => { const {unitToSatoshi} = dispatch(GetPrecision(currencyAbbreviation, chain, tokenAddress)) || {}; + useEffect(() => { + if (key && wallet) { + const isTss = isTSSKey(key); + setIsTSSWallet(isTss); + if (isTss) { + const copayersList = + wallet.credentials?.publicKeyRing?.map((pkr: any, index: number) => ({ + id: pkr.copayerId || `copayer-${index}`, + name: pkr.copayerName || `Co-signer ${index + 1}`, + signed: false, + })) || []; + setTssCopayers(copayersList); + logManager.debug( + `[TSS Confirm] Initialized with ${copayersList.length} copayers`, + ); + } + } + }, [key, wallet]); + useLayoutEffect(() => { navigation.setOptions({ headerTitle: () => ( @@ -292,8 +336,6 @@ const Confirm = () => { const isTxLevelAvailable = () => { const includedChains = ['btc', 'eth', 'matic', 'arb', 'base', 'op']; - // TODO: exclude paypro, coinbase, usingMerchantFee txs, - // const {payProUrl} = txDetails; return includedChains.includes(chain.toLowerCase()); }; @@ -305,7 +347,7 @@ const Confirm = () => { if (newLevel) { updateTxProposal({ feeLevel: newLevel, - feePerKb: customFeePerKB, // this will be ignore in select input context + feePerKb: customFeePerKB, }); } }; @@ -405,11 +447,60 @@ const Confirm = () => { hidePaymentSent(); }; + const tssCallbacks: TSSSigningCallbacks = { + onStatusChange: (status: TSSSigningStatus) => { + logManager.debug(`[TSS Confirm] Status changed: ${status}`); + setTssStatus(status); + }, + onProgressUpdate: (progress: TSSSigningProgress) => { + logManager.debug( + `[TSS Confirm] Progress: Round ${progress.currentRound}/${progress.totalRounds}`, + ); + setTssProgress(progress); + }, + onCopayerStatusChange: ( + copayerId: string, + status: TSSCopayerSignStatus, + ) => { + logManager.debug(`[TSS Confirm] Copayer ${copayerId} ${status}`); + setTssCopayers(prev => + prev.map(c => + c.id === copayerId ? {...c, signed: status === 'signed'} : c, + ), + ); + }, + onRoundUpdate: ( + round: number, + type: 'ready' | 'processed' | 'submitted', + ) => { + logManager.debug(`[TSS Confirm] Round ${round} ${type}`); + }, + onError: (error: Error) => { + logManager.error(`[TSS Confirm] Error: ${error.message}`); + setShowTSSProgressModal(false); + setResetSwipeButton(true); + showErrorMessage( + CustomErrorMessage({ + errMsg: error.message, + title: t('TSS Signing Error'), + }), + ); + }, + onComplete: (signature: string) => { + logManager.debug(`[TSS Confirm] Signing complete`); + }, + }; + const startSendingPayment = async ({ transport, }: {transport?: Transport} = {}) => { const isUsingHardwareWallet = !!transport; + if (isTSSWallet) { + setShowTSSProgressModal(true); + setTssStatus('initializing'); + } + try { if (isUsingHardwareWallet) { const {chain, network} = wallet.credentials; @@ -440,15 +531,23 @@ const Confirm = () => { await sleep(1000); setConfirmHardwareWalletVisible(false); } else { - await dispatch( + const result = await dispatch( startSendPayment({ txp, key, wallet, recipient, transport, + ...(isTSSWallet && {tssCallbacks}), }), ); + + if (isTSSWallet && result?.txid) { + setTssTransactionId(result.txid); + setTssStatus('complete'); + await sleep(1500); + setShowTSSProgressModal(false); + } } await sleep(300); showPaymentSent({ @@ -509,6 +608,10 @@ const Confirm = () => { } } } catch (err) { + if (isTSSWallet) { + setShowTSSProgressModal(false); + } + if (isUsingHardwareWallet) { setConfirmHardwareWalletVisible(false); setConfirmHardwareState(null); @@ -660,6 +763,16 @@ const Confirm = () => { recipientData = sendingTo; } + const formatDate = (date: Date) => { + return date.toLocaleDateString('en-US', { + month: '2-digit', + day: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }; + return ( <> @@ -669,6 +782,19 @@ const Confirm = () => { keyboardShouldPersistTaps={'handled'}>
{t('Summary')}
+ + {isTSSWallet && ( + + )} + {solanaPayOpts ? ( <>
diff --git a/src/navigation/wallet/screens/wallet-settings/Addresses.tsx b/src/navigation/wallet/screens/wallet-settings/Addresses.tsx index db5b5d14b3..7fc58d0f99 100644 --- a/src/navigation/wallet/screens/wallet-settings/Addresses.tsx +++ b/src/navigation/wallet/screens/wallet-settings/Addresses.tsx @@ -300,7 +300,7 @@ const Addresses = () => { try { setButtonState('loading'); - if (!wallet.isComplete()) { + if (!wallet.isComplete() && wallet.pendingTssSession) { setButtonState('failed'); await sleep(1000); setButtonState(null); diff --git a/src/navigation/wallet/screens/wallet-settings/ExportTSSWallet.tsx b/src/navigation/wallet/screens/wallet-settings/ExportTSSWallet.tsx new file mode 100644 index 0000000000..ee47bd80c1 --- /dev/null +++ b/src/navigation/wallet/screens/wallet-settings/ExportTSSWallet.tsx @@ -0,0 +1,386 @@ +import React, {useLayoutEffect, useState} from 'react'; +import { + HeaderTitle, + Paragraph, + BaseText, + H2, +} from '../../../../components/styled/Text'; +import {useNavigation, useRoute, CommonActions} from '@react-navigation/native'; +import styled from 'styled-components/native'; +import {ScreenGutter} from '../../../../components/styled/Containers'; +import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; +import {SlateDark, White, Black, Slate30} from '../../../../styles/colors'; +import yup from '../../../../lib/yup'; +import {Controller, useForm} from 'react-hook-form'; +import BoxInput from '../../../../components/form/BoxInput'; +import Button, {ButtonState} from '../../../../components/button/Button'; +import {yupResolver} from '@hookform/resolvers/yup'; +import {useAppDispatch, useAppSelector} from '../../../../utils/hooks'; +import {RouteProp} from '@react-navigation/core'; +import {WalletGroupParamList, WalletScreens} from '../../WalletGroup'; +import {BwcProvider} from '../../../../lib/bwc'; +import { + isAndroidStoragePermissionGranted, + sleep, +} from '../../../../utils/helper-methods'; +import {useTranslation} from 'react-i18next'; +import {Platform, Modal} from 'react-native'; +import Share, {ShareOptions} from 'react-native-share'; +import RNFS from 'react-native-fs'; +import {APP_NAME_UPPERCASE} from '../../../../constants/config'; +import {logManager} from '../../../../managers/LogManager'; +import {RootStacks} from '../../../../Root'; +import {TabsScreens} from '../../../tabs/TabsStack'; +import WalletCreatedSvg from '../../../../../assets/img/shared-success.svg'; + +const BWC = BwcProvider.getInstance(); + +const ExportContainer = styled.SafeAreaView` + flex: 1; +`; + +const ScrollView = styled(KeyboardAwareScrollView)` + margin-top: 20px; + padding: 0 ${ScreenGutter}; +`; + +const PasswordFormContainer = styled.View` + margin: 15px 0; +`; + +const ExportParagraph = styled(Paragraph)` + margin-bottom: 15px; + color: ${({theme: {dark}}) => (dark ? White : SlateDark)}; +`; + +const PasswordActionContainer = styled.View` + margin-top: 20px; +`; + +const PasswordInputContainer = styled.View` + margin: 15px 0; +`; + +const BottomButtonContainer = styled.View` + padding: 16px ${ScreenGutter}; + padding-bottom: 32px; +`; + +const ModalWrapper = styled.View` + flex: 1; + background-color: ${({theme: {dark}}) => (dark ? Black : White)}; +`; + +const ModalContainer = styled.SafeAreaView` + flex: 1; +`; + +const ModalHeader = styled.View` + flex-direction: row; + align-items: center; + justify-content: center; + padding: 16px ${ScreenGutter}; +`; + +const ModalTitle = styled(BaseText)` + font-size: 20px; + font-weight: 700; + color: ${({theme: {dark}}) => (dark ? White : Black)}; + text-align: center; +`; + +const ModalContent = styled.View` + flex: 1; + align-items: center; + justify-content: center; + padding: 0 ${ScreenGutter}; +`; + +const SuccessImageContainer = styled.View` + margin-bottom: 32px; +`; + +const SuccessTitle = styled(H2)` + margin-bottom: 16px; + text-align: center; +`; + +const SuccessDescription = styled(Paragraph)` + text-align: center; + color: ${({theme: {dark}}) => (dark ? Slate30 : SlateDark)}; +`; + +const ModalButtonContainer = styled.View` + width: 100%; + padding: 16px ${ScreenGutter} 32px; +`; + +interface ExportPasswordFieldValues { + password: string; + confirmPassword: string; +} + +export type ExportTSSWalletParamList = { + keyId: string; + context: 'createNewTSSKey' | 'joinTSSKey' | 'backupExistingTSSKey'; +}; + +const ExportTSSWallet = () => { + const {t} = useTranslation(); + const { + params: {keyId, context}, + } = useRoute>(); + + const dispatch = useAppDispatch(); + const navigation = useNavigation(); + const key = useAppSelector(({WALLET}) => (keyId ? WALLET.keys[keyId] : null)); + + const [shareButtonState, setShareButtonState] = useState(); + const [backupCompleted, setBackupCompleted] = useState(false); + const [showSuccessModal, setShowSuccessModal] = useState(false); + + const showContinueButton = + context === 'createNewTSSKey' || context === 'joinTSSKey'; + + useLayoutEffect(() => { + navigation.setOptions({ + headerTitle: () => {t('Backup Keyshare')}, + }); + }, [navigation, t]); + + const schema = yup.object().shape({ + password: yup.string().required(), + confirmPassword: yup + .string() + .required() + .oneOf([yup.ref('password')], t('Passwords must match')), + }); + + const { + control, + handleSubmit, + formState: {errors}, + } = useForm({ + resolver: yupResolver(schema), + }); + + const keyshareExport = (password: string) => { + if (!password || !key) { + return null; + } + + const bufferToArray = ( + value: Buffer | {data: number[]} | undefined, + ): number[] | null => { + if (!value) return null; + if (Buffer.isBuffer(value)) { + return Array.from(value); + } + if ('data' in value) { + return value.data; + } + return null; + }; + + const keychain = key.properties?.keychain; + + const backup = { + isTSS: true, + version: 1, + mnemonic: key.properties?.mnemonic, + keychain: keychain + ? { + commonKeyChain: keychain.commonKeyChain, + privateKeyShare: bufferToArray(keychain.privateKeyShare), + reducedPrivateKeyShare: bufferToArray( + keychain.reducedPrivateKeyShare, + ), + } + : undefined, + keyId: key.id, + keyName: key.keyName, + createdOn: Date.now(), + }; + + return BWC.getSJCL().encrypt(password, JSON.stringify(backup), { + iter: 10000, + }); + }; + + const shareKeyshareFile = async ({password}: {password: string}) => { + try { + setShareButtonState('loading'); + + if (Platform.OS === 'android' && Platform.Version < 30) { + await isAndroidStoragePermissionGranted(dispatch); + } + + const encryptedKeyshare = keyshareExport(password); + + if (!encryptedKeyshare) { + throw new Error('Failed to export keyshare'); + } + + const walletName = key?.wallets?.[0]?.walletName || 'SharedWallet'; + const filename = `${APP_NAME_UPPERCASE}-Keyshare-${walletName}`; + + const rootPath = + Platform.OS === 'ios' + ? RNFS.LibraryDirectoryPath + : RNFS.TemporaryDirectoryPath; + + let filePath = `${rootPath}/${filename}`; + await RNFS.mkdir(filePath); + filePath += '.txt'; + + const txt = t( + 'Here is the encrypted keyshare backup for wallet: {{name}}\n\n{{keyshare}}\n\nTo import this backup, copy all text between {...}, including the symbols {}', + {name: walletName, keyshare: encryptedKeyshare}, + ); + + const opts: ShareOptions = { + title: filename, + url: `file://${filePath}`, + subject: `${walletName} Keyshare Backup`, + }; + + await RNFS.writeFile(filePath, txt, 'utf8'); + await Share.open(opts); + + setShareButtonState('success'); + await sleep(500); + setShareButtonState(undefined); + + setBackupCompleted(true); + } catch (err: any) { + logManager.debug(`[shareKeyshareFile]: ${err.message}`); + if (err && err.message === 'User did not share') { + setShareButtonState(undefined); + return; + } else { + setShareButtonState('failed'); + await sleep(500); + setShareButtonState(undefined); + } + } + }; + + const handleContinue = () => { + setShowSuccessModal(true); + }; + + const handleViewWallet = () => { + setShowSuccessModal(false); + navigation.dispatch( + CommonActions.reset({ + index: 1, + routes: [ + {name: RootStacks.TABS, params: {screen: TabsScreens.HOME}}, + {name: WalletScreens.KEY_OVERVIEW, params: {id: keyId}}, + ], + }), + ); + }; + + return ( + + + + {t('Create a password to encrypt your keyshare backup file.')} + + + + + ( + onChange(text)} + error={errors.password?.message} + value={value} + /> + )} + name="password" + defaultValue="" + /> + + + + ( + onChange(text)} + error={errors.confirmPassword?.message} + value={value} + /> + )} + name="confirmPassword" + defaultValue="" + /> + + + + + + + + + {showContinueButton && backupCompleted && ( + + + + )} + + + + + + {t('Wallet Created')} + + + + + + + + {t('Success!')} + + + {t( + 'Your shared wallet has successfully been created. Go check it out!', + )} + + + + + + + + + + + ); +}; + +export default ExportTSSWallet; diff --git a/src/navigation/zenledger/screens/ZenLedgerImport.tsx b/src/navigation/zenledger/screens/ZenLedgerImport.tsx index 9751262bea..23cd5ee199 100644 --- a/src/navigation/zenledger/screens/ZenLedgerImport.tsx +++ b/src/navigation/zenledger/screens/ZenLedgerImport.tsx @@ -63,8 +63,10 @@ const ZenLedgerImport: React.FC = () => { return _allKeys.map((key, index) => { const formattedWallet: ZenLedgerWalletObj[] = key.wallets .filter( - ({network, credentials}) => - network === Network.mainnet && credentials.isComplete(), + ({network, credentials, pendingTssSession}) => + network === Network.mainnet && + credentials.isComplete() && + !pendingTssSession, ) .map(wallet => { const { @@ -287,12 +289,9 @@ const ZenLedgerImport: React.FC = () => { return item.blockchain; }) .toString(); - Analytics.track( - 'ZenLedger Imported Wallet Address', - { - cryptoType: coins, - }, - ); + Analytics.track('ZenLedger Imported Wallet Address', { + cryptoType: coins, + }); dispatch(dismissBottomNotificationModal()); await sleep(500); goToZenLedger(requestWallets); diff --git a/src/store/transforms/transforms.ts b/src/store/transforms/transforms.ts index c1aae5e2bd..ed1bea4433 100644 --- a/src/store/transforms/transforms.ts +++ b/src/store/transforms/transforms.ts @@ -64,7 +64,7 @@ export const bootstrapWallets = (wallets: Wallet[]) => { buildWalletObj({ ...walletClient.credentials, ...wallet, - }), + } as any), ); } catch (err: unknown) { const errorLog = `Failed to bindWalletClient - ${ @@ -84,8 +84,36 @@ export const bootstrapKey = (key: Key, id: string) => { } else if (key.properties?.metadata) { try { const TssKey = BWCProvider.getTssKey(); - const tssKey = new TssKey(key.properties); + const properties = JSON.parse(JSON.stringify(key.properties)); + if (key.properties.keychain?.privateKeyShare?.data) { + properties.keychain.privateKeyShare = Buffer.from( + key.properties.keychain.privateKeyShare.data, + ); + console.log( + '[bootstrapKey] privateKeyShare restored, length:', + properties.keychain.privateKeyShare.length, + ); + } else if (Buffer.isBuffer(key.properties.keychain?.privateKeyShare)) { + properties.keychain.privateKeyShare = + key.properties.keychain.privateKeyShare; + console.log( + '[bootstrapKey] privateKeyShare already Buffer, length:', + properties.keychain.privateKeyShare.length, + ); + } + + if (key.properties.keychain?.reducedPrivateKeyShare?.data) { + properties.keychain.reducedPrivateKeyShare = Buffer.from( + key.properties.keychain.reducedPrivateKeyShare.data, + ); + } else if ( + Buffer.isBuffer(key.properties.keychain?.reducedPrivateKeyShare) + ) { + properties.keychain.reducedPrivateKeyShare = + key.properties.keychain.reducedPrivateKeyShare; + } + const tssKey = new TssKey(properties); const _key = merge(key, { methods: tssKey, }); diff --git a/src/store/wallet/effects/create-multisig/create-multisig.ts b/src/store/wallet/effects/create-multisig/create-multisig.ts index 117fcd2fbf..ebe6eb7a3f 100644 --- a/src/store/wallet/effects/create-multisig/create-multisig.ts +++ b/src/store/wallet/effects/create-multisig/create-multisig.ts @@ -3,17 +3,37 @@ import {BwcProvider} from '../../../../lib/bwc'; import merge from 'lodash.merge'; import { buildKeyObj, + buildTssKeyObj, buildWalletObj, mapAbbreviationAndName, } from '../../utils/wallet'; -import {successCreateKey, successAddWallet} from '../../wallet.actions'; -import {Key, KeyOptions, Wallet} from '../../wallet.models'; +import { + successCreateKey, + successAddWallet, + successUpdateKey, + setPendingJoinerSession, + removePendingJoinerSession, +} from '../../wallet.actions'; +import { + JoinerSessionId, + Key, + KeyOptions, + PendingJoinerSession, + TSSCopayerInfo, + TssSessionData, + Wallet, +} from '../../wallet.models'; import {createWalletWithOpts} from '../create/create'; import { subscribePushNotifications, subscribeEmailNotifications, } from '../../../app/app.effects'; import {logManager} from '../../../../managers/LogManager'; +import {TssKeyGen} from 'bitcore-wallet-client/ts_build/src/lib/tsskey'; +import {BASE_BWS_URL} from '../../../../constants/config'; +import {Network} from '../../../../constants'; +import {setHomeCarouselConfig} from '../../../../store/app/app.actions'; +import {createWalletAddress} from '../address/address'; const BWC = BwcProvider.getInstance(); @@ -71,7 +91,7 @@ export const startCreateKeyMultisig = ..._wallet.credentials, currencyAbbreviation, currencyName, - }), + } as any), ) as Wallet; const key = buildKeyObj({key: _key, wallets: [wallet]}); @@ -143,7 +163,7 @@ export const addWalletMultisig = ...newWallet.credentials, currencyAbbreviation, currencyName, - }), + } as any), ) as Wallet, ); @@ -158,3 +178,904 @@ export const addWalletMultisig = } }); }; + +export const encodeJoinerSessionId = (data: JoinerSessionId): string => { + return Buffer.from(JSON.stringify(data)).toString('base64'); +}; + +export const decodeJoinerSessionId = (code: string): JoinerSessionId => { + return JSON.parse(Buffer.from(code, 'base64').toString('utf8')); +}; + +const getPubKeyFromKey = (partyKey: any): string => { + const credentials = partyKey.createCredentials(null, { + chain: 'BTC', // Doesn't matter for requestPubKey + network: 'livenet', + n: 1, + account: 0, + }); + return credentials.requestPubKey; +}; + +export const startCreateTSSKey = + (opts: { + chain: string; + network: string; + m: number; + n: number; + password?: string; + myName: string; + walletName: string; + }): Effect> => + async (dispatch, getState): Promise<{key: Key}> => { + try { + const {chain: _chain, network, m, n, password, walletName, myName} = opts; + const chain = _chain === 'pol' ? 'matic' : _chain.toLowerCase(); // for creating a polygon wallet, we use matic as symbol + const { + WALLET: {tokenOptionsByAddress}, + } = getState(); + + const BWCKey = BWC.getKey(); + + const partyKey = new BWCKey({seedType: 'new'}); + logManager.debug('[TSS] Created party key for creator'); + + const tssKeyGen = new TssKeyGen({ + chain: chain.toUpperCase(), + network: network as Network, + baseUrl: BASE_BWS_URL, + key: partyKey, + }); + + logManager.debug('[TSS] Calling newKey() to create session on server...'); + await tssKeyGen.newKey({ + m: m, + n: n, + password: password, + }); + + const sessionId = tssKeyGen.id; + const sessionExport = tssKeyGen.exportSession(); + logManager.debug(`[TSS] Session created with ID: ${sessionId}`); + + const {currencyAbbreviation, currencyName} = dispatch( + mapAbbreviationAndName(chain, chain, undefined), + ); + + const walletClient = BWC.getClient(); + const credentials = partyKey.createCredentials(null, { + chain, + network, + n: 1, // TODO: review if this should be opts.n + account: 0, + }); + walletClient.fromObj(credentials); + + const placeholderWallet = merge( + walletClient, + buildWalletObj( + { + ...walletClient.credentials, + currencyAbbreviation, + currencyName, + } as any, + tokenOptionsByAddress, + ), + ) as Wallet; + + placeholderWallet.pendingTssSession = true; + + const key = buildKeyObj({ + key: partyKey, + wallets: [placeholderWallet], + keyName: 'My Key', + }); + + const copayers: TSSCopayerInfo[] = []; + for (let i = 1; i < n; i++) { + copayers.push({ + partyId: i, + pubKey: '', + name: `Co-signer ${i}`, + status: 'pending', + }); + } + + key.tssSession = { + id: sessionId, + partyKey: partyKey.toObj(), + sessionExport, + chain: chain, + network, + m, + n, + password, + myName, + walletName, + createdAt: Date.now(), + isCreator: true, + partyId: 0, + status: 'collecting_copayers', + copayers, + creatorPubKey: credentials.requestPubKey, + }; + + dispatch(successCreateKey({key})); + dispatch(setHomeCarouselConfig({id: key.id, show: true})); + + logManager.debug(`[TSS] Creator key saved with session ID: ${sessionId}`); + + return {key}; + } catch (err) { + const errorStr = err instanceof Error ? err.message : JSON.stringify(err); + logManager.error(`Error creating TSS key: ${errorStr}`); + throw err; + } + }; + +export const addCoSignerToTSS = + (opts: { + keyId: string; + joinerSessionId: string; + partyId: number; + }): Effect> => + async (dispatch, getState): Promise<{joinCode: string}> => { + try { + const {WALLET} = getState(); + const key = WALLET.keys[opts.keyId]; + + if (!key?.tssSession) { + throw new Error('Key not found or no TSS session'); + } + + if (!key.tssSession.isCreator) { + throw new Error('Only creator can add co-signers'); + } + + const joinerData = decodeJoinerSessionId(opts.joinerSessionId); + logManager.debug(`[TSS] Adding co-signer party ${opts.partyId}`); + + const BWCKey = BWC.getKey(); + const partyKey = new BWCKey({ + seedType: 'object', + seedData: key.tssSession.partyKey, + }); + + const tssKeyGen = new TssKeyGen({ + chain: key.tssSession.chain.toUpperCase(), + network: key.tssSession.network, + baseUrl: BASE_BWS_URL, + key: partyKey, + }); + + await tssKeyGen.restoreSession({session: key.tssSession.sessionExport}); + logManager.debug(`[TSS] Session restored with ID: ${tssKeyGen.id}`); + + const joinCode = tssKeyGen.createJoinCode({ + partyId: opts.partyId, + partyPubKey: joinerData.pubKey, + opts: {encoding: 'base64'}, + }); + + logManager.debug(`[TSS] Created joinCode for party ${opts.partyId}`); + + const updatedCopayers = [...(key.tssSession.copayers || [])]; + const copayerIndex = updatedCopayers.findIndex( + c => c.partyId === opts.partyId, + ); + + if (copayerIndex >= 0) { + updatedCopayers[copayerIndex] = { + ...updatedCopayers[copayerIndex], + pubKey: joinerData.pubKey, + name: joinerData.name || `Co-signer ${opts.partyId}`, + joinCode, + status: 'invited', + }; + } + + const allInvited = updatedCopayers.every(c => c.status === 'invited'); + const newStatus = allInvited ? 'ready_to_start' : 'collecting_copayers'; + + const updatedKey: Key = { + ...key, + tssSession: { + ...key.tssSession, + copayers: updatedCopayers, + status: newStatus, + }, + }; + + dispatch(successUpdateKey({key: updatedKey})); + + return {joinCode}; + } catch (err) { + const errorStr = err instanceof Error ? err.message : JSON.stringify(err); + logManager.error(`Error adding co-signer: ${errorStr}`); + throw err; + } + }; + +export const startTSSCeremony = + (keyId: string): Effect> => + async (dispatch, getState): Promise => { + return new Promise(async (resolve, reject) => { + try { + const { + APP: { + notificationsAccepted, + emailNotifications, + brazeEid, + defaultLanguage, + }, + WALLET: {tokenOptionsByAddress, keys}, + } = getState(); + + const key = keys[keyId]; + if (!key?.tssSession) { + throw new Error('Key not found or no TSS session'); + } + + if (key.tssSession.status !== 'ready_to_start') { + throw new Error('Not all co-signers have been invited'); + } + + const {chain, network, myName, walletName, sessionExport, partyKey} = + key.tssSession; + + const BWCKey = BWC.getKey(); + const restoredPartyKey = new BWCKey({ + seedType: 'object', + seedData: partyKey, + }); + + const tssKeyGen = new TssKeyGen({ + chain: chain.toUpperCase(), + network, + baseUrl: BASE_BWS_URL, + key: restoredPartyKey, + }); + + await tssKeyGen.restoreSession({session: sessionExport}); + logManager.debug(`[TSS Ceremony] Session restored: ${tssKeyGen.id}`); + + dispatch( + successUpdateKey({ + key: { + ...key, + tssSession: {...key.tssSession, status: 'ceremony_in_progress'}, + }, + }), + ); + + let walletFromBWS: any; + const Bitcore = BWC.getBitcore(); + const walletPrivKey = new Bitcore.PrivateKey().toString(); + + await new Promise((resolve, reject) => { + tssKeyGen + .on('roundready', (r: number) => { + logManager.debug(`[TSS Ceremony roundready] ${r}`); + try { + const currentKey = getState().WALLET.keys[keyId]; + dispatch( + successUpdateKey({ + key: { + ...currentKey, + tssSession: { + ...currentKey.tssSession!, + sessionExport: tssKeyGen.exportSession(), + }, + }, + }), + ); + } catch (e) {} + }) + .on('roundprocessed', (r: number) => + logManager.debug(`[TSS Ceremony roundprocessed] ${r}`), + ) + .on('roundsubmitted', (r: number) => + logManager.debug(`[TSS Ceremony roundsubmitted] ${r}`), + ) + .on('wallet', (w: any) => { + logManager.debug(`[TSS Ceremony wallet] ${w?.id}`); + walletFromBWS = w; + }) + .on('error', (e: Error) => { + logManager.error(`[TSS Ceremony error] ${e.message}`); + reject(e); + }) + .on('complete', () => { + logManager.debug(`[TSS Ceremony complete]`); + resolve(); + }); + + tssKeyGen.subscribe({ + walletName, + copayerName: myName, + createWalletOpts: { + network, + coin: chain, + chain, + walletPrivKey, + }, + }); + }); + + if (!walletFromBWS) { + throw new Error('Failed to get TSS wallet'); + } + logManager.debug(`[TSS BWS wallet]:`, walletFromBWS); + + const _tssKey = tssKeyGen.getTssKey(); + if (!_tssKey) { + throw new Error('Failed to get TSS key'); + } + + const {currencyAbbreviation, currencyName} = dispatch( + mapAbbreviationAndName(chain, chain, undefined), + ); + + const credentials = _tssKey.createCredentials(null, { + chain, + network, + account: 0, + walletPrivKey, + }); + + credentials.addWalletInfo( + walletFromBWS.id, + walletFromBWS.name, + walletFromBWS.m, + walletFromBWS.n, + myName, + { + tssKeyId: walletFromBWS.tssKeyId, + useNativeSegwit: ['P2WPKH', 'P2WSH', 'P2TR'].includes( + walletFromBWS.addressType, + ), + segwitVersion: walletFromBWS.addressType === 'P2TR' ? 1 : 0, + allowOverwrite: true, + }, + ); + + credentials.addPublicKeyRing(walletFromBWS.publicKeyRing); + + const finalWalletClient = BWC.getClient(); + finalWalletClient.fromObj(credentials.toObj()); + + if (notificationsAccepted) { + dispatch(subscribePushNotifications(finalWalletClient, brazeEid!)); + } + if (emailNotifications?.accepted && emailNotifications?.email) { + dispatch( + subscribeEmailNotifications(finalWalletClient, { + email: emailNotifications.email, + language: defaultLanguage, + unit: 'btc', + }), + ); + } + + const walletAddress = (await dispatch( + createWalletAddress({wallet: finalWalletClient, newAddress: true}), + )) as string; + logManager.info( + `[TSS Ceremony] New address generated: ${walletAddress}`, + ); + + const refreshWalletWithRetry = async ( + maxRetries: number = 5, + delayMs: number = 1000, + ) => { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + await new Promise(resolveRefresh => { + finalWalletClient.getStatus( + { + includeExtendedInfo: true, + }, + (err: Error, status: any) => { + if (err) { + logManager.warn( + `[TSS Ceremony] Attempt ${attempt}/${maxRetries} - Could not refresh wallet status: ${err.message}`, + ); + resolveRefresh(); + } else { + logManager.debug( + `[TSS Ceremony] Attempt ${attempt}/${maxRetries} - Status returned for wallet: ${status.wallet?.id}`, + ); + logManager.debug( + `[TSS Ceremony] Copayers count: ${ + status.wallet?.copayers?.length || 0 + }`, + ); + + if ( + status.wallet?.id === walletFromBWS.id && + status.wallet?.copayers?.length >= key.tssSession.n + ) { + walletFromBWS = status.wallet; + if (status.wallet?.publicKeyRing) { + credentials.addPublicKeyRing( + status.wallet.publicKeyRing, + ); + finalWalletClient.fromObj(credentials.toObj()); + } + Object.assign(finalWalletClient, status.wallet); + logManager.debug( + `[TSS Ceremony] Successfully refreshed wallet with ${status.wallet.copayers.length} copayers`, + ); + } + resolveRefresh(); + } + }, + ); + }); + + const currentCopayersCount = + finalWalletClient.credentials?.publicKeyRing?.length || 0; + + if (currentCopayersCount >= key.tssSession.n) { + logManager.debug( + `[TSS Ceremony] All ${key.tssSession.n} copayers found after ${attempt} attempt(s)`, + ); + break; + } + + if (attempt < maxRetries) { + logManager.debug( + `[TSS Ceremony] Only ${currentCopayersCount}/${key.tssSession.n} copayers found, retrying in ${delayMs}ms...`, + ); + await new Promise(resolve => setTimeout(resolve, delayMs)); + } else { + logManager.warn( + `[TSS Ceremony] Max retries reached. Only ${currentCopayersCount}/${key.tssSession.n} copayers found. Continuing anyway...`, + ); + } + } + }; + + await refreshWalletWithRetry(5, 1000); + + const finalWallet = merge( + finalWalletClient, + walletFromBWS, + buildWalletObj( + { + ...credentials.toObj(), + currencyAbbreviation, + currencyName, + } as any, + tokenOptionsByAddress, + ), + ) as Wallet; + + delete finalWallet.pendingTssSession; + + const finalKey = buildTssKeyObj({ + tssKey: _tssKey, + wallets: [finalWallet], + keyName: 'My Key', + }); + + finalKey.tssSession = { + ...key.tssSession, + status: 'complete', + sessionExport: undefined, + }; + + dispatch(successUpdateKey({key: finalKey})); + tssKeyGen.unsubscribe(); + + logManager.debug( + `[TSS Ceremony] Complete! Wallet ID: ${walletFromBWS.id}`, + ); + resolve(finalKey); + } catch (err) { + const errorStr = + err instanceof Error ? err.message : JSON.stringify(err); + logManager.error(`[TSS Ceremony] Error: ${errorStr}`); + reject(err); + } + }); + }; + +export const generateJoinerSessionId = + (opts?: { + name?: string; + }): Effect> => + async (dispatch, getState): Promise<{sessionId: string; partyKey: any}> => { + try { + const Key = BWC.getKey(); + + const partyKey = new Key({seedType: 'new'}); + + const pubKey = getPubKeyFromKey(partyKey); + + const sessionIdData: JoinerSessionId = { + pubKey, + name: opts?.name, + }; + const sessionId = encodeJoinerSessionId(sessionIdData); + + const pendingSession: PendingJoinerSession = { + sessionId, + partyKey: partyKey.toObj(), + copayerName: opts?.name, + createdAt: Date.now(), + }; + dispatch(setPendingJoinerSession(pendingSession)); + + logManager.debug(`[TSS Join] Generated and persisted session ID`); + + return { + sessionId, + partyKey: partyKey.toObj(), + }; + } catch (err) { + const errorStr = err instanceof Error ? err.message : JSON.stringify(err); + logManager.error(`Error generating joiner session ID: ${errorStr}`); + throw err; + } + }; + +export const clearPendingJoinerSession = (): Effect => dispatch => { + dispatch(removePendingJoinerSession()); + logManager.debug(`[TSS Join] Cleared pending joiner session`); +}; + +export const joinTSSWithCode = + (opts: { + joinCode: string; + partyKey: any; + myName: string; + }): Effect> => + async (dispatch, getState): Promise => { + return new Promise(async (resolve, reject) => { + try { + const { + APP: { + notificationsAccepted, + emailNotifications, + brazeEid, + defaultLanguage, + }, + WALLET: {tokenOptionsByAddress}, + } = getState(); + + const BWCKey = BWC.getKey(); + const {myName} = opts; + const copayerName = myName; + + const partyKey = new BWCKey({ + seedType: 'object', + seedData: opts.partyKey, + }); + logManager.debug('[TSS Join] Party key restored'); + + const tempTssKeyGen = new TssKeyGen({ + chain: 'BTC', + network: 'livenet', + baseUrl: BASE_BWS_URL, + key: partyKey, + }); + + const decoded = tempTssKeyGen.checkJoinCode({ + code: opts.joinCode, + opts: {encoding: 'base64'}, + }); + logManager.debug( + `[TSS Join] Decoded joinCode: ${JSON.stringify(decoded)}`, + ); + + const chain = decoded.chain.toLowerCase(); + const network = decoded.network as 'livenet' | 'testnet' | 'regtest'; + + const tssKeyGen = new TssKeyGen({ + chain: chain.toUpperCase(), + network: network, + baseUrl: BASE_BWS_URL, + key: partyKey, + }); + + logManager.debug('[TSS Join] Calling joinKey...'); + await tssKeyGen.joinKey({ + code: opts.joinCode, + opts: {encoding: 'base64'}, + }); + logManager.debug(`[TSS Join] Joined session: ${tssKeyGen.id}`); + + const m = tssKeyGen.m; + const n = tssKeyGen.n; + + const {currencyAbbreviation, currencyName} = dispatch( + mapAbbreviationAndName(chain, chain, undefined), + ); + + const walletClient = BWC.getClient(); + const tempCredentials = partyKey.createCredentials(null, { + chain: chain.toUpperCase(), + network, + n: 1, + account: 0, + }); + walletClient.fromObj(tempCredentials); + + const placeholderWallet = merge( + walletClient, + buildWalletObj( + { + ...walletClient.credentials, + currencyAbbreviation, + currencyName, + } as any, + tokenOptionsByAddress, + ), + ) as Wallet; + + placeholderWallet.pendingTssSession = true; + + const key = buildKeyObj({ + key: partyKey, + wallets: [placeholderWallet], + keyName: 'My Key', + }); + + key.tssSession = { + id: tssKeyGen.id, + partyKey: partyKey.toObj(), + sessionExport: tssKeyGen.exportSession(), + chain, + network, + m, + n, + myName, + createdAt: Date.now(), + isCreator: false, + partyId: tssKeyGen.partyId || 1, + status: 'ceremony_in_progress', + }; + + dispatch(successCreateKey({key})); + dispatch(setHomeCarouselConfig({id: key.id, show: true})); + + let walletFromBWS: any; + + await new Promise((resolve, reject) => { + tssKeyGen + .on('roundready', (r: number) => { + logManager.debug(`[TSS Join roundready] ${r}`); + try { + const currentKey = getState().WALLET.keys[key.id]; + if (currentKey?.tssSession) { + dispatch( + successUpdateKey({ + key: { + ...currentKey, + tssSession: { + ...currentKey.tssSession, + sessionExport: tssKeyGen.exportSession(), + }, + }, + }), + ); + } + } catch (e) {} + }) + .on('roundprocessed', (r: number) => + logManager.debug(`[TSS Join roundprocessed] ${r}`), + ) + .on('roundsubmitted', (r: number) => + logManager.debug(`[TSS Join roundsubmitted] ${r}`), + ) + .on('wallet', (w: any) => { + logManager.debug(`[TSS Join wallet] ${w?.id}`); + walletFromBWS = w; + }) + .on('error', (e: Error) => { + if (e.message.includes('Copayer ID already registered')) { + logManager.debug( + '[TSS Join] Copayer already registered, this is a reconnection', + ); + return; + } + logManager.error(`[TSS Join error] ${e.message}`); + reject(e); + }) + .on('complete', () => { + logManager.debug(`[TSS Join complete]`); + resolve(); + }); + + tssKeyGen.subscribe({ + copayerName: myName, + createWalletOpts: { + network, + coin: chain, + chain, + }, + }); + }); + + if (!walletFromBWS) { + throw new Error('Failed to get TSS wallet'); + } + logManager.debug(`[TSS BWS wallet]:`, walletFromBWS); + + const _tssKey = tssKeyGen.getTssKey(); + if (!_tssKey) { + throw new Error('Failed to get TSS key'); + } + + const credentials = _tssKey.createCredentials(null, { + chain: chain.toUpperCase(), + network, + account: 0, + }); + + logManager.debug( + `[TSS Join] TssKey copayerId: ${credentials.copayerId}`, + ); + logManager.debug(`[TSS Join] TssKey xPubKey: ${credentials.xPubKey}`); + + const finalWalletClient = BWC.getClient(); + finalWalletClient.fromObj(credentials.toObj()); + + const refreshWalletWithRetry = async ( + maxRetries: number = 5, + delayMs: number = 1000, + ) => { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + await new Promise(resolveRefresh => { + finalWalletClient.getStatus( + { + includeExtendedInfo: true, + }, + (err: Error, status: any) => { + if (err) { + logManager.warn( + `[TSS Ceremony] Attempt ${attempt}/${maxRetries} - Could not refresh wallet status: ${err.message}`, + ); + resolveRefresh(); + } else { + logManager.debug( + `[TSS Ceremony] Attempt ${attempt}/${maxRetries} - Status returned for wallet: ${status.wallet?.id}`, + ); + logManager.debug( + `[TSS Ceremony] Copayers count: ${ + status.wallet?.copayers?.length || 0 + }`, + ); + + if ( + status.wallet?.id === walletFromBWS.id && + status.wallet?.copayers?.length >= key.tssSession!.n + ) { + walletFromBWS = status.wallet; + credentials.addWalletInfo( + walletFromBWS.id, + walletFromBWS.name, + walletFromBWS.m, + walletFromBWS.n, + copayerName, + { + tssKeyId: walletFromBWS.tssKeyId, + useNativeSegwit: ['P2WPKH', 'P2WSH', 'P2TR'].includes( + walletFromBWS.addressType, + ), + segwitVersion: + walletFromBWS.addressType === 'P2TR' ? 1 : 0, + allowOverwrite: true, + }, + ); + if (status.wallet?.publicKeyRing) { + credentials.addPublicKeyRing( + status.wallet.publicKeyRing, + ); + } + if (status.customData.walletPrivateKey) { + credentials.addWalletPrivateKey( + status.customData.walletPrivateKey, + ); + } + finalWalletClient.fromObj(credentials.toObj()); + Object.assign(finalWalletClient, status.wallet); + logManager.debug( + `[TSS Ceremony] Successfully refreshed wallet with ${status.wallet.copayers.length} copayers`, + ); + } + resolveRefresh(); + } + }, + ); + }); + + const currentCopayersCount = + finalWalletClient.credentials?.publicKeyRing?.length || 0; + + if (currentCopayersCount >= key.tssSession!.n) { + logManager.debug( + `[TSS Ceremony] All ${ + key.tssSession!.n + } copayers found after ${attempt} attempt(s)`, + ); + break; + } + + if (attempt < maxRetries) { + logManager.debug( + `[TSS Ceremony] Only ${currentCopayersCount}/${ + key.tssSession!.n + } copayers found, retrying in ${delayMs}ms...`, + ); + await new Promise(resolve => setTimeout(resolve, delayMs)); + } else { + logManager.warn( + `[TSS Ceremony] Max retries reached. Only ${currentCopayersCount}/${ + key.tssSession!.n + } copayers found. Continuing anyway...`, + ); + } + } + }; + + await refreshWalletWithRetry(5, 1000); + + if (notificationsAccepted) { + dispatch(subscribePushNotifications(finalWalletClient, brazeEid!)); + } + if (emailNotifications?.accepted && emailNotifications?.email) { + dispatch( + subscribeEmailNotifications(finalWalletClient, { + email: emailNotifications.email, + language: defaultLanguage, + unit: 'btc', + }), + ); + } + + const walletAddress = (await dispatch( + createWalletAddress({wallet: finalWalletClient, newAddress: true}), + )) as string; + logManager.info(`[TSS Join] New address generated: ${walletAddress}`); + + const finalWallet = merge( + finalWalletClient, + walletFromBWS, + buildWalletObj( + { + ...credentials.toObj(), + currencyAbbreviation, + currencyName, + } as any, + tokenOptionsByAddress, + ), + ) as Wallet; + + delete finalWallet.pendingTssSession; + + const finalKey = buildTssKeyObj({ + tssKey: _tssKey, + wallets: [finalWallet], + keyName: 'My Key', + }); + + finalKey.tssSession = { + ...key.tssSession, + status: 'complete', + sessionExport: undefined, + }; + + dispatch(successUpdateKey({key: finalKey})); + tssKeyGen.unsubscribe(); + + logManager.debug(`[TSS Join] Complete! Wallet ID: ${walletFromBWS.id}`); + resolve(finalKey); + } catch (err) { + const errorStr = + err instanceof Error ? err.message : JSON.stringify(err); + logManager.error(`[TSS Join] Error: ${errorStr}`); + reject(err); + } + }); + }; diff --git a/src/store/wallet/effects/create/create.ts b/src/store/wallet/effects/create/create.ts index c8fd0aa1ee..8233578b40 100644 --- a/src/store/wallet/effects/create/create.ts +++ b/src/store/wallet/effects/create/create.ts @@ -223,7 +223,7 @@ export const addWallet = ...associatedWallet.credentials, currencyAbbreviation, currencyName, - }, + } as any, tokenOptsByAddress, ), ), @@ -489,7 +489,7 @@ export const createMultipleWallets = return merge( wallet, buildWalletObj( - {...wallet.credentials, currencyAbbreviation, currencyName}, + {...wallet.credentials, currencyAbbreviation, currencyName} as any, tokenOpts, ), ); diff --git a/src/store/wallet/effects/import/import.ts b/src/store/wallet/effects/import/import.ts index 82e9616d67..d008c4dbd8 100644 --- a/src/store/wallet/effects/import/import.ts +++ b/src/store/wallet/effects/import/import.ts @@ -1609,3 +1609,74 @@ const linkTokenToWallet = (tokens: Wallet[], wallets: Wallet[]) => { return wallets; }; + +export const startImportTSSFile = + (decryptedBackupText: string): Effect => + async (dispatch, getState): Promise => { + return new Promise(async (resolve, reject) => { + try { + const data = JSON.parse(decryptedBackupText); + + if (!data.isTSS) { + throw new Error(t('Invalid TSS backup file.')); + } + + if (!data.mnemonic) { + throw new Error(t('Missing mnemonic in TSS backup.')); + } + + if (!data.keychain) { + throw new Error(t('Missing keychain in TSS backup.')); + } + + const arrayToBuffer = ( + arr: number[] | null | undefined, + ): Buffer | undefined => { + if (!arr) return undefined; + return Buffer.from(arr); + }; + + const importData = { + words: data.mnemonic, + }; + + const key = (await dispatch( + startImportMnemonic(importData, {}), + )) as Key; + + if (key && data.keychain) { + const privateKeyShare = arrayToBuffer(data.keychain.privateKeyShare); + const reducedPrivateKeyShare = arrayToBuffer( + data.keychain.reducedPrivateKeyShare, + ); + + if (privateKeyShare && reducedPrivateKeyShare) { + key.properties = { + ...key.properties, + keychain: { + commonKeyChain: data.keychain.commonKeyChain, + privateKeyShare, + reducedPrivateKeyShare, + }, + } as KeyProperties; + } else { + throw new Error(t('Invalid keychain data in TSS backup.')); + } + + dispatch( + successImport({ + key, + }), + ); + } + + logManager.info('[ImportTSS] Successfully imported TSS wallet'); + resolve(key); + } catch (e) { + const errorMsg = e instanceof Error ? e.message : JSON.stringify(e); + logManager.error(`[ImportTSS] Failed to import: ${errorMsg}`); + dispatch(failedImport()); + reject(e); + } + }); + }; diff --git a/src/store/wallet/effects/join-multisig/join-multisig.ts b/src/store/wallet/effects/join-multisig/join-multisig.ts index dc9dea8dae..9307f7aea4 100644 --- a/src/store/wallet/effects/join-multisig/join-multisig.ts +++ b/src/store/wallet/effects/join-multisig/join-multisig.ts @@ -81,7 +81,7 @@ export const startJoinMultisig = ..._wallet.credentials, currencyAbbreviation, currencyName, - }), + } as any), ) as Wallet; const key = buildKeyObj({key: _key, wallets: [wallet]}); @@ -157,7 +157,7 @@ export const addWalletJoinMultisig = ...newWallet.credentials, currencyAbbreviation, currencyName, - }), + } as any), ) as Wallet, ); diff --git a/src/store/wallet/effects/send/send.ts b/src/store/wallet/effects/send/send.ts index 2601ec825b..1c9e2f8726 100644 --- a/src/store/wallet/effects/send/send.ts +++ b/src/store/wallet/effects/send/send.ts @@ -124,6 +124,11 @@ import {logManager} from '../../../../managers/LogManager'; import {ongoingProcessManager} from '../../../../managers/OngoingProcessManager'; import {DeviceEmitterEvents} from '../../../../constants/device-emitter-events'; import {ExternalServicesScreens} from '../../../../navigation/services/ExternalServicesGroup'; +import { + requiresTSSSigning, + TSSSigningCallbacks, + startTSSSigning, +} from '../tss-send/tss-send'; export const createProposalAndBuildTxDetails = ( @@ -1190,16 +1195,14 @@ export const startSendPayment = wallet, recipient, transport, + tssCallbacks, }: { txp: Partial; key: Key; wallet: Wallet; recipient?: Recipient; - - /** - * Transport for hardware wallet - */ transport?: Transport; + tssCallbacks?: TSSSigningCallbacks; }): Effect> => async dispatch => { return new Promise(async (resolve, reject) => { @@ -1220,6 +1223,7 @@ export const startSendPayment = recipient, transport, ataOwnerAddress: txp.ataOwnerAddress, + tssCallbacks, }), ); return resolve(broadcastedTx); @@ -1248,6 +1252,7 @@ export const publishAndSign = password, signingMultipleProposals, ataOwnerAddress, + tssCallbacks, }: { txp: TransactionProposal; key: Key; @@ -1257,6 +1262,7 @@ export const publishAndSign = password?: string; signingMultipleProposals?: boolean; // when signing multiple proposals from a wallet we ask for decrypt password and biometric before ataOwnerAddress?: string; // only for solana tokens, if the recipient needs to create an associated token account + tssCallbacks?: TSSSigningCallbacks; }): Effect | void>> => async (dispatch, getState) => { return new Promise(async (resolve, reject) => { @@ -1299,6 +1305,22 @@ export const publishAndSign = } } + const isTSSSigning = requiresTSSSigning(wallet, key); + + if (isTSSSigning && !tssCallbacks) { + tssCallbacks = { + onStatusChange: status => logManager.debug(`[TSS] Status: ${status}`), + onProgressUpdate: progress => + logManager.debug(`[TSS] Progress: ${JSON.stringify(progress)}`), + onCopayerStatusChange: (id, status) => + logManager.debug(`[TSS] Copayer ${id}: ${status}`), + onRoundUpdate: (round, type) => + logManager.debug(`[TSS] Round ${round} ${type}`), + onError: err => logManager.error(`[TSS] Error: ${err.message}`), + onComplete: sig => logManager.debug(`[TSS] Complete`), + }; + } + if (ataOwnerAddress && txp.tokenAddress && IsSVMChain(txp.chain)) { try { const xPrivKeyEDDSA = password @@ -1328,15 +1350,17 @@ export const publishAndSign = } try { - let publishedTx, - broadcastedTx: Partial | null = null; + let publishedTx: TransactionProposal | undefined; + let broadcastedTx: Partial | null = null; - // Already published? + // Publish if needed if (txp.status !== 'pending' || txp.refreshOnPublish) { - publishedTx = await publishTx(wallet, txp); + publishedTx = (await publishTx(wallet, txp)) as TransactionProposal; logManager.debug('success publish [publishAndSign]'); } + const txpToSign = publishedTx || txp; + if (key.isReadOnly && !key.hardwareSource) { // read only wallet return resolve(publishedTx); @@ -1344,33 +1368,51 @@ export const publishAndSign = let signedTx: TransactionProposal | null = null; - if (key.hardwareSource) { + if (isTSSSigning) { + signedTx = await dispatch( + startTSSSigning({ + key, + wallet, + txp: txpToSign as TransactionProposal, + callbacks: tssCallbacks!, + }), + ); + logManager.debug('success TSS sign [publishAndSign]'); + } else if (key.hardwareSource) { if (!transport) { return reject( new Error('No transport provided for hardware signing.'), ); } - signedTx = await signTxWithHardwareWallet( transport, wallet, key, - (publishedTx || txp) as TransactionProposal, + txpToSign as TransactionProposal, ); + logManager.debug('success hardware sign [publishAndSign]'); } else { signedTx = (await signTx( wallet, key, - publishedTx || txp, + txpToSign, password, )) as TransactionProposal; + logManager.debug('success sign [publishAndSign]'); } - logManager.debug('success sign [publishAndSign]'); - if (signedTx.status === 'accepted') { + if (isTSSSigning && tssCallbacks) { + tssCallbacks.onStatusChange('broadcasting'); + } + broadcastedTx = await broadcastTx(wallet, signedTx); logManager.debug('success broadcast [publishAndSign]'); + + if (isTSSSigning && tssCallbacks) { + tssCallbacks.onStatusChange('complete'); + } + const {fee, amount} = broadcastedTx as { fee: number; amount: number; @@ -1415,6 +1457,13 @@ export const publishAndSign = const errorStr = err instanceof Error ? err.message : JSON.stringify(err); logManager.error(`[publishAndSign] err: ${errorStr}`); + + if (isTSSSigning && tssCallbacks) { + tssCallbacks.onError( + err instanceof Error ? err : new Error(errorStr), + ); + } + // if broadcast fails, remove transaction proposal try { // except for multisig pending transactions @@ -1596,42 +1645,9 @@ export const signTx = ( ): Promise> => { return new Promise(async (resolve, reject) => { try { - const promisifiedSign = ( - keyMethods: KeyMethods | undefined, - rootPath: string, - txp: any, - password: string | undefined, - ) => { - return new Promise((resolve, reject) => { - try { - const result = keyMethods?.sign( - rootPath, - txp, - password, - (err: any, signatures: string[]) => { - if (err) { - return reject(err); - } - resolve(signatures); - }, - ); - - if (result && Array.isArray(result)) { - return resolve(result); - } - } catch (err) { - reject(err); - } - }); - }; - const rootPath = wallet.getRootPath(); - const signatures = await promisifiedSign( - key.methods, - rootPath, - txp, - password, - ); + const signatures = await key.methods?.sign(rootPath, txp, password); + wallet.pushSignatures( txp, signatures, @@ -2720,7 +2736,8 @@ export const sendCrypto = wallet => !wallet.hideWallet && !wallet.hideWalletByAccount && - wallet.isComplete(), + wallet.isComplete() && + !wallet.pendingTssSession, ) .filter(wallet => wallet.balance.sat > 0); diff --git a/src/store/wallet/effects/status/status.ts b/src/store/wallet/effects/status/status.ts index a5b6bde853..f9fca6570d 100644 --- a/src/store/wallet/effects/status/status.ts +++ b/src/store/wallet/effects/status/status.ts @@ -332,12 +332,14 @@ export const updateKeyStatus = wallet => wallet.receiveAddress === accountAddress, ); } - // remote token wallets from getStatusAll + + // remove token wallets from getStatusAll const noTokenWallets = walletsToUpdate.filter(wallet => { return ( !wallet.credentials.token && !wallet.credentials.multisigEthInfo && - wallet.credentials.isComplete() + wallet.credentials.isComplete() && + !wallet.pendingTssSession ); }); diff --git a/src/store/wallet/effects/transactions/transactions.ts b/src/store/wallet/effects/transactions/transactions.ts index 5ecd513660..44d288ec16 100644 --- a/src/store/wallet/effects/transactions/transactions.ts +++ b/src/store/wallet/effects/transactions/transactions.ts @@ -790,7 +790,7 @@ export const GetTransactionHistory = if (!keyId) { keyId = keyId; } - if (!walletId || !wallet.isComplete()) { + if (!walletId || !wallet.isComplete() || wallet.pendingTssSession) { return resolve({ transactions: [], loadMore: false, diff --git a/src/store/wallet/effects/tss-send/tss-send.ts b/src/store/wallet/effects/tss-send/tss-send.ts new file mode 100644 index 0000000000..7bb8be11de --- /dev/null +++ b/src/store/wallet/effects/tss-send/tss-send.ts @@ -0,0 +1,319 @@ +import {Effect} from '../../../index'; +import {BwcProvider} from '../../../../lib/bwc'; +import { + Key, + TransactionProposal, + Wallet, + TSSSigningStatus, + TSSSigningProgress, + TSSCopayerSignStatus, +} from '../../wallet.models'; +import {logManager} from '../../../../managers/LogManager'; +import {BASE_BWS_URL} from '../../../../constants/config'; + +const BWC = BwcProvider.getInstance(); + +const {TssSign} = require('bitcore-wallet-client/ts_build/src/lib/tsssign'); + +export interface TSSSigningCallbacks { + onStatusChange: (status: TSSSigningStatus) => void; + onProgressUpdate: (progress: TSSSigningProgress) => void; + onCopayerStatusChange: ( + copayerId: string, + status: TSSCopayerSignStatus, + ) => void; + onRoundUpdate: ( + round: number, + type: 'ready' | 'processed' | 'submitted', + ) => void; + onError: (error: Error) => void; + onComplete: (signature: string) => void; +} + +export const isTSSKey = (key: Key): boolean => { + return !!(key.tssSession?.status === 'complete' && key.tssSession?.n > 1); +}; + +export const requiresTSSSigning = (wallet: Wallet, key: Key): boolean => { + return isTSSKey(key) && !!wallet.tssKeyId; +}; + +export const getTxpMessageHash = ( + wallet: Wallet, + txp: TransactionProposal, +): Buffer => { + const Bitcore = BWC.getBitcore(); + const utils = BWC.getUtils(); + + const tx = utils.buildTx(txp); + + if (['btc', 'bch', 'ltc', 'doge'].includes(txp.chain?.toLowerCase())) { + const sighash = tx.inputs[0].getSighash( + tx, + Bitcore.crypto.Signature.SIGHASH_ALL, + ); + return sighash; + } else { + const serialized = tx.uncheckedSerialize()[0]; + + const hexString = serialized.startsWith('0x') + ? serialized.slice(2) + : serialized; + + const txBuffer = Buffer.from(hexString, 'hex'); + const hash = Bitcore.crypto.Hash.sha256(txBuffer); + + logManager.debug(`[getTxpMessageHash] txBuffer length: ${txBuffer.length}`); + logManager.debug(`[getTxpMessageHash] hash: ${hash.toString('hex')}`); + + return hash; + } +}; + +export const getDerivationPath = ( + wallet: Wallet, + txp: TransactionProposal, +): string => { + // TSS uses simple paths, not full BIP44 paths + return 'm/0/0'; +}; + +export const generateSessionId = (txp: TransactionProposal): string => { + return `sign-${txp.id}`; +}; + +const restoreKeychain = (tssKey: any): any => { + if (!tssKey?.keychain) return tssKey; + + const keychain = tssKey.keychain; + + if (keychain.privateKeyShare && !Buffer.isBuffer(keychain.privateKeyShare)) { + const data = keychain.privateKeyShare.data; + if (Array.isArray(data)) { + keychain.privateKeyShare = Buffer.from(data); + } + } + + if ( + keychain.reducedPrivateKeyShare && + !Buffer.isBuffer(keychain.reducedPrivateKeyShare) + ) { + const data = keychain.reducedPrivateKeyShare.data; + if (Array.isArray(data)) { + keychain.reducedPrivateKeyShare = Buffer.from(data); + } + } + + return tssKey; +}; + +export const startTSSSigning = + (opts: { + key: Key; + wallet: Wallet; + txp: TransactionProposal; + callbacks: TSSSigningCallbacks; + timeout?: number; + }): Effect> => + async (dispatch, getState): Promise => { + const {key, wallet, txp, callbacks, timeout = 300000} = opts; + + return new Promise(async (resolve, reject) => { + let tssSign: any = null; + let timeoutId: NodeJS.Timeout | null = null; + + try { + logManager.debug('[TSS Sign] Starting TSS signing process'); + callbacks.onStatusChange('initializing'); + + if (!isTSSKey(key)) { + throw new Error('Key is not a TSS key'); + } + + const tssKey = restoreKeychain(key.methods); + + logManager.debug( + `[TSS Sign] privateKeyShare isBuffer: ${Buffer.isBuffer( + tssKey?.keychain?.privateKeyShare, + )}`, + ); + logManager.debug( + `[TSS Sign] privateKeyShare length: ${tssKey?.keychain?.privateKeyShare?.length}`, + ); + + if (!tssKey) { + throw new Error('TSS key methods not available'); + } + + const messageHash = getTxpMessageHash(wallet, txp); + const derivationPath = getDerivationPath(wallet, txp); + + logManager.debug( + `[TSS Sign] Message hash: ${messageHash.toString('hex')}`, + ); + logManager.debug(`[TSS Sign] Derivation path: ${derivationPath}`); + + tssSign = new TssSign({ + baseUrl: BASE_BWS_URL, + credentials: wallet.credentials, + tssKey: tssKey, + }); + + const sessionId = generateSessionId(txp); + logManager.debug(`[TSS Sign] Session ID: ${sessionId}`); + + callbacks.onStatusChange('waiting_for_cosigners'); + + try { + await tssSign.start({ + id: sessionId, + messageHash, + derivationPath, + }); + logManager.debug('[TSS Sign] Signing session started successfully'); + } catch (startError: any) { + logManager.warn( + `[TSS Sign] Initial start warning: ${startError.message}`, + ); + } + + logManager.debug('[TSS Sign] Waiting for co-signers...'); + + timeoutId = setTimeout(() => { + if (tssSign) { + tssSign.unsubscribe(); + } + reject( + new Error( + 'TSS signing timeout - co-signers did not respond in time', + ), + ); + }, timeout); + + const signPromise = new Promise((resolveSign, rejectSign) => { + tssSign + .on('roundready', (round: number) => { + logManager.debug(`[TSS Sign] Round ${round} ready`); + callbacks.onRoundUpdate(round, 'ready'); + + if (round === 1) { + callbacks.onStatusChange('signature_generation'); + } + + callbacks.onProgressUpdate({ + currentRound: round, + totalRounds: 4, + status: 'processing', + }); + }) + .on('roundprocessed', (round: number) => { + logManager.debug(`[TSS Sign] Round ${round} processed`); + callbacks.onRoundUpdate(round, 'processed'); + }) + .on('roundsubmitted', (round: number) => { + logManager.debug(`[TSS Sign] Round ${round} submitted`); + callbacks.onRoundUpdate(round, 'submitted'); + }) + .on('copayerjoined', (copayerId: string) => { + logManager.debug(`[TSS Sign] Copayer joined: ${copayerId}`); + callbacks.onCopayerStatusChange(copayerId, 'joined'); + }) + .on('copayersigned', (copayerId: string) => { + logManager.debug(`[TSS Sign] Copayer signed: ${copayerId}`); + callbacks.onCopayerStatusChange(copayerId, 'signed'); + }) + .on('signature', (signature: string) => { + logManager.debug(`[TSS Sign] Signature received`); + resolveSign(signature); + }) + .on('complete', () => { + logManager.debug('[TSS Sign] Signing complete'); + }) + .on('error', (error: Error) => { + logManager.error(`[TSS Sign] Error: ${error.message}`); + rejectSign(error); + }); + + tssSign.subscribe({ + timeout: 250, + }); + }); + + const signature = await signPromise; + + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + + callbacks.onComplete(signature); + callbacks.onStatusChange('broadcasting'); + + logManager.debug('[TSS Sign] Pushing signature to txp'); + + const signedTxp = await new Promise( + (resolvePush, rejectPush) => { + wallet.pushSignatures( + txp, + [signature], + (err: Error, result: TransactionProposal) => { + if (err) { + rejectPush(err); + } else { + resolvePush(result); + } + }, + null, + ); + }, + ); + + if (tssSign) { + tssSign.unsubscribe(); + } + + logManager.debug('[TSS Sign] TSS signing completed successfully'); + callbacks.onStatusChange('complete'); + + resolve(signedTxp); + } catch (err) { + if (timeoutId) { + clearTimeout(timeoutId); + } + if (tssSign) { + try { + tssSign.unsubscribe(); + } catch (e) {} + } + const errorStr = + err instanceof Error ? err.message : JSON.stringify(err); + logManager.error(`[TSS Sign] Error: ${errorStr}`); + callbacks.onError(err instanceof Error ? err : new Error(errorStr)); + reject(err); + } + }); + }; + +export const joinTSSSigningSession = + (opts: { + key: Key; + wallet: Wallet; + txp: TransactionProposal; + callbacks: TSSSigningCallbacks; + }): Effect> => + async (dispatch, getState): Promise => { + const {key, wallet, txp, callbacks} = opts; + + logManager.debug(`[TSS Join] Joining signing session for txp: ${txp.id}`); + + // The joiner flow is the same as initiator - they both use startTSSSigning + // with the same deterministic sessionId + return dispatch( + startTSSSigning({ + key, + wallet, + txp, + callbacks, + }), + ); + }; diff --git a/src/store/wallet/utils/wallet.ts b/src/store/wallet/utils/wallet.ts index f816a16099..7c808fa342 100644 --- a/src/store/wallet/utils/wallet.ts +++ b/src/store/wallet/utils/wallet.ts @@ -911,7 +911,9 @@ export const getAllWalletClients = (keys: { key.wallets .filter( wallet => - !wallet.credentials.token && wallet.credentials.isComplete(), + !wallet.credentials.token && + wallet.credentials.isComplete() && + !wallet.pendingTssSession, ) .forEach(walletClient => { walletClients.push(walletClient); @@ -1075,7 +1077,7 @@ export const buildUIFormattedWallet: ( credentials.n > 1 ? `- Multisig ${credentials.m}/${credentials.n}` : undefined, - isComplete: credentials.isComplete(), + isComplete: credentials.isComplete() && !wallet.pendingTssSession, receiveAddress, account: credentials.account, } as WalletRowProps; @@ -1210,7 +1212,8 @@ export const buildAccountList = ( ?.toLowerCase() ?.includes(searchInput.toLowerCase()) : true, - isComplete: wallet.credentials.isComplete(), + isComplete: + wallet.credentials.isComplete() && !wallet.pendingTssSession, }; const allMatch = Object.values(matches).every(Boolean); @@ -1221,7 +1224,7 @@ export const buildAccountList = ( } if (opts?.filterByComplete) { - if (!wallet.credentials.isComplete()) { + if (!wallet.credentials.isComplete() && wallet.pendingTssSession) { return; } } @@ -1252,7 +1255,11 @@ export const buildAccountList = ( let accountKey = receiveAddress; - if (!accountKey && !wallet?.credentials?.isComplete()) { + if ( + !accountKey && + !wallet?.credentials?.isComplete() && + wallet.pendingTssSession + ) { // Workaround for incomplete multisig wallets accountKey = walletId; } diff --git a/src/store/wallet/wallet.actions.ts b/src/store/wallet/wallet.actions.ts index 54dbaeae73..38ed488ba5 100644 --- a/src/store/wallet/wallet.actions.ts +++ b/src/store/wallet/wallet.actions.ts @@ -3,6 +3,7 @@ import { CacheFeeLevel, CryptoBalance, Key, + PendingJoinerSession, Token, TransactionProposal, Wallet, @@ -280,3 +281,14 @@ export const successUpdateWalletBalancesAndStatus = (payload: { type: WalletActionTypes.SUCCESS_UPDATE_WALLET_BALANCES_AND_STATUS, payload, }); + +export const setPendingJoinerSession = ( + session: PendingJoinerSession | null, +) => ({ + type: WalletActionTypes.SET_PENDING_JOINER_SESSION, + payload: session, +}); + +export const removePendingJoinerSession = () => ({ + type: WalletActionTypes.REMOVE_PENDING_JOINER_SESSION, +}); diff --git a/src/store/wallet/wallet.models.ts b/src/store/wallet/wallet.models.ts index 7c38d95440..fd2cb22b71 100644 --- a/src/store/wallet/wallet.models.ts +++ b/src/store/wallet/wallet.models.ts @@ -54,6 +54,12 @@ export interface KeyProperties { m: string; n: string; }; + serializedKeychain?: any; + keychain?: { + privateKeyShare: Buffer | {data: number[]}; + reducedPrivateKeyShare: Buffer | {data: number[]}; + commonKeyChain: string; + }; } export interface Key { @@ -77,6 +83,7 @@ export interface Key { }; }; hardwareSource?: SupportedHardwareSource; + tssSession?: TssSessionData; } export interface Wallet extends WalletObj, API {} @@ -162,6 +169,7 @@ export interface WalletObj { m: number; n: number; }; + pendingTssSession?: boolean; } export interface KeyOptions { @@ -562,6 +570,74 @@ export interface Utxo { checked?: boolean; } +export interface JoinerSessionId { + pubKey: string; + name?: string; +} + +export interface TSSCopayerInfo { + partyId: number; + pubKey: string; + name: string; + joinCode?: string; + status: 'pending' | 'invited' | 'joined'; +} + +export interface TssSessionData { + id: string; + partyKey: any; + sessionExport?: string; + chain: string; + network: string; + m: number; + n: number; + password?: string; + myName: string; + walletName?: string; + createdAt: number; + isCreator: boolean; + partyId: number; + status: + | 'collecting_copayers' + | 'ready_to_start' + | 'ceremony_in_progress' + | 'complete'; + invitationCode?: string; + copayers?: TSSCopayerInfo[]; + creatorPubKey?: string; +} + +export interface PendingJoinerSession { + sessionId: string; + partyKey: any; + copayerName?: string; + createdAt: number; +} + +export type TSSSigningStatus = + | 'initializing' + | 'waiting_for_cosigners' + | 'signature_generation' + | 'broadcasting' + | 'complete' + | 'error'; + +export interface TSSSigningProgress { + currentRound: number; + totalRounds: number; + status: 'pending' | 'processing' | 'complete'; + message?: string; +} + +export type TSSCopayerSignStatus = 'pending' | 'joined' | 'signed' | 'error'; + +export interface PendingJoinerSession { + sessionId: string; + partyKey: any; + copayerName?: string; + createdAt: number; +} + /** * Partial interface for the bitcore-lib Script type representing a * bitcoin transaction script. diff --git a/src/store/wallet/wallet.reducer.ts b/src/store/wallet/wallet.reducer.ts index ae975d701c..ae98457c22 100644 --- a/src/store/wallet/wallet.reducer.ts +++ b/src/store/wallet/wallet.reducer.ts @@ -1,4 +1,4 @@ -import {Key, Token} from './wallet.models'; +import {Key, PendingJoinerSession, Token} from './wallet.models'; import {WalletActionType, WalletActionTypes} from './wallet.types'; import {FeeLevels} from './effects/fee/fee'; import {CurrencyOpts} from '../../constants/currencies'; @@ -35,6 +35,7 @@ export interface WalletState { accountEvmCreationMigrationComplete: boolean; accountSvmCreationMigrationComplete: boolean; svmAddressFixComplete: boolean; + pendingJoinerSession: PendingJoinerSession | null; } export const initialState: WalletState = { @@ -69,6 +70,7 @@ export const initialState: WalletState = { accountEvmCreationMigrationComplete: false, accountSvmCreationMigrationComplete: false, svmAddressFixComplete: false, + pendingJoinerSession: null, }; export const walletReducer = ( @@ -616,6 +618,18 @@ export const walletReducer = ( }; } + case WalletActionTypes.SET_PENDING_JOINER_SESSION: + return { + ...state, + pendingJoinerSession: action.payload, + }; + + case WalletActionTypes.REMOVE_PENDING_JOINER_SESSION: + return { + ...state, + pendingJoinerSession: null, + }; + default: return state; } diff --git a/src/store/wallet/wallet.types.ts b/src/store/wallet/wallet.types.ts index ea50d7432f..ff995a8459 100644 --- a/src/store/wallet/wallet.types.ts +++ b/src/store/wallet/wallet.types.ts @@ -6,6 +6,7 @@ import { TransactionProposal, CacheFeeLevel, CryptoBalance, + PendingJoinerSession, } from './wallet.models'; export enum WalletActionTypes { @@ -54,6 +55,11 @@ export enum WalletActionTypes { SET_ACCOUNT_SVM_CREATION_MIGRATION_COMPLETE = 'APP/SET_ACCOUNT_SVM_CREATION_MIGRATION_COMPLETE', SET_SVM_ADDRESS_CREATION_FIX_COMPLETE = 'APP/SET_SVM_ADDRESS_CREATION_FIX_COMPLETE', SUCCESS_UPDATE_WALLET_BALANCES_AND_STATUS = 'WALLET/SUCCESS_UPDATE_WALLET_BALANCES_AND_STATUS', + SET_PENDING_TSS_SESSION = 'WALLET/SET_PENDING_TSS_SESSION', + UPDATE_PENDING_TSS_SESSION = 'WALLET/UPDATE_PENDING_TSS_SESSION', + REMOVE_PENDING_TSS_SESSION = 'WALLET/REMOVE_PENDING_TSS_SESSION', + SET_PENDING_JOINER_SESSION = 'WALLET/SET_PENDING_JOINER_SESSION', + REMOVE_PENDING_JOINER_SESSION = 'WALLET/REMOVE_PENDING_JOINER_SESSION', } interface successWalletStoreInit { @@ -334,6 +340,16 @@ interface successUpdateWalletBalancesAndStatus { }; } +interface removePendingJoinerSession { + type: typeof WalletActionTypes.REMOVE_PENDING_JOINER_SESSION; + payload: string; +} + +interface setPendingJoinerSession { + type: typeof WalletActionTypes.SET_PENDING_JOINER_SESSION; + payload: PendingJoinerSession | null; +} + export type WalletActionType = | successWalletStoreInit | failedWalletStoreInit @@ -376,4 +392,6 @@ export type WalletActionType = | setAccountEVMCreationMigrationComplete | setAccountSVMCreationMigrationComplete | setSvmAddressCreationFixComplete - | successUpdateWalletBalancesAndStatus; + | successUpdateWalletBalancesAndStatus + | setPendingJoinerSession + | removePendingJoinerSession; diff --git a/src/utils/helper-methods.ts b/src/utils/helper-methods.ts index 54be261d3a..24c7f7f1d7 100644 --- a/src/utils/helper-methods.ts +++ b/src/utils/helper-methods.ts @@ -665,7 +665,11 @@ export const fixWalletAddresses = async ({ await Promise.all( wallets.map(async wallet => { try { - if (!wallet.receiveAddress && wallet?.credentials?.isComplete()) { + if ( + !wallet.receiveAddress && + wallet?.credentials?.isComplete() && + !wallet.pendingTssSession + ) { const walletAddress = (await appDispatch( createWalletAddress({wallet, newAddress: false, skipDispatch}), )) as string; diff --git a/yarn.lock b/yarn.lock index e781efbb2e..fc808361f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6785,10 +6785,10 @@ bitcoinjs-lib@^5.2.0: varuint-bitcoin "^1.0.4" wif "^2.0.1" -bitcore-lib-cash@^11.3.7: - version "11.3.7" - resolved "https://registry.yarnpkg.com/bitcore-lib-cash/-/bitcore-lib-cash-11.3.7.tgz#f78ba226b4b588e2136846dbeff3d4a1f2faa1ac" - integrity sha512-29tduOOCku6xwwVHRkY/wnN5zdQx06xiOYDJuTPkiZMSU7AmAgqcfFuZOQDhplaplek5RD2XIDWC3xibY4l68w== +bitcore-lib-cash@^11.4.5: + version "11.4.5" + resolved "https://registry.yarnpkg.com/bitcore-lib-cash/-/bitcore-lib-cash-11.4.5.tgz#d86afef7d9c3ad281c8689274769b0c5ff1929d3" + integrity sha512-6xArtkHhofhbNtvR/Ye9rGJrQZuPmAr1cR3OUmCLhTtBUAfhHquD41gF4JyGrZ+ydKF2V0IiI5x2qiFqmnTLGg== dependencies: bn.js "=4.11.8" bs58 "^4.0.1" @@ -6797,10 +6797,10 @@ bitcore-lib-cash@^11.3.7: inherits "=2.0.1" lodash "^4.17.20" -bitcore-lib-doge@^11.3.7: - version "11.3.7" - resolved "https://registry.yarnpkg.com/bitcore-lib-doge/-/bitcore-lib-doge-11.3.7.tgz#ac3dfc885380b68ed92273bd328ac51a70635d5c" - integrity sha512-PtIIHBjtD9zuI1Ch/tXBO1398WPh8uThCITeWQS3oJkTC5JJTp9KZFuNaGmbClhCRf9O1S0HXqdbKlNsauvK4g== +bitcore-lib-doge@^11.4.5: + version "11.4.5" + resolved "https://registry.yarnpkg.com/bitcore-lib-doge/-/bitcore-lib-doge-11.4.5.tgz#4021170159662bd3584021fa0705c738857f4937" + integrity sha512-hVQRxtp+7hqjnLaAs1MQ7wtGqbOYMZcG6fo+5MdBCZ0VSbzactXc9rRdRLGx/DBDF+MNC8xxvPMbbYvIkG1D5Q== dependencies: bn.js "=4.11.8" bs58 "^4.0.1" @@ -6810,10 +6810,10 @@ bitcore-lib-doge@^11.3.7: lodash "^4.17.20" scryptsy "2.1.0" -bitcore-lib-ltc@^11.3.7: - version "11.3.7" - resolved "https://registry.yarnpkg.com/bitcore-lib-ltc/-/bitcore-lib-ltc-11.3.7.tgz#81ab21314cc1aa1e614167e3ce6fa6a7fe8d9ca9" - integrity sha512-PZt9Hae7mlT8AMxZZcOo11eVYFbmFPi3U7kCovHwSMUaeBa8TSeL/145d0B9YSmVkkyA4BrTIqSUQwwbh8KoAg== +bitcore-lib-ltc@^11.4.5: + version "11.4.5" + resolved "https://registry.yarnpkg.com/bitcore-lib-ltc/-/bitcore-lib-ltc-11.4.5.tgz#ef9ec83c4518be8ce796b569f347b1a9a63e0926" + integrity sha512-p6ZwrH4rJOQ4pLpiRsEtgnov2o6Fd9ZE6nNL2JBou+jUTiSlaMdivkk8WLMTHbUvYv2r1ZOjqLcXU0tDvuED+g== dependencies: bech32 "=2.0.0" bn.js "=4.11.8" @@ -6824,10 +6824,10 @@ bitcore-lib-ltc@^11.3.7: lodash "^4.17.20" scryptsy "2.1.0" -bitcore-lib@^11.3.7: - version "11.3.7" - resolved "https://registry.yarnpkg.com/bitcore-lib/-/bitcore-lib-11.3.7.tgz#b4c0ae852e8733f3d6c942b9ddef017c45335b79" - integrity sha512-Smibdh/fOqVU88XJN/MEYxsqCi55bs8aDBf1V7hslZl+0L3oUbXAt3McLBDrKY02RmYwjYTZ81TDfmHxcMwvgg== +bitcore-lib@^11.4.5: + version "11.4.5" + resolved "https://registry.yarnpkg.com/bitcore-lib/-/bitcore-lib-11.4.5.tgz#3627f05e3080ae213f5d8792e671cb1311ca9c63" + integrity sha512-MGONtDex24aYIcCMJDtnbniTu4gwZcuUpDODNURpzGh8wUggppA6Yp1VMJLkHTfAekH2zUzZPn89PIFJqddr2g== dependencies: bech32 "=2.0.0" bn.js "=4.11.8" @@ -6837,32 +6837,32 @@ bitcore-lib@^11.3.7: inherits "=2.0.1" lodash "^4.17.20" -bitcore-mnemonic@^11.3.6: - version "11.3.7" - resolved "https://registry.yarnpkg.com/bitcore-mnemonic/-/bitcore-mnemonic-11.3.7.tgz#1ac233c50bb45e20a8ba926f259af551825800dd" - integrity sha512-fyjjrkfDkUnjZ4l3he6y7Cyit9NKbVGbWGRlpCefyGGZUxXf3wkbEeQwa2H7iQhJqVyyEmpecL6XmZvudRSkcA== +bitcore-mnemonic@^11.4.5: + version "11.4.5" + resolved "https://registry.yarnpkg.com/bitcore-mnemonic/-/bitcore-mnemonic-11.4.5.tgz#6c39bbc78921c21f300def13bc517a9624367738" + integrity sha512-U7vYDPNt9xODGPPIUHevsJiOrBus5+E5uo7/dJQYpBkNcwiARE3q5pqn78eZ21bttJ81dCtaLdbkL76UAVSbIg== dependencies: - bitcore-lib "^11.3.7" + bitcore-lib "^11.4.5" unorm "^1.4.1" -bitcore-tss@^11.3.6: - version "11.3.7" - resolved "https://registry.yarnpkg.com/bitcore-tss/-/bitcore-tss-11.3.7.tgz#6dfe7ccf2ccd58634865c68a11ba73f995b6693e" - integrity sha512-NnPy+pSEz10VNcgVfmjbhGUX/tzHP1OY9By7JhTC+yHETVL+W1U7x6VT7BRFPtLHmVteMQh6iUgAtQZwF5TfWw== +bitcore-tss@^11.4.5: + version "11.4.5" + resolved "https://registry.yarnpkg.com/bitcore-tss/-/bitcore-tss-11.4.5.tgz#ba969ab7bca6027e2eb8f2ed820968782028f9d6" + integrity sha512-HBIT0bNqa9OHob0l1xH6o+/ig2cy/4ozMGjykhkYZvI6o2f0rk+5IfYLC9Rlq7xDcdngRREf+yv2tEcTrfA3mw== dependencies: "@bitgo/sdk-lib-mpc" "^10.1.2" - bitcore-lib "^11.3.7" + bitcore-lib "^11.4.5" -bitcore-wallet-client@11.3.6: - version "11.3.6" - resolved "https://registry.yarnpkg.com/bitcore-wallet-client/-/bitcore-wallet-client-11.3.6.tgz#cae255f1571044a1d50f56ba23329fe25cc750fc" - integrity sha512-9nvNVU1VN7LHiiidSoA3B7YdZ6LNqzIjW+JmqIU4AQPDESWNWzDPLk0aWcF4Ioj5qpXtvIx0zlAXejgNE2q/JQ== +bitcore-wallet-client@11.4.6: + version "11.4.6" + resolved "https://registry.yarnpkg.com/bitcore-wallet-client/-/bitcore-wallet-client-11.4.6.tgz#1b2a8848f337d479cf5299d35b3ae2a2e1a3dba1" + integrity sha512-oDqWsA3syl5NujE3R9SQ9vwfPi4Y98WbSAb70ENiAFC6upB3BK7QfyjKWWZfPGy7HOgy7DhJdgCfln8gXXqWog== dependencies: async "0.9.2" bip38 "1.4.0" - bitcore-mnemonic "^11.3.6" - bitcore-tss "^11.3.6" - crypto-wallet-core "^11.3.6" + bitcore-mnemonic "^11.4.5" + bitcore-tss "^11.4.5" + crypto-wallet-core "^11.4.5" json-stable-stringify "1.0.1" preconditions "2.2.3" sjcl "1.0.8" @@ -8090,23 +8090,24 @@ crypto-js@3.1.9-1: resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-3.1.9-1.tgz#fda19e761fc077e01ffbfdc6e9fdfc59e8806cd8" integrity sha512-W93aKztssqf29OvUlqfikzGyYbD1rpkXvGP9IQ1JchLY3bxaLXZSWYbwrtib2vk8DobrDzX7PIXcDWHp0B6Ymw== -crypto-wallet-core@^11.3.6: - version "11.3.7" - resolved "https://registry.yarnpkg.com/crypto-wallet-core/-/crypto-wallet-core-11.3.7.tgz#b0182bc1f934895891c1aec17f06370799994f8c" - integrity sha512-gyjCSAU41TIFPBuzZ8XOi1Fxyl4BJAJePGKEshXpMuySSACSRFe7/y67UntzUFNYqoY1OH/9jWSJ3XQQDBsUiw== +crypto-wallet-core@^11.4.5: + version "11.4.5" + resolved "https://registry.yarnpkg.com/crypto-wallet-core/-/crypto-wallet-core-11.4.5.tgz#c275cfcf2bee50a46106c536df7edb1f9797c5d5" + integrity sha512-7HrGIBcvl32/hw4HOMwsxgMHayzLI7aX/VFR0+kuM0f6loiBQqPvY7RwgG/kf+lhdBfguRaXMSH1UVrfMhhgSw== dependencies: "@solana-program/compute-budget" "^0.7.0" "@solana-program/memo" "^0.7.0" "@solana-program/system" "^0.7.0" "@solana-program/token" "^0.5.1" "@solana/kit" "^2.1.0" - bitcore-lib "^11.3.7" - bitcore-lib-cash "^11.3.7" - bitcore-lib-doge "^11.3.7" - bitcore-lib-ltc "^11.3.7" + bitcore-lib "^11.4.5" + bitcore-lib-cash "^11.4.5" + bitcore-lib-doge "^11.4.5" + bitcore-lib-ltc "^11.4.5" ed25519-hd-key "^1.3.0" ethers "6.13.5" info "0.0.6-beta.0" + ripple-binary-codec "^1.10.0" web3 "1.4.0" xrpl "2.13.0" From e729a8d2cf723f6b7f12cd107728dffe39bdb01f Mon Sep 17 00:00:00 2001 From: Gabriel Masclef Date: Mon, 22 Dec 2025 15:28:45 -0300 Subject: [PATCH 03/16] Ref: external services responses with bwc v11 --- .../externalServicesOfferSelector.tsx | 29 +++++++++------ .../services/screens/BuyAndSellRoot.tsx | 37 +++++++++++++------ .../screens/MoonpaySellCheckout.tsx | 9 +++-- .../swap-crypto/screens/SwapCryptoOffers.tsx | 4 +- .../swap-crypto/screens/ThorswapCheckout.tsx | 12 +++--- .../swap-crypto/utils/changelly-utils.ts | 9 +++-- .../screens/MoonpaySellDetails.tsx | 9 +++-- 7 files changed, 69 insertions(+), 40 deletions(-) diff --git a/src/navigation/services/components/externalServicesOfferSelector.tsx b/src/navigation/services/components/externalServicesOfferSelector.tsx index a758a2ad08..c6a569ad11 100644 --- a/src/navigation/services/components/externalServicesOfferSelector.tsx +++ b/src/navigation/services/components/externalServicesOfferSelector.tsx @@ -812,7 +812,8 @@ const ExternalServicesOfferSelector: React.FC< selectedWallet .banxaGetQuote(requestData) - .then((quoteData: BanxaQuoteData) => { + .then((data: any) => { + const quoteData: BanxaQuoteData = data?.body ?? data; if (quoteData?.data?.prices?.[0]?.coin_amount) { const data = quoteData.data.prices[0]; @@ -969,7 +970,8 @@ const ExternalServicesOfferSelector: React.FC< selectedWallet .moonpayGetQuote(requestData) - .then(data => { + .then((data: any) => { + data = data?.body ?? data; if (data?.baseCurrencyAmount) { offers.moonpay.amountLimits = { min: data.baseCurrency.minBuyAmount, @@ -1141,9 +1143,8 @@ const ExternalServicesOfferSelector: React.FC< env: rampEnv, }; - const data: RampQuoteRequestData = await selectedWallet.rampGetQuote( - requestData, - ); + const _data: any = await selectedWallet.rampGetQuote(requestData); + const data: RampQuoteRequestData = _data?.body ?? _data; let paymentMethodData: RampQuoteResultForPaymentMethod | undefined; if (data?.asset) { @@ -1342,7 +1343,8 @@ const ExternalServicesOfferSelector: React.FC< selectedWallet .sardineGetQuote(requestData) - .then((data: any) => { + .then((_data: any) => { + const data = _data?.body ?? _data; if (data && data.quantity) { offers.sardine.outOfLimitMsg = undefined; offers.sardine.errorMsg = undefined; @@ -1477,7 +1479,8 @@ const ExternalServicesOfferSelector: React.FC< } selectedWallet .simplexGetQuote(requestData) - .then(data => { + .then((_data: any) => { + const data = _data?.body ?? _data; if (data && data.quote_id) { offers.simplex.outOfLimitMsg = undefined; offers.simplex.errorMsg = undefined; @@ -1637,7 +1640,8 @@ const ExternalServicesOfferSelector: React.FC< selectedWallet .transakGetQuote(requestData) - .then((data: TransakQuoteData) => { + .then((_data: any) => { + const data: TransakQuoteData = _data?.body ?? _data; if (data?.response?.cryptoAmount) { const transakQuoteData = data.response; offers.transak.outOfLimitMsg = undefined; @@ -1761,7 +1765,8 @@ const ExternalServicesOfferSelector: React.FC< selectedWallet .moonpayGetSellQuote(requestData) - .then(data => { + .then(_data => { + const data = _data?.body ?? _data; if (data?.baseCurrencyAmount) { sellOffers.moonpay.amountLimits = { min: data.baseCurrency.minsellAmount, @@ -1915,7 +1920,8 @@ const ExternalServicesOfferSelector: React.FC< selectedWallet .rampGetSellQuote(requestData) - .then((data: RampGetSellQuoteData) => { + .then((_data: any) => { + const data: RampGetSellQuoteData = _data?.body ?? _data; let paymentMethodData: RampSellQuoteResultForPayoutMethod | undefined; if (data?.asset) { switch (withdrawalMethod?.method) { @@ -2172,7 +2178,8 @@ const ExternalServicesOfferSelector: React.FC< selectedWallet .simplexGetSellQuote(requestData) - .then((data: SimplexGetSellQuoteData) => { + .then(_data => { + const data: SimplexGetSellQuoteData = _data?.body ?? _data; if (data && data.fiat_amount) { sellOffers.simplex.outOfLimitMsg = undefined; sellOffers.simplex.errorMsg = undefined; diff --git a/src/navigation/services/screens/BuyAndSellRoot.tsx b/src/navigation/services/screens/BuyAndSellRoot.tsx index dfc1ee60ee..98396c08d4 100644 --- a/src/navigation/services/screens/BuyAndSellRoot.tsx +++ b/src/navigation/services/screens/BuyAndSellRoot.tsx @@ -2194,7 +2194,8 @@ const BuyAndSellRoot = ({ let data: BanxaCreateOrderData, banxaOrderData: BanxaOrderData; try { - data = await selectedWallet.banxaCreateOrder(quoteData); + const _data = await selectedWallet.banxaCreateOrder(quoteData); + data = _data?.body ?? _data; } catch (err) { setOpeningBrowser(false); const title = t('Banxa Error'); @@ -2334,9 +2335,10 @@ const BuyAndSellRoot = ({ let data: MoonpayGetSignedPaymentUrlData; try { - data = (await selectedWallet.moonpayGetSignedPaymentUrl( + const _data: any = await selectedWallet.moonpayGetSignedPaymentUrl( quoteData, - )) as MoonpayGetSignedPaymentUrlData; + ); + data = _data?.body ?? _data; } catch (err) { setOpeningBrowser(false); const title = t('MoonPay Error'); @@ -2445,7 +2447,8 @@ const BuyAndSellRoot = ({ let data: RampGetSellSignedPaymentUrlData; try { - data = await selectedWallet.rampGetSignedPaymentUrl(quoteData); + const _data = await selectedWallet.rampGetSignedPaymentUrl(quoteData); + data = _data?.body ?? _data; } catch (err) { setOpeningBrowser(false); const title = t('Ramp Network Error'); @@ -2522,7 +2525,8 @@ const BuyAndSellRoot = ({ enabled: ['ach', 'apple_pay', 'card', 'sepa'], }, }; - authTokenData = await selectedWallet.sardineGetToken(quoteData); + const _authTokenData = await selectedWallet.sardineGetToken(quoteData); + authTokenData = _authTokenData?.body ?? _authTokenData; } catch (err) { setOpeningBrowser(false); const title = t('Sardine Error'); @@ -2655,7 +2659,8 @@ const BuyAndSellRoot = ({ }; simplexPaymentRequest(selectedWallet, address, quoteData, createdOn) - .then(async req => { + .then(async _req => { + const req = _req?.body ?? _req; if (req && req.error) { const title = t('Simplex Error'); const reason = 'simplexPaymentRequest Error'; @@ -2795,9 +2800,11 @@ const BuyAndSellRoot = ({ let _accessToken = accessTokenTransak?.accessToken; if (!_accessToken || accessTokenTransak.expiresAt < nowTimestamp) { try { - const {data} = await selectedWallet.transakGetAccessToken(''); + let _data: any = await selectedWallet.transakGetAccessToken(''); + _data = _data?.body ?? _data; + const {data} = _data; dispatch(BuyCryptoActions.updateAccessTokenTransak(data)); - _accessToken = data.accessToken; + _accessToken = _data.accessToken; } catch (err: any) { const title = t('Transak Error'); const msg = getErrorMessage(err); @@ -2849,7 +2856,8 @@ const BuyAndSellRoot = ({ let data: TransakSignedUrlData; try { - data = await selectedWallet.transakGetSignedPaymentUrl(quoteData); + const _data = await selectedWallet.transakGetSignedPaymentUrl(quoteData); + data = _data?.body ?? _data; } catch (err) { const title = t('Transak Error'); const msg = getErrorMessage(err); @@ -2977,7 +2985,10 @@ const BuyAndSellRoot = ({ let data: MoonpayGetSellSignedPaymentUrlData; try { - data = await selectedWallet.moonpayGetSellSignedPaymentUrl(requestData); + const _data = await selectedWallet.moonpayGetSellSignedPaymentUrl( + requestData, + ); + data = _data?.body ?? _data; if (!data?.urlWithSignature) { const msg = t( 'Our partner Moonpay is not currently available. Please try again later.', @@ -3105,7 +3116,8 @@ const BuyAndSellRoot = ({ let data: RampGetSellSignedPaymentUrlData; try { - data = await selectedWallet.rampGetSignedPaymentUrl(requestData); + const _data = await selectedWallet.rampGetSignedPaymentUrl(requestData); + data = _data?.body ?? _data; if (!data?.urlWithSignature) { const msg = t( 'Our partner Ramp Network is not currently available. Please try again later.', @@ -3460,7 +3472,8 @@ const BuyAndSellRoot = ({ selectedWallet .simplexSellPaymentRequest(quoteData) - .then(async (data: SimplexSellPaymentRequestData) => { + .then(async (_data: any) => { + const data: SimplexSellPaymentRequestData = _data?.body ?? _data; if (data?.error) { const msg = getErrorMessage(data.error); const reason = 'simplexSellPaymentRequest Error'; diff --git a/src/navigation/services/sell-crypto/screens/MoonpaySellCheckout.tsx b/src/navigation/services/sell-crypto/screens/MoonpaySellCheckout.tsx index 29111653ba..e4457bae0a 100644 --- a/src/navigation/services/sell-crypto/screens/MoonpaySellCheckout.tsx +++ b/src/navigation/services/sell-crypto/screens/MoonpaySellCheckout.tsx @@ -118,6 +118,7 @@ import TransportBLE from '@ledgerhq/react-native-hw-transport-ble'; import TransportHID from '@ledgerhq/react-native-hid'; import {LISTEN_TIMEOUT, OPEN_TIMEOUT} from '../../../../constants/config'; import {useOngoingProcess, usePaymentSent} from '../../../../contexts'; +import {Network} from '../../../../constants'; // Styled export const SellCheckoutContainer = styled.SafeAreaView` @@ -191,7 +192,7 @@ const MoonpaySellCheckout: React.FC = () => { const {showOngoingProcess, hideOngoingProcess} = useOngoingProcess(); let destinationTag: string | undefined; // handle this if XRP is enabled to sell - let status: string; + let status: string | undefined; let ataOwnerAddress: string | undefined; // use the ref when doing any work that could cause disconnects and cause a new transport to be passed in mid-function @@ -328,7 +329,9 @@ const MoonpaySellCheckout: React.FC = () => { )}`, ); try { - const sellQuote = await wallet.moonpayGetSellQuote(requestData); + const _sellQuote = await wallet.moonpayGetSellQuote(requestData); + const sellQuote = _sellQuote?.body ?? _sellQuote; + if (sellQuote?.quoteCurrencyAmount) { sellQuote.totalFee = sellQuote.extraFeeAmount + sellQuote.feeAmount; @@ -581,7 +584,7 @@ const MoonpaySellCheckout: React.FC = () => { if (!configFn) { throw new Error(`Unsupported currency: ${chain.toUpperCase()}`); } - const params = configFn(network); + const params = configFn(network as Network); await prepareLedgerApp( params.appName, transportRef, diff --git a/src/navigation/services/swap-crypto/screens/SwapCryptoOffers.tsx b/src/navigation/services/swap-crypto/screens/SwapCryptoOffers.tsx index b7abf57450..612b5b26a1 100644 --- a/src/navigation/services/swap-crypto/screens/SwapCryptoOffers.tsx +++ b/src/navigation/services/swap-crypto/screens/SwapCryptoOffers.tsx @@ -687,8 +687,8 @@ const SwapCryptoOffers: React.FC = () => { } try { - const thorswapQuoteData: ThorswapGetSwapQuoteData = - await selectedWalletFrom.thorswapGetSwapQuote(requestData); + const _data = await selectedWalletFrom.thorswapGetSwapQuote(requestData); + const thorswapQuoteData: ThorswapGetSwapQuoteData = _data?.body ?? _data; // TODO: remove this if(...) when Thorswap team fix the 1inch issue // Workaround to prevent an issue from Thorswap in which 1inch v4 is the spender and 1inch v5 is the destination address diff --git a/src/navigation/services/swap-crypto/screens/ThorswapCheckout.tsx b/src/navigation/services/swap-crypto/screens/ThorswapCheckout.tsx index 5d1b7233c7..e4334b8340 100644 --- a/src/navigation/services/swap-crypto/screens/ThorswapCheckout.tsx +++ b/src/navigation/services/swap-crypto/screens/ThorswapCheckout.tsx @@ -133,6 +133,7 @@ import { } from '../constants/ThorswapConstants'; import {ExchangeConfig} from '../../../../store/external-services/external-services.types'; import {useOngoingProcess, usePaymentSent} from '../../../../contexts'; +import {Network} from '../../../../constants'; // Styled export const SwapCheckoutContainer = styled.SafeAreaView` @@ -210,8 +211,8 @@ const ThorswapCheckout: React.FC = () => { const alternativeIsoCode = 'USD'; let addressFrom: string; // Refund address let addressTo: string; // Receiving address - let payinExtraId: string; - let status: string; + let payinExtraId: string | undefined; + let status: string | undefined; let payinAddress: string; // use the ref when doing any work that could cause disconnects and cause a new transport to be passed in mid-function @@ -321,9 +322,8 @@ const ThorswapCheckout: React.FC = () => { let thorswapQuoteData: ThorswapGetSwapQuoteData | undefined; try { - thorswapQuoteData = await fromWalletSelected.thorswapGetSwapQuote( - requestData, - ); + const _data = await fromWalletSelected.thorswapGetSwapQuote(requestData); + thorswapQuoteData = _data?.body ?? _data; } catch (err) { logger.error( 'Thorswap createThorswapTransaction > thorswapGetSwapQuote Error: ' + @@ -835,7 +835,7 @@ const ThorswapCheckout: React.FC = () => { if (!configFn) { throw new Error(`Unsupported currency: ${coin.toUpperCase()}`); } - const params = configFn(network); + const params = configFn(network as Network); await prepareLedgerApp( params.appName, transportRef, diff --git a/src/navigation/services/swap-crypto/utils/changelly-utils.ts b/src/navigation/services/swap-crypto/utils/changelly-utils.ts index ce96befada..833e0244d6 100644 --- a/src/navigation/services/swap-crypto/utils/changelly-utils.ts +++ b/src/navigation/services/swap-crypto/utils/changelly-utils.ts @@ -561,7 +561,8 @@ export const changellyGetFixRateForAmount = async ( amountFrom: data.amountFrom, }; - const response = await wallet.changellyGetFixRateForAmount(messageData); + const _response = await wallet.changellyGetFixRateForAmount(messageData); + const response = _response?.body ?? _response; if (response.id && response?.id !== messageData.id) { return Promise.reject( @@ -586,7 +587,8 @@ export const changellyGetPairsParams = async ( coinTo: data.coinTo, }; - const response = await wallet.changellyGetPairsParams(messageData); + const _response = await wallet.changellyGetPairsParams(messageData); + const response = _response?.body ?? _response; if (response.id && response.id !== messageData.id) { return Promise.reject( @@ -615,7 +617,8 @@ export const changellyCreateFixTransaction = async ( refundAddress: data.refundAddress, }; - const response = await wallet.changellyCreateFixTransaction(messageData); + const _response = await wallet.changellyCreateFixTransaction(messageData); + const response = _response?.body ?? _response; if (response.id && response.id !== messageData.id) { return Promise.reject( diff --git a/src/navigation/tabs/settings/external-services/screens/MoonpaySellDetails.tsx b/src/navigation/tabs/settings/external-services/screens/MoonpaySellDetails.tsx index 7f8fe7e732..3c2f1ad116 100644 --- a/src/navigation/tabs/settings/external-services/screens/MoonpaySellDetails.tsx +++ b/src/navigation/tabs/settings/external-services/screens/MoonpaySellDetails.tsx @@ -62,7 +62,7 @@ import { MoonpaySellTransactionDetails, } from '../../../../../store/sell-crypto/models/moonpay-sell.models'; import {RootState} from '../../../../../store'; -import {Wallet} from '../../../../../store/wallet/wallet.models'; +import {Key, Wallet} from '../../../../../store/wallet/wallet.models'; export interface MoonpaySellDetailsProps { sellOrder: MoonpaySellOrderData; @@ -82,7 +82,9 @@ const MoonpaySellDetails: React.FC = () => { const logger = useLogger(); const theme = useTheme(); const dispatch = useAppDispatch(); - const allKeys = useAppSelector(({WALLET}: RootState) => WALLET.keys); + const allKeys: {[key: string]: Key} = useAppSelector( + ({WALLET}: RootState) => WALLET.keys, + ); const [status, setStatus] = useState({ statusTitle: undefined, statusDescription: undefined, @@ -526,10 +528,11 @@ const MoonpaySellDetails: React.FC = () => { transactionId: sellOrder.transaction_id, externalId: sellOrder.external_id, }; - const res = + const _res = await sourceWallet.moonpayCancelSellTransaction( reqData, ); + const res = _res?.body ?? _res; if (res?.statusCode == 204) { // Canceled successfully sellOrder.status = 'bitpayCanceled'; From 1022dc34533bc2cc9a8990a743e0b4fd111c3e91 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 26 Dec 2025 16:02:36 -0300 Subject: [PATCH 04/16] [FIX] support l2 chains - fix bad signature err - copayers sign events --- patches/bitcore-wallet-client+11.4.6.patch | 25 ++++ .../wallet/components/TSSProgressTracker.tsx | 43 +++++-- .../wallet/screens/CreateMultisig.tsx | 8 +- .../wallet/screens/CurrencySelection.tsx | 4 +- .../screens/TransactionProposalDetails.tsx | 30 +++-- .../wallet/screens/send/confirm/Confirm.tsx | 14 +- .../create-multisig/create-multisig.ts | 64 +++++++--- src/store/wallet/effects/create/create.ts | 4 +- src/store/wallet/effects/import/import.ts | 4 +- .../effects/join-multisig/join-multisig.ts | 2 +- src/store/wallet/effects/tss-send/tss-send.ts | 120 +++++++++++++----- src/store/wallet/wallet.models.ts | 2 + 12 files changed, 231 insertions(+), 89 deletions(-) diff --git a/patches/bitcore-wallet-client+11.4.6.patch b/patches/bitcore-wallet-client+11.4.6.patch index 65443b607f..08fec7a64e 100644 --- a/patches/bitcore-wallet-client+11.4.6.patch +++ b/patches/bitcore-wallet-client+11.4.6.patch @@ -128,3 +128,28 @@ index 43e3f5d..4767846 100644 x.copayerId = common_1.Utils.xPubToCopayerId(x.chain, x.xPubKey); x.publicKeyRing = [ { +diff --git a/node_modules/bitcore-wallet-client/ts_build/src/lib/tsskey.js b/node_modules/bitcore-wallet-client/ts_build/src/lib/tsskey.js +index d073bca..cdeaea3 100644 +--- a/node_modules/bitcore-wallet-client/ts_build/src/lib/tsskey.js ++++ b/node_modules/bitcore-wallet-client/ts_build/src/lib/tsskey.js +@@ -118,17 +118,19 @@ class TssKeyGen extends events_1.EventEmitter { + _TssKeyGen_subscriptionId.set(this, void 0); + _TssKeyGen_subscriptionRunning.set(this, void 0); + $.checkArgument(params.chain, 'Missing required param: chain'); ++ $.checkArgument(params.coin, 'Missing required param: coin'); + $.checkArgument(params.network, 'Missing required param: network'); + $.checkArgument(params.baseUrl, 'Missing required param: baseUrl'); + $.checkArgument(params.key, 'Missing required param: key'); + $.checkArgument(params.key instanceof key_1.Key, 'key must be an instance of Key'); + this.chain = params.chain.toLowerCase(); ++ this.coin = params.coin.toLowerCase(); + this.network = params.network; + __classPrivateFieldSet(this, _TssKeyGen_request, new request_1.Request(params.baseUrl, { + r: params.request, + }), "f"); + __classPrivateFieldSet(this, _TssKeyGen_key, params.key, "f"); +- __classPrivateFieldSet(this, _TssKeyGen_credentials, __classPrivateFieldGet(this, _TssKeyGen_key, "f").createCredentials(params.password, { chain: this.chain, network: this.network, n: 1, account: 0 }), "f"); ++ __classPrivateFieldSet(this, _TssKeyGen_credentials, __classPrivateFieldGet(this, _TssKeyGen_key, "f").createCredentials(params.password, { coin: this.coin, chain: this.chain, network: this.network, n: 1, account: 0 }), "f"); + __classPrivateFieldGet(this, _TssKeyGen_request, "f").setCredentials(__classPrivateFieldGet(this, _TssKeyGen_credentials, "f")); + __classPrivateFieldSet(this, _TssKeyGen_requestPrivateKey, crypto_wallet_core_1.BitcoreLib.PrivateKey.fromString(__classPrivateFieldGet(this, _TssKeyGen_credentials, "f").requestPrivKey), "f"); + const baseXPrivKey = __classPrivateFieldGet(this, _TssKeyGen_key, "f").get(params.password).xPrivKey; diff --git a/src/navigation/wallet/components/TSSProgressTracker.tsx b/src/navigation/wallet/components/TSSProgressTracker.tsx index 3fec8f617e..5275d4fffc 100644 --- a/src/navigation/wallet/components/TSSProgressTracker.tsx +++ b/src/navigation/wallet/components/TSSProgressTracker.tsx @@ -171,6 +171,7 @@ const CopayerRow = styled.View` flex-direction: row; align-items: center; padding: 4px 0; + position: relative; `; const CopayerIndicator = styled.View<{signed: boolean}>` @@ -190,6 +191,23 @@ const CopayerName = styled(BaseText)<{signed: boolean}>` font-size: 14px; `; +const CopayerRail = styled.View` + width: 20px; + align-items: center; + margin-right: 8px; + position: relative; +`; + +const CopayerConnector = styled.View<{signed: boolean}>` + width: 2px; + height: 28px; + position: absolute; + top: 20px; + left: 5px; + background-color: ${({theme: {dark}, signed}) => + signed ? (dark ? '#004D27' : Success25) : dark ? '#2A2A2A' : '#F5F5F5'}; +`; + export interface TSSCopayer { id: string; name: string; @@ -352,9 +370,7 @@ const TSSProgressTracker: React.FC = ({ const stepStatus = getStepStatus(index); const isActive = stepStatus === 'active'; const isComplete = stepStatus === 'complete'; - const showCopayers = - step.showCopayers && (isActive || isComplete); - + const showCopayers = step.showCopayers; const connectorHeight = showCopayers ? 100 : 20; return ( @@ -378,7 +394,7 @@ const TSSProgressTracker: React.FC = ({ )} - + = 2 ? 10 : 5}}> {step.title} @@ -396,13 +412,20 @@ const TSSProgressTracker: React.FC = ({ {copayers.map((copayer, idx) => ( - - {copayer.signed ? ( - - ) : ( - + + + {copayer.signed ? ( + + ) : ( + + )} + + {idx < copayers.length - 1 && ( + )} - + {copayer.name} diff --git a/src/navigation/wallet/screens/CreateMultisig.tsx b/src/navigation/wallet/screens/CreateMultisig.tsx index 52bbe10907..d60f49dbfc 100644 --- a/src/navigation/wallet/screens/CreateMultisig.tsx +++ b/src/navigation/wallet/screens/CreateMultisig.tsx @@ -67,6 +67,7 @@ import Banner from '../../../components/banner/Banner'; export interface CreateMultisigParamsList { context: 'addTSSWalletMultisig' | 'addWalletMultisig'; currency: string; + chain?: string; key?: Key; } @@ -187,7 +188,7 @@ const CreateMultisig: React.FC = ({navigation, route}) => { const {t} = useTranslation(); const logger = useLogger(); const {showOngoingProcess, hideOngoingProcess} = useOngoingProcess(); - const {currency, key, context} = route.params; + const {currency, chain, key, context} = route.params; const segwitSupported = IsSegwitCoin(currency); const [showOptions, setShowOptions] = useState(false); const [testnetEnabled, setTestnetEnabled] = useState(false); @@ -246,7 +247,7 @@ const CreateMultisig: React.FC = ({navigation, route}) => { opts.networkName = options.networkName; opts.singleAddress = options.singleAddress; opts.coin = currency?.toLowerCase(); - opts.chain = opts.coin; + opts.chain = chain?.toLowerCase() || opts.coin; CreateMultisigWallet(opts); }; @@ -259,7 +260,8 @@ const CreateMultisig: React.FC = ({navigation, route}) => { const {key: tssKey} = await dispatch( startCreateTSSKey({ - chain: opts.coin!, + coin: opts.coin!, + chain: opts.chain!, network: opts.networkName!, m: opts.m, n: opts.n, diff --git a/src/navigation/wallet/screens/CurrencySelection.tsx b/src/navigation/wallet/screens/CurrencySelection.tsx index 9ef03ed220..f92e4bd281 100644 --- a/src/navigation/wallet/screens/CurrencySelection.tsx +++ b/src/navigation/wallet/screens/CurrencySelection.tsx @@ -175,7 +175,6 @@ const CurrencySelection = ({route}: CurrencySelectionScreenProps) => { imgSrc: undefined, selected: false, disabled: false, - chain: currency.currencyAbbreviation, } as CurrencySelectionItem, }; return item; @@ -206,7 +205,6 @@ const CurrencySelection = ({route}: CurrencySelectionScreenProps) => { imgSrc: undefined, selected: false, disabled: false, - chain: currency.currencyAbbreviation.toLowerCase(), } as CurrencySelectionItem, }); }); @@ -224,7 +222,6 @@ const CurrencySelection = ({route}: CurrencySelectionScreenProps) => { imgSrc: undefined, selected: false, disabled: false, - chain: currency.currencyAbbreviation.toLowerCase(), } as CurrencySelectionItem, }); }); @@ -365,6 +362,7 @@ const CurrencySelection = ({route}: CurrencySelectionScreenProps) => { if (context === 'addTSSWalletMultisig') { navigation.navigate('CreateMultisig', { currency: currencyAbbreviation.toLowerCase(), + chain: chain?.toLowerCase()!, key, context, }); diff --git a/src/navigation/wallet/screens/TransactionProposalDetails.tsx b/src/navigation/wallet/screens/TransactionProposalDetails.tsx index 90715d27f3..cfe654bab7 100644 --- a/src/navigation/wallet/screens/TransactionProposalDetails.tsx +++ b/src/navigation/wallet/screens/TransactionProposalDetails.tsx @@ -271,9 +271,9 @@ const TransactionProposalDetails = () => { if (isTss) { logManager.debug(`[TxpDetails] Is TSS wallet: ${isTss}`); const copayersList = - wallet.credentials?.publicKeyRing?.map((pkr: any, index: number) => ({ - id: pkr.copayerId || `copayer-${index}`, - name: pkr.copayerName || `Co-signer ${index + 1}`, + wallet.copayers?.map(copayer => ({ + id: copayer.id, + name: copayer.name, signed: false, })) || []; setTssCopayers(copayersList); @@ -524,7 +524,7 @@ const TransactionProposalDetails = () => { const tssCallbacks: TSSSigningCallbacks = { onStatusChange: (status: TSSSigningStatus) => { - logManager.debug(`[TxpDetails TSS] Status: ${status}`); + logManager.debug(`[TxpDetails TSS] Status changed: ${status}`); setTssStatus(status); }, onProgressUpdate: (progress: TSSSigningProgress) => { @@ -532,11 +532,18 @@ const TransactionProposalDetails = () => { `[TxpDetails TSS] Progress: Round ${progress.currentRound}/${progress.totalRounds}`, ); setTssProgress(progress); + + // When round 1 starts, mark all copayers as joined/signing + // TODO remove this when onCopayerStatusChange is added + if (progress.currentRound === 1) { + setTssCopayers(prev => prev.map(c => ({...c, signed: true}))); + } }, onCopayerStatusChange: ( copayerId: string, status: TSSCopayerSignStatus, ) => { + // This will never fire - keeping for future when event exist logManager.debug(`[TxpDetails TSS] Copayer ${copayerId} ${status}`); setTssCopayers(prev => prev.map(c => @@ -554,20 +561,17 @@ const TransactionProposalDetails = () => { logManager.error(`[TxpDetails TSS] Error: ${error.message}`); setShowTSSProgressModal(false); setResetSwipeButton(true); - dispatch( - showBottomNotificationModal( - CustomErrorMessage({ - errMsg: error.message, - title: t('TSS Signing Error'), - }), - ), + showErrorMessage( + CustomErrorMessage({ + errMsg: error.message, + title: t('TSS Signing Error'), + }), ); }, onComplete: (signature: string) => { - logManager.debug(`[TxpDetails TSS] Complete`); + logManager.debug(`[TxpDetails TSS] Signing complete`); }, }; - const joinTSSSigning = async () => { try { logManager.debug( diff --git a/src/navigation/wallet/screens/send/confirm/Confirm.tsx b/src/navigation/wallet/screens/send/confirm/Confirm.tsx index 4e3f6228bd..ddabbbe0b8 100644 --- a/src/navigation/wallet/screens/send/confirm/Confirm.tsx +++ b/src/navigation/wallet/screens/send/confirm/Confirm.tsx @@ -255,9 +255,9 @@ const Confirm = () => { setIsTSSWallet(isTss); if (isTss) { const copayersList = - wallet.credentials?.publicKeyRing?.map((pkr: any, index: number) => ({ - id: pkr.copayerId || `copayer-${index}`, - name: pkr.copayerName || `Co-signer ${index + 1}`, + wallet.copayers?.map(copayer => ({ + id: copayer.id, + name: copayer.name, signed: false, })) || []; setTssCopayers(copayersList); @@ -457,11 +457,18 @@ const Confirm = () => { `[TSS Confirm] Progress: Round ${progress.currentRound}/${progress.totalRounds}`, ); setTssProgress(progress); + + // When round 1 starts, mark all copayers as joined/signing + // TODO remove this when onCopayerStatusChange is added + if (progress.currentRound === 1) { + setTssCopayers(prev => prev.map(c => ({...c, signed: true}))); + } }, onCopayerStatusChange: ( copayerId: string, status: TSSCopayerSignStatus, ) => { + // This will never fire - keeping for future when event exist logManager.debug(`[TSS Confirm] Copayer ${copayerId} ${status}`); setTssCopayers(prev => prev.map(c => @@ -490,7 +497,6 @@ const Confirm = () => { logManager.debug(`[TSS Confirm] Signing complete`); }, }; - const startSendingPayment = async ({ transport, }: {transport?: Transport} = {}) => { diff --git a/src/store/wallet/effects/create-multisig/create-multisig.ts b/src/store/wallet/effects/create-multisig/create-multisig.ts index ebe6eb7a3f..84dd2c222c 100644 --- a/src/store/wallet/effects/create-multisig/create-multisig.ts +++ b/src/store/wallet/effects/create-multisig/create-multisig.ts @@ -20,7 +20,6 @@ import { KeyOptions, PendingJoinerSession, TSSCopayerInfo, - TssSessionData, Wallet, } from '../../wallet.models'; import {createWalletWithOpts} from '../create/create'; @@ -34,6 +33,7 @@ import {BASE_BWS_URL} from '../../../../constants/config'; import {Network} from '../../../../constants'; import {setHomeCarouselConfig} from '../../../../store/app/app.actions'; import {createWalletAddress} from '../address/address'; +import {BitpaySupportedCoins} from '../../../../constants/currencies'; const BWC = BwcProvider.getInstance(); @@ -191,7 +191,6 @@ const getPubKeyFromKey = (partyKey: any): string => { const credentials = partyKey.createCredentials(null, { chain: 'BTC', // Doesn't matter for requestPubKey network: 'livenet', - n: 1, account: 0, }); return credentials.requestPubKey; @@ -199,6 +198,7 @@ const getPubKeyFromKey = (partyKey: any): string => { export const startCreateTSSKey = (opts: { + coin: string; chain: string; network: string; m: number; @@ -209,7 +209,16 @@ export const startCreateTSSKey = }): Effect> => async (dispatch, getState): Promise<{key: Key}> => { try { - const {chain: _chain, network, m, n, password, walletName, myName} = opts; + const { + coin, + chain: _chain, + network, + m, + n, + password, + walletName, + myName, + } = opts; const chain = _chain === 'pol' ? 'matic' : _chain.toLowerCase(); // for creating a polygon wallet, we use matic as symbol const { WALLET: {tokenOptionsByAddress}, @@ -221,7 +230,8 @@ export const startCreateTSSKey = logManager.debug('[TSS] Created party key for creator'); const tssKeyGen = new TssKeyGen({ - chain: chain.toUpperCase(), + coin, + chain, network: network as Network, baseUrl: BASE_BWS_URL, key: partyKey, @@ -239,11 +249,12 @@ export const startCreateTSSKey = logManager.debug(`[TSS] Session created with ID: ${sessionId}`); const {currencyAbbreviation, currencyName} = dispatch( - mapAbbreviationAndName(chain, chain, undefined), + mapAbbreviationAndName(coin, chain, undefined), ); const walletClient = BWC.getClient(); const credentials = partyKey.createCredentials(null, { + coin, chain, network, n: 1, // TODO: review if this should be opts.n @@ -285,7 +296,8 @@ export const startCreateTSSKey = id: sessionId, partyKey: partyKey.toObj(), sessionExport, - chain: chain, + coin, + chain, network, m, n, @@ -342,7 +354,8 @@ export const addCoSignerToTSS = }); const tssKeyGen = new TssKeyGen({ - chain: key.tssSession.chain.toUpperCase(), + coin: key.tssSession.coin, + chain: key.tssSession.chain, network: key.tssSession.network, baseUrl: BASE_BWS_URL, key: partyKey, @@ -420,8 +433,15 @@ export const startTSSCeremony = throw new Error('Not all co-signers have been invited'); } - const {chain, network, myName, walletName, sessionExport, partyKey} = - key.tssSession; + const { + coin, + chain, + network, + myName, + walletName, + sessionExport, + partyKey, + } = key.tssSession; const BWCKey = BWC.getKey(); const restoredPartyKey = new BWCKey({ @@ -430,7 +450,8 @@ export const startTSSCeremony = }); const tssKeyGen = new TssKeyGen({ - chain: chain.toUpperCase(), + coin, + chain, network, baseUrl: BASE_BWS_URL, key: restoredPartyKey, @@ -495,7 +516,7 @@ export const startTSSCeremony = copayerName: myName, createWalletOpts: { network, - coin: chain, + coin, chain, walletPrivKey, }, @@ -513,10 +534,11 @@ export const startTSSCeremony = } const {currencyAbbreviation, currencyName} = dispatch( - mapAbbreviationAndName(chain, chain, undefined), + mapAbbreviationAndName(coin, chain, undefined), ); const credentials = _tssKey.createCredentials(null, { + coin, chain, network, account: 0, @@ -751,10 +773,12 @@ export const joinTSSWithCode = seedType: 'object', seedData: opts.partyKey, }); + logManager.debug('[TSS Join] Party key restored'); const tempTssKeyGen = new TssKeyGen({ - chain: 'BTC', + coin: 'btc', + chain: 'btc', network: 'livenet', baseUrl: BASE_BWS_URL, key: partyKey, @@ -769,10 +793,12 @@ export const joinTSSWithCode = ); const chain = decoded.chain.toLowerCase(); + const coin = BitpaySupportedCoins[chain].coin; const network = decoded.network as 'livenet' | 'testnet' | 'regtest'; const tssKeyGen = new TssKeyGen({ - chain: chain.toUpperCase(), + coin, + chain, network: network, baseUrl: BASE_BWS_URL, key: partyKey, @@ -789,12 +815,13 @@ export const joinTSSWithCode = const n = tssKeyGen.n; const {currencyAbbreviation, currencyName} = dispatch( - mapAbbreviationAndName(chain, chain, undefined), + mapAbbreviationAndName(coin, chain, undefined), ); const walletClient = BWC.getClient(); const tempCredentials = partyKey.createCredentials(null, { - chain: chain.toUpperCase(), + coin, + chain, network, n: 1, account: 0, @@ -825,6 +852,7 @@ export const joinTSSWithCode = id: tssKeyGen.id, partyKey: partyKey.toObj(), sessionExport: tssKeyGen.exportSession(), + coin, chain, network, m, @@ -891,7 +919,7 @@ export const joinTSSWithCode = copayerName: myName, createWalletOpts: { network, - coin: chain, + coin, chain, }, }); @@ -908,7 +936,7 @@ export const joinTSSWithCode = } const credentials = _tssKey.createCredentials(null, { - chain: chain.toUpperCase(), + chain, network, account: 0, }); diff --git a/src/store/wallet/effects/create/create.ts b/src/store/wallet/effects/create/create.ts index 8233578b40..d135ae7308 100644 --- a/src/store/wallet/effects/create/create.ts +++ b/src/store/wallet/effects/create/create.ts @@ -533,7 +533,7 @@ const createWallet = bwcClient.fromString( key.createCredentials(password, { coin, - chain, // chain === coin for stored clients. THIS IS NO TRUE ANYMORE + chain, network, account, n: 1, @@ -763,7 +763,7 @@ export const createWalletWithOpts = bwcClient.fromString( key.createCredentials(opts.password, { coin: opts.coin || 'btc', - chain: opts.chain || 'btc', // chain === coin for stored clients. THIS IS NO TRUE ANYMORE + chain: opts.chain || 'btc', network: opts.networkName || 'livenet', account: opts.account || 0, n: opts.n || 1, diff --git a/src/store/wallet/effects/import/import.ts b/src/store/wallet/effects/import/import.ts index d008c4dbd8..3fbcbba8a1 100644 --- a/src/store/wallet/effects/import/import.ts +++ b/src/store/wallet/effects/import/import.ts @@ -1411,7 +1411,7 @@ const createKeyAndCredentials = async ( bwcClient.fromString( key.createCredentials(undefined, { coin, - chain, // chain === coin for stored clients. THIS IS NO TRUE ANYMORE + chain, network, account, n, @@ -1432,7 +1432,7 @@ const createKeyAndCredentials = async ( bwcClient.fromString( key.createCredentials(undefined, { coin, - chain, // chain === coin for stored clients. THIS IS NO TRUE ANYMORE + chain, network, account, n, diff --git a/src/store/wallet/effects/join-multisig/join-multisig.ts b/src/store/wallet/effects/join-multisig/join-multisig.ts index 9307f7aea4..c35af5d59a 100644 --- a/src/store/wallet/effects/join-multisig/join-multisig.ts +++ b/src/store/wallet/effects/join-multisig/join-multisig.ts @@ -182,7 +182,7 @@ const joinMultisigWallet = (params: { bwcClient.fromString( key.createCredentials(opts.password, { coin: opts.coin, - chain: opts.chain, // chain === coin for stored clients. THIS IS NO TRUE ANYMORE + chain: opts.chain, network: opts.networkName, account: opts.account || 0, n: opts.n, diff --git a/src/store/wallet/effects/tss-send/tss-send.ts b/src/store/wallet/effects/tss-send/tss-send.ts index 7bb8be11de..6c44397471 100644 --- a/src/store/wallet/effects/tss-send/tss-send.ts +++ b/src/store/wallet/effects/tss-send/tss-send.ts @@ -10,6 +10,7 @@ import { } from '../../wallet.models'; import {logManager} from '../../../../managers/LogManager'; import {BASE_BWS_URL} from '../../../../constants/config'; +import {utils as ethersUtils} from 'ethers'; const BWC = BwcProvider.getInstance(); @@ -38,6 +39,37 @@ export const requiresTSSSigning = (wallet: Wallet, key: Key): boolean => { return isTSSKey(key) && !!wallet.tssKeyId; }; +export const toBwsSignatureFormat = (sig: any, chain: string): string => { + const strip0x = (h: string) => (h?.startsWith('0x') ? h.slice(2) : h); + const pad32 = (h: string) => strip0x(h).padStart(64, '0'); + + if (!sig) throw new Error('Missing signature'); + + if (typeof sig === 'string') { + const hex = strip0x(sig); + if (!/^[0-9a-fA-F]+$/.test(hex)) throw new Error('Signature is not hex'); + return '0x' + hex; + } + + const isEvm = ['eth', 'matic', 'arb', 'base', 'op'].includes( + (chain || '').toLowerCase(), + ); + if (!isEvm) { + throw new Error(`Unsupported chain for this helper: ${chain}`); + } + + const r = pad32(sig.r); + const s = pad32(sig.s); + + let v = Number(sig.v); + if (v === 0 || v === 1) { + v = v + 27; + } + const vHex = v.toString(16).padStart(2, '0'); + + return `0x${r}${s}${vHex}`; +}; + export const getTxpMessageHash = ( wallet: Wallet, txp: TransactionProposal, @@ -53,20 +85,31 @@ export const getTxpMessageHash = ( Bitcore.crypto.Signature.SIGHASH_ALL, ); return sighash; - } else { + } else if ( + ['eth', 'matic', 'arb', 'base', 'op'].includes(txp.chain?.toLowerCase()) + ) { const serialized = tx.uncheckedSerialize()[0]; - const hexString = serialized.startsWith('0x') ? serialized.slice(2) : serialized; const txBuffer = Buffer.from(hexString, 'hex'); - const hash = Bitcore.crypto.Hash.sha256(txBuffer); - logManager.debug(`[getTxpMessageHash] txBuffer length: ${txBuffer.length}`); - logManager.debug(`[getTxpMessageHash] hash: ${hash.toString('hex')}`); + const hashHex = ethersUtils.keccak256('0x' + txBuffer.toString('hex')); + const hash = Buffer.from(hashHex.slice(2), 'hex'); + + logManager.debug( + `[getTxpMessageHash] Keccak256 hash: ${hash.toString('hex')}`, + ); return hash; + } else { + const serialized = tx.uncheckedSerialize()[0]; + const hexString = serialized.startsWith('0x') + ? serialized.slice(2) + : serialized; + const txBuffer = Buffer.from(hexString, 'hex'); + return Bitcore.crypto.Hash.sha256(txBuffer); } }; @@ -114,9 +157,10 @@ export const startTSSSigning = txp: TransactionProposal; callbacks: TSSSigningCallbacks; timeout?: number; + joiner?: boolean; }): Effect> => async (dispatch, getState): Promise => { - const {key, wallet, txp, callbacks, timeout = 300000} = opts; + const {key, wallet, txp, callbacks, timeout = 300000, joiner} = opts; return new Promise(async (resolve, reject) => { let tssSign: any = null; @@ -214,17 +258,23 @@ export const startTSSSigning = logManager.debug(`[TSS Sign] Round ${round} submitted`); callbacks.onRoundUpdate(round, 'submitted'); }) - .on('copayerjoined', (copayerId: string) => { - logManager.debug(`[TSS Sign] Copayer joined: ${copayerId}`); - callbacks.onCopayerStatusChange(copayerId, 'joined'); - }) - .on('copayersigned', (copayerId: string) => { - logManager.debug(`[TSS Sign] Copayer signed: ${copayerId}`); - callbacks.onCopayerStatusChange(copayerId, 'signed'); - }) - .on('signature', (signature: string) => { + // TODO BWC + // .on('copayersigned', (copayerId: string) => { + // logManager.debug(`[TSS Sign] Copayer signed: ${copayerId}`); + // callbacks.onCopayerStatusChange(copayerId, 'signed'); + // }) + .on('signature', signature => { logManager.debug(`[TSS Sign] Signature received`); - resolveSign(signature); + try { + const bwsSig = toBwsSignatureFormat(signature, txp.chain); + resolveSign(bwsSig); + } catch (err) { + rejectSign( + new Error( + `Failed to convert/verify signature: ${err.message}`, + ), + ); + } }) .on('complete', () => { logManager.debug('[TSS Sign] Signing complete'); @@ -251,22 +301,25 @@ export const startTSSSigning = logManager.debug('[TSS Sign] Pushing signature to txp'); - const signedTxp = await new Promise( - (resolvePush, rejectPush) => { - wallet.pushSignatures( - txp, - [signature], - (err: Error, result: TransactionProposal) => { - if (err) { - rejectPush(err); - } else { - resolvePush(result); - } - }, - null, - ); - }, - ); + let signedTXP: TransactionProposal | null = null; + if (!joiner) { + signedTXP = await new Promise( + (resolvePush, rejectPush) => { + wallet.pushSignatures( + txp, + [signature], + (err: Error, result: TransactionProposal) => { + if (err) { + rejectPush(err); + } else { + resolvePush(result); + } + }, + null, + ); + }, + ); + } if (tssSign) { tssSign.unsubscribe(); @@ -275,7 +328,7 @@ export const startTSSSigning = logManager.debug('[TSS Sign] TSS signing completed successfully'); callbacks.onStatusChange('complete'); - resolve(signedTxp); + resolve(signedTXP ?? txp); } catch (err) { if (timeoutId) { clearTimeout(timeoutId); @@ -314,6 +367,7 @@ export const joinTSSSigningSession = wallet, txp, callbacks, + joiner: true, }), ); }; diff --git a/src/store/wallet/wallet.models.ts b/src/store/wallet/wallet.models.ts index fd2cb22b71..0a8d1f9c6e 100644 --- a/src/store/wallet/wallet.models.ts +++ b/src/store/wallet/wallet.models.ts @@ -129,6 +129,7 @@ export interface WalletObj { m: number; n: number; balance: CryptoBalance; + copayers: any[]; singleAddress?: boolean; pendingTxps: TransactionProposal[]; tokenAddress?: string; @@ -587,6 +588,7 @@ export interface TssSessionData { id: string; partyKey: any; sessionExport?: string; + coin: string; chain: string; network: string; m: number; From 37ea4bba2e2140b3f1e49e23db9a30f0c44dd627 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 2 Jan 2026 15:03:18 -0300 Subject: [PATCH 05/16] [REF] improve UI - handle files for import - fix sjcl encrypt - fix xrp --- assets/img/cancel-dark.svg | 3 + assets/img/cancel.svg | 3 + assets/img/upload-dark.svg | 3 + assets/img/upload.svg | 3 + ios/Podfile.lock | 32 ++ package.json | 1 + patches/@bitgo+sdk-lib-mpc+10.8.1.patch | 6 +- ...+11.3.7.patch => bitcore-tss+11.4.5.patch} | 0 patches/bitcore-wallet-client+11.4.6.patch | 53 +++- patches/crypto-wallet-core+11.4.5.patch | 13 + src/components/form/BoxInput.tsx | 4 +- .../tabs/shop/bill/components/BillAlert.tsx | 5 +- src/navigation/tabs/shop/components/Bills.tsx | 6 +- .../wallet/components/FileOrText.tsx | 299 ++++++++++++++++-- .../wallet/components/TSSProgressTracker.tsx | 25 +- src/navigation/wallet/screens/Import.tsx | 2 +- .../wallet/screens/InviteCosigners.tsx | 102 +++--- .../wallet/screens/JoinTSSWallet.tsx | 22 +- .../wallet-settings/ExportTSSWallet.tsx | 96 ++++-- .../create-multisig/create-multisig.ts | 12 +- src/store/wallet/effects/import/import.ts | 148 ++++++--- src/store/wallet/effects/tss-send/tss-send.ts | 6 +- src/store/wallet/utils/wallet.ts | 6 - src/store/wallet/wallet.models.ts | 13 +- yarn.lock | 7 + 25 files changed, 660 insertions(+), 210 deletions(-) create mode 100644 assets/img/cancel-dark.svg create mode 100644 assets/img/cancel.svg create mode 100644 assets/img/upload-dark.svg create mode 100644 assets/img/upload.svg rename patches/{bitcore-tss+11.3.7.patch => bitcore-tss+11.4.5.patch} (100%) create mode 100644 patches/crypto-wallet-core+11.4.5.patch diff --git a/assets/img/cancel-dark.svg b/assets/img/cancel-dark.svg new file mode 100644 index 0000000000..37395e9d70 --- /dev/null +++ b/assets/img/cancel-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/cancel.svg b/assets/img/cancel.svg new file mode 100644 index 0000000000..5e8e680f5e --- /dev/null +++ b/assets/img/cancel.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/upload-dark.svg b/assets/img/upload-dark.svg new file mode 100644 index 0000000000..040ce9093b --- /dev/null +++ b/assets/img/upload-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/upload.svg b/assets/img/upload.svg new file mode 100644 index 0000000000..6654f41adf --- /dev/null +++ b/assets/img/upload.svg @@ -0,0 +1,3 @@ + + + diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6801f35f87..e661b7eb4c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1939,6 +1939,34 @@ PODS: - react-native-config/App (= 1.5.0) - react-native-config/App (1.5.0): - React-Core + - react-native-document-picker (9.3.1): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - react-native-get-random-values (1.11.0): - React-Core - react-native-in-app-review (4.3.1): @@ -3410,6 +3438,7 @@ DEPENDENCIES: - "react-native-blur (from `../node_modules/@react-native-community/blur`)" - "react-native-compat (from `../node_modules/@walletconnect/react-native-compat`)" - react-native-config (from `../node_modules/react-native-config`) + - react-native-document-picker (from `../node_modules/react-native-document-picker`) - react-native-get-random-values (from `../node_modules/react-native-get-random-values`) - react-native-in-app-review (from `../node_modules/react-native-in-app-review`) - react-native-keyevent (from `../node_modules/react-native-keyevent`) @@ -3604,6 +3633,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@walletconnect/react-native-compat" react-native-config: :path: "../node_modules/react-native-config" + react-native-document-picker: + :path: "../node_modules/react-native-document-picker" react-native-get-random-values: :path: "../node_modules/react-native-get-random-values" react-native-in-app-review: @@ -3817,6 +3848,7 @@ SPEC CHECKSUMS: react-native-blur: ba0e9ad6274783c8d45f42da82acae02e25784ad react-native-compat: f3f307c339d7755e2562293567ec0bd47235eca9 react-native-config: 5330c8258265c1e5fdb8c009d2cabd6badd96727 + react-native-document-picker: 6151275a22fd452b9241855250f574aa2520d1f9 react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06 react-native-in-app-review: ae45cc55e168a3b78eea9eea031ca57dccd7eb5a react-native-keyevent: fa167ff93e90b5d86b1678885669ff8ec099bf09 diff --git a/package.json b/package.json index 3debcdbf36..d93ce5ea68 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "react-native-config": "1.5.0", "react-native-crypto": "2.2.0", "react-native-device-info": "8.4.7", + "react-native-document-picker": "^9.3.1", "react-native-error-boundary": "1.2.1", "react-native-exception-handler": "2.10.10", "react-native-fast-image": "8.6.3", diff --git a/patches/@bitgo+sdk-lib-mpc+10.8.1.patch b/patches/@bitgo+sdk-lib-mpc+10.8.1.patch index 0b0fb54fc6..360df60cf8 100644 --- a/patches/@bitgo+sdk-lib-mpc+10.8.1.patch +++ b/patches/@bitgo+sdk-lib-mpc+10.8.1.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@bitgo/sdk-lib-mpc/dist/src/tss/ecdsa-dkls/dkg.js b/node_modules/@bitgo/sdk-lib-mpc/dist/src/tss/ecdsa-dkls/dkg.js -index d4075db..a5da83c 100644 +index d4075db..f611457 100644 --- a/node_modules/@bitgo/sdk-lib-mpc/dist/src/tss/ecdsa-dkls/dkg.js +++ b/node_modules/@bitgo/sdk-lib-mpc/dist/src/tss/ecdsa-dkls/dkg.js @@ -51,7 +51,9 @@ class Dkg { @@ -7,8 +7,8 @@ index d4075db..a5da83c 100644 async loadDklsWasm() { if (!this.dklsWasm) { - this.dklsWasm = await Promise.resolve().then(() => __importStar(require('@silencelaboratories/dkls-wasm-ll-node'))); -+ const shim = await import('@silencelaboratories/dkls-wasm-ll-web'); -+ await shim.default(); ++ const shim = await require('@silencelaboratories/dkls-wasm-ll-web'); ++ // await shim.default(); + this.dklsWasm = shim; } } diff --git a/patches/bitcore-tss+11.3.7.patch b/patches/bitcore-tss+11.4.5.patch similarity index 100% rename from patches/bitcore-tss+11.3.7.patch rename to patches/bitcore-tss+11.4.5.patch diff --git a/patches/bitcore-wallet-client+11.4.6.patch b/patches/bitcore-wallet-client+11.4.6.patch index 08fec7a64e..bfcc69139d 100644 --- a/patches/bitcore-wallet-client+11.4.6.patch +++ b/patches/bitcore-wallet-client+11.4.6.patch @@ -12,10 +12,18 @@ index 443c525..93b1d56 100644 error = true; } diff --git a/node_modules/bitcore-wallet-client/ts_build/src/lib/api.js b/node_modules/bitcore-wallet-client/ts_build/src/lib/api.js -index 7a27efd..d916b5f 100644 +index 7a27efd..c606ce4 100644 --- a/node_modules/bitcore-wallet-client/ts_build/src/lib/api.js +++ b/node_modules/bitcore-wallet-client/ts_build/src/lib/api.js -@@ -50,6 +50,7 @@ const common_1 = require("./common"); +@@ -40,6 +40,7 @@ exports.API = void 0; + const events_1 = require("events"); + const querystring_1 = __importDefault(require("querystring")); + const async_1 = __importDefault(require("async")); ++const sjcl_1 = __importDefault(require("sjcl")); + const bip38_1 = __importDefault(require("bip38")); + const bitcore_mnemonic_1 = __importDefault(require("bitcore-mnemonic")); + const CWC = __importStar(require("crypto-wallet-core")); +@@ -50,6 +51,7 @@ const common_1 = require("./common"); const credentials_1 = require("./credentials"); const errors_1 = require("./errors"); const key_1 = require("./key"); @@ -23,7 +31,41 @@ index 7a27efd..d916b5f 100644 const log_1 = __importDefault(require("./log")); const paypro_1 = require("./paypro"); const payproV2_1 = require("./payproV2"); -@@ -2819,6 +2820,8 @@ exports.API = API; +@@ -2503,7 +2505,8 @@ class API extends events_1.EventEmitter { + return cb2(null, new Error('Copayer not in wallet')); + try { + credentials.addWalletInfo(wallet.id, wallet.name, wallet.m, wallet.n, me.name, { +- allowOverwrite: !!wallet.tssKeyId ++ allowOverwrite: !!wallet.tssKeyId, ++ tssKeyId: wallet.tssKeyId + }); + } + catch (e) { +@@ -2521,7 +2524,7 @@ class API extends events_1.EventEmitter { + credentials.addWalletPrivateKey(item.status.customData.walletPrivKey); + } + if (credentials.walletPrivKey) { +- if (!verifier_1.Verifier.checkCopayers(credentials, wallet.copayers)) { ++ if (!verifier_1.Verifier.checkCopayers(credentials, wallet.copayers, { isTss: !!wallet.tssKeyId })) { + return cb2(null, new errors_1.Errors.SERVER_COMPROMISED()); + } + } +@@ -2719,6 +2722,14 @@ class API extends events_1.EventEmitter { + ...set + }); + } ++ if (opts.tssKeychain && opts.tssMetadata) { ++ const tssKeyObj = { ++ ...k.toObj(), ++ keychain: opts.tssKeychain, ++ metadata: opts.tssMetadata ++ }; ++ k = new tsskey_1.TssKey(tssKeyObj); ++ } + } + catch (e) { + log_1.default.info('Backup error:', e); +@@ -2819,10 +2830,13 @@ exports.API = API; API.PayProV2 = payproV2_1.PayProV2; API.PayPro = paypro_1.PayPro; API.Key = key_1.Key; @@ -32,6 +74,11 @@ index 7a27efd..d916b5f 100644 API.Verifier = verifier_1.Verifier; API.Core = CWC; API.Utils = common_1.Utils; + API.Encryption = common_1.Encryption; ++API.sjcl = sjcl_1.default; + API.errors = errors_1.Errors; + API.Bitcore = CWC.BitcoreLib; + API.BitcoreCash = CWC.BitcoreLibCash; diff --git a/node_modules/bitcore-wallet-client/ts_build/src/lib/common/encryption.js b/node_modules/bitcore-wallet-client/ts_build/src/lib/common/encryption.js index ffb28f7..92db28f 100644 --- a/node_modules/bitcore-wallet-client/ts_build/src/lib/common/encryption.js diff --git a/patches/crypto-wallet-core+11.4.5.patch b/patches/crypto-wallet-core+11.4.5.patch new file mode 100644 index 0000000000..0f9c4ffeeb --- /dev/null +++ b/patches/crypto-wallet-core+11.4.5.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/crypto-wallet-core/ts_build/src/validation/xrp/index.js b/node_modules/crypto-wallet-core/ts_build/src/validation/xrp/index.js +index 59f4b97..fe9df8f 100644 +--- a/node_modules/crypto-wallet-core/ts_build/src/validation/xrp/index.js ++++ b/node_modules/crypto-wallet-core/ts_build/src/validation/xrp/index.js +@@ -17,7 +17,7 @@ class XrpValidation { + const buffer = Buffer.from(base58.decode(address)); + const prefix = buffer.subarray(0, 1); + const data = buffer.subarray(1, -4); +- let hash = Buffer.concat([prefix, data]); ++ let hash = Buffer.concat([Buffer.from(prefix), Buffer.from(data)]); + hash = bitcore_lib_1.default.crypto.Hash.sha256(hash); + hash = bitcore_lib_1.default.crypto.Hash.sha256(hash); + const checksum = buffer.subarray(-4).reduce((acc, check, index) => { diff --git a/src/components/form/BoxInput.tsx b/src/components/form/BoxInput.tsx index b0a39ef7bd..aeaf6fe005 100644 --- a/src/components/form/BoxInput.tsx +++ b/src/components/form/BoxInput.tsx @@ -26,8 +26,8 @@ import {TouchableOpacity} from '@components/base/TouchableOpacity'; type InputType = 'password' | 'phone' | 'search' | 'number'; -const INPUT_HEIGHT = 55; -const SEPARATOR_HEIGHT = 37; +export const INPUT_HEIGHT = 55; +export const SEPARATOR_HEIGHT = 37; interface InputProps { isFocused: boolean; diff --git a/src/navigation/tabs/shop/bill/components/BillAlert.tsx b/src/navigation/tabs/shop/bill/components/BillAlert.tsx index d4f11de0a2..b4b6d72253 100644 --- a/src/navigation/tabs/shop/bill/components/BillAlert.tsx +++ b/src/navigation/tabs/shop/bill/components/BillAlert.tsx @@ -65,8 +65,9 @@ export default ({ {variant === 'servicePaused' ? ( <> - Bill Pay service has been temporarily paused. At this time, we are unable to provide - a confirmed timeline for when the Bill Pay service will resume. + Bill Pay service has been temporarily paused. At this time, we are + unable to provide a confirmed timeline for when the Bill Pay + service will resume. Linking.openURL( diff --git a/src/navigation/tabs/shop/components/Bills.tsx b/src/navigation/tabs/shop/components/Bills.tsx index 6ef4e0ce28..a492d32b42 100644 --- a/src/navigation/tabs/shop/components/Bills.tsx +++ b/src/navigation/tabs/shop/components/Bills.tsx @@ -147,7 +147,7 @@ export const Bills = () => { } else { setWaitlistButtonState('loading'); if (!isJoinedWaitlist) { - await dispatch( + await dispatch( joinWaitlist(user.email, 'BillPay Waitlist', 'bill-pay'), ); } @@ -271,7 +271,9 @@ export const Bills = () => { ), ); dispatch( - Analytics.track('Bill Pay - Clicked I Already Have an Account'), + Analytics.track( + 'Bill Pay - Clicked I Already Have an Account', + ), ); }}> {t('I already have an account')} diff --git a/src/navigation/wallet/components/FileOrText.tsx b/src/navigation/wallet/components/FileOrText.tsx index 5f63bde17a..96835f0139 100644 --- a/src/navigation/wallet/components/FileOrText.tsx +++ b/src/navigation/wallet/components/FileOrText.tsx @@ -1,18 +1,28 @@ -import React, {useEffect, useRef} from 'react'; -import { - ImportTextInput, - HeaderContainer, - ScreenGutter, -} from '../../../components/styled/Containers'; +import React, {useEffect, useRef, useState} from 'react'; +import {ScreenGutter} from '../../../components/styled/Containers'; import Button from '../../../components/button/Button'; -import BoxInput from '../../../components/form/BoxInput'; -import styled from 'styled-components/native'; +import BoxInput, {INPUT_HEIGHT} from '../../../components/form/BoxInput'; +import styled, {css} from 'styled-components/native'; import {yupResolver} from '@hookform/resolvers/yup'; import yup from '../../../lib/yup'; import {useForm, Controller} from 'react-hook-form'; import {Key, KeyOptions} from '../../../store/wallet/wallet.models'; -import {BaseText, ImportTitle} from '../../../components/styled/Text'; -import {Caution} from '../../../styles/colors'; +import {BaseText} from '../../../components/styled/Text'; +import { + Caution, + LightBlue, + White, + SlateDark, + Midnight, + LinkBlue, + Action, + Black, + LightBlack, + LuckySevens, + NeutralSlate, + ProgressBlue, + Slate, +} from '../../../styles/colors'; import {BwcProvider} from '../../../lib/bwc'; import {useLogger} from '../../../utils/hooks/useLogger'; import {useNavigation, useRoute} from '@react-navigation/native'; @@ -42,6 +52,16 @@ import { useSensitiveRefClear, } from '../../../utils/hooks'; import {useOngoingProcess} from '../../../contexts'; +import DocumentPicker from 'react-native-document-picker'; +import RNFS from 'react-native-fs'; +import {logManager} from '../../../managers/LogManager'; +import {TouchableOpacity} from '@components/base/TouchableOpacity'; +import UploadSvg from '../../../../assets/img/upload.svg'; +import UploadDarkSvg from '../../../../assets/img/upload-dark.svg'; +import CancelSvg from '../../../../assets/img/cancel.svg'; +import CancelDarkSvg from '../../../../assets/img/cancel-dark.svg'; +import {useTheme} from 'styled-components'; +import Clipboard from '@react-native-clipboard/clipboard'; const BWCProvider = BwcProvider.getInstance(); @@ -64,6 +84,110 @@ const FormRow = styled.View` margin-bottom: 24px; `; +const DescriptionText = styled(BaseText)` + font-size: 14px; + line-height: 20px; + color: ${({theme}) => (theme.dark ? '#999' : SlateDark)}; + margin-bottom: 24px; +`; + +interface FileContainerProps { + isFocused: boolean; + isError?: boolean; + disabled?: boolean; +} + +const FileInputLabel = styled(BaseText)` + color: ${({theme}) => (theme.dark ? White : '#1b1b1b')}; + font-size: 13px; + font-weight: 500; + opacity: 0.75; + margin-bottom: 6px; +`; + +const FileInputContainer = styled.View` + border: 0.75px solid ${({theme}) => (theme.dark ? LuckySevens : Slate)}; + padding: 1px; + flex-direction: row; + align-items: center; + position: relative; + height: ${INPUT_HEIGHT}px; + background-color: ${({theme}) => (theme.dark ? Black : White)}; + border-radius: 4px; + + ${({isFocused, theme}) => + isFocused && + css` + background: ${theme.dark ? 'transparent' : '#fafbff'}; + border-color: ${theme.dark ? LuckySevens : Slate}; + border-bottom-color: ${ProgressBlue}; + `} + + ${({isError, theme}) => + isError && + css` + background: ${theme.dark ? '#090304' : '#EF476F0A'}; + border-color: #fbc7d1; + border-bottom-color: ${Caution}; + `} + + ${({disabled, theme}) => + disabled && + css` + border-color: ${theme.dark ? LightBlack : NeutralSlate}; + background: ${theme.dark ? LightBlack : NeutralSlate}; + `} +`; + +const FileInputText = styled(BaseText)` + flex: 1; + padding: 10px; + font-size: 14px; + font-weight: 500; + color: ${({theme}) => theme.colors.text}; +`; + +const FileInputPlaceholder = styled(BaseText)` + flex: 1; + padding: 10px; + font-size: 14px; + font-weight: 500; + color: ${Slate}; +`; + +const PasteContainer = styled(TouchableOpacity)` + display: flex; + padding: 4px 8px; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 100px; + background-color: ${({theme}) => (theme.dark ? Midnight : LightBlue)}; +`; + +const PasteContainerText = styled(BaseText)` + color: ${({theme}) => (theme.dark ? LinkBlue : Action)}; + font-size: 16px; + font-weight: 400; + line-height: 24px; +`; + +const IconButton = styled(TouchableOpacity)` + width: ${INPUT_HEIGHT}px; + height: ${INPUT_HEIGHT}px; + border-radius: 0px; + background-color: transparent; + align-items: center; + justify-content: center; +`; + +const ClearButton = styled(TouchableOpacity)` + width: ${INPUT_HEIGHT}px; + height: ${INPUT_HEIGHT}px; + align-items: center; + justify-content: center; +`; + interface FileOrTextFieldValues { text: string; password: string; @@ -77,19 +201,26 @@ const schema = yup.object().shape({ const FileOrText = () => { const {t} = useTranslation(); const logger = useLogger(); + const theme = useTheme(); const dispatch = useAppDispatch(); const navigation = useNavigation(); const {showOngoingProcess, hideOngoingProcess} = useOngoingProcess(); const route = useRoute>(); + const walletTermsAccepted = useAppSelector( ({WALLET}: RootState) => WALLET.walletTermsAccepted, ); + const plainTextRef = useRef(null); const {clearSensitive} = useSensitiveRefClear([plainTextRef]); + const [uploadedFileName, setUploadedFileName] = useState(''); + const [fileFocused, setFileFocused] = useState(false); + const { control, handleSubmit, + setValue, formState: {errors}, } = useForm({resolver: yupResolver(schema)}); @@ -104,6 +235,7 @@ const FileOrText = () => { const key = await dispatch(startImportFile(decryptBackupText, opts)); hideOngoingProcess(); await sleep(1000); + try { showOngoingProcess('IMPORT_SCANNING_FUNDS'); await dispatch(startGetRates({force: true})); @@ -124,6 +256,7 @@ const FileOrText = () => { } catch (error) { // ignore error } + dispatch(setHomeCarouselConfig({id: key.id, show: true})); backupRedirect({ @@ -132,12 +265,14 @@ const FileOrText = () => { walletTermsAccepted, key, }); + dispatch( Analytics.track('Imported Key', { context: route.params?.context || '', source: 'FileOrText', }), ); + hideOngoingProcess(); } catch (e: any) { logger.error(e.message); @@ -168,13 +303,13 @@ const FileOrText = () => { const onSubmit = handleSubmit(formData => { const {text, password} = formData; clearSensitive(); - Keyboard.dismiss(); let opts: Partial = {}; if (route.params?.keyId) { opts.keyId = route.params.keyId; } + let decryptBackupText: string; try { decryptBackupText = BWCProvider.getSJCL().decrypt(password, text); @@ -183,6 +318,7 @@ const FileOrText = () => { showErrorModal(t('Could not decrypt file, check your password')); return; } + try { const parsed = JSON.parse(decryptBackupText); if (parsed.isTSS) { @@ -190,6 +326,7 @@ const FileOrText = () => { return; } } catch {} + importWallet(decryptBackupText, opts); }); @@ -257,38 +394,132 @@ const FileOrText = () => { } }; + const handlePickFile = async () => { + try { + const result = await DocumentPicker.pickSingle({ + type: [DocumentPicker.types.plainText, DocumentPicker.types.allFiles], + copyTo: 'cachesDirectory', + }); + + if (result.fileCopyUri) { + const fileContent = await RNFS.readFile(result.fileCopyUri, 'utf8'); + const encryptedMatch = fileContent.match(/\{[^}]+\}/); + + if (encryptedMatch) { + const encryptedText = encryptedMatch[0]; + setValue('text', encryptedText); + setUploadedFileName(result.name || 'file uploaded'); + logManager.debug( + `[FileOrText] Successfully loaded file: ${result.name}`, + ); + } else { + setValue('text', fileContent.trim()); + setUploadedFileName(result.name || 'file uploaded'); + } + } + } catch (err) { + if (DocumentPicker.isCancel(err)) { + logManager.debug('[FileOrText] User cancelled file picker'); + } else { + const errorMsg = err instanceof Error ? err.message : 'Unknown error'; + logManager.error(`[FileOrText] Error picking file: ${errorMsg}`); + showErrorModal(t('Failed to load file. Please try again.')); + } + } + }; + + const handlePasteClipboard = async () => { + try { + const clipboardContent = await Clipboard.getString(); + + if (!clipboardContent) { + showErrorModal(t('Clipboard is empty')); + return; + } + + const encryptedMatch = clipboardContent.match(/\{[^}]+\}/); + + if (encryptedMatch) { + setValue('text', encryptedMatch[0]); + setUploadedFileName('pasted text'); + logManager.debug('[FileOrText] Pasted encrypted text from clipboard'); + } else { + setValue('text', clipboardContent.trim()); + setUploadedFileName('pasted text'); + } + } catch (err) { + const errorMsg = err instanceof Error ? err.message : 'Unknown error'; + logManager.error(`[FileOrText] Error pasting clipboard: ${errorMsg}`); + showErrorModal(t('Failed to paste from clipboard')); + } + }; + + const handleClearFile = () => { + setUploadedFileName(''); + setValue('text', ''); + }; + + const fileError = errors.text?.message; + return ( + + {t( + 'Upload or paste in the file that was generated when you backed up your key. Exported wallet files and keyshare files are supported.', + )} + + - - {t('Backup plain text code')} - - ( - + {t('FILE')} + + + {uploadedFileName ? ( + + {uploadedFileName} + + ) : ( + )} - name="text" - defaultValue="" - /> - {errors.text?.message && {errors.text.message}} + {uploadedFileName ? ( + setFileFocused(true)} + onPressOut={() => setFileFocused(false)}> + {theme.dark ? : } + + ) : ( + <> + setFileFocused(true)} + onPressOut={() => setFileFocused(false)}> + {t('Paste')} + + + setFileFocused(true)} + onPressOut={() => setFileFocused(false)}> + {theme.dark ? : } + + + )} + + + {fileError ? ( + + {typeof fileError === 'string' + ? fileError.charAt(0).toUpperCase() + fileError.slice(1) + : String(fileError)} + + ) : null} diff --git a/src/navigation/wallet/components/TSSProgressTracker.tsx b/src/navigation/wallet/components/TSSProgressTracker.tsx index 5275d4fffc..35587c42fa 100644 --- a/src/navigation/wallet/components/TSSProgressTracker.tsx +++ b/src/navigation/wallet/components/TSSProgressTracker.tsx @@ -33,8 +33,7 @@ const ProgressButton = styled(TouchableOpacity)` padding: 16px; border-radius: 12px; border-width: 1px; - border-color: ${({theme: {dark}}) => - dark ? 'rgba(255,255,255,0.1)' : Slate30}; + border-color: ${({theme: {dark}}) => (dark ? SlateDark : Slate30)}; `; const ProgressIndicator = styled.View<{status: TSSSigningStatus}>` @@ -309,9 +308,6 @@ const TSSProgressTracker: React.FC = ({ }, { title: t('Waiting for co-signers'), - subtitle: `${copayers.filter(c => c.signed).length}/${ - copayers.length - } ${t('signed')}`, showCopayers: true, }, { @@ -412,20 +408,13 @@ const TSSProgressTracker: React.FC = ({ {copayers.map((copayer, idx) => ( - - - {copayer.signed ? ( - - ) : ( - - )} - - {idx < copayers.length - 1 && ( - + + {copayer.signed ? ( + + ) : ( + )} - + {copayer.name} diff --git a/src/navigation/wallet/screens/Import.tsx b/src/navigation/wallet/screens/Import.tsx index d51d4c33bc..95e2b1eb46 100644 --- a/src/navigation/wallet/screens/Import.tsx +++ b/src/navigation/wallet/screens/Import.tsx @@ -45,7 +45,7 @@ const Import: React.FC = ({navigation, route}) => { initialParams={route.params} /> diff --git a/src/navigation/wallet/screens/InviteCosigners.tsx b/src/navigation/wallet/screens/InviteCosigners.tsx index 1f4ed5805e..11905a65c4 100644 --- a/src/navigation/wallet/screens/InviteCosigners.tsx +++ b/src/navigation/wallet/screens/InviteCosigners.tsx @@ -42,6 +42,7 @@ import ShareIcon from '../../../../assets/img/share-icon.svg'; import haptic from '../../../components/haptic-feedback/haptic'; import {useTheme} from 'styled-components/native'; import {sleep} from '../../../utils/helper-methods'; +import {useNavigation} from '@react-navigation/native'; const Container = styled.SafeAreaView` flex: 1; @@ -69,8 +70,7 @@ const CoSignerContainer = styled.View` padding: 16px; border-radius: 12px; border-width: 1px; - border-color: ${({theme: {dark}}) => - dark ? 'rgba(255,255,255,0.1)' : Slate30}; + border-color: ${({theme: {dark}}) => (dark ? SlateDark : Slate30)}; gap: 17px; `; @@ -115,8 +115,8 @@ const CheckMark = styled.View` width: 40px; height: 40px; padding: 12px; - border-radius: 8px; - background-color: ${({theme: {dark}}) => (dark ? LightBlack : Success25)}; + border-radius: 12px; + background-color: ${({theme: {dark}}) => (dark ? '#004D27' : Success25)}; align-items: center; justify-content: center; `; @@ -150,15 +150,6 @@ const HeaderButton = styled.TouchableOpacity` min-width: 60px; `; -const HeaderButtonText = styled(BaseText)` - font-size: 16px; - color: ${Action}; -`; - -const HeaderButtonRight = styled(HeaderButton)` - align-items: flex-end; -`; - const PlaceholderView = styled.View` min-width: 60px; `; @@ -177,8 +168,7 @@ const TopSectionContainer = styled.View` align-items: center; border-radius: 12px; border-width: 1px; - border-color: ${({theme: {dark}}) => - dark ? 'rgba(255,255,255,0.1)' : Slate30}; + border-color: ${({theme: {dark}}) => (dark ? SlateDark : Slate30)}; min-height: 340px; justify-content: center; `; @@ -242,8 +232,7 @@ const QRSectionContainer = styled.View` align-items: center; border-radius: 12px; border-width: 1px; - border-color: ${({theme: {dark}}) => - dark ? 'rgba(255,255,255,0.1)' : Slate30}; + border-color: ${({theme: {dark}}) => (dark ? SlateDark : Slate30)}; min-height: 355px; `; @@ -264,8 +253,7 @@ const ShareContainer = styled.View` padding-top: 16px; padding-bottom: 16px; border-top-width: 1px; - border-top-color: ${({theme: {dark}}) => - dark ? 'rgba(255,255,255,0.1)' : Slate30}; + border-top-color: ${({theme: {dark}}) => (dark ? SlateDark : Slate30)}; `; const QRCodeWrapper = styled.View` @@ -297,8 +285,7 @@ const StepsContainer = styled.View` padding: 16px; border-radius: 12px; border-width: 1px; - border-color: ${({theme: {dark}}) => - dark ? 'rgba(255,255,255,0.1)' : Slate30}; + border-color: ${({theme: {dark}}) => (dark ? SlateDark : Slate30)}; `; const StepsSectionTitle = styled(BaseText)` @@ -393,8 +380,9 @@ type Props = NativeStackScreenProps< WalletScreens.INVITE_COSIGNERS >; -const InviteCosigners: React.FC = ({navigation, route}) => { +const InviteCosigners: React.FC = ({route}) => { const dispatch = useAppDispatch(); + const navigation = useNavigation(); const {t} = useTranslation(); const logger = useLogger(); const theme = useTheme(); @@ -468,12 +456,24 @@ const InviteCosigners: React.FC = ({navigation, route}) => { setCurrentStep(3); }; + const handleShareAgain = () => { + setIsInviteShared(false); + setCurrentStep(2); + }; + const handleScanQR = () => { setIsModalVisible(false); - navigation.navigate(WalletScreens.SCAN, { - onScanComplete: (data: string) => { - setSessionId(data); - setIsModalVisible(true); + navigation.navigate('ScanRoot', { + onScanComplete: data => { + try { + setIsModalVisible(true); + if (data) { + setSessionId(data); + } + } catch (err) { + const e = err instanceof Error ? err.message : JSON.stringify(err); + logger.error('[OpenScanner SendTo] ' + e); + } }, }); }; @@ -559,9 +559,10 @@ const InviteCosigners: React.FC = ({navigation, route}) => { }; const getModalTitle = () => { - if (currentStep === 3) { - return t('Add Another Co-signer'); - } + // if (currentStep === 3) { + // const isLastCopayer = copayers.every(c => c.status === 'invited'); + // return isLastCopayer ? t('Ready to Continue') : t('Add Another Co-signer'); + // } return t('Invite {{name}}', {name: selectedCopayer?.name || ''}); }; @@ -658,24 +659,33 @@ const InviteCosigners: React.FC = ({navigation, route}) => { } if (currentStep === 3 && isInviteShared) { + const isLastCopayer = copayers.every(c => c.status === 'invited'); + return ( - - - - - - - - {t('{{name}} added', {name: selectedCopayer?.name || ''})} - - - - - - - + <> + + + + + + + + {t('{{name}} added', {name: selectedCopayer?.name || ''})} + + + + + + + + + + {t('Check Invite Code Again')} + + + ); } diff --git a/src/navigation/wallet/screens/JoinTSSWallet.tsx b/src/navigation/wallet/screens/JoinTSSWallet.tsx index e0e74a059e..f26012a5a0 100644 --- a/src/navigation/wallet/screens/JoinTSSWallet.tsx +++ b/src/navigation/wallet/screens/JoinTSSWallet.tsx @@ -62,8 +62,7 @@ const QRSectionContainer = styled.View` align-items: center; border-radius: 12px; border-width: 1px; - border-color: ${({theme: {dark}}) => - dark ? 'rgba(255,255,255,0.1)' : Slate30}; + border-color: ${({theme: {dark}}) => (dark ? SlateDark : Slate30)}; min-height: 355px; `; @@ -103,8 +102,7 @@ const ShareContainer = styled.View` padding-top: 16px; padding-bottom: 16px; border-top-width: 1px; - border-top-color: ${({theme: {dark}}) => - dark ? 'rgba(255,255,255,0.1)' : Slate30}; + border-top-color: ${({theme: {dark}}) => (dark ? SlateDark : Slate30)}; `; const ShareButton = styled.TouchableOpacity` @@ -166,8 +164,7 @@ const StepsContainer = styled.View` padding: 16px; border-radius: 12px; border-width: 1px; - border-color: ${({theme: {dark}}) => - dark ? 'rgba(255,255,255,0.1)' : Slate30}; + border-color: ${({theme: {dark}}) => (dark ? SlateDark : Slate30)}; `; const StepsSectionTitle = styled(BaseText)` @@ -353,7 +350,7 @@ const JoinTSSWallet: React.FC = ({navigation, route}) => { setPartyKey(result.partyKey); logger.debug('[TSS Join] Session ID generated'); } catch (err: any) { - logger.error(`[TSS Join] Error: ${err.message}`); + logger.error(`[TSS Join - generateNewSession] Error: ${err.message}`); } finally { setIsLoading(false); } @@ -408,7 +405,7 @@ const JoinTSSWallet: React.FC = ({navigation, route}) => { setIsWalletReady(true); } catch (err: any) { setCurrentStep(2); - logger.error(`[TSS Join] Error: ${err.message}`); + logger.error(`[TSS Join - handleJoin] Error: ${err.message}`); dispatch( showBottomNotificationModal({ type: 'error', @@ -435,7 +432,7 @@ const JoinTSSWallet: React.FC = ({navigation, route}) => { setSessionId(result.sessionId); setPartyKey(result.partyKey); } catch (err: any) { - logger.error(`[TSS Join] Error: ${err.message}`); + logger.error(`[TSS Join - onSubmitStart] Error: ${err.message}`); setShowSession(false); setShowProcessing(false); dispatch( @@ -584,6 +581,9 @@ const JoinTSSWallet: React.FC = ({navigation, route}) => { {t('Continue')} + setCurrentStep(1)}> + {t('Check Session ID again')} + ); // } @@ -666,9 +666,9 @@ const JoinTSSWallet: React.FC = ({navigation, route}) => { 1}> - {currentStep === 1 ? ( + {currentStep === 1 || currentStep === 0 ? ( ) : ( diff --git a/src/navigation/wallet/screens/wallet-settings/ExportTSSWallet.tsx b/src/navigation/wallet/screens/wallet-settings/ExportTSSWallet.tsx index ee47bd80c1..9eaea172fe 100644 --- a/src/navigation/wallet/screens/wallet-settings/ExportTSSWallet.tsx +++ b/src/navigation/wallet/screens/wallet-settings/ExportTSSWallet.tsx @@ -3,7 +3,7 @@ import { HeaderTitle, Paragraph, BaseText, - H2, + H3, } from '../../../../components/styled/Text'; import {useNavigation, useRoute, CommonActions} from '@react-navigation/native'; import styled from 'styled-components/native'; @@ -100,7 +100,7 @@ const SuccessImageContainer = styled.View` margin-bottom: 32px; `; -const SuccessTitle = styled(H2)` +const SuccessTitle = styled(H3)` margin-bottom: 16px; text-align: center; `; @@ -169,41 +169,71 @@ const ExportTSSWallet = () => { return null; } - const bufferToArray = ( - value: Buffer | {data: number[]} | undefined, - ): number[] | null => { - if (!value) return null; - if (Buffer.isBuffer(value)) { - return Array.from(value); + const keychain = key.properties?.keychain; + const metadata = key.properties?.metadata; + + const bufferToArray = (buffer: any): number[] | null => { + if (!buffer) { + logManager.debug('[bufferToArray] buffer is null/undefined'); + return null; } - if ('data' in value) { - return value.data; + + if (Array.isArray(buffer)) { + logManager.debug('[bufferToArray] Using Array.isArray path'); + return buffer; + } + + if (Buffer.isBuffer(buffer)) { + logManager.debug('[bufferToArray] Using Buffer.isBuffer path'); + return Array.from(buffer); + } + + if (buffer && typeof buffer === 'object' && 'data' in buffer) { + if (Array.isArray(buffer.data)) { + logManager.debug('[bufferToArray] Using buffer.data array path'); + return buffer.data; + } + } + + if (buffer && typeof buffer === 'object') { + const keys = Object.keys(buffer); + if (keys.length > 0 && keys.every(k => !isNaN(Number(k)))) { + logManager.debug('[bufferToArray] Using numeric keys object path'); + const arr: number[] = []; + for (let i = 0; i < keys.length; i++) { + if (buffer[i] !== undefined) { + arr.push(buffer[i]); + } + } + return arr.length > 0 ? arr : null; + } } + + logManager.debug('[bufferToArray] No matching condition, returning null'); return null; }; - - const keychain = key.properties?.keychain; + if (!keychain) { + throw new Error('Keychain data is missing'); + } const backup = { isTSS: true, version: 1, - mnemonic: key.properties?.mnemonic, - keychain: keychain - ? { - commonKeyChain: keychain.commonKeyChain, - privateKeyShare: bufferToArray(keychain.privateKeyShare), - reducedPrivateKeyShare: bufferToArray( - keychain.reducedPrivateKeyShare, - ), - } - : undefined, - keyId: key.id, - keyName: key.keyName, - createdOn: Date.now(), + key: { + mnemonic: key.properties?.mnemonic, + keychain: { + commonKeyChain: keychain.commonKeyChain, + privateKeyShare: bufferToArray(keychain.privateKeyShare), + reducedPrivateKeyShare: bufferToArray( + keychain.reducedPrivateKeyShare, + ), + }, + metadata: metadata, + }, }; return BWC.getSJCL().encrypt(password, JSON.stringify(backup), { - iter: 10000, + iter: 1000, }); }; @@ -222,29 +252,29 @@ const ExportTSSWallet = () => { } const walletName = key?.wallets?.[0]?.walletName || 'SharedWallet'; - const filename = `${APP_NAME_UPPERCASE}-Keyshare-${walletName}`; + const filename = `${APP_NAME_UPPERCASE}-Keyshare-${walletName}.txt`; const rootPath = Platform.OS === 'ios' ? RNFS.LibraryDirectoryPath : RNFS.TemporaryDirectoryPath; - let filePath = `${rootPath}/${filename}`; - await RNFS.mkdir(filePath); - filePath += '.txt'; + const filePath = `${rootPath}/${filename}`; const txt = t( 'Here is the encrypted keyshare backup for wallet: {{name}}\n\n{{keyshare}}\n\nTo import this backup, copy all text between {...}, including the symbols {}', {name: walletName, keyshare: encryptedKeyshare}, ); + await RNFS.writeFile(filePath, txt, 'utf8'); + const opts: ShareOptions = { title: filename, - url: `file://${filePath}`, + url: Platform.OS === 'android' ? `file://${filePath}` : filePath, subject: `${walletName} Keyshare Backup`, + type: 'text/plain', }; - await RNFS.writeFile(filePath, txt, 'utf8'); await Share.open(opts); setShareButtonState('success'); @@ -333,7 +363,7 @@ const ExportTSSWallet = () => { onPress={handleSubmit(shareKeyshareFile)} state={shareButtonState} buttonStyle={'primary'}> - {t('Backup Shared Wallet')} + {t('Share Backup File')} diff --git a/src/store/wallet/effects/create-multisig/create-multisig.ts b/src/store/wallet/effects/create-multisig/create-multisig.ts index 84dd2c222c..71356dafcc 100644 --- a/src/store/wallet/effects/create-multisig/create-multisig.ts +++ b/src/store/wallet/effects/create-multisig/create-multisig.ts @@ -192,6 +192,7 @@ const getPubKeyFromKey = (partyKey: any): string => { chain: 'BTC', // Doesn't matter for requestPubKey network: 'livenet', account: 0, + n: 1, }); return credentials.requestPubKey; }; @@ -279,7 +280,8 @@ export const startCreateTSSKey = const key = buildKeyObj({ key: partyKey, wallets: [placeholderWallet], - keyName: 'My Key', + keyName: 'My TSSKey', + backupComplete: true, }); const copayers: TSSCopayerInfo[] = []; @@ -677,7 +679,7 @@ export const startTSSCeremony = const finalKey = buildTssKeyObj({ tssKey: _tssKey, wallets: [finalWallet], - keyName: 'My Key', + keyName: 'My TSSKey', }); finalKey.tssSession = { @@ -845,7 +847,7 @@ export const joinTSSWithCode = const key = buildKeyObj({ key: partyKey, wallets: [placeholderWallet], - keyName: 'My Key', + keyName: 'My TSSKey', }); key.tssSession = { @@ -1085,7 +1087,7 @@ export const joinTSSWithCode = const finalKey = buildTssKeyObj({ tssKey: _tssKey, wallets: [finalWallet], - keyName: 'My Key', + keyName: 'My TSSKey', }); finalKey.tssSession = { @@ -1102,7 +1104,7 @@ export const joinTSSWithCode = } catch (err) { const errorStr = err instanceof Error ? err.message : JSON.stringify(err); - logManager.error(`[TSS Join] Error: ${errorStr}`); + logManager.error(`[TSS Join - joinTSSWithCode] Error: ${errorStr}`); reject(err); } }); diff --git a/src/store/wallet/effects/import/import.ts b/src/store/wallet/effects/import/import.ts index 3fbcbba8a1..fd0d506c5e 100644 --- a/src/store/wallet/effects/import/import.ts +++ b/src/store/wallet/effects/import/import.ts @@ -1615,60 +1615,136 @@ export const startImportTSSFile = async (dispatch, getState): Promise => { return new Promise(async (resolve, reject) => { try { + const { + WALLET, + APP: { + notificationsAccepted, + emailNotifications, + brazeEid, + defaultLanguage, + }, + } = getState(); + const {tokenOptionsByAddress} = tokenManager.getTokenOptions(); + + const tokenOptsByAddress = { + ...BitpaySupportedTokenOptsByAddress, + ...tokenOptionsByAddress, + ...WALLET.customTokenOptionsByAddress, + }; + const data = JSON.parse(decryptedBackupText); if (!data.isTSS) { - throw new Error(t('Invalid TSS backup file.')); + throw new Error(t('Invalid TSS backup file format.')); } - if (!data.mnemonic) { + if (!data.key?.mnemonic) { throw new Error(t('Missing mnemonic in TSS backup.')); } - if (!data.keychain) { + if (!data.key?.keychain) { throw new Error(t('Missing keychain in TSS backup.')); } - const arrayToBuffer = ( - arr: number[] | null | undefined, - ): Buffer | undefined => { - if (!arr) return undefined; - return Buffer.from(arr); + logManager.info('[ImportTSS] Starting TSS wallet import...'); + + const arrayToBuffer = (arr: any): Buffer | null => { + if (!arr) return null; + if (Buffer.isBuffer(arr)) return arr; + if (Array.isArray(arr)) return Buffer.from(arr); + if ( + arr && + typeof arr === 'object' && + 'data' in arr && + Array.isArray(arr.data) + ) { + return Buffer.from(arr.data); + } + return null; }; - const importData = { - words: data.mnemonic, + const privateKeyShare = arrayToBuffer( + data.key.keychain.privateKeyShare, + ); + const reducedPrivateKeyShare = arrayToBuffer( + data.key.keychain.reducedPrivateKeyShare, + ); + + if (!privateKeyShare || privateKeyShare.length === 0) { + throw new Error(t('Invalid privateKeyShare in backup file.')); + } + + if (!reducedPrivateKeyShare || reducedPrivateKeyShare.length === 0) { + throw new Error(t('Invalid reducedPrivateKeyShare in backup file.')); + } + + logManager.info('[ImportTSS] Keyshare conversion successful'); + + const opts: Partial = { + words: normalizeMnemonic(data.key.mnemonic), + tssKeychain: { + commonKeyChain: data.key.keychain.commonKeyChain, + privateKeyShare: privateKeyShare, + reducedPrivateKeyShare: reducedPrivateKeyShare, + }, + tssMetadata: data.key.metadata, }; - const key = (await dispatch( - startImportMnemonic(importData, {}), - )) as Key; + const importResult = await serverAssistedImport(opts); - if (key && data.keychain) { - const privateKeyShare = arrayToBuffer(data.keychain.privateKeyShare); - const reducedPrivateKeyShare = arrayToBuffer( - data.keychain.reducedPrivateKeyShare, - ); + const { + key: _key, + wallets, + keyName, + } = findMatchedKeyAndUpdate( + importResult.wallets, + importResult.key, + Object.values(WALLET.keys).filter(k => !k.id.includes('readonly')), + opts, + ); - if (privateKeyShare && reducedPrivateKeyShare) { - key.properties = { - ...key.properties, - keychain: { - commonKeyChain: data.keychain.commonKeyChain, - privateKeyShare, - reducedPrivateKeyShare, - }, - } as KeyProperties; - } else { - throw new Error(t('Invalid keychain data in TSS backup.')); - } + const key = buildKeyObj({ + key: _key, + keyName, + wallets: wallets.map(wallet => { + if (notificationsAccepted) { + dispatch(subscribePushNotifications(wallet, brazeEid!)); + } + if ( + emailNotifications && + emailNotifications.accepted && + emailNotifications.email + ) { + const prefs = { + email: emailNotifications.email, + language: defaultLanguage, + unit: 'btc', + }; + dispatch(subscribeEmailNotifications(wallet, prefs)); + } + const {currencyAbbreviation, currencyName} = dispatch( + mapAbbreviationAndName( + wallet.credentials.coin, + wallet.credentials.chain, + wallet.credentials.token?.address, + ), + ); + return merge( + wallet, + buildWalletObj( + {...wallet.credentials, currencyAbbreviation, currencyName}, + tokenOptsByAddress, + ), + ); + }), + backupComplete: true, + }); - dispatch( - successImport({ - key, - }), - ); - } + dispatch( + successImport({ + key, + }), + ); logManager.info('[ImportTSS] Successfully imported TSS wallet'); resolve(key); diff --git a/src/store/wallet/effects/tss-send/tss-send.ts b/src/store/wallet/effects/tss-send/tss-send.ts index 6c44397471..f7494bb8fe 100644 --- a/src/store/wallet/effects/tss-send/tss-send.ts +++ b/src/store/wallet/effects/tss-send/tss-send.ts @@ -32,7 +32,11 @@ export interface TSSSigningCallbacks { } export const isTSSKey = (key: Key): boolean => { - return !!(key.tssSession?.status === 'complete' && key.tssSession?.n > 1); + return !!( + key.properties?.keychain?.privateKeyShare || + key.properties?.keychain?.reducedPrivateKeyShare || + key.properties?.keychain?.commonKeyChain + ); }; export const requiresTSSSigning = (wallet: Wallet, key: Key): boolean => { diff --git a/src/store/wallet/utils/wallet.ts b/src/store/wallet/utils/wallet.ts index 7c808fa342..2c03d0a6a8 100644 --- a/src/store/wallet/utils/wallet.ts +++ b/src/store/wallet/utils/wallet.ts @@ -135,9 +135,7 @@ export const buildWalletObj = ( hardwareData = {}, singleAddress, receiveAddress, - isTssWallet = false, tssKeyId, - tssPartyId, }: Credentials & { balance?: WalletBalance; tokens?: any; @@ -161,9 +159,7 @@ export const buildWalletObj = ( }; singleAddress: boolean; receiveAddress?: string; - isTssWallet?: boolean; tssKeyId?: string; - tssPartyId?: number; }, tokenOptsByAddress?: {[key in string]: Token}, ): WalletObj => { @@ -218,9 +214,7 @@ export const buildWalletObj = ( hardwareData, singleAddress, receiveAddress, - isTssWallet, tssKeyId, - tssPartyId, }; }; diff --git a/src/store/wallet/wallet.models.ts b/src/store/wallet/wallet.models.ts index 0a8d1f9c6e..d381440127 100644 --- a/src/store/wallet/wallet.models.ts +++ b/src/store/wallet/wallet.models.ts @@ -54,7 +54,6 @@ export interface KeyProperties { m: string; n: string; }; - serializedKeychain?: any; keychain?: { privateKeyShare: Buffer | {data: number[]}; reducedPrivateKeyShare: Buffer | {data: number[]}; @@ -163,13 +162,7 @@ export interface WalletObj { */ accountPath?: string; }; - isTssWallet?: boolean; tssKeyId?: string; - tssPartyId?: number; - tssThreshold?: { - m: number; - n: number; - }; pendingTssSession?: boolean; } @@ -201,6 +194,12 @@ export interface KeyOptions { password?: string; includeTestnetWallets?: boolean; includeLegacyWallets?: boolean; + tssKeychain?: { + commonKeyChain: string; + privateKeyShare: Buffer; + reducedPrivateKeyShare: Buffer; + }; + tssMetadata?: any; } export interface Token { diff --git a/yarn.lock b/yarn.lock index fc808361f1..d113d8ca98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15107,6 +15107,13 @@ react-native-device-info@8.4.7: resolved "https://registry.yarnpkg.com/react-native-device-info/-/react-native-device-info-8.4.7.tgz#1546232a297e0776dc46c90875055ff65aab891a" integrity sha512-I0z7FfbtxKW5+1wWE7L1bzy0iuiQzgUNVUrvpQoVQSz/BpC9kNIwZokC8O3HpUJDkFVr2Bhd5WO3tISzS7OXxw== +react-native-document-picker@^9.3.1: + version "9.3.1" + resolved "https://registry.yarnpkg.com/react-native-document-picker/-/react-native-document-picker-9.3.1.tgz#f2c33237a906fd0893130e0605c8f18a3aef1605" + integrity sha512-Vcofv9wfB0j67zawFjfq9WQPMMzXxOZL9kBmvWDpjVuEcVK73ndRmlXHlkeFl5ZHVsv4Zb6oZYhqm9u5omJOPA== + dependencies: + invariant "^2.2.4" + react-native-dotenv@3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/react-native-dotenv/-/react-native-dotenv-3.3.0.tgz#fb1f50347be6ac0714a5e4772fad538c01ffb423" From f5a030e9c07e6ef15b553e0a74687d195ee61bae Mon Sep 17 00:00:00 2001 From: Gabriel Date: Tue, 6 Jan 2026 23:03:00 -0300 Subject: [PATCH 06/16] [REF] tss send - utxo inputPaths > 0 support - remove sol - improve message hash calculation --- .../wallet/screens/CurrencySelection.tsx | 4 +- src/store/wallet/effects/tss-send/tss-send.ts | 418 +++++++++--------- 2 files changed, 210 insertions(+), 212 deletions(-) diff --git a/src/navigation/wallet/screens/CurrencySelection.tsx b/src/navigation/wallet/screens/CurrencySelection.tsx index f92e4bd281..989524b75b 100644 --- a/src/navigation/wallet/screens/CurrencySelection.tsx +++ b/src/navigation/wallet/screens/CurrencySelection.tsx @@ -187,7 +187,9 @@ const CurrencySelection = ({route}: CurrencySelectionScreenProps) => { IsUtxoChain(currency.currencyAbbreviation.toLowerCase()), ); const accountCurrencies = SupportedTSSCurrencyOptions.filter( - currency => !IsUtxoChain(currency.currencyAbbreviation.toLowerCase()), + currency => + !IsUtxoChain(currency.currencyAbbreviation.toLowerCase()) && + currency.currencyAbbreviation.toLowerCase() !== 'sol', // TODO: need to add EDDSA support to bitcore-tss ); const tssItems: CurrencySelectionListItem[] = []; diff --git a/src/store/wallet/effects/tss-send/tss-send.ts b/src/store/wallet/effects/tss-send/tss-send.ts index f7494bb8fe..de26c50ef0 100644 --- a/src/store/wallet/effects/tss-send/tss-send.ts +++ b/src/store/wallet/effects/tss-send/tss-send.ts @@ -10,7 +10,7 @@ import { } from '../../wallet.models'; import {logManager} from '../../../../managers/LogManager'; import {BASE_BWS_URL} from '../../../../constants/config'; -import {utils as ethersUtils} from 'ethers'; +import {IsUtxoChain} from '../../utils/currency'; const BWC = BwcProvider.getInstance(); @@ -43,92 +43,6 @@ export const requiresTSSSigning = (wallet: Wallet, key: Key): boolean => { return isTSSKey(key) && !!wallet.tssKeyId; }; -export const toBwsSignatureFormat = (sig: any, chain: string): string => { - const strip0x = (h: string) => (h?.startsWith('0x') ? h.slice(2) : h); - const pad32 = (h: string) => strip0x(h).padStart(64, '0'); - - if (!sig) throw new Error('Missing signature'); - - if (typeof sig === 'string') { - const hex = strip0x(sig); - if (!/^[0-9a-fA-F]+$/.test(hex)) throw new Error('Signature is not hex'); - return '0x' + hex; - } - - const isEvm = ['eth', 'matic', 'arb', 'base', 'op'].includes( - (chain || '').toLowerCase(), - ); - if (!isEvm) { - throw new Error(`Unsupported chain for this helper: ${chain}`); - } - - const r = pad32(sig.r); - const s = pad32(sig.s); - - let v = Number(sig.v); - if (v === 0 || v === 1) { - v = v + 27; - } - const vHex = v.toString(16).padStart(2, '0'); - - return `0x${r}${s}${vHex}`; -}; - -export const getTxpMessageHash = ( - wallet: Wallet, - txp: TransactionProposal, -): Buffer => { - const Bitcore = BWC.getBitcore(); - const utils = BWC.getUtils(); - - const tx = utils.buildTx(txp); - - if (['btc', 'bch', 'ltc', 'doge'].includes(txp.chain?.toLowerCase())) { - const sighash = tx.inputs[0].getSighash( - tx, - Bitcore.crypto.Signature.SIGHASH_ALL, - ); - return sighash; - } else if ( - ['eth', 'matic', 'arb', 'base', 'op'].includes(txp.chain?.toLowerCase()) - ) { - const serialized = tx.uncheckedSerialize()[0]; - const hexString = serialized.startsWith('0x') - ? serialized.slice(2) - : serialized; - - const txBuffer = Buffer.from(hexString, 'hex'); - - const hashHex = ethersUtils.keccak256('0x' + txBuffer.toString('hex')); - const hash = Buffer.from(hashHex.slice(2), 'hex'); - - logManager.debug( - `[getTxpMessageHash] Keccak256 hash: ${hash.toString('hex')}`, - ); - - return hash; - } else { - const serialized = tx.uncheckedSerialize()[0]; - const hexString = serialized.startsWith('0x') - ? serialized.slice(2) - : serialized; - const txBuffer = Buffer.from(hexString, 'hex'); - return Bitcore.crypto.Hash.sha256(txBuffer); - } -}; - -export const getDerivationPath = ( - wallet: Wallet, - txp: TransactionProposal, -): string => { - // TSS uses simple paths, not full BIP44 paths - return 'm/0/0'; -}; - -export const generateSessionId = (txp: TransactionProposal): string => { - return `sign-${txp.id}`; -}; - const restoreKeychain = (tssKey: any): any => { if (!tssKey?.keychain) return tssKey; @@ -154,6 +68,151 @@ const restoreKeychain = (tssKey: any): any => { return tssKey; }; +export const toBwsSignatureFormat = (sig: any, chain: string): string => { + const CWC = BWC.getCore(); + const transformed = CWC.Transactions.transformSignatureObject({ + chain: chain.toUpperCase(), + obj: sig, + }); + logManager.debug( + `[transformSignatureObject] ${chain} signature: ${transformed}`, + ); + return transformed; +}; + +export const generateSessionId = ( + txp: TransactionProposal, + derivationPath: string, +): string => { + return `${txp.id}:${derivationPath.replace(/\//g, '-')}`; +}; + +const signInput = async (params: { + tssKey: any; + wallet: Wallet; + txp: TransactionProposal; + messageHash: Buffer; + derivationPath: string; + sessionId: string; + inputIndex: number; + totalInputs: number; + callbacks: TSSSigningCallbacks; + timeout: number; +}): Promise => { + const { + tssKey, + wallet, + txp, + messageHash, + derivationPath, + sessionId, + inputIndex, + totalInputs, + callbacks, + timeout, + } = params; + + const tssSign = new TssSign({ + baseUrl: BASE_BWS_URL, + credentials: wallet.credentials, + tssKey: tssKey, + }); + + try { + await tssSign.start({ + id: sessionId, + messageHash, + derivationPath, + }); + } catch (startError: any) { + if (startError.message?.startsWith('TSS_ROUND_ALREADY_DONE')) { + const sig = await tssSign.getSignatureFromServer(); + if (sig) { + logManager.debug( + `[TSS Sign] Input ${inputIndex + 1} recovered from server`, + ); + return toBwsSignatureFormat(sig, txp.chain); + } + throw new Error( + 'TSS session interrupted. Try deleting this proposal and creating a new one.', + ); + } + throw startError; + } + + const signature = await new Promise((resolveSign, rejectSign) => { + let timeoutId: NodeJS.Timeout | null = null; + + timeoutId = setTimeout(() => { + tssSign.unsubscribe(); + rejectSign(new Error(`Timeout signing input ${inputIndex + 1}`)); + }, timeout); + + tssSign + .on('roundready', (round: number) => { + logManager.debug( + `[TSS Sign] Input ${inputIndex + 1} Round ${round} ready`, + ); + callbacks.onRoundUpdate(round, 'ready'); + + if (round === 1 && inputIndex === 0) { + callbacks.onStatusChange('signature_generation'); + } + + callbacks.onProgressUpdate({ + currentRound: round, + totalRounds: 4, + status: 'processing', + }); + }) + .on('roundprocessed', (round: number) => { + logManager.debug( + `[TSS Sign] Input ${inputIndex + 1} Round ${round} processed`, + ); + callbacks.onRoundUpdate(round, 'processed'); + }) + .on('roundsubmitted', (round: number) => { + logManager.debug( + `[TSS Sign] Input ${inputIndex + 1} Round ${round} submitted`, + ); + callbacks.onRoundUpdate(round, 'submitted'); + }) + .on('complete', () => { + logManager.debug(`[TSS Sign] Input ${inputIndex + 1} complete`); + try { + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + + const sig = tssSign.getSignature(); + logManager.debug( + `[TSS Sign] Input ${inputIndex + 1} Signature from getSignature():`, + sig, + ); + + const bwsSig = toBwsSignatureFormat(sig, txp.chain); + resolveSign(bwsSig); + } catch (err: any) { + rejectSign(new Error(`Failed to convert signature: ${err.message}`)); + } + }) + .on('error', (error: Error) => { + logManager.error( + `[TSS Sign] Input ${inputIndex + 1} Error: ${error.message}`, + ); + rejectSign(error); + }); + + tssSign.subscribe(); + }); + + tssSign.unsubscribe(); + logManager.debug(`[TSS Sign] Input ${inputIndex + 1} signed successfully`); + + return signature; +}; + export const startTSSSigning = (opts: { key: Key; @@ -167,9 +226,6 @@ export const startTSSSigning = const {key, wallet, txp, callbacks, timeout = 300000, joiner} = opts; return new Promise(async (resolve, reject) => { - let tssSign: any = null; - let timeoutId: NodeJS.Timeout | null = null; - try { logManager.debug('[TSS Sign] Starting TSS signing process'); callbacks.onStatusChange('initializing'); @@ -179,131 +235,84 @@ export const startTSSSigning = } const tssKey = restoreKeychain(key.methods); - - logManager.debug( - `[TSS Sign] privateKeyShare isBuffer: ${Buffer.isBuffer( - tssKey?.keychain?.privateKeyShare, - )}`, - ); - logManager.debug( - `[TSS Sign] privateKeyShare length: ${tssKey?.keychain?.privateKeyShare?.length}`, - ); - if (!tssKey) { throw new Error('TSS key methods not available'); } - const messageHash = getTxpMessageHash(wallet, txp); - const derivationPath = getDerivationPath(wallet, txp); + const Bitcore = BWC.getBitcore(); + const CWC = BWC.getCore(); + const utils = BWC.getUtils(); - logManager.debug( - `[TSS Sign] Message hash: ${messageHash.toString('hex')}`, - ); - logManager.debug(`[TSS Sign] Derivation path: ${derivationPath}`); + const chain = wallet.credentials.chain; + const network = wallet.credentials.network; + const isUtxo = IsUtxoChain(chain); - tssSign = new TssSign({ - baseUrl: BASE_BWS_URL, - credentials: wallet.credentials, - tssKey: tssKey, - }); + const inputPaths = + isUtxo && txp.inputPaths?.length ? txp.inputPaths : ['m/0/0']; - const sessionId = generateSessionId(txp); - logManager.debug(`[TSS Sign] Session ID: ${sessionId}`); + const tx = utils.buildTx(txp); + const txHex = tx.uncheckedSerialize(); + const rawTx = Array.isArray(txHex) ? txHex[0] : txHex; + const SIGHASH_TYPE = + Bitcore.crypto.Signature.SIGHASH_ALL | + Bitcore.crypto.Signature.SIGHASH_FORKID; + + const xPubKey = new Bitcore.HDPublicKey( + wallet.credentials.clientDerivedPublicKey, + ); + + logManager.debug(`[TSS Sign] Chain: ${chain}, isUtxo: ${isUtxo}`); + logManager.debug( + `[TSS Sign] Total inputs to sign: ${inputPaths.length}`, + ); callbacks.onStatusChange('waiting_for_cosigners'); - try { - await tssSign.start({ - id: sessionId, - messageHash, - derivationPath, - }); - logManager.debug('[TSS Sign] Signing session started successfully'); - } catch (startError: any) { - logManager.warn( - `[TSS Sign] Initial start warning: ${startError.message}`, - ); - } + const signatures: string[] = []; - logManager.debug('[TSS Sign] Waiting for co-signers...'); + for (let i = 0; i < inputPaths.length; i++) { + const derivationPath = inputPaths[i]; + const pubKey = xPubKey + .deriveChild(derivationPath) + .publicKey.toString(); - timeoutId = setTimeout(() => { - if (tssSign) { - tssSign.unsubscribe(); - } - reject( - new Error( - 'TSS signing timeout - co-signers did not respond in time', - ), + logManager.debug( + `[TSS Sign] Signing input ${i + 1}/${ + inputPaths.length + } with path: ${derivationPath}`, ); - }, timeout); - - const signPromise = new Promise((resolveSign, rejectSign) => { - tssSign - .on('roundready', (round: number) => { - logManager.debug(`[TSS Sign] Round ${round} ready`); - callbacks.onRoundUpdate(round, 'ready'); - - if (round === 1) { - callbacks.onStatusChange('signature_generation'); - } - - callbacks.onProgressUpdate({ - currentRound: round, - totalRounds: 4, - status: 'processing', - }); - }) - .on('roundprocessed', (round: number) => { - logManager.debug(`[TSS Sign] Round ${round} processed`); - callbacks.onRoundUpdate(round, 'processed'); - }) - .on('roundsubmitted', (round: number) => { - logManager.debug(`[TSS Sign] Round ${round} submitted`); - callbacks.onRoundUpdate(round, 'submitted'); - }) - // TODO BWC - // .on('copayersigned', (copayerId: string) => { - // logManager.debug(`[TSS Sign] Copayer signed: ${copayerId}`); - // callbacks.onCopayerStatusChange(copayerId, 'signed'); - // }) - .on('signature', signature => { - logManager.debug(`[TSS Sign] Signature received`); - try { - const bwsSig = toBwsSignatureFormat(signature, txp.chain); - resolveSign(bwsSig); - } catch (err) { - rejectSign( - new Error( - `Failed to convert/verify signature: ${err.message}`, - ), - ); - } - }) - .on('complete', () => { - logManager.debug('[TSS Sign] Signing complete'); - }) - .on('error', (error: Error) => { - logManager.error(`[TSS Sign] Error: ${error.message}`); - rejectSign(error); - }); - - tssSign.subscribe({ - timeout: 250, + const sighashHex = CWC.Transactions.getSighash({ + chain, + network, + tx: rawTx, + index: i, + utxos: txp.inputs, + pubKey, + sigtype: SIGHASH_TYPE, }); - }); - - const signature = await signPromise; - - if (timeoutId) { - clearTimeout(timeoutId); - timeoutId = null; + const messageHash = Buffer.from(sighashHex, 'hex'); + const sessionId = generateSessionId(txp, derivationPath!); + const signature = await signInput({ + tssKey, + wallet, + txp, + messageHash, + derivationPath: derivationPath!, + sessionId, + inputIndex: i, + totalInputs: inputPaths.length, + callbacks, + timeout, + }); + signatures.push(signature); } - callbacks.onComplete(signature); + callbacks.onComplete(signatures[0]); callbacks.onStatusChange('broadcasting'); - logManager.debug('[TSS Sign] Pushing signature to txp'); + logManager.debug( + `[TSS Sign] All ${signatures.length} signature(s) collected, pushing to BWS`, + ); let signedTXP: TransactionProposal | null = null; if (!joiner) { @@ -311,7 +320,7 @@ export const startTSSSigning = (resolvePush, rejectPush) => { wallet.pushSignatures( txp, - [signature], + signatures, (err: Error, result: TransactionProposal) => { if (err) { rejectPush(err); @@ -325,23 +334,11 @@ export const startTSSSigning = ); } - if (tssSign) { - tssSign.unsubscribe(); - } - logManager.debug('[TSS Sign] TSS signing completed successfully'); callbacks.onStatusChange('complete'); resolve(signedTXP ?? txp); } catch (err) { - if (timeoutId) { - clearTimeout(timeoutId); - } - if (tssSign) { - try { - tssSign.unsubscribe(); - } catch (e) {} - } const errorStr = err instanceof Error ? err.message : JSON.stringify(err); logManager.error(`[TSS Sign] Error: ${errorStr}`); @@ -364,7 +361,6 @@ export const joinTSSSigningSession = logManager.debug(`[TSS Join] Joining signing session for txp: ${txp.id}`); // The joiner flow is the same as initiator - they both use startTSSSigning - // with the same deterministic sessionId return dispatch( startTSSSigning({ key, From 7e83545471115842b1225e1d4b5ecc27ceda62de Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 7 Jan 2026 15:41:04 -0300 Subject: [PATCH 07/16] [FIX] tss sessionId - Bad Signature err --- src/navigation/wallet/components/FileOrText.tsx | 4 +--- src/store/wallet/effects/tss-send/tss-send.ts | 6 ++++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/navigation/wallet/components/FileOrText.tsx b/src/navigation/wallet/components/FileOrText.tsx index 96835f0139..fdfc66657b 100644 --- a/src/navigation/wallet/components/FileOrText.tsx +++ b/src/navigation/wallet/components/FileOrText.tsx @@ -482,9 +482,7 @@ const FileOrText = () => { {uploadedFileName} ) : ( - + )} {uploadedFileName ? ( diff --git a/src/store/wallet/effects/tss-send/tss-send.ts b/src/store/wallet/effects/tss-send/tss-send.ts index de26c50ef0..88052e388b 100644 --- a/src/store/wallet/effects/tss-send/tss-send.ts +++ b/src/store/wallet/effects/tss-send/tss-send.ts @@ -83,8 +83,9 @@ export const toBwsSignatureFormat = (sig: any, chain: string): string => { export const generateSessionId = ( txp: TransactionProposal, derivationPath: string, + i: number, ): string => { - return `${txp.id}:${derivationPath.replace(/\//g, '-')}`; + return `${txp.id}:${derivationPath.replace(/\//g, '-')}-input${i}`; }; const signInput = async (params: { @@ -291,7 +292,8 @@ export const startTSSSigning = sigtype: SIGHASH_TYPE, }); const messageHash = Buffer.from(sighashHex, 'hex'); - const sessionId = generateSessionId(txp, derivationPath!); + const sessionId = generateSessionId(txp, derivationPath!, i + 1); + logManager.debug(`Session ID for input ${i + 1}: ${sessionId}`); const signature = await signInput({ tssKey, wallet, From b2232d6756fd68ca993832f2b808c57cb3825b09 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 8 Jan 2026 10:41:38 -0300 Subject: [PATCH 08/16] [REF] use hook for tsscallbacks --- .../wallet/components/TSSProgressTracker.tsx | 22 +++- .../screens/TransactionProposalDetails.tsx | 106 ++++------------ .../wallet/screens/send/confirm/Confirm.tsx | 116 ++++------------- src/utils/hooks/useTSSCalbacks.ts | 119 ++++++++++++++++++ 4 files changed, 186 insertions(+), 177 deletions(-) create mode 100644 src/utils/hooks/useTSSCalbacks.ts diff --git a/src/navigation/wallet/components/TSSProgressTracker.tsx b/src/navigation/wallet/components/TSSProgressTracker.tsx index 35587c42fa..62400377bb 100644 --- a/src/navigation/wallet/components/TSSProgressTracker.tsx +++ b/src/navigation/wallet/components/TSSProgressTracker.tsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React, {useEffect, useState} from 'react'; import {View} from 'react-native'; import styled, {useTheme} from 'styled-components/native'; import { @@ -12,6 +12,7 @@ import {useTranslation} from 'react-i18next'; import { TSSSigningStatus, TSSSigningProgress, + Wallet, } from '../../../store/wallet/wallet.models'; import { ActiveOpacity, @@ -221,6 +222,8 @@ interface TSSProgressTrackerProps { copayers: TSSCopayer[]; isModalVisible?: boolean; onModalVisibilityChange?: (visible: boolean) => void; + wallet?: Wallet; + onCopayersInitialized?: (copayers: TSSCopayer[]) => void; } const TSSProgressTracker: React.FC = ({ @@ -230,6 +233,8 @@ const TSSProgressTracker: React.FC = ({ date, copayers, isModalVisible: externalIsVisible, + wallet, + onCopayersInitialized, onModalVisibilityChange, }) => { const {t} = useTranslation(); @@ -328,6 +333,19 @@ const TSSProgressTracker: React.FC = ({ } }; + useEffect(() => { + if (wallet && onCopayersInitialized && copayers.length === 0) { + const initialCopayers = + wallet.copayers?.map(copayer => ({ + id: copayer.id, + name: copayer.name, + signed: false, + })) || []; + + onCopayersInitialized(initialCopayers); + } + }, [wallet, onCopayersInitialized, copayers.length]); + return ( <> @@ -367,7 +385,7 @@ const TSSProgressTracker: React.FC = ({ const isActive = stepStatus === 'active'; const isComplete = stepStatus === 'complete'; const showCopayers = step.showCopayers; - const connectorHeight = showCopayers ? 100 : 20; + const connectorHeight = showCopayers ? 35 * copayers.length : 25; return ( diff --git a/src/navigation/wallet/screens/TransactionProposalDetails.tsx b/src/navigation/wallet/screens/TransactionProposalDetails.tsx index cfe654bab7..1f48a04209 100644 --- a/src/navigation/wallet/screens/TransactionProposalDetails.tsx +++ b/src/navigation/wallet/screens/TransactionProposalDetails.tsx @@ -74,7 +74,6 @@ import { Wallet, TSSSigningStatus, TSSSigningProgress, - TSSCopayerSignStatus, } from '../../../store/wallet/wallet.models'; import { DetailColumn, @@ -93,9 +92,9 @@ import {DeviceEmitterEvents} from '../../../constants/device-emitter-events'; import { isTSSKey, joinTSSSigningSession, - TSSSigningCallbacks, } from '../../../store/wallet/effects/tss-send/tss-send'; import TSSProgressTracker from '../components/TSSProgressTracker'; +import {useTSSCallbacks} from '../../../utils/hooks/useTSSCalbacks'; const TxpDetailsContainer = styled.SafeAreaView` flex: 1; @@ -239,7 +238,15 @@ const TransactionProposalDetails = () => { const {showPaymentSent, hidePaymentSent} = usePaymentSent(); const {showOngoingProcess, hideOngoingProcess} = useOngoingProcess(); - const [isTSSWallet, setIsTSSWallet] = useState(false); + const showErrorMessage = useCallback( + async (msg: BottomNotificationConfig) => { + await sleep(500); + dispatch(showBottomNotificationModal(msg)); + }, + [dispatch], + ); + + const isTSSWallet = isTSSKey(key); const [showTSSProgressModal, setShowTSSProgressModal] = useState(false); const [tssStatus, setTssStatus] = useState('initializing'); const [tssProgress, setTssProgress] = useState({ @@ -251,6 +258,17 @@ const TransactionProposalDetails = () => { Array<{id: string; name: string; signed: boolean}> >([]); + const tssCallbacks = useTSSCallbacks({ + wallet, + setTssStatus, + setTssProgress, + setTssCopayers, + tssCopayers, + setShowTSSProgressModal, + setResetSwipeButton, + showErrorMessage, + }); + const title = getDetailsTitle(transaction, wallet); let {currencyAbbreviation, chain, network, tokenAddress} = wallet; currencyAbbreviation = currencyAbbreviation.toLowerCase(); @@ -263,27 +281,6 @@ const TransactionProposalDetails = () => { }); }, [navigation, title]); - useEffect(() => { - if (key) { - const isTss = isTSSKey(key); - setIsTSSWallet(isTss); - - if (isTss) { - logManager.debug(`[TxpDetails] Is TSS wallet: ${isTss}`); - const copayersList = - wallet.copayers?.map(copayer => ({ - id: copayer.id, - name: copayer.name, - signed: false, - })) || []; - setTssCopayers(copayersList); - logManager.debug( - `[TxpDetails] Initialized with ${copayersList.length} copayers`, - ); - } - } - }, [key, wallet]); - const init = async () => { try { if (!transaction) { @@ -522,56 +519,6 @@ const TransactionProposalDetails = () => { } }; - const tssCallbacks: TSSSigningCallbacks = { - onStatusChange: (status: TSSSigningStatus) => { - logManager.debug(`[TxpDetails TSS] Status changed: ${status}`); - setTssStatus(status); - }, - onProgressUpdate: (progress: TSSSigningProgress) => { - logManager.debug( - `[TxpDetails TSS] Progress: Round ${progress.currentRound}/${progress.totalRounds}`, - ); - setTssProgress(progress); - - // When round 1 starts, mark all copayers as joined/signing - // TODO remove this when onCopayerStatusChange is added - if (progress.currentRound === 1) { - setTssCopayers(prev => prev.map(c => ({...c, signed: true}))); - } - }, - onCopayerStatusChange: ( - copayerId: string, - status: TSSCopayerSignStatus, - ) => { - // This will never fire - keeping for future when event exist - logManager.debug(`[TxpDetails TSS] Copayer ${copayerId} ${status}`); - setTssCopayers(prev => - prev.map(c => - c.id === copayerId ? {...c, signed: status === 'signed'} : c, - ), - ); - }, - onRoundUpdate: ( - round: number, - type: 'ready' | 'processed' | 'submitted', - ) => { - logManager.debug(`[TxpDetails TSS] Round ${round} ${type}`); - }, - onError: (error: Error) => { - logManager.error(`[TxpDetails TSS] Error: ${error.message}`); - setShowTSSProgressModal(false); - setResetSwipeButton(true); - showErrorMessage( - CustomErrorMessage({ - errMsg: error.message, - title: t('TSS Signing Error'), - }), - ); - }, - onComplete: (signature: string) => { - logManager.debug(`[TxpDetails TSS] Signing complete`); - }, - }; const joinTSSSigning = async () => { try { logManager.debug( @@ -712,15 +659,6 @@ const TransactionProposalDetails = () => { } }; }, []); - - const showErrorMessage = useCallback( - async (msg: BottomNotificationConfig) => { - await sleep(500); - dispatch(showBottomNotificationModal(msg)); - }, - [dispatch], - ); - return ( {isLoading ? ( @@ -842,7 +780,9 @@ const TransactionProposalDetails = () => { progress={tssProgress} createdBy={wallet.walletName || 'You'} date={new Date()} + wallet={wallet} copayers={tssCopayers} + onCopayersInitialized={setTssCopayers} isModalVisible={showTSSProgressModal} onModalVisibilityChange={setShowTSSProgressModal} /> diff --git a/src/navigation/wallet/screens/send/confirm/Confirm.tsx b/src/navigation/wallet/screens/send/confirm/Confirm.tsx index ddabbbe0b8..26a4ac8266 100644 --- a/src/navigation/wallet/screens/send/confirm/Confirm.tsx +++ b/src/navigation/wallet/screens/send/confirm/Confirm.tsx @@ -23,7 +23,6 @@ import { Wallet, TSSSigningStatus, TSSSigningProgress, - TSSCopayerSignStatus, } from '../../../../../store/wallet/wallet.models'; import SwipeButton from '../../../../../components/swipe-button/SwipeButton'; import { @@ -32,10 +31,7 @@ import { showConfirmAmountInfoSheet, startSendPayment, } from '../../../../../store/wallet/effects/send/send'; -import { - isTSSKey, - TSSSigningCallbacks, -} from '../../../../../store/wallet/effects/tss-send/tss-send'; +import {isTSSKey} from '../../../../../store/wallet/effects/tss-send/tss-send'; import { formatCurrencyAbbreviation, formatFiatAmount, @@ -118,10 +114,8 @@ import {CommonActions} from '@react-navigation/native'; import {TabsScreens} from '../../../../tabs/TabsStack'; import {RootStacks} from '../../../../../Root'; import {useOngoingProcess, usePaymentSent} from '../../../../../contexts'; -import {logManager} from '../../../../../managers/LogManager'; -import TSSProgressTracker, { - TSSCopayer, -} from '../../../components/TSSProgressTracker'; +import TSSProgressTracker from '../../../components/TSSProgressTracker'; +import {useTSSCallbacks} from '../../../../../utils/hooks/useTSSCalbacks'; const VerticalPadding = styled.View` padding: ${ScreenGutter} 0; @@ -207,7 +201,15 @@ const Confirm = () => { useState(null); const [showTSSProgressModal, setShowTSSProgressModal] = useState(false); - const [isTSSWallet, setIsTSSWallet] = useState(false); + const showErrorMessage = useCallback( + async (msg: BottomNotificationConfig) => { + await sleep(500); + dispatch(showBottomNotificationModal(msg)); + }, + [dispatch], + ); + + const isTSSWallet = isTSSKey(key); const [tssStatus, setTssStatus] = useState('initializing'); const [tssProgress, setTssProgress] = useState({ currentRound: 0, @@ -217,9 +219,16 @@ const Confirm = () => { const [tssCopayers, setTssCopayers] = useState< Array<{id: string; name: string; signed: boolean}> >([]); - const [tssTransactionId, setTssTransactionId] = useState< - string | undefined - >(); + const tssCallbacks = useTSSCallbacks({ + wallet, + setTssStatus, + setTssProgress, + setTssCopayers, + tssCopayers, + setShowTSSProgressModal, + setResetSwipeButton, + showErrorMessage, + }); const { fee: _fee, @@ -249,25 +258,6 @@ const Confirm = () => { const {unitToSatoshi} = dispatch(GetPrecision(currencyAbbreviation, chain, tokenAddress)) || {}; - useEffect(() => { - if (key && wallet) { - const isTss = isTSSKey(key); - setIsTSSWallet(isTss); - if (isTss) { - const copayersList = - wallet.copayers?.map(copayer => ({ - id: copayer.id, - name: copayer.name, - signed: false, - })) || []; - setTssCopayers(copayersList); - logManager.debug( - `[TSS Confirm] Initialized with ${copayersList.length} copayers`, - ); - } - } - }, [key, wallet]); - useLayoutEffect(() => { navigation.setOptions({ headerTitle: () => ( @@ -447,56 +437,6 @@ const Confirm = () => { hidePaymentSent(); }; - const tssCallbacks: TSSSigningCallbacks = { - onStatusChange: (status: TSSSigningStatus) => { - logManager.debug(`[TSS Confirm] Status changed: ${status}`); - setTssStatus(status); - }, - onProgressUpdate: (progress: TSSSigningProgress) => { - logManager.debug( - `[TSS Confirm] Progress: Round ${progress.currentRound}/${progress.totalRounds}`, - ); - setTssProgress(progress); - - // When round 1 starts, mark all copayers as joined/signing - // TODO remove this when onCopayerStatusChange is added - if (progress.currentRound === 1) { - setTssCopayers(prev => prev.map(c => ({...c, signed: true}))); - } - }, - onCopayerStatusChange: ( - copayerId: string, - status: TSSCopayerSignStatus, - ) => { - // This will never fire - keeping for future when event exist - logManager.debug(`[TSS Confirm] Copayer ${copayerId} ${status}`); - setTssCopayers(prev => - prev.map(c => - c.id === copayerId ? {...c, signed: status === 'signed'} : c, - ), - ); - }, - onRoundUpdate: ( - round: number, - type: 'ready' | 'processed' | 'submitted', - ) => { - logManager.debug(`[TSS Confirm] Round ${round} ${type}`); - }, - onError: (error: Error) => { - logManager.error(`[TSS Confirm] Error: ${error.message}`); - setShowTSSProgressModal(false); - setResetSwipeButton(true); - showErrorMessage( - CustomErrorMessage({ - errMsg: error.message, - title: t('TSS Signing Error'), - }), - ); - }, - onComplete: (signature: string) => { - logManager.debug(`[TSS Confirm] Signing complete`); - }, - }; const startSendingPayment = async ({ transport, }: {transport?: Transport} = {}) => { @@ -549,7 +489,6 @@ const Confirm = () => { ); if (isTSSWallet && result?.txid) { - setTssTransactionId(result.txid); setTssStatus('complete'); await sleep(1500); setShowTSSProgressModal(false); @@ -617,7 +556,6 @@ const Confirm = () => { if (isTSSWallet) { setShowTSSProgressModal(false); } - if (isUsingHardwareWallet) { setConfirmHardwareWalletVisible(false); setConfirmHardwareState(null); @@ -696,14 +634,6 @@ const Confirm = () => { return () => clearTimeout(timer); }, [resetSwipeButton]); - const showErrorMessage = useCallback( - async (msg: BottomNotificationConfig) => { - await sleep(500); - dispatch(showBottomNotificationModal(msg)); - }, - [dispatch], - ); - const checkHighFees = async () => { const {feeUnitAmount} = GetFeeUnits(chain); let feePerKb: number; @@ -795,7 +725,9 @@ const Confirm = () => { progress={tssProgress} createdBy={sendingFrom.walletName || 'You'} date={new Date()} + wallet={wallet} copayers={tssCopayers} + onCopayersInitialized={setTssCopayers} isModalVisible={showTSSProgressModal} onModalVisibilityChange={setShowTSSProgressModal} /> diff --git a/src/utils/hooks/useTSSCalbacks.ts b/src/utils/hooks/useTSSCalbacks.ts new file mode 100644 index 0000000000..6634d608cf --- /dev/null +++ b/src/utils/hooks/useTSSCalbacks.ts @@ -0,0 +1,119 @@ +import React, {useCallback} from 'react'; +import {useTranslation} from 'react-i18next'; + +import { + TSSCopayerSignStatus, + TSSSigningProgress, + TSSSigningStatus, + Wallet, +} from '../../store/wallet/wallet.models'; +import {BottomNotificationConfig} from '../../components/modal/bottom-notification/BottomNotification'; +import {TSSSigningCallbacks} from '../../store/wallet/effects/tss-send/tss-send'; +import {logManager} from '../../managers/LogManager'; +import {CustomErrorMessage} from '../../navigation/wallet/components/ErrorMessages'; + +interface UseTSSCallbacksParams { + wallet: Wallet; + setTssStatus: (status: TSSSigningStatus) => void; + setTssProgress: (progress: TSSSigningProgress) => void; + setTssCopayers: any; + tssCopayers: Array<{id: string; name: string; signed: boolean}>; + setShowTSSProgressModal: (show: boolean) => void; + setResetSwipeButton: (reset: boolean) => void; + showErrorMessage: (msg: BottomNotificationConfig) => Promise; +} + +export const useTSSCallbacks = ({ + wallet, + setTssStatus, + setTssProgress, + setTssCopayers, + tssCopayers, + setShowTSSProgressModal, + setResetSwipeButton, + showErrorMessage, +}: UseTSSCallbacksParams): TSSSigningCallbacks => { + const {t} = useTranslation(); + + const onStatusChange = useCallback( + (status: TSSSigningStatus) => { + logManager.debug(`[TSS Callbacks] Status changed: ${status}`); + setTssStatus(status); + }, + [setTssStatus], + ); + + const onProgressUpdate = useCallback( + (progress: TSSSigningProgress) => { + logManager.debug( + `[TSS Callbacks] Progress: Round ${progress.currentRound}/${progress.totalRounds}`, + ); + setTssProgress(progress); + + // When round 1 starts, mark all copayers as joined/signing + // TODO remove this when onCopayerStatusChange is properly implemented + if (progress.currentRound === 1) { + setTssCopayers(prev => { + // Only update if copayers aren't already signed + const allSigned = prev.every(c => c.signed); + if (!allSigned) { + logManager.debug( + `[TSS Callbacks] Marking all ${prev.length} copayers as signed`, + ); + return prev.map(c => ({...c, signed: true})); + } + return prev; + }); + } + }, + [setTssProgress, setTssCopayers], + ); + + const onCopayerStatusChange = useCallback( + (copayerId: string, status: TSSCopayerSignStatus) => { + // This will never fire - keeping for future when event exists + logManager.debug(`[TSS Callbacks] Copayer ${copayerId} ${status}`); + setTssCopayers(prev => + prev.map(c => + c.id === copayerId ? {...c, signed: status === 'signed'} : c, + ), + ); + }, + [setTssCopayers], + ); + + const onRoundUpdate = useCallback( + (round: number, type: 'ready' | 'processed' | 'submitted') => { + logManager.debug(`[TSS Callbacks] Round ${round} ${type}`); + }, + [], + ); + + const onError = useCallback( + (error: Error) => { + logManager.error(`[TSS Callbacks] Error: ${error.message}`); + setShowTSSProgressModal(false); + setResetSwipeButton(true); + showErrorMessage( + CustomErrorMessage({ + errMsg: error.message, + title: t('TSS Signing Error'), + }), + ); + }, + [setShowTSSProgressModal, setResetSwipeButton, showErrorMessage, t], + ); + + const onComplete = useCallback((signature: string) => { + logManager.debug(`[TSS Callbacks] Signing complete`); + }, []); + + return { + onStatusChange, + onProgressUpdate, + onCopayerStatusChange, + onRoundUpdate, + onError, + onComplete, + }; +}; From 72f9b969720fb79b6ecc60e5690c06f85877ebe8 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 8 Jan 2026 11:47:36 -0300 Subject: [PATCH 09/16] [FEAT] support signing tss proposals in proposal notifications view --- .../TransactionProposalNotifications.tsx | 209 ++++++++++++++---- 1 file changed, 165 insertions(+), 44 deletions(-) diff --git a/src/navigation/wallet/screens/TransactionProposalNotifications.tsx b/src/navigation/wallet/screens/TransactionProposalNotifications.tsx index d586ee68b2..d3bd88e227 100644 --- a/src/navigation/wallet/screens/TransactionProposalNotifications.tsx +++ b/src/navigation/wallet/screens/TransactionProposalNotifications.tsx @@ -33,6 +33,8 @@ import { Key, TransactionProposal, Wallet, + TSSSigningStatus, + TSSSigningProgress, } from '../../../store/wallet/wallet.models'; import {RefreshControl, SectionList, View} from 'react-native'; import TransactionProposalRow from '../../../components/list/TransactionProposalRow'; @@ -62,8 +64,13 @@ import {Analytics} from '../../../store/analytics/analytics.effects'; import {TransactionIcons} from '../../../constants/TransactionIcons'; import {TouchableOpacity} from '@components/base/TouchableOpacity'; import haptic from '../../../components/haptic-feedback/haptic'; -import {AppActions} from '../../../store/app'; -import {useOngoingProcess, usePaymentSent} from '../../../contexts'; +import {usePaymentSent} from '../../../contexts'; +import { + isTSSKey, + joinTSSSigningSession, +} from '../../../store/wallet/effects/tss-send/tss-send'; +import TSSProgressTracker from '../components/TSSProgressTracker'; +import {useTSSCallbacks} from '../../../utils/hooks/useTSSCalbacks'; const NotificationsContainer = styled.SafeAreaView` flex: 1; @@ -148,6 +155,54 @@ const TransactionProposalNotifications = () => { const [selectAll, setSelectAll] = useState(false); const {showPaymentSent, hidePaymentSent} = usePaymentSent(); + const [showTSSProgressModal, setShowTSSProgressModal] = useState(false); + const [tssStatus, setTssStatus] = useState('initializing'); + const [tssProgress, setTssProgress] = useState({ + currentRound: 0, + totalRounds: 4, + status: 'pending', + }); + const [tssCopayers, setTssCopayers] = useState< + Array<{id: string; name: string; signed: boolean}> + >([]); + + const showErrorMessage = useCallback( + async (msg: BottomNotificationConfig) => { + await sleep(500); + dispatch(showBottomNotificationModal(msg)); + }, + [dispatch], + ); + + const currentWallet = useMemo(() => { + if (selectingProposalsWalletId) { + return findWalletById(wallets, selectingProposalsWalletId) as Wallet; + } + return null; + }, [selectingProposalsWalletId, wallets]); + + const currentKey = useMemo(() => { + if (currentWallet) { + return keys[currentWallet.keyId]; + } + return null; + }, [currentWallet, keys]); + + const isTSSWallet = useMemo(() => { + return currentKey ? isTSSKey(currentKey) : false; + }, [currentKey]); + + const tssCallbacks = useTSSCallbacks({ + wallet: currentWallet!, + setTssStatus, + setTssProgress, + setTssCopayers, + tssCopayers, + setShowTSSProgressModal, + setResetSwipeButton, + showErrorMessage, + }); + let pendingTxps: TransactionProposal[] = wallets.flatMap(w => w.pendingTxps); if (walletId) { @@ -352,32 +407,43 @@ const TransactionProposalNotifications = () => { setSelectingProposalsWalletId(walletId); } + const wallet = findWalletById(wallets, walletId) as Wallet; + const key = keys[wallet.keyId]; + const isTSS = isTSSKey(key); + if (_.indexOf(txpsToSign, txp) >= 0) { _.remove(txpsToSign, txpToSign => { return txpToSign.id === txp.id; }); _txpChecked[txp.id] = false; } else { + // For TSS wallets, only allow one transaction at a time + if (isTSS) { + setTxpsToSign([]); + setTxpChecked({}); + _txpChecked = {}; + } + _txpChecked[txp.id] = true; _txpsToSign.push(txp); } - selectingFromAnotherWallet + selectingFromAnotherWallet || isTSS ? setTxpsToSign(_txpsToSign) : setTxpsToSign(txpsToSign.concat(_txpsToSign)); - selectingFromAnotherWallet + selectingFromAnotherWallet || isTSS ? setTxpChecked(_txpChecked) : setTxpChecked({...txpChecked, ..._txpChecked}); setSelectAll(false); }, - [setTxpsToSign, setTxpChecked, setSelectAll, txpsToSign, txpChecked], - ); - - const showErrorMessage = useCallback( - async (msg: BottomNotificationConfig) => { - await sleep(500); - dispatch(showBottomNotificationModal(msg)); - }, - [dispatch], + [ + setTxpsToSign, + setTxpChecked, + setSelectAll, + txpsToSign, + txpChecked, + wallets, + keys, + ], ); const renderTxpByWallet = useCallback( @@ -391,6 +457,10 @@ const TransactionProposalNotifications = () => { keyId, credentials: {walletName, m, n, walletId: _walletId}, } = fullWalletObj; + + const key = keys[fullWalletObj.keyId]; + const isTSS = isTSSKey(key); + return ( <> @@ -409,7 +479,7 @@ const TransactionProposalNotifications = () => { {keyId.includes('readonly') ? '- Read Only' : null} - {item.needSign && item.txps.length > 1 ? ( + {item.needSign && item.txps.length > 1 && !isTSS ? ( { haptic('impactLight'); @@ -463,6 +533,7 @@ const TransactionProposalNotifications = () => { }, [ wallets, + keys, selectingProposalsWalletId, txpChecked, txpSelectionChange, @@ -603,54 +674,90 @@ const TransactionProposalNotifications = () => { selectingProposalsWalletId, ) as Wallet; const key = keys[wallet.keyId]; - const data = (await dispatch( - publishAndSignMultipleProposals({ - txps: Object.values(txpsToSign), - key, - wallet, - }), - )) as (TransactionProposal | Error)[]; - const count = countSuccessAndFailed(data); - if (count.failed > 0) { - const errMsgs = [ - `There was problem while trying to sign ${count.failed} of your transactions proposals. Please, try again`, - ]; - data.forEach((element, index) => { - if (element instanceof Error) { - errMsgs.push( - `[ERROR ${index + 1}] ${BWCErrorMessage(element)}`, - ); - } - }); - await showErrorMessage( - CustomErrorMessage({ - errMsg: errMsgs.join('\n\n'), - title: t('Uh oh, something went wrong'), + + if (isTSSKey(key)) { + setShowTSSProgressModal(true); + setTssStatus('initializing'); + + const txp = txpsToSign[0]; + + await dispatch( + joinTSSSigningSession({ + key, + wallet, + txp, + callbacks: tssCallbacks, }), ); - } - if (count.success > 0) { + setTssStatus('complete'); + await sleep(1500); + setShowTSSProgressModal(false); + dispatch( Analytics.track('Sent Crypto', { context: 'Transaction Proposal Notifications', coin: wallet.currencyAbbreviation || '', }), ); - const title = - count.success > 1 - ? t('proposals signed', {sucess: count.success}) - : t('Proposal signed'); + showPaymentSent({ onCloseModal, - title, + title: t('Proposal signed'), }); + } else { + const data = (await dispatch( + publishAndSignMultipleProposals({ + txps: Object.values(txpsToSign), + key, + wallet, + }), + )) as (TransactionProposal | Error)[]; + const count = countSuccessAndFailed(data); + if (count.failed > 0) { + const errMsgs = [ + `There was problem while trying to sign ${count.failed} of your transactions proposals. Please, try again`, + ]; + data.forEach((element, index) => { + if (element instanceof Error) { + errMsgs.push( + `[ERROR ${index + 1}] ${BWCErrorMessage(element)}`, + ); + } + }); + await showErrorMessage( + CustomErrorMessage({ + errMsg: errMsgs.join('\n\n'), + title: t('Uh oh, something went wrong'), + }), + ); + } + + if (count.success > 0) { + dispatch( + Analytics.track('Sent Crypto', { + context: 'Transaction Proposal Notifications', + coin: wallet.currencyAbbreviation || '', + }), + ); + const title = + count.success > 1 + ? t('proposals signed', {sucess: count.success}) + : t('Proposal signed'); + showPaymentSent({ + onCloseModal, + title, + }); + } } setSelectingProposalsWalletId(''); setTxpsToSign([]); setTxpChecked({}); setResetSwipeButton(true); } catch (err) { + if (isTSSWallet) { + setShowTSSProgressModal(false); + } await sleep(500); setResetSwipeButton(true); switch (err) { @@ -671,6 +778,20 @@ const TransactionProposalNotifications = () => { }} /> ) : null} + + {isTSSWallet && currentWallet && showTSSProgressModal ? ( + + ) : null} ); }; From cbb3377265b9ce38bbe971e735838ad33026e6d8 Mon Sep 17 00:00:00 2001 From: Gabriel Masclef Date: Thu, 8 Jan 2026 12:03:04 -0300 Subject: [PATCH 10/16] Feat: TSS integration in swap crypto --- .../swap-crypto/screens/ChangellyCheckout.tsx | 74 ++++++++++++++++++- .../swap-crypto/screens/ThorswapCheckout.tsx | 69 +++++++++++++++++ src/store/swap-crypto/swap-crypto.models.ts | 2 + 3 files changed, 143 insertions(+), 2 deletions(-) diff --git a/src/navigation/services/swap-crypto/screens/ChangellyCheckout.tsx b/src/navigation/services/swap-crypto/screens/ChangellyCheckout.tsx index 46e4a672d1..8f5ad10ce4 100644 --- a/src/navigation/services/swap-crypto/screens/ChangellyCheckout.tsx +++ b/src/navigation/services/swap-crypto/screens/ChangellyCheckout.tsx @@ -30,6 +30,8 @@ import { TransactionProposal, SendMaxInfo, Key, + TSSSigningStatus, + TSSSigningProgress, } from '../../../../store/wallet/wallet.models'; import {createWalletAddress} from '../../../../store/wallet/effects/address/address'; import { @@ -113,6 +115,11 @@ import TransportBLE from '@ledgerhq/react-native-hw-transport-ble'; import TransportHID from '@ledgerhq/react-native-hid'; import {LISTEN_TIMEOUT, OPEN_TIMEOUT} from '../../../../constants/config'; import {useOngoingProcess, usePaymentSent} from '../../../../contexts'; +import TSSProgressTracker from '../../../wallet/components/TSSProgressTracker'; +import {isTSSKey} from '../../../../store/wallet/effects/tss-send/tss-send'; +import {useTSSCallbacks} from '../../../../utils/hooks/useTSSCalbacks'; +import {Network} from '../../../../constants'; +import {BottomNotificationConfig} from '../../../../components/modal/bottom-notification/BottomNotification'; // Styled export const SwapCheckoutContainer = styled.SafeAreaView` @@ -178,6 +185,38 @@ const ChangellyCheckout: React.FC = () => { const [confirmHardwareState, setConfirmHardwareState] = useState(null); + const [showTSSProgressModal, setShowTSSProgressModal] = useState(false); + const showTssErrorMessage = useCallback( + async (config: BottomNotificationConfig) => { + const msg = config?.message || t('An error occurred during TSS signing'); + const reason = 'TSS Signing Error'; + const title = config?.title || t('TSS Signing Error'); + showError(msg, reason, undefined, title); + }, + [dispatch], + ); + const isTSSWallet = isTSSKey(key); + const [tssStatus, setTssStatus] = useState('initializing'); + const [tssProgress, setTssProgress] = useState({ + currentRound: 0, + totalRounds: 4, + status: 'pending', + }); + const [tssCopayers, setTssCopayers] = useState< + Array<{id: string; name: string; signed: boolean}> + >([]); + + const tssCallbacks = useTSSCallbacks({ + wallet: fromWalletSelected, + setTssStatus, + setTssProgress, + setTssCopayers, + tssCopayers, + setShowTSSProgressModal, + setResetSwipeButton, + showErrorMessage: showTssErrorMessage, + }); + const {showPaymentSent, hidePaymentSent} = usePaymentSent(); const {showOngoingProcess, hideOngoingProcess} = useOngoingProcess(); @@ -694,6 +733,12 @@ const ChangellyCheckout: React.FC = () => { const makePayment = async ({transport}: {transport?: Transport}) => { const isUsingHardwareWallet = !!transport; + + if (isTSSWallet) { + setShowTSSProgressModal(true); + setTssStatus('initializing'); + } + try { if (isUsingHardwareWallet) { const {chain, network} = fromWalletSelected.credentials; @@ -701,7 +746,7 @@ const ChangellyCheckout: React.FC = () => { if (!configFn) { throw new Error(`Unsupported currency: ${chain.toUpperCase()}`); } - const params = configFn(network); + const params = configFn(network as Network); await prepareLedgerApp( params.appName, transportRef, @@ -724,14 +769,21 @@ const ChangellyCheckout: React.FC = () => { await sleep(1000); setConfirmHardwareWalletVisible(false); } else { - await dispatch( + const broadcastedTx = await dispatch( publishAndSign({ txp: ctxp! as TransactionProposal, key, wallet: fromWalletSelected, ataOwnerAddress, + ...(isTSSWallet && {tssCallbacks}), }), ); + + if (isTSSWallet && broadcastedTx?.txid) { + setTssStatus('complete'); + await sleep(1500); + setShowTSSProgressModal(false); + } } saveChangellyTx(); @@ -759,6 +811,10 @@ const ChangellyCheckout: React.FC = () => { }), ); } catch (err: any) { + if (isTSSWallet) { + setShowTSSProgressModal(false); + } + if (isUsingHardwareWallet) { setConfirmHardwareWalletVisible(false); setConfirmHardwareState(null); @@ -854,6 +910,7 @@ const ChangellyCheckout: React.FC = () => { payinExtraId: txData.payinExtraId, totalExchangeFee: totalExchangeFee!, status: txData.status, + isTSSWallet: isTSSWallet, }; dispatch( @@ -989,6 +1046,19 @@ const ChangellyCheckout: React.FC = () => {
{t('SUMMARY')}
+ {isTSSWallet && ( + + )} {t('Swapping')} diff --git a/src/navigation/services/swap-crypto/screens/ThorswapCheckout.tsx b/src/navigation/services/swap-crypto/screens/ThorswapCheckout.tsx index e4334b8340..3455a2c383 100644 --- a/src/navigation/services/swap-crypto/screens/ThorswapCheckout.tsx +++ b/src/navigation/services/swap-crypto/screens/ThorswapCheckout.tsx @@ -33,6 +33,8 @@ import { SendMaxInfo, Key, TransactionProposalOutputs, + TSSSigningStatus, + TSSSigningProgress, } from '../../../../store/wallet/wallet.models'; import {createWalletAddress} from '../../../../store/wallet/effects/address/address'; import { @@ -134,6 +136,10 @@ import { import {ExchangeConfig} from '../../../../store/external-services/external-services.types'; import {useOngoingProcess, usePaymentSent} from '../../../../contexts'; import {Network} from '../../../../constants'; +import TSSProgressTracker from '../../../wallet/components/TSSProgressTracker'; +import {isTSSKey} from '../../../../store/wallet/effects/tss-send/tss-send'; +import {useTSSCallbacks} from '../../../../utils/hooks/useTSSCalbacks'; +import {BottomNotificationConfig} from '../../../../components/modal/bottom-notification/BottomNotification'; // Styled export const SwapCheckoutContainer = styled.SafeAreaView` @@ -205,6 +211,38 @@ const ThorswapCheckout: React.FC = () => { const [confirmHardwareState, setConfirmHardwareState] = useState(null); + const [showTSSProgressModal, setShowTSSProgressModal] = useState(false); + const showTssErrorMessage = useCallback( + async (config: BottomNotificationConfig) => { + const msg = config?.message || t('An error occurred during TSS signing'); + const reason = 'TSS Signing Error'; + const title = config?.title || t('TSS Signing Error'); + showError(msg, reason, undefined, title); + }, + [dispatch], + ); + const isTSSWallet = isTSSKey(key); + const [tssStatus, setTssStatus] = useState('initializing'); + const [tssProgress, setTssProgress] = useState({ + currentRound: 0, + totalRounds: 4, + status: 'pending', + }); + const [tssCopayers, setTssCopayers] = useState< + Array<{id: string; name: string; signed: boolean}> + >([]); + + const tssCallbacks = useTSSCallbacks({ + wallet: fromWalletSelected, + setTssStatus, + setTssProgress, + setTssCopayers, + tssCopayers, + setShowTSSProgressModal, + setResetSwipeButton, + showErrorMessage: showTssErrorMessage, + }); + const {showPaymentSent, hidePaymentSent} = usePaymentSent(); const {showOngoingProcess, hideOngoingProcess} = useOngoingProcess(); @@ -828,6 +866,12 @@ const ThorswapCheckout: React.FC = () => { const makePayment = async ({transport}: {transport?: Transport}) => { const isUsingHardwareWallet = !!transport; let broadcastedTx; + + if (isTSSWallet) { + setShowTSSProgressModal(true); + setTssStatus('initializing'); + } + try { if (isUsingHardwareWallet) { const {coin, network} = fromWalletSelected.credentials; @@ -862,8 +906,15 @@ const ThorswapCheckout: React.FC = () => { txp: ctxp! as TransactionProposal, key, wallet: fromWalletSelected, + ...(isTSSWallet && {tssCallbacks}), }), ); + + if (isTSSWallet && broadcastedTx?.txid) { + setTssStatus('complete'); + await sleep(1500); + setShowTSSProgressModal(false); + } } const reqData: ThorswapGetSwapTxRequestData = { @@ -904,6 +955,10 @@ const ThorswapCheckout: React.FC = () => { }), ); } catch (err) { + if (isTSSWallet) { + setShowTSSProgressModal(false); + } + if (isUsingHardwareWallet) { setConfirmHardwareWalletVisible(false); setConfirmHardwareState(null); @@ -1005,6 +1060,7 @@ const ThorswapCheckout: React.FC = () => { spenderKey: spenderKey!, slippage: slippage, status: newStatus, + isTSSWallet: isTSSWallet, }; dispatch( @@ -1140,6 +1196,19 @@ const ThorswapCheckout: React.FC = () => {
{t('SUMMARY')}
+ {isTSSWallet && ( + + )} {t('Swapping')} diff --git a/src/store/swap-crypto/swap-crypto.models.ts b/src/store/swap-crypto/swap-crypto.models.ts index 0edafc419d..6be65e4446 100644 --- a/src/store/swap-crypto/swap-crypto.models.ts +++ b/src/store/swap-crypto/swap-crypto.models.ts @@ -23,6 +23,7 @@ export interface changellyTxData { payinExtraId?: string; totalExchangeFee: number; status: string; + isTSSWallet?: boolean; error?: any; env?: 'dev' | 'prod'; } @@ -46,6 +47,7 @@ export interface thorswapTxData { spenderKey: ThorswapProvider; slippage?: number; status: ThorswapTrackingStatus; + isTSSWallet?: boolean; error?: any; env?: 'dev' | 'prod'; } From 33e5b025f08ee78d8ea17061c91621c91f97b89d Mon Sep 17 00:00:00 2001 From: Gabriel Masclef Date: Thu, 8 Jan 2026 12:05:41 -0300 Subject: [PATCH 11/16] Feat: TSS integration in sell crypto --- .../screens/MoonpaySellCheckout.tsx | 69 ++++++++++++++++++ .../sell-crypto/screens/RampSellCheckout.tsx | 70 +++++++++++++++++- .../screens/SimplexSellCheckout.tsx | 71 ++++++++++++++++++- .../sell-crypto/models/moonpay-sell.models.ts | 2 + .../sell-crypto/models/ramp-sell.models.ts | 2 + .../sell-crypto/models/simplex-sell.models.ts | 1 + src/store/sell-crypto/sell-crypto.reducer.ts | 7 ++ 7 files changed, 220 insertions(+), 2 deletions(-) diff --git a/src/navigation/services/sell-crypto/screens/MoonpaySellCheckout.tsx b/src/navigation/services/sell-crypto/screens/MoonpaySellCheckout.tsx index e4457bae0a..e32514586c 100644 --- a/src/navigation/services/sell-crypto/screens/MoonpaySellCheckout.tsx +++ b/src/navigation/services/sell-crypto/screens/MoonpaySellCheckout.tsx @@ -31,6 +31,8 @@ import { TransactionProposal, SendMaxInfo, Key, + TSSSigningStatus, + TSSSigningProgress, } from '../../../../store/wallet/wallet.models'; import { GetName, @@ -118,7 +120,11 @@ import TransportBLE from '@ledgerhq/react-native-hw-transport-ble'; import TransportHID from '@ledgerhq/react-native-hid'; import {LISTEN_TIMEOUT, OPEN_TIMEOUT} from '../../../../constants/config'; import {useOngoingProcess, usePaymentSent} from '../../../../contexts'; +import TSSProgressTracker from '../../../wallet/components/TSSProgressTracker'; +import {isTSSKey} from '../../../../store/wallet/effects/tss-send/tss-send'; +import {useTSSCallbacks} from '../../../../utils/hooks/useTSSCalbacks'; import {Network} from '../../../../constants'; +import {BottomNotificationConfig} from '../../../../components/modal/bottom-notification/BottomNotification'; // Styled export const SellCheckoutContainer = styled.SafeAreaView` @@ -188,6 +194,38 @@ const MoonpaySellCheckout: React.FC = () => { const [confirmHardwareState, setConfirmHardwareState] = useState(null); + const [showTSSProgressModal, setShowTSSProgressModal] = useState(false); + const showTssErrorMessage = useCallback( + async (config: BottomNotificationConfig) => { + const msg = config?.message || t('An error occurred during TSS signing'); + const reason = 'TSS Signing Error'; + const title = config?.title || t('TSS Signing Error'); + showError(msg, reason, undefined, title); + }, + [dispatch], + ); + const isTSSWallet = isTSSKey(key); + const [tssStatus, setTssStatus] = useState('initializing'); + const [tssProgress, setTssProgress] = useState({ + currentRound: 0, + totalRounds: 4, + status: 'pending', + }); + const [tssCopayers, setTssCopayers] = useState< + Array<{id: string; name: string; signed: boolean}> + >([]); + + const tssCallbacks = useTSSCallbacks({ + wallet, + setTssStatus, + setTssProgress, + setTssCopayers, + tssCopayers, + setShowTSSProgressModal, + setResetSwipeButton, + showErrorMessage: showTssErrorMessage, + }); + const {showPaymentSent, hidePaymentSent} = usePaymentSent(); const {showOngoingProcess, hideOngoingProcess} = useOngoingProcess(); @@ -577,6 +615,12 @@ const MoonpaySellCheckout: React.FC = () => { const makePayment = async ({transport}: {transport?: Transport}) => { const isUsingHardwareWallet = !!transport; let broadcastedTx; + + if (isTSSWallet) { + setShowTSSProgressModal(true); + setTssStatus('initializing'); + } + try { if (isUsingHardwareWallet) { const {chain, network} = wallet.credentials; @@ -613,8 +657,15 @@ const MoonpaySellCheckout: React.FC = () => { key, wallet, ataOwnerAddress, + ...(isTSSWallet && {tssCallbacks}), }), ); + + if (isTSSWallet && broadcastedTx?.txid) { + setTssStatus('complete'); + await sleep(1500); + setShowTSSProgressModal(false); + } } updateMoonpayTx(txData!, broadcastedTx as Partial); showPaymentSent({ @@ -651,6 +702,10 @@ const MoonpaySellCheckout: React.FC = () => { }), ); } catch (err: any) { + if (isTSSWallet) { + setShowTSSProgressModal(false); + } + if (isUsingHardwareWallet) { setConfirmHardwareWalletVisible(false); setConfirmHardwareState(null); @@ -747,6 +802,7 @@ const MoonpaySellCheckout: React.FC = () => { moonpayTxData.quoteCurrency.code, ).toUpperCase(), totalFee: totalExchangeFee, + isTSSWallet: isTSSWallet, }; dispatch( @@ -970,6 +1026,19 @@ const MoonpaySellCheckout: React.FC = () => {
{t('SUMMARY')}
+ {isTSSWallet && ( + + )} {t('Selling')} {amountExpected ? ( diff --git a/src/navigation/services/sell-crypto/screens/RampSellCheckout.tsx b/src/navigation/services/sell-crypto/screens/RampSellCheckout.tsx index 0acc4dbd80..76277dcd08 100644 --- a/src/navigation/services/sell-crypto/screens/RampSellCheckout.tsx +++ b/src/navigation/services/sell-crypto/screens/RampSellCheckout.tsx @@ -31,6 +31,8 @@ import { TransactionProposal, SendMaxInfo, Key, + TSSSigningStatus, + TSSSigningProgress, } from '../../../../store/wallet/wallet.models'; import { GetName, @@ -119,6 +121,11 @@ import {LISTEN_TIMEOUT, OPEN_TIMEOUT} from '../../../../constants/config'; import {rampGetSellTransactionDetails} from '../../../../store/buy-crypto/effects/ramp/ramp'; import {useOngoingProcess, usePaymentSent} from '../../../../contexts'; import {SellCryptoOffer} from '../../components/externalServicesOfferSelector'; +import TSSProgressTracker from '../../../wallet/components/TSSProgressTracker'; +import {isTSSKey} from '../../../../store/wallet/effects/tss-send/tss-send'; +import {useTSSCallbacks} from '../../../../utils/hooks/useTSSCalbacks'; +import {Network} from '../../../../constants'; +import {BottomNotificationConfig} from '../../../../components/modal/bottom-notification/BottomNotification'; // Styled export const SellCheckoutContainer = styled.SafeAreaView` @@ -192,6 +199,38 @@ const RampSellCheckout: React.FC = () => { const [confirmHardwareState, setConfirmHardwareState] = useState(null); + const [showTSSProgressModal, setShowTSSProgressModal] = useState(false); + const showTssErrorMessage = useCallback( + async (config: BottomNotificationConfig) => { + const msg = config?.message || t('An error occurred during TSS signing'); + const reason = 'TSS Signing Error'; + const title = config?.title || t('TSS Signing Error'); + showError(msg, reason, undefined, title); + }, + [dispatch], + ); + const isTSSWallet = isTSSKey(key); + const [tssStatus, setTssStatus] = useState('initializing'); + const [tssProgress, setTssProgress] = useState({ + currentRound: 0, + totalRounds: 4, + status: 'pending', + }); + const [tssCopayers, setTssCopayers] = useState< + Array<{id: string; name: string; signed: boolean}> + >([]); + + const tssCallbacks = useTSSCallbacks({ + wallet, + setTssStatus, + setTssProgress, + setTssCopayers, + tssCopayers, + setShowTSSProgressModal, + setResetSwipeButton, + showErrorMessage: showTssErrorMessage, + }); + const {showPaymentSent, hidePaymentSent} = usePaymentSent(); const {showOngoingProcess, hideOngoingProcess} = useOngoingProcess(); @@ -528,13 +567,18 @@ const RampSellCheckout: React.FC = () => { const isUsingHardwareWallet = !!transport; let broadcastedTx; try { + if (isTSSWallet) { + setShowTSSProgressModal(true); + setTssStatus('initializing'); + } + if (isUsingHardwareWallet) { const {chain, network} = wallet.credentials; const configFn = currencyConfigs[chain]; if (!configFn) { throw new Error(`Unsupported currency: ${chain.toUpperCase()}`); } - const params = configFn(network); + const params = configFn(network as Network); await prepareLedgerApp( params.appName, transportRef, @@ -563,8 +607,15 @@ const RampSellCheckout: React.FC = () => { key, wallet, ataOwnerAddress, + ...(isTSSWallet && {tssCallbacks}), }), ); + + if (isTSSWallet && broadcastedTx?.txid) { + setTssStatus('complete'); + await sleep(1500); + setShowTSSProgressModal(false); + } } updateRampTx(txData!, broadcastedTx as Partial); showPaymentSent({ @@ -600,6 +651,9 @@ const RampSellCheckout: React.FC = () => { }), ); } catch (err: any) { + if (isTSSWallet) { + setShowTSSProgressModal(false); + } if (isUsingHardwareWallet) { setConfirmHardwareWalletVisible(false); setConfirmHardwareState(null); @@ -691,6 +745,7 @@ const RampSellCheckout: React.FC = () => { txSentOn: Date.now(), txSentId: broadcastedTx?.txid, status: 'bitpayTxSent', + isTSSWallet: isTSSWallet, }; dispatch( @@ -898,6 +953,19 @@ const RampSellCheckout: React.FC = () => {
{t('SUMMARY')}
+ {isTSSWallet && ( + + )} {t('Selling')} {amountExpected ? ( diff --git a/src/navigation/services/sell-crypto/screens/SimplexSellCheckout.tsx b/src/navigation/services/sell-crypto/screens/SimplexSellCheckout.tsx index 8a730f54a6..adfa250ffd 100644 --- a/src/navigation/services/sell-crypto/screens/SimplexSellCheckout.tsx +++ b/src/navigation/services/sell-crypto/screens/SimplexSellCheckout.tsx @@ -34,6 +34,8 @@ import { TransactionProposal, SendMaxInfo, Key, + TSSSigningStatus, + TSSSigningProgress, } from '../../../../store/wallet/wallet.models'; import { GetName, @@ -116,6 +118,11 @@ import {ValidateCoinAddress} from '../../../../store/wallet/utils/validations'; import {SellBalanceContainer} from '../styled/SellCryptoCard'; import {useOngoingProcess, usePaymentSent} from '../../../../contexts'; import {SellCryptoOffer} from '../../components/externalServicesOfferSelector'; +import TSSProgressTracker from '../../../wallet/components/TSSProgressTracker'; +import {isTSSKey} from '../../../../store/wallet/effects/tss-send/tss-send'; +import {useTSSCallbacks} from '../../../../utils/hooks/useTSSCalbacks'; +import {Network} from '../../../../constants'; +import {BottomNotificationConfig} from '../../../../components/modal/bottom-notification/BottomNotification'; // Styled export const SellCheckoutContainer = styled.SafeAreaView` @@ -213,6 +220,38 @@ const SimplexSellCheckout: React.FC = () => { const [confirmHardwareState, setConfirmHardwareState] = useState(null); + const [showTSSProgressModal, setShowTSSProgressModal] = useState(false); + const showTssErrorMessage = useCallback( + async (config: BottomNotificationConfig) => { + const msg = config?.message || t('An error occurred during TSS signing'); + const reason = 'TSS Signing Error'; + const title = config?.title || t('TSS Signing Error'); + showError(msg, reason, undefined, title); + }, + [dispatch], + ); + const isTSSWallet = isTSSKey(key); + const [tssStatus, setTssStatus] = useState('initializing'); + const [tssProgress, setTssProgress] = useState({ + currentRound: 0, + totalRounds: 4, + status: 'pending', + }); + const [tssCopayers, setTssCopayers] = useState< + Array<{id: string; name: string; signed: boolean}> + >([]); + + const tssCallbacks = useTSSCallbacks({ + wallet, + setTssStatus, + setTssProgress, + setTssCopayers, + tssCopayers, + setShowTSSProgressModal, + setResetSwipeButton, + showErrorMessage: showTssErrorMessage, + }); + const {showPaymentSent, hidePaymentSent} = usePaymentSent(); const {showOngoingProcess, hideOngoingProcess} = useOngoingProcess(); @@ -512,13 +551,18 @@ const SimplexSellCheckout: React.FC = () => { const isUsingHardwareWallet = !!transport; let broadcastedTx; try { + if (isTSSWallet) { + setShowTSSProgressModal(true); + setTssStatus('initializing'); + } + if (isUsingHardwareWallet) { const {chain, network} = wallet.credentials; const configFn = currencyConfigs[chain]; if (!configFn) { throw new Error(`Unsupported currency: ${chain.toUpperCase()}`); } - const params = configFn(network); + const params = configFn(network as Network); await prepareLedgerApp( params.appName, transportRef, @@ -547,8 +591,15 @@ const SimplexSellCheckout: React.FC = () => { key, wallet, ataOwnerAddress, + ...(isTSSWallet && {tssCallbacks}), }), ); + + if (isTSSWallet && broadcastedTx?.txid) { + setTssStatus('complete'); + await sleep(1500); + setShowTSSProgressModal(false); + } } const newData: SimplexSellOrderData = { @@ -570,6 +621,7 @@ const SimplexSellCheckout: React.FC = () => { tx_sent_id: broadcastedTx?.txid, quote_id: simplexQuoteOffer.quoteData.quote_id ?? '', send_max: useSendMax, + isTSSWallet: isTSSWallet, }; dispatch( @@ -624,6 +676,10 @@ const SimplexSellCheckout: React.FC = () => { }), ); } catch (err: any) { + if (isTSSWallet) { + setShowTSSProgressModal(false); + } + if (isUsingHardwareWallet) { setConfirmHardwareWalletVisible(false); setConfirmHardwareState(null); @@ -936,6 +992,19 @@ const SimplexSellCheckout: React.FC = () => {
{t('SUMMARY')}
+ {isTSSWallet && ( + + )} {t('Selling')} {amountExpected ? ( diff --git a/src/store/sell-crypto/models/moonpay-sell.models.ts b/src/store/sell-crypto/models/moonpay-sell.models.ts index e8daa29539..4b5f9b04d9 100644 --- a/src/store/sell-crypto/models/moonpay-sell.models.ts +++ b/src/store/sell-crypto/models/moonpay-sell.models.ts @@ -30,6 +30,7 @@ export interface MoonpaySellOrderData { tx_sent_on?: number; tx_sent_id?: string; failure_reason?: string; // if status === failed + isTSSWallet?: boolean; } export interface MoonpaySellIncomingData { @@ -46,6 +47,7 @@ export interface MoonpaySellIncomingData { txSentOn?: number; txSentId?: string; failureReason?: string; + isTSSWallet?: boolean; } export interface MoonpayCurrencyMetadata { diff --git a/src/store/sell-crypto/models/ramp-sell.models.ts b/src/store/sell-crypto/models/ramp-sell.models.ts index 3d8e56c9ce..12ac832285 100644 --- a/src/store/sell-crypto/models/ramp-sell.models.ts +++ b/src/store/sell-crypto/models/ramp-sell.models.ts @@ -33,6 +33,7 @@ export interface RampSellOrderData { send_max?: boolean; tx_sent_on?: number; tx_sent_id?: string; + isTSSWallet?: boolean; } export interface RampSellIncomingData { @@ -48,6 +49,7 @@ export interface RampSellIncomingData { depositWalletAddress?: string; txSentOn?: number; txSentId?: string; + isTSSWallet?: boolean; } export interface RampGetSellQuoteRequestData { diff --git a/src/store/sell-crypto/models/simplex-sell.models.ts b/src/store/sell-crypto/models/simplex-sell.models.ts index 0b672a9315..792a15efaa 100644 --- a/src/store/sell-crypto/models/simplex-sell.models.ts +++ b/src/store/sell-crypto/models/simplex-sell.models.ts @@ -25,6 +25,7 @@ export interface SimplexSellOrderData { send_max?: boolean; tx_sent_on?: number; tx_sent_id?: string; + isTSSWallet?: boolean; } export interface SimplexSellIncomingData { diff --git a/src/store/sell-crypto/sell-crypto.reducer.ts b/src/store/sell-crypto/sell-crypto.reducer.ts index 98aeb62326..faba460f7b 100644 --- a/src/store/sell-crypto/sell-crypto.reducer.ts +++ b/src/store/sell-crypto/sell-crypto.reducer.ts @@ -96,6 +96,9 @@ export const sellCryptoReducer = ( tx_sent_id: moonpaySellIncomingData.txSentId ?? state.moonpay[moonpaySellIncomingData.externalId].tx_sent_id, + isTSSWallet: + moonpaySellIncomingData.isTSSWallet ?? + state.moonpay[moonpaySellIncomingData.externalId].isTSSWallet, }; return { ...state, @@ -186,6 +189,10 @@ export const sellCryptoReducer = ( rampSellIncomingData.txSentId, currentData.tx_sent_id, ), + isTSSWallet: setOrDefault( + rampSellIncomingData.isTSSWallet, + currentData.isTSSWallet, + ), }; return { From 5c1a9f8593be5de7ba9cab028d660dd6c7caa526 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 8 Jan 2026 13:03:44 -0300 Subject: [PATCH 12/16] [REF] add hideTracker property --- .../wallet/components/TSSProgressTracker.tsx | 53 +++++++++++-------- .../TransactionProposalNotifications.tsx | 28 +++++----- 2 files changed, 44 insertions(+), 37 deletions(-) diff --git a/src/navigation/wallet/components/TSSProgressTracker.tsx b/src/navigation/wallet/components/TSSProgressTracker.tsx index 62400377bb..b5a3736857 100644 --- a/src/navigation/wallet/components/TSSProgressTracker.tsx +++ b/src/navigation/wallet/components/TSSProgressTracker.tsx @@ -224,6 +224,7 @@ interface TSSProgressTrackerProps { onModalVisibilityChange?: (visible: boolean) => void; wallet?: Wallet; onCopayersInitialized?: (copayers: TSSCopayer[]) => void; + hideTracker?: boolean; } const TSSProgressTracker: React.FC = ({ @@ -236,6 +237,7 @@ const TSSProgressTracker: React.FC = ({ wallet, onCopayersInitialized, onModalVisibilityChange, + hideTracker, }) => { const {t} = useTranslation(); const theme = useTheme(); @@ -348,29 +350,34 @@ const TSSProgressTracker: React.FC = ({ return ( <> - - setModalVisible(true)}> - - {status === 'complete' ? ( - - ) : ( - - )} - - - {getButtonText()} - - - - - - - + {!hideTracker ? ( + + setModalVisible(true)}> + + {status === 'complete' ? ( + + ) : ( + + )} + + + {getButtonText()} + + + + + + + + ) : ( + <> + )} +
diff --git a/src/navigation/wallet/screens/TransactionProposalNotifications.tsx b/src/navigation/wallet/screens/TransactionProposalNotifications.tsx index d3bd88e227..cba5c98d9f 100644 --- a/src/navigation/wallet/screens/TransactionProposalNotifications.tsx +++ b/src/navigation/wallet/screens/TransactionProposalNotifications.tsx @@ -634,6 +634,20 @@ const TransactionProposalNotifications = () => { return ( + {isTSSWallet && currentWallet && showTSSProgressModal ? ( + + ) : null} { }} /> ) : null} - - {isTSSWallet && currentWallet && showTSSProgressModal ? ( - - ) : null} ); }; From ed2160d32c35a2f723be33e37f416632aae55c84 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 8 Jan 2026 14:01:06 -0300 Subject: [PATCH 13/16] [FEAT] BillConfirm - GiftCardConfirm - PayProConfirm - WalletConnectConfirm tss support --- .../screens/WalletConnectConfirm.tsx | 72 ++++++++++++++++--- .../screens/send/confirm/BillConfirm.tsx | 67 +++++++++++++++++ .../screens/send/confirm/GiftCardConfirm.tsx | 66 +++++++++++++++++ .../screens/send/confirm/PayProConfirm.tsx | 67 +++++++++++++++++ 4 files changed, 264 insertions(+), 8 deletions(-) diff --git a/src/navigation/wallet-connect/screens/WalletConnectConfirm.tsx b/src/navigation/wallet-connect/screens/WalletConnectConfirm.tsx index 74da6d5852..fef15f90c1 100644 --- a/src/navigation/wallet-connect/screens/WalletConnectConfirm.tsx +++ b/src/navigation/wallet-connect/screens/WalletConnectConfirm.tsx @@ -8,6 +8,8 @@ import { TransactionProposal, TxDetails, Wallet, + TSSSigningStatus, + TSSSigningProgress, } from '../../../store/wallet/wallet.models'; import SwipeButton from '../../../components/swipe-button/SwipeButton'; import {sleep} from '../../../utils/helper-methods'; @@ -76,6 +78,9 @@ import {EIP155_SIGNING_METHODS} from '../../../constants/WalletConnectV2'; import {formatJsonRpcResult} from '@json-rpc-tools/utils'; import {GetPrecision} from '../../../store/wallet/utils/currency'; import {usePaymentSent} from '../../../contexts'; +import {isTSSKey} from '../../../store/wallet/effects/tss-send/tss-send'; +import TSSProgressTracker from '../../wallet/components/TSSProgressTracker'; +import {useTSSCallbacks} from '../../../utils/hooks/useTSSCalbacks'; const HeaderRightContainer = styled.View``; @@ -121,12 +126,42 @@ const WalletConnectConfirm = () => { const [accountDisconnected, setAccountDisconnected] = useState(false); const [requestDismissed, setRequestDismissed] = useState(false); const [imageError, setImageError] = useState(false); + const [showTSSProgressModal, setShowTSSProgressModal] = useState(false); const [txDetails, setTxDetails] = useState(); const [txp, setTxp] = useState | undefined>(); const allKeys = useAppSelector(({WALLET}) => WALLET.keys); const key = allKeys[wallet?.keyId!]; + const showErrorMessage = useCallback( + async (msg: BottomNotificationConfig) => { + await sleep(500); + dispatch(showBottomNotificationModal(msg)); + }, + [dispatch], + ); + + const isTSSWallet = isTSSKey(key); + const [tssStatus, setTssStatus] = useState('initializing'); + const [tssProgress, setTssProgress] = useState({ + currentRound: 0, + totalRounds: 4, + status: 'pending', + }); + const [tssCopayers, setTssCopayers] = useState< + Array<{id: string; name: string; signed: boolean}> + >([]); + const tssCallbacks = useTSSCallbacks({ + wallet, + setTssStatus, + setTssProgress, + setTssCopayers, + tssCopayers, + setShowTSSProgressModal, + setResetSwipeButton, + showErrorMessage, + }); + const sessionV2: WCV2SessionType | undefined = useAppSelector( ({WALLET_CONNECT_V2}) => WALLET_CONNECT_V2.sessions.find(session => session.topic === topic), @@ -202,6 +237,11 @@ const WalletConnectConfirm = () => { const feeOptions = GetFeeOptions(wallet.chain); const approveCallRequest = async () => { + if (isTSSWallet) { + setShowTSSProgressModal(true); + setTssStatus('initializing'); + } + try { const {params, id} = request as WCV2RequestType; const {request: requestProps} = params; @@ -216,6 +256,7 @@ const WalletConnectConfirm = () => { key, wallet, recipient, + ...(isTSSWallet && {tssCallbacks}), }), ); await dispatch( @@ -228,6 +269,13 @@ const WalletConnectConfirm = () => { } else { await dispatch(walletConnectV2ApproveCallRequest(request, wallet)); } + + if (isTSSWallet) { + setTssStatus('complete'); + await sleep(1500); + setShowTSSProgressModal(false); + } + dispatch( Analytics.track('Sent Crypto', { context: 'WalletConnect Confirm', @@ -240,6 +288,9 @@ const WalletConnectConfirm = () => { wallet?.credentials.n > 1 ? t('Proposal created') : t('Payment Sent'), }); } catch (err) { + if (isTSSWallet) { + setShowTSSProgressModal(false); + } await sleep(500); setResetSwipeButton(true); switch (err) { @@ -263,14 +314,6 @@ const WalletConnectConfirm = () => { } }; - const showErrorMessage = useCallback( - async (msg: BottomNotificationConfig) => { - await sleep(500); - dispatch(showBottomNotificationModal(msg)); - }, - [dispatch], - ); - const rejectCallRequest = useCallback(async () => { haptic('impactLight'); try { @@ -423,6 +466,19 @@ const WalletConnectConfirm = () => {
Summary
+ {isTSSWallet && ( + + )} (null); const [confirmHardwareState, setConfirmHardwareState] = useState(null); + const [showTSSProgressModal, setShowTSSProgressModal] = useState(false); const {showPaymentSent, hidePaymentSent} = usePaymentSent(); const {showOngoingProcess, hideOngoingProcess} = useOngoingProcess(); + const showErrorMessage = useCallback( + async (msg: BottomNotificationConfig) => { + await sleep(500); + dispatch(showBottomNotificationModal(msg)); + }, + [dispatch], + ); + + const isTSSWallet = key ? isTSSKey(key) : false; + const [tssStatus, setTssStatus] = useState('initializing'); + const [tssProgress, setTssProgress] = useState({ + currentRound: 0, + totalRounds: 4, + status: 'pending', + }); + const [tssCopayers, setTssCopayers] = useState< + Array<{id: string; name: string; signed: boolean}> + >([]); + const tssCallbacks = useTSSCallbacks({ + wallet: wallet!, + setTssStatus, + setTssProgress, + setTssCopayers, + tssCopayers, + setShowTSSProgressModal, + setResetSwipeButton, + showErrorMessage, + }); + const baseEventParams = { ...getBillAccountEventParamsForMultipleBills( billPayments.map(({billPayAccount: account}) => account), @@ -407,6 +444,7 @@ const BillConfirm: React.FC< key, wallet, recipient, + ...(isTSSWallet && {tssCallbacks}), }), ) : await dispatch( @@ -419,6 +457,12 @@ const BillConfirm: React.FC< }; const handlePaymentSuccess = async () => { + if (isTSSWallet) { + setTssStatus('complete'); + await sleep(1500); + setShowTSSProgressModal(false); + } + showPaymentSent({ onCloseModal, title: @@ -462,6 +506,10 @@ const BillConfirm: React.FC< }; const handlePaymentFailure = async (error: any) => { + if (isTSSWallet) { + setShowTSSProgressModal(false); + } + const handled = dispatch( handleSendError({error, onDismiss: () => openWalletSelector(400)}), ); @@ -533,6 +581,12 @@ const BillConfirm: React.FC< transport, }: {transport?: Transport} = {}) => { const isUsingHardwareWallet = !!transport; + + if (isTSSWallet) { + setShowTSSProgressModal(true); + setTssStatus('initializing'); + } + dispatch( Analytics.track('Bill Pay - Clicked Slide to Confirm', baseEventParams), ); @@ -601,6 +655,19 @@ const BillConfirm: React.FC< <>
Summary
+ {isTSSWallet && wallet && ( + + )} { useState(null); const [confirmHardwareState, setConfirmHardwareState] = useState(null); + const [showTSSProgressModal, setShowTSSProgressModal] = useState(false); + + const showErrorMessage = useCallback( + async (msg: BottomNotificationConfig) => { + await sleep(500); + dispatch(showBottomNotificationModal(msg)); + }, + [dispatch], + ); + + const isTSSWallet = key ? isTSSKey(key) : false; + const [tssStatus, setTssStatus] = useState('initializing'); + const [tssProgress, setTssProgress] = useState({ + currentRound: 0, + totalRounds: 4, + status: 'pending', + }); + const [tssCopayers, setTssCopayers] = useState< + Array<{id: string; name: string; signed: boolean}> + >([]); + const tssCallbacks = useTSSCallbacks({ + wallet: wallet!, + setTssStatus, + setTssProgress, + setTssCopayers, + tssCopayers, + setShowTSSProgressModal, + setResetSwipeButton, + showErrorMessage, + }); const unsoldGiftCard = giftCards.find( giftCard => giftCard.invoiceId === txp?.invoiceID, @@ -466,6 +502,12 @@ const Confirm = () => { transport?: Transport; }) => { const isUsingHardwareWallet = !!transport; + + if (isTSSWallet) { + setShowTSSProgressModal(true); + setTssStatus('initializing'); + } + try { if (isUsingHardwareWallet) { if (txp && wallet && recipient) { @@ -514,6 +556,7 @@ const Confirm = () => { key, wallet, recipient, + ...(isTSSWallet && {tssCallbacks}), }), ) : await dispatch( @@ -524,8 +567,18 @@ const Confirm = () => { ), ); } + + if (isTSSWallet) { + setTssStatus('complete'); + await sleep(1500); + setShowTSSProgressModal(false); + } + await redeemGiftCardAndNavigateToGiftCardDetails(); } catch (err: any) { + if (isTSSWallet) { + setShowTSSProgressModal(false); + } if (isUsingHardwareWallet) { setConfirmHardwareWalletVisible(false); setConfirmHardwareState(null); @@ -707,6 +760,19 @@ const Confirm = () => { {wallet || coinbaseAccount ? ( <>
Summary
+ {isTSSWallet && wallet && ( + + )} { useState(null); const [confirmHardwareState, setConfirmHardwareState] = useState(null); + const [showTSSProgressModal, setShowTSSProgressModal] = useState(false); + + const _showErrorMessage = useCallback( + async (msg: BottomNotificationConfig) => { + await sleep(500); + dispatch(showBottomNotificationModal(msg)); + }, + [dispatch], + ); + + const isTSSWallet = key ? isTSSKey(key) : false; + const [tssStatus, setTssStatus] = useState('initializing'); + const [tssProgress, setTssProgress] = useState({ + currentRound: 0, + totalRounds: 4, + status: 'pending', + }); + const [tssCopayers, setTssCopayers] = useState< + Array<{id: string; name: string; signed: boolean}> + >([]); + const tssCallbacks = useTSSCallbacks({ + wallet: wallet!, + setTssStatus, + setTssProgress, + setTssCopayers, + tssCopayers, + setShowTSSProgressModal, + setResetSwipeButton, + showErrorMessage: _showErrorMessage, + }); const payProHost = payProOptions.payProUrl .replace('https://', '') @@ -331,6 +368,12 @@ const PayProConfirm = () => { transport?: Transport; }) => { const isUsingHardwareWallet = !!transport; + + if (isTSSWallet) { + setShowTSSProgressModal(true); + setTssStatus('initializing'); + } + try { if (isUsingHardwareWallet) { if (txp && wallet && recipient) { @@ -370,6 +413,7 @@ const PayProConfirm = () => { key, wallet, recipient, + ...(isTSSWallet && {tssCallbacks}), }), ) : await dispatch( @@ -380,6 +424,13 @@ const PayProConfirm = () => { ), ); } + + if (isTSSWallet) { + setTssStatus('complete'); + await sleep(1500); + setShowTSSProgressModal(false); + } + dispatch( Analytics.track('Sent Crypto', { context: 'PayPro Confirm', @@ -459,6 +510,9 @@ const PayProConfirm = () => { } } } catch (err: any) { + if (isTSSWallet) { + setShowTSSProgressModal(false); + } if (isUsingHardwareWallet) { setConfirmHardwareWalletVisible(false); setConfirmHardwareState(null); @@ -616,6 +670,19 @@ const PayProConfirm = () => { keyboardShouldPersistTaps={'handled'}>
Summary
+ {isTSSWallet && wallet && ( + + )} {invoice ? ( Date: Thu, 8 Jan 2026 15:49:11 -0300 Subject: [PATCH 14/16] [FIX] log warning --- src/utils/hooks/useTSSCalbacks.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/utils/hooks/useTSSCalbacks.ts b/src/utils/hooks/useTSSCalbacks.ts index 6634d608cf..70eb44fb43 100644 --- a/src/utils/hooks/useTSSCalbacks.ts +++ b/src/utils/hooks/useTSSCalbacks.ts @@ -53,13 +53,11 @@ export const useTSSCallbacks = ({ // When round 1 starts, mark all copayers as joined/signing // TODO remove this when onCopayerStatusChange is properly implemented if (progress.currentRound === 1) { + logManager.debug(`[TSS Callbacks] Marking copayers as signed`); setTssCopayers(prev => { // Only update if copayers aren't already signed const allSigned = prev.every(c => c.signed); if (!allSigned) { - logManager.debug( - `[TSS Callbacks] Marking all ${prev.length} copayers as signed`, - ); return prev.map(c => ({...c, signed: true})); } return prev; From ab6141a8432db1d00cf76649b7e2d206f7506dec Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 8 Jan 2026 16:10:44 -0300 Subject: [PATCH 15/16] [FIX] use ecdsa for tss bch wallets --- src/store/wallet/effects/send/send.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/store/wallet/effects/send/send.ts b/src/store/wallet/effects/send/send.ts index 1c9e2f8726..18ad49f11c 100644 --- a/src/store/wallet/effects/send/send.ts +++ b/src/store/wallet/effects/send/send.ts @@ -178,6 +178,10 @@ export const createProposalAndBuildTxDetails = tx.signingMethod = 'ecdsa'; } + if (wallet.tssKeyId && credentials.chain === 'bch') { + tx.signingMethod = 'ecdsa'; + } + if ( chain === 'eth' && wallet.transactionHistory?.hasConfirmingTxs && From c932059204ee8ba17392e43a57ac1615c8a768a8 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 9 Jan 2026 12:57:49 -0300 Subject: [PATCH 16/16] [REF] improve tss keys backups --- .../wallet-settings/ExportTSSWallet.tsx | 73 ++------- src/store/wallet/effects/import/import.ts | 138 ++++++++++++++---- 2 files changed, 118 insertions(+), 93 deletions(-) diff --git a/src/navigation/wallet/screens/wallet-settings/ExportTSSWallet.tsx b/src/navigation/wallet/screens/wallet-settings/ExportTSSWallet.tsx index 9eaea172fe..133d8da2ce 100644 --- a/src/navigation/wallet/screens/wallet-settings/ExportTSSWallet.tsx +++ b/src/navigation/wallet/screens/wallet-settings/ExportTSSWallet.tsx @@ -32,6 +32,7 @@ import {logManager} from '../../../../managers/LogManager'; import {RootStacks} from '../../../../Root'; import {TabsScreens} from '../../../tabs/TabsStack'; import WalletCreatedSvg from '../../../../../assets/img/shared-success.svg'; +import {Wallet} from '../../../../store/wallet/wallet.models'; const BWC = BwcProvider.getInstance(); @@ -169,72 +170,22 @@ const ExportTSSWallet = () => { return null; } - const keychain = key.properties?.keychain; - const metadata = key.properties?.metadata; - - const bufferToArray = (buffer: any): number[] | null => { - if (!buffer) { - logManager.debug('[bufferToArray] buffer is null/undefined'); - return null; - } - - if (Array.isArray(buffer)) { - logManager.debug('[bufferToArray] Using Array.isArray path'); - return buffer; - } - - if (Buffer.isBuffer(buffer)) { - logManager.debug('[bufferToArray] Using Buffer.isBuffer path'); - return Array.from(buffer); - } - - if (buffer && typeof buffer === 'object' && 'data' in buffer) { - if (Array.isArray(buffer.data)) { - logManager.debug('[bufferToArray] Using buffer.data array path'); - return buffer.data; - } - } - - if (buffer && typeof buffer === 'object') { - const keys = Object.keys(buffer); - if (keys.length > 0 && keys.every(k => !isNaN(Number(k)))) { - logManager.debug('[bufferToArray] Using numeric keys object path'); - const arr: number[] = []; - for (let i = 0; i < keys.length; i++) { - if (buffer[i] !== undefined) { - arr.push(buffer[i]); - } - } - return arr.length > 0 ? arr : null; - } - } - - logManager.debug('[bufferToArray] No matching condition, returning null'); - return null; - }; - if (!keychain) { - throw new Error('Keychain data is missing'); - } - const backup = { isTSS: true, version: 1, - key: { - mnemonic: key.properties?.mnemonic, - keychain: { - commonKeyChain: keychain.commonKeyChain, - privateKeyShare: bufferToArray(keychain.privateKeyShare), - reducedPrivateKeyShare: bufferToArray( - keychain.reducedPrivateKeyShare, - ), - }, - metadata: metadata, - }, + key: key.methods.toObj(), + credentials: key.wallets.map((wallet: Wallet) => + wallet.credentials.toObj(), + ), }; - return BWC.getSJCL().encrypt(password, JSON.stringify(backup), { - iter: 1000, - }); + const encrypted = BWC.getEncryption().encryptWithPassword( + JSON.stringify(backup), + password, + {iter: 1000}, + ); + + return JSON.stringify(encrypted); }; const shareKeyshareFile = async ({password}: {password: string}) => { diff --git a/src/store/wallet/effects/import/import.ts b/src/store/wallet/effects/import/import.ts index fd0d506c5e..399ec3578c 100644 --- a/src/store/wallet/effects/import/import.ts +++ b/src/store/wallet/effects/import/import.ts @@ -1638,15 +1638,15 @@ export const startImportTSSFile = throw new Error(t('Invalid TSS backup file format.')); } - if (!data.key?.mnemonic) { - throw new Error(t('Missing mnemonic in TSS backup.')); + if (!data.key) { + throw new Error(t('Missing key in TSS backup.')); } - if (!data.key?.keychain) { - throw new Error(t('Missing keychain in TSS backup.')); + if (!data.credentials || !Array.isArray(data.credentials)) { + throw new Error(t('Missing credentials in TSS backup.')); } - logManager.info('[ImportTSS] Starting TSS wallet import...'); + logManager.info('[ImportTSS] Starting direct TSS wallet import...'); const arrayToBuffer = (arr: any): Buffer | null => { if (!arr) return null; @@ -1663,50 +1663,124 @@ export const startImportTSSFile = return null; }; - const privateKeyShare = arrayToBuffer( - data.key.keychain.privateKeyShare, - ); - const reducedPrivateKeyShare = arrayToBuffer( - data.key.keychain.reducedPrivateKeyShare, - ); + if (data.key.keychain) { + const privateKeyShare = arrayToBuffer( + data.key.keychain.privateKeyShare, + ); + const reducedPrivateKeyShare = arrayToBuffer( + data.key.keychain.reducedPrivateKeyShare, + ); - if (!privateKeyShare || privateKeyShare.length === 0) { - throw new Error(t('Invalid privateKeyShare in backup file.')); + if (privateKeyShare) { + data.key.keychain.privateKeyShare = privateKeyShare; + } + if (reducedPrivateKeyShare) { + data.key.keychain.reducedPrivateKeyShare = reducedPrivateKeyShare; + } } - if (!reducedPrivateKeyShare || reducedPrivateKeyShare.length === 0) { - throw new Error(t('Invalid reducedPrivateKeyShare in backup file.')); - } + const BWCProvider = BwcProvider.getInstance(); - logManager.info('[ImportTSS] Keyshare conversion successful'); + const TssKey = BWCProvider.getTssKey(); + const tssKey = new TssKey(data.key); - const opts: Partial = { - words: normalizeMnemonic(data.key.mnemonic), - tssKeychain: { - commonKeyChain: data.key.keychain.commonKeyChain, - privateKeyShare: privateKeyShare, - reducedPrivateKeyShare: reducedPrivateKeyShare, - }, - tssMetadata: data.key.metadata, - }; + logManager.info('[ImportTSS] TssKey recreated successfully'); + + const wallets = await Promise.all( + data.credentials.map(async (credObj: any) => { + try { + const walletClient = BWCProvider.getClient( + JSON.stringify(credObj), + ); + + logManager.info( + `[ImportTSS] Recreated wallet client - ${credObj.walletId}`, + ); - const importResult = await serverAssistedImport(opts); + await new Promise((resolve, reject) => + walletClient.openWallet({}, (err: any, result: any) => { + if (err) { + logManager.warn( + `[ImportTSS] Could not open wallet ${credObj.walletId}:`, + err, + ); + return reject(err); + } + resolve(result); + }), + ); + + const status: any = await new Promise((resolve, reject) => + walletClient.getStatus( + {includeExtendedInfo: true}, + (err: any, result: any) => { + if (err) { + logManager.warn( + `[ImportTSS] Could not get status for ${credObj.walletId}:`, + err, + ); + return reject(err); + } + resolve(result); + }, + ), + ); + + const {currencyAbbreviation, currencyName} = dispatch( + mapAbbreviationAndName( + walletClient.credentials.coin, + walletClient.credentials.chain, + walletClient.credentials.token?.address, + ), + ); + + const fullWallet = merge( + walletClient, + status.wallet, + buildWalletObj( + { + ...walletClient.credentials, + currencyAbbreviation, + currencyName, + } as any, + tokenOptsByAddress, + ), + ); + + return fullWallet; + } catch (error: unknown) { + const errMsg = + error instanceof Error ? error.message : JSON.stringify(error); + logManager.error( + `[ImportTSS] Failed to recreate wallet - ${credObj.walletId}: ${errMsg}`); + return undefined; + } + }), + ); + + const validWallets = wallets.filter( + (w): w is NonNullable => w !== undefined, + ); + + if (validWallets.length === 0) { + throw new Error(t('Failed to recreate any wallets from backup')); + } const { key: _key, - wallets, + wallets: processedWallets, keyName, } = findMatchedKeyAndUpdate( - importResult.wallets, - importResult.key, + validWallets, + tssKey, Object.values(WALLET.keys).filter(k => !k.id.includes('readonly')), - opts, + {}, ); const key = buildKeyObj({ key: _key, keyName, - wallets: wallets.map(wallet => { + wallets: processedWallets.map(wallet => { if (notificationsAccepted) { dispatch(subscribePushNotifications(wallet, brazeEid!)); }