refactor: add derivation path, ref #3868

This commit is contained in:
kyranjamie
2023-06-16 12:47:18 +02:00
committed by kyranjamie
parent 861b7153b2
commit 392aa01933
11 changed files with 131 additions and 79 deletions

View File

@@ -11,21 +11,19 @@ import {
} from '@app/common/transactions/bitcoin/coinselect/local-coin-selection';
import { useSpendableCurrentNativeSegwitAccountUtxos } from '@app/query/bitcoin/address/use-spendable-native-segwit-utxos';
import { useBitcoinScureLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain';
import {
useCurrentAccountNativeSegwitAddressIndexZero,
useCurrentAccountNativeSegwitSigner,
useCurrentBitcoinNativeSegwitAddressIndexPublicKeychain,
} from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
interface GenerateNativeSegwitTxValues {
amount: Money;
recipient: string;
}
export function useGenerateSignedNativeSegwitTx() {
const currentAccountBtcAddress = useCurrentAccountNativeSegwitAddressIndexZero();
const { data: utxos } = useSpendableCurrentNativeSegwitAccountUtxos();
const currentAddressIndexKeychain = useCurrentBitcoinNativeSegwitAddressIndexPublicKeychain();
const createSigner = useCurrentAccountNativeSegwitSigner();
const {
address,
publicKeychain: currentAddressIndexKeychain,
sign,
} = useCurrentAccountNativeSegwitIndexZeroSigner();
const networkMode = useBitcoinScureLibNetworkConfig();
@@ -33,11 +31,8 @@ export function useGenerateSignedNativeSegwitTx() {
(values: GenerateNativeSegwitTxValues, feeRate: number, isSendingMax?: boolean) => {
if (!utxos) return;
if (!feeRate) return;
if (!createSigner) return;
try {
const signer = createSigner(0);
const tx = new btc.Transaction();
const amountAsNumber = values.amount.amount.toNumber();
@@ -77,12 +72,12 @@ export function useGenerateSignedNativeSegwitTx() {
// When coin selection returns output with no address we assume it is
// a change output
if (!output.address) {
tx.addOutputAddress(currentAccountBtcAddress, BigInt(output.value), networkMode);
tx.addOutputAddress(address, BigInt(output.value), networkMode);
return;
}
tx.addOutputAddress(values.recipient, BigInt(output.value), networkMode);
});
signer.sign(tx);
sign(tx);
tx.finalize();
return { hex: tx.hex, fee };
@@ -92,12 +87,6 @@ export function useGenerateSignedNativeSegwitTx() {
return null;
}
},
[
createSigner,
currentAccountBtcAddress,
currentAddressIndexKeychain?.publicKey,
networkMode,
utxos,
]
[address, currentAddressIndexKeychain?.publicKey, networkMode, sign, utxos]
);
}

View File

@@ -2,13 +2,13 @@ import {
Brc20Token,
useBrc20TokensByAddressQuery,
} from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.query';
import { useCurrentAccountTaprootAddressIndexZeroPayment } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { useCurrentAccountTaprootIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
interface Brc20TokensLoaderProps {
children(brc20Tokens: Brc20Token[]): JSX.Element;
}
export function Brc20TokensLoader({ children }: Brc20TokensLoaderProps) {
const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootAddressIndexZeroPayment();
const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootIndexZeroSigner();
const { data: brc20Tokens } = useBrc20TokensByAddressQuery(bitcoinAddressTaproot);
if (!bitcoinAddressTaproot || !brc20Tokens) return null;
return children(brc20Tokens);

View File

@@ -2,7 +2,7 @@ import * as btc from '@scure/btc-signer';
import { Stack, color } from '@stacks/ui';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountTaprootAddressIndexZeroPayment } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { useCurrentAccountTaprootIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { usePsbtDecodedRequest } from '../../hooks/use-psbt-decoded-request';
import { DecodedPsbt } from '../../hooks/use-psbt-signer';
@@ -15,7 +15,7 @@ interface PsbtDecodedRequestProps {
}
export function PsbtDecodedRequest({ psbt }: PsbtDecodedRequestProps) {
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootAddressIndexZeroPayment();
const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootIndexZeroSigner();
const unsignedInputs: btc.TransactionInputRequired[] = psbt.global.unsignedTx?.inputs ?? [];
const unsignedOutputs: btc.TransactionOutputRequired[] = psbt.global.unsignedTx?.outputs ?? [];

View File

@@ -10,7 +10,7 @@ import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-search-params';
import { initialSearchParams } from '@app/common/initial-search-params';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountTaprootAddressIndexZeroPayment } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { useCurrentAccountTaprootIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import { useAppPermissions } from '@app/store/app-permissions/app-permissions.slice';
@@ -32,14 +32,15 @@ export function useGetAddresses() {
const { tabId, origin, requestId } = useRpcRequestParams();
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const taprootPayment = useCurrentAccountTaprootAddressIndexZeroPayment();
const taprootSigner = useCurrentAccountTaprootIndexZeroSigner();
const stacksAccount = useCurrentStacksAccount();
const taprootAddressResponse: BtcAddress = {
symbol: 'BTC',
type: 'p2tr',
address: taprootPayment.address,
publicKey: bytesToHex(taprootPayment.publicKey),
address: taprootSigner.address,
publicKey: bytesToHex(taprootSigner.publicKey),
derivationPath: taprootSigner.derivationPath,
};
const nativeSegwitAddressResponse: BtcAddress = {
@@ -47,6 +48,7 @@ export function useGetAddresses() {
type: 'p2wpkh',
address: nativeSegwitSigner.address,
publicKey: bytesToHex(nativeSegwitSigner.publicKey),
derivationPath: nativeSegwitSigner.derivationPath,
};
const stacksAddressResponse = {

View File

@@ -1,7 +1,7 @@
import { useConfigOrdinalsbot } from '@app/query/common/remote-config/remote-config.query';
import { useAppDispatch } from '@app/store';
import { useCurrentAccountIndex } from '@app/store/accounts/account';
import { useCurrentAccountTaprootAddressIndexZeroPayment } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { useCurrentAccountTaprootIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
import { brc20TransferInitiated } from '@app/store/ordinals/ordinals.slice';
@@ -34,7 +34,7 @@ export function useBrc20Transfers() {
const dispatch = useAppDispatch();
const currentAccountIndex = useCurrentAccountIndex();
const ordinalsbotClient = useOrdinalsbotClient();
const { address } = useCurrentAccountTaprootAddressIndexZeroPayment();
const { address } = useCurrentAccountTaprootIndexZeroSigner();
const { data: fees } = useAverageBitcoinFeeRates();
return {

View File

@@ -9,11 +9,16 @@ import { getBtcSignerLibNetworkConfigByMode } from '@shared/crypto/bitcoin/bitco
import {
deriveAddressIndexKeychainFromAccount,
deriveAddressIndexZeroFromAccount,
whenPaymentType,
} from '@shared/crypto/bitcoin/bitcoin.utils';
import { deriveTaprootAccountFromRootKeychain } from '@shared/crypto/bitcoin/p2tr-address-gen';
import {
deriveTaprootAccountFromRootKeychain,
getTaprootAddressIndexDerivationPath,
} from '@shared/crypto/bitcoin/p2tr-address-gen';
import {
deriveNativeSegWitAccountKeychain,
getNativeSegWitPaymentFromAddressIndex,
getNativeSegwitAddressIndexDerivationPath,
} from '@shared/crypto/bitcoin/p2wpkh-address-gen';
import { mnemonicToRootNode } from '@app/common/keychain/keychain';
@@ -77,12 +82,13 @@ export function useBitcoinScureLibNetworkConfig() {
}
interface BitcoinSignerFactoryArgs {
accountIndex: number;
accountKeychain: HDKey;
paymentFn(keychain: HDKey, network: BitcoinNetworkModes): any;
network: BitcoinNetworkModes;
}
export function bitcoinSignerFactory<T extends BitcoinSignerFactoryArgs>(args: T) {
const { network, paymentFn, accountKeychain } = args;
const { accountIndex, network, paymentFn, accountKeychain } = args;
return (addressIndex: number) => {
const addressIndexKeychain =
deriveAddressIndexKeychainFromAccount(accountKeychain)(addressIndex);
@@ -96,6 +102,13 @@ export function bitcoinSignerFactory<T extends BitcoinSignerFactoryArgs>(args: T
payment,
addressIndex,
publicKeychain,
derivationPath: whenPaymentType(payment.type)({
p2wpkh: getNativeSegwitAddressIndexDerivationPath(network, accountIndex, addressIndex),
p2tr: getTaprootAddressIndexDerivationPath(network, accountIndex, addressIndex),
'p2wpkh-p2sh': 'Not supported',
p2pkh: 'Not supported',
p2sh: 'Not supported',
}),
get address() {
if (!payment.address) throw new Error('Unable to get address from payment');
return payment.address;
@@ -136,6 +149,7 @@ function createSignersForAllNetworkTypes<T extends CreateSignersForAllNetworkTyp
function makeNetworkSigner(keychain: HDKey, network: BitcoinNetworkModes) {
return bitcoinSignerFactory({
accountIndex,
accountKeychain: keychain,
paymentFn: paymentFn as T['paymentFn'],
network,

View File

@@ -1,3 +1,4 @@
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { bitcoinNetworkModeToCoreNetworkMode } from '@shared/crypto/bitcoin/bitcoin.utils';
@@ -16,18 +17,21 @@ import {
function useNativeSegwitActiveNetworkAccountPrivateKeychain() {
const network = useCurrentNetwork();
return useSelector(
whenNetwork(bitcoinNetworkModeToCoreNetworkMode(network.chain.bitcoin.network))({
mainnet: selectMainnetNativeSegWitKeychain,
testnet: selectTestnetNativeSegWitKeychain,
})
const selector = useMemo(
() =>
whenNetwork(bitcoinNetworkModeToCoreNetworkMode(network.chain.bitcoin.network))({
mainnet: selectMainnetNativeSegWitKeychain,
testnet: selectTestnetNativeSegWitKeychain,
}),
[network.chain.bitcoin.network]
);
return useSelector(selector);
}
export function useNativeSegwitCurrentAccountPrivateKeychain() {
const keychain = useNativeSegwitActiveNetworkAccountPrivateKeychain();
const currentAccountIndex = useCurrentAccountIndex();
return keychain?.(currentAccountIndex);
return useMemo(() => keychain?.(currentAccountIndex), [currentAccountIndex, keychain]);
}
export function useNativeSegwitNetworkSigners() {
@@ -44,13 +48,16 @@ export function useNativeSegwitNetworkSigners() {
function useNativeSegwitSigner(accountIndex: number) {
const network = useCurrentNetwork();
const accountKeychain = useNativeSegwitActiveNetworkAccountPrivateKeychain()?.(accountIndex);
if (!accountKeychain) return;
return bitcoinSignerFactory({
accountKeychain,
paymentFn: getNativeSegWitPaymentFromAddressIndex,
network: network.chain.bitcoin.network,
});
return useMemo(() => {
if (!accountKeychain) return;
return bitcoinSignerFactory({
accountIndex,
accountKeychain,
paymentFn: getNativeSegWitPaymentFromAddressIndex,
network: network.chain.bitcoin.network,
});
}, [accountIndex, accountKeychain, network.chain.bitcoin.network]);
}
export function useCurrentAccountNativeSegwitSigner() {
@@ -60,8 +67,10 @@ export function useCurrentAccountNativeSegwitSigner() {
export function useCurrentAccountNativeSegwitIndexZeroSigner() {
const signer = useCurrentAccountNativeSegwitSigner();
if (!signer) throw new Error('No signer');
return signer(0);
return useMemo(() => {
if (!signer) throw new Error('No signer');
return signer(0);
}, [signer]);
}
/**
@@ -69,7 +78,7 @@ export function useCurrentAccountNativeSegwitIndexZeroSigner() {
*/
export function useCurrentAccountNativeSegwitAddressIndexZero() {
const signer = useCurrentAccountNativeSegwitSigner();
return signer?.(0).payment.address as string;
return useMemo(() => signer?.(0).payment.address, [signer]) as string;
}
/**
@@ -79,11 +88,3 @@ export function useNativeSegwitAccountIndexAddressIndexZero(accountIndex: number
const signer = useNativeSegwitSigner(accountIndex)?.(0);
return signer?.payment.address as string;
}
/**
* @deprecated Use signer.publicKeychain directly instead
*/
export function useCurrentBitcoinNativeSegwitAddressIndexPublicKeychain() {
const signer = useCurrentAccountNativeSegwitSigner();
return signer?.(0).publicKeychain;
}

View File

@@ -1,3 +1,4 @@
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { BitcoinNetworkModes } from '@shared/constants';
@@ -32,8 +33,10 @@ export function useTaprootCurrentAccountPrivateKeychain() {
export function useTaprootAccountKeychain(accountIndex: number) {
const accountKeychain = useTaprootActiveNetworkAccountPrivateKeychain();
if (!accountKeychain) return; // TODO: Revisit this return early
return accountKeychain(accountIndex);
return useMemo(() => {
if (!accountKeychain) return; // TODO: Revisit this return early
return accountKeychain(accountIndex);
}, [accountIndex, accountKeychain]);
}
export function useTaprootNetworkSigners() {
@@ -49,13 +52,24 @@ export function useTaprootNetworkSigners() {
function useTaprootSigner(accountIndex: number, network: BitcoinNetworkModes) {
const accountKeychain = useTaprootAccountKeychain(accountIndex);
if (!accountKeychain) return; // TODO: Revisit this return early
return bitcoinSignerFactory({
accountKeychain,
paymentFn: getTaprootPaymentFromAddressIndex,
network,
});
return useMemo(() => {
if (!accountKeychain) return; // TODO: Revisit this return early
return bitcoinSignerFactory({
accountIndex,
accountKeychain,
paymentFn: getTaprootPaymentFromAddressIndex,
network,
});
}, [accountIndex, accountKeychain, network]);
}
export function useCurrentAccountTaprootIndexZeroSigner() {
const signer = useCurrentAccountTaprootSigner();
return useMemo(() => {
if (!signer) throw new Error('No signer');
return signer(0);
}, [signer]);
}
export function useCurrentAccountTaprootSigner() {
@@ -63,17 +77,3 @@ export function useCurrentAccountTaprootSigner() {
const network = useCurrentNetwork();
return useTaprootSigner(currentAccountIndex, network.chain.bitcoin.network);
}
export function useCurrentAccountTaprootAddressIndexZeroPayment() {
const createSigner = useCurrentAccountTaprootSigner();
const indexZeroSigner = createSigner?.(0);
if (!indexZeroSigner?.payment.address) throw new Error('No address found');
const publicKey = indexZeroSigner.publicKeychain.publicKey;
if (!publicKey) throw new Error('No public key found');
// Creating new object to have known property types
return {
address: indexZeroSigner.payment.address,
publicKey,
type: indexZeroSigner.payment.type,
};
}

View File

@@ -1,3 +1,4 @@
import { PaymentTypes } from '@btckit/types';
import { hexToBytes } from '@noble/hashes/utils';
import { HDKey } from '@scure/bip32';
import * as btc from '@scure/btc-signer';
@@ -84,3 +85,32 @@ export function getAddressFromOutScript(script: Uint8Array, network: BitcoinNetw
logger.error(`Unknown address type=${outScript.type}`);
return '';
}
type BtcSignerLibPaymentTypeIdentifers = 'wpkh' | 'wsh' | 'tr' | 'pkh' | 'sh';
const paymentTypeMap: Record<BtcSignerLibPaymentTypeIdentifers, PaymentTypes> = {
wpkh: 'p2wpkh',
wsh: 'p2wpkh-p2sh',
tr: 'p2tr',
pkh: 'p2pkh',
sh: 'p2sh',
};
function btcSignerLibPaymentTypeToPaymentTypeMap(payment: BtcSignerLibPaymentTypeIdentifers) {
return paymentTypeMap[payment];
}
function isBtcSignerLibPaymentType(payment: string): payment is BtcSignerLibPaymentTypeIdentifers {
return payment in paymentTypeMap;
}
function parseKnownPaymentType(payment: BtcSignerLibPaymentTypeIdentifers | PaymentTypes) {
return isBtcSignerLibPaymentType(payment)
? btcSignerLibPaymentTypeToPaymentTypeMap(payment)
: payment;
}
type PaymentTypeMap<T> = Record<PaymentTypes, T>;
export function whenPaymentType(mode: PaymentTypes | BtcSignerLibPaymentTypeIdentifers) {
return <T>(paymentMap: PaymentTypeMap<T>): T => paymentMap[parseKnownPaymentType(mode)];
}

View File

@@ -11,6 +11,14 @@ function getTaprootAccountDerivationPath(network: BitcoinNetworkModes, accountIn
return `m/86'/${getBitcoinCoinTypeIndexByNetwork(network)}'/${accountIndex}'`;
}
export function getTaprootAddressIndexDerivationPath(
network: BitcoinNetworkModes,
accountIndex: number,
addressIndex: number
) {
return getTaprootAccountDerivationPath(network, accountIndex) + `/0/${addressIndex}`;
}
export function deriveTaprootAccountFromRootKeychain(
keychain: HDKey,
network: BitcoinNetworkModes

View File

@@ -10,13 +10,21 @@ import {
getBitcoinCoinTypeIndexByNetwork,
} from './bitcoin.utils';
function getNativeSegWitAccountDerivationPath(network: BitcoinNetworkModes, accountIndex: number) {
function getNativeSegwitAccountDerivationPath(network: BitcoinNetworkModes, accountIndex: number) {
return `m/84'/${getBitcoinCoinTypeIndexByNetwork(network)}'/${accountIndex}'`;
}
export function getNativeSegwitAddressIndexDerivationPath(
network: BitcoinNetworkModes,
accountIndex: number,
addressIndex: number
) {
return getNativeSegwitAccountDerivationPath(network, accountIndex) + `/0/${addressIndex}`;
}
export function deriveNativeSegWitAccountKeychain(keychain: HDKey, network: NetworkModes) {
if (keychain.depth !== DerivationPathDepth.Root) throw new Error('Keychain passed is not a root');
return (index: number) => keychain.derive(getNativeSegWitAccountDerivationPath(network, index));
return (index: number) => keychain.derive(getNativeSegwitAccountDerivationPath(network, index));
}
export function getNativeSegWitPaymentFromAddressIndex(