Skip to content

Commit

Permalink
feat: address monitor and btc ntx notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
alexp3y committed Feb 4, 2025
1 parent 160ba72 commit 1f4aa1e
Show file tree
Hide file tree
Showing 17 changed files with 530 additions and 174 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@
"ts-node": "10.9.2",
"ts-unused-exports": "10.0.1",
"tsconfig-paths-webpack-plugin": "4.1.0",
"type-fest": "4.30.2",
"typescript": "5.4.5",
"vitest": "2.0.5",
"vm-browserify": "1.1.2",
Expand All @@ -370,8 +371,7 @@
"webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.1",
"webpack-hot-middleware": "2.26.1",
"webpack-shell-plugin": "0.5.0",
"type-fest": "4.30.2"
"webpack-shell-plugin": "0.5.0"
},
"resolutions": {
"cross-spawn": "7.0.5",
Expand Down
200 changes: 100 additions & 100 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion scripts/generate-manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const manifest = {
manifest_version: 3,
author: 'Leather Wallet, LLC',
description: 'Leather Bitcoin Wallet - Your Bitcoin Wallet for DeFi, NFTs, Ordinals, and dApps',
permissions: ['contextMenus', 'storage', 'unlimitedStorage', 'alarms', 'notifications'],
permissions: ['contextMenus', 'storage', 'unlimitedStorage', 'notifications'],
commands: {
_execute_browser_action: {
suggested_key: {
Expand Down
78 changes: 78 additions & 0 deletions src/app/features/address-monitor/use-monitorable-addresses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useMemo } from 'react';

import type { HDKey } from '@scure/bip32';
import type { P2Ret } from '@scure/btc-signer/payment';

import {
type SupportedPaymentType,
deriveAddressIndexZeroFromAccount,
getNativeSegwitPaymentFromAddressIndex,
getTaprootPaymentFromAddressIndex,
} from '@leather.io/bitcoin';
import type { BitcoinNetworkModes } from '@leather.io/models';
import { createNullArrayOfLength, isDefined } from '@leather.io/utils';

import { useCurrentAccountIndex } from '@app/store/accounts/account';
import { useGenerateNativeSegwitAccount } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useGenerateTaprootAccount } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { useStacksAccounts } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import { useCurrentNetworkId } from '@app/store/networks/networks.selectors';
import type { MonitoredAddress } from '@background/monitors/address-monitor';

const paymentFnMap: Record<
SupportedPaymentType,
(keychain: HDKey, network: BitcoinNetworkModes) => P2Ret
> = {
p2tr: getTaprootPaymentFromAddressIndex,
p2wpkh: getNativeSegwitPaymentFromAddressIndex,
};

export function useMonitorableAddresses() {
const currentAccountIndex = useCurrentAccountIndex();
const currentNetworkId = useCurrentNetworkId();
const createNativeSegwitAccount = useGenerateNativeSegwitAccount();
const createTaprootAccount = useGenerateTaprootAccount();

const stacksAccounts = useStacksAccounts();

return useMemo(() => {
if (!stacksAccounts || !currentNetworkId) return;

const stacksAddresses = stacksAccounts.map(
account =>
({
accountIndex: account.index,
address: account.address,
chain: 'stacks',
isCurrent: account.index === currentAccountIndex,
}) as MonitoredAddress
);
const btcAddresses = createNullArrayOfLength(stacksAccounts.length).flatMap((_, index) =>
[createNativeSegwitAccount(index), createTaprootAccount(index)]
.filter(isDefined)
.map(account => {
const addressIndexKeychain = deriveAddressIndexZeroFromAccount(account.keychain);
if (account.type !== 'p2tr' && account.type !== 'p2wpkh') return undefined;
const payment = paymentFnMap[account.type](addressIndexKeychain, 'mainnet');
return {
accountIndex: index,
address: payment.address,
chain: 'bitcoin',
isCurrent: index === currentAccountIndex,
} as MonitoredAddress;
})
.filter(isDefined)
);
// if one address array is empty and the other not, we're in an intermediate state
return (stacksAddresses.length === 0 && btcAddresses.length > 0) ||
(btcAddresses.length === 0 && stacksAddresses.length > 0)
? undefined
: [...stacksAddresses, ...btcAddresses];
}, [
createNativeSegwitAccount,
createTaprootAccount,
stacksAccounts,
currentNetworkId,
currentAccountIndex,
]);
}
29 changes: 29 additions & 0 deletions src/app/features/address-monitor/use-sync-address-monitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useEffect, useRef } from 'react';

import isEqual from 'lodash.isequal';

import { logger } from '@shared/logger';
import { InternalMethods } from '@shared/message-types';
import { sendMessage } from '@shared/messages';

import { useMonitorableAddresses } from '@app/features/address-monitor/use-monitorable-addresses';
import type { MonitoredAddress } from '@background/monitors/address-monitor';

export function useSyncAddressMonitor() {
const addresses = useMonitorableAddresses();
const prevAddresses = useRef<MonitoredAddress[]>([]);

useEffect(() => {
if (addresses && !isEqual(addresses, prevAddresses.current)) {
prevAddresses.current = addresses;

logger.debug('Syncing Monitored Addresses: ', addresses);
sendMessage({
method: InternalMethods.AddressMonitorUpdated,
payload: {
addresses,
},
});
}
}, [addresses]);
}
3 changes: 2 additions & 1 deletion src/app/features/container/container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { useOnWalletLock } from '@app/routes/hooks/use-on-wallet-lock';
import { useAppDispatch, useHasStateRehydrated } from '@app/store';
import { stxChainSlice } from '@app/store/chains/stx-chain.slice';

import { useSyncAddressMonitor } from '../address-monitor/use-sync-address-monitor';
import { useRestoreFormState } from '../popup-send-form-restoration/use-restore-form-state';

export function Container() {
Expand All @@ -28,7 +29,7 @@ export function Container() {
const dispatch = useAppDispatch();

const hasStateRehydrated = useHasStateRehydrated();

useSyncAddressMonitor();
useOnWalletLock(() => closeWindow());
useOnSignOut(() => closeWindow());
useRestoreFormState();
Expand Down
2 changes: 2 additions & 0 deletions src/app/pages/unlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Content } from '@app/components/layout';
import { RequestPassword } from '@app/components/request-password';
import { UnlockHeader } from '@app/features/container/headers/unlock.header';

import { useGetAddresses } from './rpc-get-addresses/use-get-addresses';

Check failure on line 7 in src/app/pages/unlock.tsx

View workflow job for this annotation

GitHub Actions / typecheck

'useGetAddresses' is declared but its value is never read.

export function Unlock() {
const navigate = useNavigate();
// Here we want to return to the previous route. The user could land on any
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const selectCurrentNetworkNativeSegwitAccountBuilder = createSelector(
nativeSegwitKeychains[bitcoinNetworkToNetworkMode(network.chain.bitcoin.bitcoinNetwork)]
);

function useNativeSegwitAccountBuilder() {
export function useGenerateNativeSegwitAccount() {
return useSelector(selectCurrentNetworkNativeSegwitAccountBuilder);
}

Expand All @@ -72,7 +72,7 @@ export function useNativeSegwitNetworkSigners() {
}

export function useNativeSegwitSigner(accountIndex: number) {
const account = useNativeSegwitAccountBuilder()(accountIndex);
const account = useGenerateNativeSegwitAccount()(accountIndex);
const extendedPublicKeyVersions = useBitcoinExtendedPublicKeyVersions();

return useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ const selectCurrentTaprootAccount = createSelector(
(taprootKeychain, accountIndex) => taprootKeychain(accountIndex)
);

export function useGenerateTaprootAccount() {
const generateTaprootAccount = useSelector(selectCurrentNetworkTaprootAccountBuilder);
return useMemo(() => generateTaprootAccount, [generateTaprootAccount]);
}

export function useTaprootAccount(accountIndex: number) {
const generateTaprootAccount = useSelector(selectCurrentNetworkTaprootAccountBuilder);
return useMemo(
Expand Down
6 changes: 6 additions & 0 deletions src/app/store/chains/stx-chain.selectors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import { useSelector } from 'react-redux';

import { RootState } from '@app/store';

export const selectStacksChain = (state: RootState) => state.chains.stx;

export function useStacksChain() {
return useSelector(selectStacksChain);
}
52 changes: 0 additions & 52 deletions src/background/alarms/transaction-monitor.ts

This file was deleted.

18 changes: 4 additions & 14 deletions src/background/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@ import { CONTENT_SCRIPT_PORT, type LegacyMessageFromContentScript } from '@share
import { WalletRequests } from '@shared/rpc/rpc-methods';
import { warnUsersAboutDevToolsDangers } from '@shared/utils/dev-tools-warning-log';

import {
monitorPendingConfirmations,
pendingConfirmationsAlarm,
} from './alarms/transaction-monitor';
import { initContextMenuActions } from './init-context-menus';
import { internalBackgroundMessageHandler } from './messaging/internal-methods/message-handler';
import {
handleLegacyExternalMethodFormat,
isLegacyMessage,
} from './messaging/legacy/legacy-external-message-handler';
import { rpcMessageHandler } from './messaging/rpc-message-handler';
import { initAddressMonitor } from './monitors/address-monitor';

initContextMenuActions();
warnUsersAboutDevToolsDangers();
Expand Down Expand Up @@ -64,13 +61,6 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
return true;
});

console.log('hello from background');

monitorPendingConfirmations()
.then(() => {
console.log('confirmation called');
})
.catch(() => {
// eslint-disable-next-line no-console
console.error('whoops!');
});
initAddressMonitor().catch(e => {
logger.error('Unable to Initialise Address Monitor: ', e);
});
14 changes: 14 additions & 0 deletions src/background/messaging/internal-methods/message-handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { logger } from '@shared/logger';
import { InternalMethods } from '@shared/message-types';
import { BackgroundMessages } from '@shared/messages';

import { syncAddressMonitor } from '@background/monitors/address-monitor';

function validateMessagesAreFromExtension(sender: chrome.runtime.MessageSender) {
// Only respond to internal messages from our UI, not content scripts in other applications
return sender.url?.startsWith(chrome.runtime.getURL(''));
Expand Down Expand Up @@ -28,5 +31,16 @@ export async function internalBackgroundMessageHandler(
return;
}
logger.debug('Internal message', message);

switch (message.method) {
case InternalMethods.AddressMonitorUpdated:
await syncAddressMonitor(message.payload.addresses);
break;
}

if (message.method.includes('bitcoinKeys/signOut')) {
await syncAddressMonitor([]);
}

sendResponse();
}
54 changes: 54 additions & 0 deletions src/background/monitors/address-monitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* eslint-disable no-console */
import { z } from 'zod';

import { BitcoinTransactionMonitor } from './address-monitors/bitcoin-transaction-monitor';

export const monitoredAddressSchema = z.object({
chain: z.enum(['bitcoin', 'stacks']),
accountIndex: z.number(),
isCurrent: z.boolean(),
address: z.string(),
});

export type MonitoredAddress = z.infer<typeof monitoredAddressSchema>;

export interface AddressMonitor {
syncAddresses(addresses: MonitoredAddress[]): void;
}

export class AddressMonitorContainer {
private _monitors: AddressMonitor[] = [];

init(addresses: MonitoredAddress[]) {
this._monitors = [new BitcoinTransactionMonitor(addresses)];
}
sync(addresses: MonitoredAddress[]) {
this._monitors.forEach(monitor => monitor.syncAddresses(addresses));
}
}

const monitorContainer = new AddressMonitorContainer();

export async function initAddressMonitor() {
const addresses = await readMonitoredAddressStore();
monitorContainer.init(addresses);
}

export async function syncAddressMonitor(addresses: MonitoredAddress[]) {
await writeMonitoredAddressStore(addresses);
monitorContainer.sync(addresses);
}

const ADDRESS_MONITOR_STORE = 'addressMonitorStore';

export async function readMonitoredAddressStore() {
const result = await chrome.storage.local.get(ADDRESS_MONITOR_STORE);
const addresses = result[ADDRESS_MONITOR_STORE] || [];
return addresses;
}

export async function writeMonitoredAddressStore(addresses: MonitoredAddress[]) {
await chrome.storage.local.set({
[ADDRESS_MONITOR_STORE]: addresses,
});
}
Loading

0 comments on commit 1f4aa1e

Please sign in to comment.