Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add padded decimals, refactor tx signing #3583

Merged
merged 4 commits into from
Apr 21, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function AddressDisplayerLayout({ isEven, ...props }: AddressDisplayerLay
color={isEven ? figmaTheme.textSubdued : figmaTheme.text}
fontFamily="Fira Code"
mr="tight"
lineHeight="24px"
{...props}
/>
);
Expand Down
2 changes: 2 additions & 0 deletions src/app/components/info-card/info-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export function InfoCardRow({ title, value, ...props }: InfoCardRowProps) {
color={figmaTheme.text}
fontWeight="500"
data-testid={SharedComponentsSelectors.InfoCardRowValue}
fontVariant="tabular-nums"
letterSpacing="-0.01em"
>
{value}
</Text>
Expand Down
35 changes: 16 additions & 19 deletions src/app/pages/psbt-request/hooks/use-psbt-request.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,8 @@ import { isNumber, isUndefined } from '@shared/utils';
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { useOnMount } from '@app/common/hooks/use-on-mount';
import { getPsbtPayloadFromToken } from '@app/common/psbt/requests';
import {
useSignBitcoinNativeSegwitInputAtIndex,
useSignBitcoinNativeSegwitTx,
} from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import {
useSignBitcoinTaprootInputAtIndex,
useSignBitcoinTaprootTx,
} from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { useCurrentAccountNativeSegwitSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountTaprootSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { usePsbtRequestSearchParams } from '@app/store/psbts/requests.hooks';

export function usePsbtRequest() {
Expand All @@ -27,10 +21,12 @@ export function usePsbtRequest() {
const [tx, setTx] = useState<btc.Transaction>();
const analytics = useAnalytics();
const { requestToken, tabId } = usePsbtRequestSearchParams();
const signNativeSegwitTxAtIndex = useSignBitcoinNativeSegwitInputAtIndex();
const signNativeSegwitTx = useSignBitcoinNativeSegwitTx();
const signTaprootTxAtIndex = useSignBitcoinTaprootInputAtIndex();
const signTaprootTx = useSignBitcoinTaprootTx();

const createNativeSegwitSigner = useCurrentAccountNativeSegwitSigner();
const createTaprootSigner = useCurrentAccountTaprootSigner();

const nativeSegwitSigner = createNativeSegwitSigner?.(0);
const taprootSigner = createTaprootSigner?.(0);

useOnMount(() => {
if (!requestToken) return;
Expand All @@ -49,6 +45,7 @@ export function usePsbtRequest() {

const getDecodedPsbt = useCallback(() => {
if (!psbtPayload || !tx) return;

try {
return btc.RawPSBTV0.decode(hexToBytes(psbtPayload.hex));
} catch (e0) {
Expand All @@ -69,16 +66,16 @@ export function usePsbtRequest() {
const signPsbtAtIndex = useCallback(
(allowedSighash: btc.SignatureHash[], idx: number, tx: btc.Transaction) => {
try {
signNativeSegwitTxAtIndex({ allowedSighash, idx, tx });
nativeSegwitSigner?.signIndex(tx, idx, allowedSighash);
} catch (e1) {
try {
signTaprootTxAtIndex({ allowedSighash, idx, tx });
taprootSigner?.signIndex(tx, idx, allowedSighash);
} catch (e2) {
logger.error('Error signing tx at provided index', e1, e2);
}
}
},
[signNativeSegwitTxAtIndex, signTaprootTxAtIndex]
[nativeSegwitSigner, taprootSigner]
);

const onSignPsbt = useCallback(() => {
Expand All @@ -101,10 +98,10 @@ export function usePsbtRequest() {
}
} else {
try {
signNativeSegwitTx(tx);
nativeSegwitSigner?.sign(tx);
} catch (e1) {
try {
signTaprootTx(tx);
taprootSigner?.sign(tx);
} catch (e2) {
logger.error('Error signing tx', e1, e2);
}
Expand All @@ -124,13 +121,13 @@ export function usePsbtRequest() {
});
}, [
analytics,
nativeSegwitSigner,
psbtPayload?.allowedSighash,
psbtPayload?.signAtIndex,
requestToken,
signNativeSegwitTx,
signPsbtAtIndex,
signTaprootTx,
tabId,
taprootSigner,
tx,
]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,35 @@ import { useCallback } from 'react';

import * as btc from '@scure/btc-signer';

import { logger } from '@shared/logger';
import { BitcoinSendFormValues } from '@shared/models/form.model';

import { btcToSat } from '@app/common/money/unit-conversion';
import { determineUtxosForSpend } from '@app/common/transactions/bitcoin/coinselect/local-coin-selection';
import { useGetUtxosByAddressQuery } from '@app/query/bitcoin/address/utxos-by-address.query';
import { useBitcoinLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain';
import {
useCurrentAccountNativeSegwitSigner,
useCurrentBitcoinNativeSegwitAddressIndexPublicKeychain,
useCurrentBtcNativeSegwitAccountAddressIndexZero,
useSignBitcoinNativeSegwitTx,
} from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';

export function useGenerateSignedBitcoinTx() {
const currentAccountBtcAddress = useCurrentBtcNativeSegwitAccountAddressIndexZero();
const { data: utxos } = useGetUtxosByAddressQuery(currentAccountBtcAddress);
const currentAddressIndexKeychain = useCurrentBitcoinNativeSegwitAddressIndexPublicKeychain();
const signTx = useSignBitcoinNativeSegwitTx();
const createSigner = useCurrentAccountNativeSegwitSigner();
const networkMode = useBitcoinLibNetworkConfig();

return useCallback(
(values: BitcoinSendFormValues, feeRate: number) => {
if (!utxos) return;
if (!feeRate) return;
if (!createSigner) return;

try {
const signer = createSigner(0);

const tx = new btc.Transaction();

const { inputs, outputs, fee } = determineUtxosForSpend({
Expand All @@ -36,11 +40,10 @@ export function useGenerateSignedBitcoinTx() {
feeRate,
});

// eslint-disable-next-line no-console
console.log('coinselect', { inputs, outputs, fee });
logger.info('coinselect', { inputs, outputs, fee });

if (!inputs) throw new Error('No inputs to sign');
if (!outputs) throw new Error('No outputs to sign');
if (!inputs.length) throw new Error('No inputs to sign');
if (!outputs.length) throw new Error('No outputs to sign');

if (outputs.length > 2)
throw new Error('Address reuse mode: wallet should have max 2 outputs');
Expand All @@ -66,15 +69,22 @@ export function useGenerateSignedBitcoinTx() {
}
tx.addOutputAddress(values.recipient, BigInt(output.value), networkMode);
});
signTx(tx);
signer.sign(tx);
tx.finalize();

return { hex: tx.hex, fee };
} catch (e) {
// eslint-disable-next-line no-console
console.log('Error signing bitcoin transaction', e);
return null;
}
},
[currentAccountBtcAddress, currentAddressIndexKeychain?.publicKey, networkMode, signTx, utxos]
[
createSigner,
currentAccountBtcAddress,
currentAddressIndexKeychain?.publicKey,
networkMode,
utxos,
]
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { RouteUrls } from '@shared/route-urls';
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { useRouteHeader } from '@app/common/hooks/use-route-header';
import { baseCurrencyAmountInQuote } from '@app/common/money/calculate-money';
import { formatMoney, formatMoneyPadded, i18nFormatCurrency } from '@app/common/money/format-money';
import { formatMoneyPadded, i18nFormatCurrency } from '@app/common/money/format-money';
import { satToBtc } from '@app/common/money/unit-conversion';
import { FormAddressDisplayer } from '@app/components/address-displayer/form-address-displayer';
import {
Expand Down Expand Up @@ -61,10 +61,10 @@ export function BtcSendFormConfirmation() {
const txFiatValueSymbol = btcMarketData.price.symbol;

const feeInBtc = satToBtc(fee);
const totalSpend = formatMoney(
const totalSpend = formatMoneyPadded(
createMoneyFromDecimal(Number(transferAmount) + Number(feeInBtc), symbol)
);
const sendingValue = formatMoney(createMoneyFromDecimal(Number(transferAmount), symbol));
const sendingValue = formatMoneyPadded(createMoneyFromDecimal(Number(transferAmount), symbol));
const summaryFee = formatMoneyPadded(createMoney(Number(fee), symbol));

async function initiateTransaction() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,22 +116,6 @@ export function useBtcNativeSegwitAccountIndexAddressIndexZero(accountIndex: num
return useDeriveNativeSegWitAccountIndexAddressIndexZero(xpub)?.address as string;
}

/**
* @deprecated
* Let's update to use the signer structure, also used in taproot
*/
export function useSignBitcoinNativeSegwitTx() {
const index = useCurrentAccountIndex();
const keychain = useNativeSegWitCurrentNetworkAccountKeychain()?.(index);
return useCallback(
(tx: btc.Transaction) => {
if (isUndefined(keychain)) return;
tx.sign(deriveAddressIndexZeroFromAccount(keychain).privateKey!);
},
[keychain]
);
}

export function useCurrentAccountNativeSegwitSigner() {
const network = useCurrentNetwork();
const index = useCurrentAccountIndex();
Expand All @@ -153,30 +137,12 @@ export function useCurrentAccountNativeSegwitSigner() {

tx.sign(addressIndexKeychain.privateKey);
},
signIndex(tx: btc.Transaction, index: number) {
signIndex(tx: btc.Transaction, index: number, allowedSighash?: btc.SignatureHash[]) {
if (!addressIndexKeychain.privateKey)
throw new Error('Unable to sign taproot transaction, no private key found');

tx.signIdx(addressIndexKeychain.privateKey, index);
tx.signIdx(addressIndexKeychain.privateKey, index, allowedSighash);
},
};
};
}

interface UseSignBitcoinNativeSegwitInputAtIndexArgs {
allowedSighash?: btc.SignatureHash[];
idx: number;
tx: btc.Transaction;
}
export function useSignBitcoinNativeSegwitInputAtIndex() {
const index = useCurrentAccountIndex();
const keychain = useNativeSegWitCurrentNetworkAccountKeychain()?.(index);

return useCallback(
({ allowedSighash, idx, tx }: UseSignBitcoinNativeSegwitInputAtIndexArgs) => {
if (isUndefined(keychain)) return;
tx.signIdx(deriveAddressIndexZeroFromAccount(keychain).privateKey!, idx, allowedSighash);
},
[keychain]
);
}
35 changes: 2 additions & 33 deletions src/app/store/accounts/blockchain/bitcoin/taproot-account.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,37 +83,6 @@ export function useCurrentBtcTaprootAccountAddressIndexZeroPayment() {
return { address: payment.address, type: payment.type };
}

export function useSignBitcoinTaprootTx() {
const index = useCurrentAccountIndex();
const keychain = useTaprootCurrentNetworkAccountPrivateKeychain()?.(index);

return useCallback(
(tx: btc.Transaction) => {
if (isUndefined(keychain)) return;
tx.sign(deriveAddressIndexZeroFromAccount(keychain).privateKey!);
},
[keychain]
);
}

interface UseSignBitcoinTaprootInputAtIndexArgs {
allowedSighash?: btc.SignatureHash[];
idx: number;
tx: btc.Transaction;
}
export function useSignBitcoinTaprootInputAtIndex() {
const index = useCurrentAccountIndex();
const keychain = useTaprootCurrentNetworkAccountPrivateKeychain()?.(index);

return useCallback(
({ allowedSighash, idx, tx }: UseSignBitcoinTaprootInputAtIndexArgs) => {
if (isUndefined(keychain)) return;
tx.signIdx(deriveAddressIndexZeroFromAccount(keychain).privateKey!, idx, allowedSighash);
},
[keychain]
);
}

// TODO: Address index 0 is hardcoded here bc this is only used to pass the first
// taproot address to the app thru the auth response. This is only temporary, it
// should be removed once the request address api is in place.
Expand Down Expand Up @@ -162,11 +131,11 @@ export function useCurrentAccountTaprootSigner() {

tx.sign(addressIndexKeychain.privateKey);
},
signIndex(tx: btc.Transaction, index: number) {
signIndex(tx: btc.Transaction, index: number, allowedSighash?: btc.SignatureHash[]) {
if (!addressIndexKeychain.privateKey)
throw new Error('Unable to sign taproot transaction, no private key found');

tx.signIdx(addressIndexKeychain.privateKey, index);
tx.signIdx(addressIndexKeychain.privateKey, index, allowedSighash);
},
};
};
Expand Down