Skip to content

Commit

Permalink
Support pausing and resuming scanning (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
dguenther authored Aug 10, 2024
1 parent 3176b71 commit b83efab
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 20 deletions.
33 changes: 27 additions & 6 deletions packages/mobile-app/app/(tabs)/contacts.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
import { Button } from "@ironfish/ui";
import { View, Text } from "react-native";
import { wallet } from "../../data/wallet/wallet";

import { Network } from "../../data/constants";
import { useFacade } from "../../data/facades";

export default function Contacts() {
const facade = useFacade();

const walletStatus = facade.getWalletStatus.useQuery(undefined, {
refetchInterval: 1000,
});

const pauseSyncing = facade.pauseSyncing.useMutation();
const resumeSyncing = facade.resumeSyncing.useMutation();

return (
<View>
<Text>Contacts</Text>
{walletStatus.data && (
<>
<Text>{`Scan status: ${walletStatus.data.status}`}</Text>
<Text>{`Latest known block: ${walletStatus.data.latestKnownBlock}`}</Text>
</>
)}
<Text>{}</Text>
<Button
onPress={async () => {
await resumeSyncing.mutateAsync(undefined);
}}
>
Resume Syncing
</Button>
<Button
onPress={() => {
wallet.scan(Network.TESTNET);
onPress={async () => {
await pauseSyncing.mutateAsync(undefined);
}}
>
Request Blocks
Pause Syncing
</Button>
</View>
);
Expand Down
14 changes: 14 additions & 0 deletions packages/mobile-app/data/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ class BlockchainClass {
start: { hash: Uint8Array; sequence: number },
end: { hash: Uint8Array; sequence: number },
onBlock: (block: LightBlock) => unknown,
abort: AbortSignal,
) {
await this.lockRequest(async () => {
const manifest = await WalletServerChunksApi.getChunksManifest(network);
Expand All @@ -131,6 +132,8 @@ class BlockchainClass {
let lastHash: null | Uint8Array = null;

const onBlockInner = (block: LightBlock) => {
if (abort.aborted) return;

// TODO: Errors thrown here don't propagate
if (!lastHash && !Uint8ArrayUtils.areEqual(block.hash, start.hash)) {
console.error(
Expand Down Expand Up @@ -170,8 +173,13 @@ class BlockchainClass {
chunk.range.start <= end.sequence,
);
for (const chunk of finalizedChunks) {
if (abort.aborted) break;

await WalletServerChunksApi.getChunkBlockAndByteRanges(network, chunk);

if (abort.aborted) break;
readPromise = readPromise.then(async () => {
if (abort.aborted) return;
await WalletServerChunksApi.readChunkBlocks(
network,
chunk,
Expand All @@ -187,14 +195,20 @@ class BlockchainClass {
const downloadSize = 100;

for (let i = serverStart; i <= lastSequence; i += downloadSize) {
if (abort.aborted) break;

const endIndex = Math.min(i + downloadSize - 1, lastSequence);
console.log("fetching blocks from wallet server", i, endIndex);
const download = await WalletServerApi.getBlockRange(
network,
i,
endIndex,
);

if (abort.aborted) break;

readPromise = readPromise.then(async () => {
if (abort.aborted) return;
await this.readWalletServerBlocks(download, onBlockInner);
});
}
Expand Down
22 changes: 17 additions & 5 deletions packages/mobile-app/data/chainProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,24 @@ import * as Uint8ArrayUtils from "../utils/uint8Array";
*/
export class ChainProcessor {
readonly network: Network;
abort: AbortSignal;
head: Readonly<{ hash: Uint8Array; sequence: number }> | null = null;
onAdd: (block: LightBlock) => unknown;
onRemove: (block: LightBlock) => unknown;

constructor(options: {
network: Network;
abort: AbortSignal;
onAdd: (block: LightBlock) => unknown;
onRemove: (block: LightBlock) => unknown;
}) {
this.network = options.network;
this.abort = options.abort;
this.onAdd = options.onAdd;
this.onRemove = options.onRemove;
}

async update({ signal }: { signal?: AbortSignal } = {}): Promise<{
async update(): Promise<{
hashChanged: boolean;
}> {
const oldHash = this.head;
Expand Down Expand Up @@ -65,17 +68,26 @@ export class ChainProcessor {
}

for (const block of result.blocksToRemove) {
if (this.abort.aborted)
return { hashChanged: !oldHash || this.head.hash !== oldHash.hash };

this.onRemove(block);
this.head = {
hash: block.previousBlockHash,
sequence: block.sequence - 1,
};
}

await Blockchain.iterateTo(this.network, this.head, chainHead, (block) => {
this.onAdd(block);
this.head = { hash: block.hash, sequence: block.sequence };
});
await Blockchain.iterateTo(
this.network,
this.head,
chainHead,
(block) => {
this.onAdd(block);
this.head = { hash: block.hash, sequence: block.sequence };
},
this.abort,
);

return { hashChanged: !oldHash || this.head.hash !== oldHash.hash };
}
Expand Down
4 changes: 2 additions & 2 deletions packages/mobile-app/data/facades/wallet/demoHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const ACCOUNTS: Account[] = [
];

const WALLET_STATUS: WalletStatus = {
status: "SYNCING",
status: "SCANNING",
latestKnownBlock: 523142,
};

Expand Down Expand Up @@ -293,7 +293,7 @@ export const walletDemoHandlers = f.facade<WalletHandlers>({
},
),
resumeSyncing: f.handler.mutation(async () => {
WALLET_STATUS.status = "SYNCING";
WALLET_STATUS.status = "SCANNING";
}),
sendTransaction: f.handler.mutation(
async (args: {
Expand Down
13 changes: 9 additions & 4 deletions packages/mobile-app/data/facades/wallet/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
LanguageUtils,
TransactionStatus,
} from "@ironfish/sdk";
import { WalletServerApi } from "../../api/walletServer";

export const walletHandlers = f.facade<WalletHandlers>({
createAccount: f.handler.mutation(
Expand Down Expand Up @@ -276,8 +277,8 @@ export const walletHandlers = f.facade<WalletHandlers>({
},
),
getWalletStatus: f.handler.query(async (): Promise<WalletStatus> => {
// TODO: Implement getWalletStatus
return { status: "PAUSED", latestKnownBlock: 0 };
const block = await WalletServerApi.getLatestBlock(Network.TESTNET);
return { status: wallet.scanState.type, latestKnownBlock: block.sequence };
}),
importAccount: f.handler.mutation(
async ({
Expand Down Expand Up @@ -312,7 +313,9 @@ export const walletHandlers = f.facade<WalletHandlers>({
};
},
),
pauseSyncing: f.handler.mutation(async () => {}),
pauseSyncing: f.handler.mutation(async () => {
wallet.pauseScan();
}),
removeAccount: f.handler.mutation(async ({ name }: { name: string }) => {
await wallet.removeAccount(name);
}),
Expand All @@ -321,7 +324,9 @@ export const walletHandlers = f.facade<WalletHandlers>({
await wallet.renameAccount(name, newName);
},
),
resumeSyncing: f.handler.mutation(async () => {}),
resumeSyncing: f.handler.mutation(async () => {
wallet.scan(Network.TESTNET);
}),
sendTransaction: f.handler.mutation(
async (args: {
accountName: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/mobile-app/data/facades/wallet/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export type Burn = {
};

export type WalletStatus = {
status: "SYNCING" | "PAUSED";
status: "SCANNING" | "PAUSED" | "IDLE";
latestKnownBlock: number;
};

Expand Down
38 changes: 36 additions & 2 deletions packages/mobile-app/data/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import { WalletServerApi } from "../api/walletServer";

type StartedState = { type: "STARTED"; db: WalletDb };
type WalletState = { type: "STOPPED" } | { type: "LOADING" } | StartedState;
type ScanState =
// Not scanning, and a scan can be started
| { type: "IDLE" }
// Not scanning, and a scan cannot be started
| { type: "PAUSED" }
// Scanning
| { type: "SCANNING"; abort: AbortController };

function assertStarted(state: WalletState): asserts state is StartedState {
if (state.type !== "STARTED") {
Expand All @@ -25,6 +32,7 @@ function assertStarted(state: WalletState): asserts state is StartedState {

class Wallet {
state: WalletState = { type: "STOPPED" };
scanState: ScanState = { type: "IDLE" };

async start() {
if (this.state.type !== "STOPPED") {
Expand Down Expand Up @@ -376,14 +384,18 @@ class Wallet {
async scan(network: Network): Promise<boolean> {
assertStarted(this.state);

if (this.scanState.type === "SCANNING") {
return false;
}
const abort = new AbortController();
this.scanState = { type: "SCANNING", abort };

const cache = new WriteCache(this.state.db, network);

let blockProcess = Promise.resolve();
let performanceTimer = performance.now();
let finished = false;

// todo: lock scanning

const dbAccounts = await this.state.db.getAccounts();
let accounts = dbAccounts.map((account) => {
return {
Expand All @@ -410,8 +422,13 @@ class Wallet {

const chainProcessor = new ChainProcessor({
network,
abort: abort.signal,
onAdd: (block) => {
blockProcess = blockProcess.then(async () => {
if (abort.signal.aborted) {
return;
}

assertStarted(this.state);

const prevHash = block.previousBlockHash;
Expand Down Expand Up @@ -546,6 +563,10 @@ class Wallet {
},
onRemove: (block) => {
blockProcess = blockProcess.then(() => {
if (abort.signal.aborted) {
return;
}

console.log(`Removing block ${block.sequence}`);

for (const account of accounts) {
Expand Down Expand Up @@ -585,11 +606,24 @@ class Wallet {
finished = true;
clearTimeout(saveLoopTimeout);
await saveLoop();
if (this.scanState.abort.signal.aborted) {
this.scanState = this.scanState = this.scanState.abort.signal.aborted
? { type: "PAUSED" }
: { type: "IDLE" };
}
console.log(`finished in ${performance.now() - performanceTimer}ms`);
}

return hashChanged;
}

pauseScan() {
if (this.scanState.type === "SCANNING") {
this.scanState.abort.abort();
} else if (this.scanState.type === "IDLE") {
this.scanState = { type: "PAUSED" };
}
}
}

export const wallet = new Wallet();

0 comments on commit b83efab

Please sign in to comment.