diff --git a/apps/electron/src/main.ts b/apps/electron/src/main.ts index 7cb3447c..a751333e 100644 --- a/apps/electron/src/main.ts +++ b/apps/electron/src/main.ts @@ -557,7 +557,7 @@ ipcMain.handle('/xpub', async (event, args: HwiXpubRequest) => { ipcMain.handle('/sign', async (event, args) => { const { deviceType, devicePath, psbt } = args; const resp = JSON.parse(await signtx(deviceType, devicePath, psbt, isTestnet)); - if (resp.error) { + if (!resp.signed) { return Promise.reject(new Error('Error signing transaction')); } return Promise.resolve(resp); diff --git a/apps/express/src/utils/setInitialConfig.ts b/apps/express/src/utils/setInitialConfig.ts index 0ec19b26..de7936dd 100644 --- a/apps/express/src/utils/setInitialConfig.ts +++ b/apps/express/src/utils/setInitialConfig.ts @@ -29,7 +29,6 @@ export const setInitialConfig = async () => { const lndHost = `${process.env.LND_IP}:${process.env.LND_GRPC_PORT}`; const emptyConfig = EMPTY_CONFIG; - emptyConfig.isEmpty = true; emptyConfig.lightning[0] = { id: uuidv4(), type: 'lightning', @@ -46,6 +45,7 @@ export const setInitialConfig = async () => { }; if (!configExists && process.env.APP_PASSWORD) { + emptyConfig.isEmpty = false; const encryptedConfigObject = AES.encrypt( JSON.stringify(emptyConfig), process.env.APP_PASSWORD diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 72664423..e45f010a 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -3,7 +3,7 @@ "author": "Lily Technologies, Inc. (https://lily-wallet.com)", "description": "Lily is the best way to secure your Bitcoin", "license": "Custom", - "version": "1.3.0", + "version": "1.4.0", "private": true, "main": "./dist/main.js", "homepage": "./", @@ -24,7 +24,7 @@ "@heroicons/react": "^1.0.3", "@lily-technologies/ecpair": "^2.0.0", "@lily-technologies/lnrpc": "^0.14.1-beta.14", - "@lily/types": "1.3.0", + "@lily/types": "1.4.0", "@styled-icons/bootstrap": "^10.34.0", "@styled-icons/boxicons-logos": "^10.38.0", "@styled-icons/boxicons-regular": "^10.38.0", diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx index 3d973b1f..dd145ad0 100644 --- a/apps/frontend/src/App.tsx +++ b/apps/frontend/src/App.tsx @@ -76,21 +76,21 @@ const App = () => { // return null; // }; - const RedirectHandler = () => { - const history = useHistory(); - const { search } = useLocation(); - const { redirect, ...rest } = queryString.parse(search); - useEffect(() => { - if (!config.isEmpty && !!redirect && typeof redirect === 'string') { - history.push({ - pathname: redirect, - search: queryString.stringify(rest) - }); - } - }, [redirect]); + // const RedirectHandler = () => { + // const history = useHistory(); + // const { search } = useLocation(); + // const { redirect, ...rest } = queryString.parse(search); + // useEffect(() => { + // if (!config.isEmpty && !!redirect && typeof redirect === 'string') { + // history.push({ + // pathname: redirect, + // search: queryString.stringify(rest) + // }); + // } + // }, [redirect]); - return null; - }; + // return null; + // }; const Overlay = () => { const { pathname } = useLocation(); @@ -265,7 +265,7 @@ const App = () => { ) : null} - + {/* */} diff --git a/apps/frontend/src/assets/bitgo.png b/apps/frontend/src/assets/bitgo.png new file mode 100644 index 00000000..805a5fab Binary files /dev/null and b/apps/frontend/src/assets/bitgo.png differ diff --git a/apps/frontend/src/assets/kingdom-trust.png b/apps/frontend/src/assets/kingdom-trust.png new file mode 100644 index 00000000..a6c10770 Binary files /dev/null and b/apps/frontend/src/assets/kingdom-trust.png differ diff --git a/apps/frontend/src/assets/onramp.png b/apps/frontend/src/assets/onramp.png new file mode 100644 index 00000000..bdbc9d6a Binary files /dev/null and b/apps/frontend/src/assets/onramp.png differ diff --git a/apps/frontend/src/components/DeviceImage.tsx b/apps/frontend/src/components/DeviceImage.tsx index af9e5732..7098834e 100644 --- a/apps/frontend/src/components/DeviceImage.tsx +++ b/apps/frontend/src/components/DeviceImage.tsx @@ -10,6 +10,9 @@ import Cobo from 'src/assets/cobo.png'; import Bitbox from 'src/assets/bitbox02.png'; import LilyLogo from 'src/assets/flower.svg'; import Unchained from 'src/assets/unchained.png'; +import Bitgo from 'src/assets/bitgo.png'; +import Onramp from 'src/assets/onramp.png'; +import KingdomTrust from 'src/assets/kingdom-trust.png'; import { Device } from '@lily/types'; @@ -48,6 +51,12 @@ export const DeviceImage = ({ device, className }: Props) => { ? Bitbox : device.type === 'unchained' ? Unchained + : device.type === 'onramp' + ? Onramp + : device.type === 'kingdom-trust' + ? KingdomTrust + : device.type === 'bitgo' + ? Bitgo : LilyLogo } /> diff --git a/apps/frontend/src/frontend-middleware/ElectronPlatform.ts b/apps/frontend/src/frontend-middleware/ElectronPlatform.ts index 2288e342..d4e01fa5 100644 --- a/apps/frontend/src/frontend-middleware/ElectronPlatform.ts +++ b/apps/frontend/src/frontend-middleware/ElectronPlatform.ts @@ -132,12 +132,19 @@ export class ElectronPlatform extends BasePlatform { } async signTransaction({ deviceType, devicePath, psbt }: HwiSignTransactionRequest) { - const response = await window.ipcRenderer.invoke('/sign', { - deviceType, - devicePath, - psbt - }); - return Promise.resolve(response); + try { + const response = await window.ipcRenderer.invoke('/sign', { + deviceType, + devicePath, + psbt + }); + return Promise.resolve(response); + } catch (e) { + console.log('e: ', e); + // @ts-ignore + console.log('e.message: ', e.message); + return Promise.reject(e); + } } async enumerate(): Promise { @@ -146,20 +153,34 @@ export class ElectronPlatform extends BasePlatform { } async promptPin({ deviceType, devicePath }: HwiPromptPinRequest): Promise { - const response: HwiPromptPinResponse = await window.ipcRenderer.invoke('/promptpin', { - deviceType, - devicePath - }); - return Promise.resolve(response); + try { + const response: HwiPromptPinResponse = await window.ipcRenderer.invoke('/promptpin', { + deviceType, + devicePath + }); + return Promise.resolve(response); + } catch (e) { + console.log('e: ', e); + // @ts-ignore + console.log('e.message: ', e.message); + return Promise.reject(e); + } } async sendPin({ deviceType, devicePath, pin }: HwiSendPinRequest): Promise { - const response: HwiSendPinResponse = await window.ipcRenderer.invoke('/sendpin', { - deviceType, - devicePath, - pin - }); - return Promise.resolve(response); + try { + const response: HwiSendPinResponse = await window.ipcRenderer.invoke('/sendpin', { + deviceType, + devicePath, + pin + }); + return Promise.resolve(response); + } catch (e) { + console.log('e: ', e); + // @ts-ignore + console.log('e.message: ', e.message); + return Promise.reject(e); + } } async estimateFee(): Promise { diff --git a/apps/frontend/src/utils/files.ts b/apps/frontend/src/utils/files.ts index e7288616..b9bb2611 100644 --- a/apps/frontend/src/utils/files.ts +++ b/apps/frontend/src/utils/files.ts @@ -36,7 +36,7 @@ export function clone(a: T): T { export const createAccountId = (config: Omit): AccountId => { const preHashId = `${config.addressType}:${config.quorum.requiredSigners}:${ config.quorum.totalSigners - }:${config.extendedPublicKeys + }:${[...config.extendedPublicKeys] // sort xpubs alphabetically to catch case where imported in different order .sort((a, b) => (a.xpub > b.xpub ? -1 : a.xpub < b.xpub ? 1 : 0)) .map((xpub) => xpub.xpub) @@ -392,7 +392,7 @@ export const createMultisigConfigFile = ( created_at: Date.now(), parentFingerprint: device.fingerprint, network: getUnchainedNetworkFromBjslibNetwork(currentBitcoinNetwork), - bip32Path: getMultisigDeriationPathForNetwork(currentBitcoinNetwork), + bip32Path: extendedPublicKey.bip32Path, xpub: xpub, device: { type: device.type, @@ -405,19 +405,20 @@ export const createMultisigConfigFile = ( ); const newAccountWithoutId: Omit = { + ...newAccountInputs, type: 'onchain', created_at: Date.now(), - name: newAccountInputs.name, + // name: newAccountInputs.name, license: { license: `trial:${currentBlockHeight + 4320}`, // one month free trial (6 * 24 * 30) signature: '' }, network: getUnchainedNetworkFromBjslibNetwork(currentBitcoinNetwork), - addressType: newAccountInputs.addressType, - quorum: { - requiredSigners: newAccountInputs.quorum.requiredSigners, - totalSigners: newAccountInputs.extendedPublicKeys.length - }, + // addressType: newAccountInputs.addressType, + // quorum: { + // requiredSigners: newAccountInputs.quorum.requiredSigners, + // totalSigners: newAccountInputs.extendedPublicKeys.length + // }, extendedPublicKeys: newKeys }; diff --git a/apps/frontend/src/utils/send.ts b/apps/frontend/src/utils/send.ts index 8ea70f90..67be917d 100644 --- a/apps/frontend/src/utils/send.ts +++ b/apps/frontend/src/utils/send.ts @@ -5,6 +5,7 @@ import BigNumber from 'bignumber.js'; import coinSelect from 'coinselect'; import coinSelectAll from 'coinselect/split'; import { Buffer } from 'buffer'; +import { decode } from 'bs58check'; import { cloneBuffer, bufferToHex } from './other'; @@ -17,7 +18,8 @@ import { FeeRates, ExtendedPublicKey, LilyOnchainAccount, - Device + Device, + PsbtInput } from '@lily/types'; import { BasePlatform } from 'src/frontend-middleware'; @@ -207,8 +209,19 @@ export const createTransaction = async ( psbt.setVersion(2); // These are defaults. This line is not needed. psbt.setLocktime(0); // These are defaults. This line is not needed. + // add global xpubs for security. see https://github.com/bitcoin-core/HWI/issues/671 + psbt.updateGlobal({ + globalXpub: currentAccount.config.extendedPublicKeys.map((key) => { + return { + extendedPubkey: decode(key.xpub), // have to decode to use 78 bytes pubkey, see https://github.com/bitcoinjs/bitcoinjs-lib/issues/1504 + masterFingerprint: Buffer.from(key.parentFingerprint, 'hex'), + path: key.bip32Path + }; + }) + }); + inputs.forEach((input) => { - const currentInput = { + const currentInput: PsbtInput = { hash: input.txid, index: input.vout, sequence: 0xfffffffd, // always enable RBF @@ -235,8 +248,18 @@ export const createTransaction = async ( } else if (config.addressType === 'p2sh') { // @ts-ignore-line currentInput.redeemScript = getBuffer(input.address.redeem.output); + } else if (config.addressType === 'P2SH-P2WSH') { + currentInput.redeemScript = getBuffer(input.address.redeem.output); + currentInput.witnessScript = getBuffer(input.address.redeem.redeem.output); + // @ts-ignore-line + currentInput.witnessUtxo = { + value: input.value, + // @ts-ignore-line + script: getBuffer(input.address.output) + }; } + // @ts-ignore psbt.addInput(currentInput); }); diff --git a/packages/HWIs/HWI_MAC/HWI_MAC b/packages/HWIs/HWI_MAC/HWI_MAC index 095386a3..cbceea03 100755 Binary files a/packages/HWIs/HWI_MAC/HWI_MAC and b/packages/HWIs/HWI_MAC/HWI_MAC differ diff --git a/packages/shared-server/src/OnchainProviders/Electrum.ts b/packages/shared-server/src/OnchainProviders/Electrum.ts index 331980a9..af4d9379 100644 --- a/packages/shared-server/src/OnchainProviders/Electrum.ts +++ b/packages/shared-server/src/OnchainProviders/Electrum.ts @@ -211,13 +211,25 @@ export class ElectrumProvider extends OnchainBaseProvider { const pos = cycles * BATCH_SIZE; // derive a batch of receive/change addresses for (let i = pos; i < pos + BATCH_SIZE; i++) { - const receiveAddress = getAddressFromAccount(account, `m/0/${i}`, this.network); + // we have different postfixes for the derivation path depending if standard or bitgo (https://bitcoin.stackexchange.com/a/105468/102518) + const recieveDerivationPostPath = account.bitgo ? `m/0/0/10/${i}` : `m/0/${i}`; + const receiveAddress = getAddressFromAccount( + account, + recieveDerivationPostPath, + this.network + ); receiveAddress.tags = await getAllLabelsForAddress(db, receiveAddress.address); receiveAddress.isChange = false; receiveAddress.isMine = true; currentReceiveAddressBatch.push(receiveAddress); - const changeAddress = getAddressFromAccount(account, `m/1/${i}`, this.network); + // we have different postfixes for the derivation path depending if standard or bitgo (https://bitcoin.stackexchange.com/a/105468/102518) + const changeDerivationPostPath = account.bitgo ? `m/0/0/11/${i}` : `m/1/${i}`; + const changeAddress = getAddressFromAccount( + account, + changeDerivationPostPath, + this.network + ); changeAddress.tags = await getAllLabelsForAddress(db, changeAddress.address); changeAddress.isChange = true; changeAddress.isMine = true; diff --git a/packages/shared-server/src/utils/accountMap.ts b/packages/shared-server/src/utils/accountMap.ts index 4c2771b9..79ba9d8d 100644 --- a/packages/shared-server/src/utils/accountMap.ts +++ b/packages/shared-server/src/utils/accountMap.ts @@ -195,7 +195,11 @@ const getMultisigAddressFromPubKeys = ( currentBitcoinNetwork: Network ): Address => { const rawPubkeys = pubkeys.map((publicKey) => publicKey.childPubKey); - rawPubkeys.sort(); + if (config.bitgo) { + // if bitgo, skip sorting + } else { + rawPubkeys.sort(); + } const address = generateMultisigFromPublicKeys( getUnchainedNetworkFromBjslibNetwork(currentBitcoinNetwork), @@ -203,6 +207,7 @@ const getMultisigAddressFromPubKeys = ( config.quorum.requiredSigners, ...rawPubkeys ); + const decoratedAddress = { ...address, bip32derivation: pubkeys.map((publicKey) => publicKey.bip32derivation) diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 2bb28172..a0494fbb 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -383,17 +383,22 @@ export interface LilyZeroDotOneConfig { exchanges: any[]; // TODO: change } +type DeviceType = + | 'coldcard' + | 'trezor' + | 'ledger' + | 'phone' + | 'lily' + | 'cobo' + | 'bitbox02' + | 'unchained' + | 'bitgo' + | 'onramp' + | 'kingdom-trust' + | 'unknown'; + export interface Device { - type: - | 'coldcard' - | 'trezor' - | 'ledger' - | 'phone' - | 'lily' - | 'cobo' - | 'bitbox02' - | 'unchained' - | 'unknown'; + type: DeviceType; fingerprint: string; model: string; // KBC-TODO: get more specific with this owner?: { @@ -468,12 +473,14 @@ export enum AddressType { P2WSH = 'P2WSH', P2WPKH = 'P2WPKH', p2sh = 'p2sh', - multisig = 'multisig' + multisig = 'multisig', + 'P2SH-P2WSH' = 'P2SH-P2WSH' } export interface OnChainConfig { id: AccountId; type: 'onchain'; + bitgo?: boolean; created_at: number; name: string; network: 'mainnet' | 'testnet';