343 lines
11 KiB
TypeScript
343 lines
11 KiB
TypeScript
import { action, observable, when, runInAction, computed } from 'mobx';
|
|
import { EmitterSubscription, NativeModules } from 'react-native';
|
|
import ReactNativeBlobUtil from 'react-native-blob-util';
|
|
|
|
import BackendUtils from '../utils/BackendUtils';
|
|
import { LndMobileToolsEventEmitter } from '../utils/EventListenerUtils';
|
|
import { sleep } from '../utils/SleepUtils';
|
|
|
|
import NodeInfo from '../models/NodeInfo';
|
|
|
|
import ConnectivityStore from './ConnectivityStore';
|
|
import SettingsStore from './SettingsStore';
|
|
|
|
export default class SyncStore {
|
|
@observable public isSyncing: boolean = false;
|
|
@observable public isRecovering: boolean = false;
|
|
@observable public recoveryProgress: number | null;
|
|
@observable public isInExpressGraphSync: boolean = false;
|
|
@observable public bestBlockHeight: number;
|
|
@observable public currentBlockHeight: number;
|
|
@observable public currentProgress: number;
|
|
@observable public numBlocksUntilSynced: number;
|
|
@observable public error: boolean = false;
|
|
// rescan tracking
|
|
@observable public isRescanning: boolean = false;
|
|
@observable public rescanStartHeight: number | null = null;
|
|
@observable public rescanCurrentHeight: number | null = null;
|
|
@observable public rescanAddressCount: number | null = null;
|
|
@observable public rescanTxnsFound: number = 0;
|
|
@observable public isLogObservationActive: boolean = false;
|
|
private logListener: EmitterSubscription | null = null;
|
|
|
|
nodeInfo: any;
|
|
settingsStore: SettingsStore;
|
|
connectivityStore: ConnectivityStore;
|
|
|
|
constructor(
|
|
settingsStore: SettingsStore,
|
|
connectivityStore: ConnectivityStore
|
|
) {
|
|
this.settingsStore = settingsStore;
|
|
this.connectivityStore = connectivityStore;
|
|
// Resume syncing when connectivity is restored (embedded-lnd only)
|
|
connectivityStore.onReconnect(() => {
|
|
if (
|
|
!this.isSyncing &&
|
|
this.settingsStore.implementation === 'embedded-lnd'
|
|
) {
|
|
this.startSyncing();
|
|
}
|
|
});
|
|
}
|
|
|
|
@action
|
|
public reset = () => {
|
|
this.isSyncing = false;
|
|
this.isRecovering = false;
|
|
this.isInExpressGraphSync = false;
|
|
this.error = false;
|
|
this.nodeInfo = undefined;
|
|
this.bestBlockHeight = 0;
|
|
this.currentBlockHeight = 0;
|
|
this.currentProgress = 0;
|
|
this.numBlocksUntilSynced = 1;
|
|
this.stopRescanTracking();
|
|
};
|
|
|
|
@computed
|
|
get isBusy() {
|
|
return this.isSyncing || this.isRecovering || this.isRescanning;
|
|
}
|
|
|
|
public setExpressGraphSyncStatus = (syncing: boolean) =>
|
|
(this.isInExpressGraphSync = syncing);
|
|
|
|
public waitForExpressGraphSyncEnd = () => {
|
|
return when(() => !this.isInExpressGraphSync);
|
|
};
|
|
|
|
private setSyncInfo = async () => {
|
|
const nodeInfo = this.nodeInfo;
|
|
|
|
if (this.currentBlockHeight !== nodeInfo?.block_height) {
|
|
this.currentBlockHeight = nodeInfo?.block_height || 0;
|
|
|
|
// if current block exceeds or equals best block height, query mempool for new tip
|
|
// if mempool call fails, set best block height to current height, then proceed
|
|
if (this.currentBlockHeight >= this.bestBlockHeight) {
|
|
await this.getBestBlockHeight();
|
|
if (this.error) this.bestBlockHeight = this.currentBlockHeight;
|
|
}
|
|
|
|
this.updateProgress();
|
|
}
|
|
|
|
if (nodeInfo?.synced_to_chain || this.numBlocksUntilSynced <= 0) {
|
|
this.isSyncing = false;
|
|
}
|
|
|
|
return;
|
|
};
|
|
|
|
private getNodeInfo = () =>
|
|
BackendUtils.getMyNodeInfo().then(
|
|
(data: any) => (this.nodeInfo = new NodeInfo(data))
|
|
);
|
|
|
|
@action
|
|
private updateProgress = () => {
|
|
this.currentProgress =
|
|
(this.currentBlockHeight ?? 0) - (this.bestBlockHeight ?? 0);
|
|
this.numBlocksUntilSynced =
|
|
(this.bestBlockHeight ?? 0) - this.currentBlockHeight;
|
|
};
|
|
|
|
private getBestBlockHeight = async () => {
|
|
await new Promise((resolve, reject) => {
|
|
ReactNativeBlobUtil.fetch(
|
|
'get',
|
|
`https://mempool.space/${
|
|
this.settingsStore.embeddedLndNetwork === 'Testnet'
|
|
? 'testnet/'
|
|
: ''
|
|
}api/blocks/tip/height`
|
|
)
|
|
.then(async (response: any) => {
|
|
const status = response.info().status;
|
|
if (status == 200) {
|
|
this.bestBlockHeight = Number.parseInt(response.json());
|
|
if (
|
|
typeof this.bestBlockHeight === 'number' &&
|
|
this.currentBlockHeight
|
|
) {
|
|
this.updateProgress();
|
|
}
|
|
resolve(this.bestBlockHeight);
|
|
} else {
|
|
this.error = true;
|
|
reject();
|
|
}
|
|
})
|
|
.catch(() => {
|
|
this.error = true;
|
|
reject();
|
|
});
|
|
});
|
|
};
|
|
|
|
@action
|
|
public startSyncing = async () => {
|
|
this.isSyncing = true;
|
|
this.error = false;
|
|
this.bestBlockHeight = 0;
|
|
this.numBlocksUntilSynced = 1;
|
|
|
|
// Fetch best block height, retrying until successful
|
|
while (!this.bestBlockHeight) {
|
|
if (!this.isSyncing) return;
|
|
try {
|
|
await this.getBestBlockHeight();
|
|
} catch {
|
|
await sleep(3000);
|
|
}
|
|
}
|
|
|
|
// initial fetch
|
|
while (!this.nodeInfo?.block_height) {
|
|
if (!this.isSyncing) return;
|
|
await this.getNodeInfo();
|
|
if (!this.nodeInfo?.block_height) {
|
|
await sleep(3000);
|
|
}
|
|
}
|
|
|
|
await this.setSyncInfo();
|
|
|
|
let i = 0;
|
|
while (this.numBlocksUntilSynced > 0) {
|
|
if (!this.isSyncing) break; // Abort when reset() called (e.g. LND stopped for wallet creation)
|
|
await sleep(2000);
|
|
this.getNodeInfo().then(() => this.setSyncInfo());
|
|
|
|
// only query Mempool instance every 30 seconds
|
|
const queryMempool = i === 14;
|
|
if (queryMempool) {
|
|
i = 0;
|
|
} else {
|
|
i++;
|
|
}
|
|
|
|
if (queryMempool) {
|
|
this.getBestBlockHeight().catch(() => {});
|
|
}
|
|
}
|
|
};
|
|
|
|
public checkRecoveryStatus = () => {
|
|
BackendUtils.getRecoveryInfo().then((data: any) => {
|
|
if (data.recovery_mode && !data.recovery_finished) {
|
|
this.startRecovering();
|
|
}
|
|
});
|
|
};
|
|
|
|
private getRecoveryStatus = async () => {
|
|
await BackendUtils.getRecoveryInfo().then((data: any) => {
|
|
runInAction(() => {
|
|
if (data.recovery_mode) {
|
|
if (data.progress) {
|
|
this.recoveryProgress = data.progress;
|
|
}
|
|
if (data.recovery_finished) {
|
|
this.isRecovering = false;
|
|
this.recoveryProgress = null;
|
|
}
|
|
} else {
|
|
this.isRecovering = false;
|
|
this.recoveryProgress = null;
|
|
}
|
|
});
|
|
return data;
|
|
});
|
|
};
|
|
|
|
private startRecovering = async () => {
|
|
this.isRecovering = true;
|
|
|
|
while (this.isRecovering) {
|
|
await sleep(2000);
|
|
await this.getRecoveryStatus();
|
|
}
|
|
};
|
|
|
|
// Rescan tracking methods
|
|
private getNetwork = (): string => {
|
|
const network = this.settingsStore.embeddedLndNetwork?.toLowerCase();
|
|
if (network === 'testnet3' || network === 'testnet') {
|
|
return 'testnet';
|
|
} else if (network === 'testnet4') {
|
|
return 'testnet4';
|
|
}
|
|
return 'mainnet';
|
|
};
|
|
|
|
@action
|
|
public startRescanTracking = (startHeight: number) => {
|
|
// Remove existing listener if any to prevent memory leaks
|
|
if (this.logListener) {
|
|
this.logListener.remove();
|
|
this.logListener = null;
|
|
}
|
|
|
|
this.isRescanning = true;
|
|
this.rescanStartHeight = startHeight;
|
|
this.rescanCurrentHeight = startHeight;
|
|
this.rescanTxnsFound = 0;
|
|
this.rescanAddressCount = null;
|
|
|
|
const network = this.getNetwork();
|
|
const lndDir = this.settingsStore.lndDir || 'lnd';
|
|
|
|
// Add listener for log events
|
|
this.logListener = LndMobileToolsEventEmitter.addListener(
|
|
'lndlog',
|
|
(data: string) => this.parseRescanLog(data)
|
|
);
|
|
|
|
// Start observing log file (may fail if file doesn't exist yet)
|
|
NativeModules.LndMobileTools.observeLndLogFile(lndDir, network)
|
|
.then(() => {
|
|
runInAction(() => {
|
|
this.isLogObservationActive = true;
|
|
});
|
|
})
|
|
.catch((e: any) => {
|
|
console.log('Could not observe log file:', e);
|
|
runInAction(() => {
|
|
this.isLogObservationActive = false;
|
|
});
|
|
});
|
|
};
|
|
|
|
@action
|
|
public stopRescanTracking = () => {
|
|
if (this.logListener) {
|
|
this.logListener.remove();
|
|
this.logListener = null;
|
|
}
|
|
this.isRescanning = false;
|
|
this.rescanStartHeight = null;
|
|
this.rescanCurrentHeight = null;
|
|
this.rescanAddressCount = null;
|
|
this.rescanTxnsFound = 0;
|
|
this.isLogObservationActive = false;
|
|
};
|
|
|
|
@action
|
|
private parseRescanLog = (logLine: string) => {
|
|
// Log lines have format: "2026-01-21 02:15:44.368 [INF] BTWL: Finished rescan..."
|
|
// Use regex that matches anywhere in the line
|
|
|
|
// Check for rescan start with height
|
|
// Pattern: "Started rescan from block ... (height 118957) for 9 addrs"
|
|
const startMatch = logLine.match(
|
|
/Started rescan from block.*\(height (\d+)\)/
|
|
);
|
|
if (startMatch) {
|
|
this.rescanStartHeight = parseInt(startMatch[1], 10);
|
|
this.rescanCurrentHeight = parseInt(startMatch[1], 10);
|
|
return;
|
|
}
|
|
|
|
// Check for rescan progress updates
|
|
// Pattern: "Rescanned through block ... (height 119000)"
|
|
const progressMatch = logLine.match(
|
|
/Rescanned through block.*\(height (\d+)\)/
|
|
);
|
|
if (progressMatch) {
|
|
this.rescanCurrentHeight = parseInt(progressMatch[1], 10);
|
|
return;
|
|
}
|
|
|
|
// Check for rescan completion
|
|
// Pattern: "Finished rescan for 9 addresses (synced to block ..., height 119438)"
|
|
const finishedMatch = logLine.match(
|
|
/Finished rescan for (\d+) addresses.*height (\d+)/
|
|
);
|
|
if (finishedMatch) {
|
|
this.rescanAddressCount = parseInt(finishedMatch[1], 10);
|
|
this.rescanCurrentHeight = parseInt(finishedMatch[2], 10);
|
|
this.stopRescanTracking();
|
|
return;
|
|
}
|
|
|
|
// Check for address count
|
|
// Pattern: "Rescan called for 9 addresses, 0 outpoints"
|
|
const addrMatch = logLine.match(/Rescan called for (\d+) addresses/);
|
|
if (addrMatch) {
|
|
this.rescanAddressCount = parseInt(addrMatch[1], 10);
|
|
}
|
|
};
|
|
}
|