Skip to content

Commit

Permalink
feat(frontend): add sol wallet worker (#4037)
Browse files Browse the repository at this point in the history
# Motivation

We integrate SOL with the same worker approach as btc and ic. 

# Changes

Implement worker and scheduler.

# Tests

Unit tests provided, same as for btc worker.

Additionally:


https://github.com/user-attachments/assets/7eca0ee7-dc69-4bd5-8597-c2aafc1b6c33

# Note
During the development there were some error messages for mainnet (even
though we use alchemy now).


![image](https://github.com/user-attachments/assets/e60af10e-211b-45ee-ad0e-e6e5367d71ef)


![image](https://github.com/user-attachments/assets/f5f26f22-9a8d-4ab5-a1c3-2c3362526fcc)


Not reproducable at the moment. Will address this with
@StefanBerger-DFINITY to check if the alchemy dashboard shows something.

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
loki344 and github-actions[bot] authored Jan 7, 2025
1 parent ce3d851 commit a3eda58
Show file tree
Hide file tree
Showing 7 changed files with 412 additions and 8 deletions.
1 change: 1 addition & 0 deletions src/frontend/src/btc/schedulers/btc-wallet.scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export class BtcWalletScheduler implements Scheduler<PostMessageDataRequestBtc>
const newBalance =
isNullish(this.store.balance) ||
this.store.balance.data !== balance.data ||
// TODO, align with sol-wallet.scheduler.ts, crash if certified changes
(!this.store.balance.certified && balance.certified);
const newTransactions = uncertifiedTransactions.length > 0;

Expand Down
116 changes: 116 additions & 0 deletions src/frontend/src/sol/schedulers/sol-wallet.scheduler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { WALLET_TIMER_INTERVAL_MILLIS } from '$lib/constants/app.constants';
import { SchedulerTimer, type Scheduler, type SchedulerJobData } from '$lib/schedulers/scheduler';
import type { SolAddress } from '$lib/types/address';
import type {
PostMessageDataRequestSol,
PostMessageDataResponseError
} from '$lib/types/post-message';
import type { CertifiedData } from '$lib/types/store';
import { loadSolLamportsBalance } from '$sol/api/solana.api';
import type { SolanaNetworkType } from '$sol/types/network';
import type { SolPostMessageDataResponseWallet } from '$sol/types/sol-post-message';
import { assertNonNullish, isNullish } from '@dfinity/utils';

interface LoadSolWalletParams {
solanaNetwork: SolanaNetworkType;
address: SolAddress;
}

interface SolWalletStore {
balance: CertifiedData<bigint | null> | undefined;
}

interface SolWalletData {
balance: CertifiedData<bigint | null>;
}

export class SolWalletScheduler implements Scheduler<PostMessageDataRequestSol> {
private timer = new SchedulerTimer('syncSolWalletStatus');

private store: SolWalletStore = {
balance: undefined
};

stop() {
this.timer.stop();
}

async start(data: PostMessageDataRequestSol | undefined) {
await this.timer.start<PostMessageDataRequestSol>({
interval: WALLET_TIMER_INTERVAL_MILLIS,
job: this.syncWallet,
data
});
}

async trigger(data: PostMessageDataRequestSol | undefined) {
await this.timer.trigger<PostMessageDataRequestSol>({
job: this.syncWallet,
data
});
}

private loadBalance = async ({
address,
solanaNetwork
}: LoadSolWalletParams): Promise<CertifiedData<bigint | null>> => ({
data: await loadSolLamportsBalance({ network: solanaNetwork, address }),
certified: false
});

private syncWallet = async ({ data }: SchedulerJobData<PostMessageDataRequestSol>) => {
assertNonNullish(data, 'No data provided to get Solana balance.');

try {
const balance = await this.loadBalance({
address: data.address.data,
solanaNetwork: data.solanaNetwork
});

//todo implement loading transactions

this.syncWalletData({ response: { balance } });
} catch (error: unknown) {
this.postMessageWalletError({ error });
}
};

private syncWalletData = ({ response: { balance } }: { response: SolWalletData }) => {
if (!this.store.balance?.certified && balance.certified) {
throw new Error('Balance certification status cannot change from uncertified to certified');
}

const newBalance = isNullish(this.store.balance) || this.store.balance.data !== balance.data;

if (!newBalance) {
return;
}

this.store = {
...this.store,
balance
};

this.postMessageWallet({
wallet: {
balance
}
});
};

private postMessageWallet(data: SolPostMessageDataResponseWallet) {
this.timer.postMsg<SolPostMessageDataResponseWallet>({
msg: 'syncSolWallet',
data
});
}

protected postMessageWalletError({ error }: { error: unknown }) {
this.timer.postMsg<PostMessageDataResponseError>({
msg: 'syncSolWalletError',
data: {
error
}
});
}
}
8 changes: 2 additions & 6 deletions src/frontend/src/sol/schema/sol-post-message.schema.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import {
JsonTransactionsTextSchema,
PostMessageDataResponseSchema
} from '$lib/schema/post-message.schema';
import { PostMessageDataResponseSchema } from '$lib/schema/post-message.schema';
import type { CertifiedData } from '$lib/types/store';
import { z } from 'zod';

const SolPostMessageWalletDataSchema = z.object({
balance: z.custom<CertifiedData<bigint | null>>(),
newTransactions: JsonTransactionsTextSchema
balance: z.custom<CertifiedData<bigint | null>>()
});

export const SolPostMessageDataResponseWalletSchema = PostMessageDataResponseSchema.extend({
Expand Down
95 changes: 95 additions & 0 deletions src/frontend/src/sol/services/worker.sol-wallet.services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
solAddressDevnetStore,
solAddressLocalnetStore,
solAddressMainnetStore,
solAddressTestnetStore
} from '$lib/stores/address.store';
import type { WalletWorker } from '$lib/types/listener';
import type {
PostMessage,
PostMessageDataRequestSol,
PostMessageDataResponseError
} from '$lib/types/post-message';
import type { Token } from '$lib/types/token';
import {
isNetworkIdSOLDevnet,
isNetworkIdSOLLocal,
isNetworkIdSOLTestnet
} from '$lib/utils/network.utils';
import { syncWallet, syncWalletError } from '$sol/services/sol-listener.services';
import type { SolPostMessageDataResponseWallet } from '$sol/types/sol-post-message';
import { mapNetworkIdToNetwork } from '$sol/utils/network.utils';
import { assertNonNullish } from '@dfinity/utils';
import { get } from 'svelte/store';

export const initSolWalletWorker = async ({ token }: { token: Token }): Promise<WalletWorker> => {
const {
id: tokenId,
network: { id: networkId }
} = token;

const WalletWorker = await import('$sol/workers/sol-wallet.worker?worker');
const worker: Worker = new WalletWorker.default();

const isTestnetNetwork = isNetworkIdSOLTestnet(networkId);
const isDevnetNetwork = isNetworkIdSOLDevnet(networkId);
const isLocalNetwork = isNetworkIdSOLLocal(networkId);

worker.onmessage = ({ data }: MessageEvent<PostMessage<SolPostMessageDataResponseWallet>>) => {
const { msg } = data;

switch (msg) {
case 'syncSolWallet':
syncWallet({
tokenId,
data: data.data as SolPostMessageDataResponseWallet
});
return;

case 'syncSolWalletError':
syncWalletError({
tokenId,
error: (data.data as PostMessageDataResponseError).error,
hideToast: isTestnetNetwork || isDevnetNetwork || isLocalNetwork
});
return;
}
};

// TODO: stop/start the worker on address change (same as for worker.btc-wallet.services.ts)
const address = get(
isTestnetNetwork
? solAddressTestnetStore
: isDevnetNetwork
? solAddressDevnetStore
: isLocalNetwork
? solAddressLocalnetStore
: solAddressMainnetStore
);
assertNonNullish(address, 'No Solana address provided to start Solana wallet worker.');

const network = mapNetworkIdToNetwork(token.network.id);
assertNonNullish(network, 'No Solana network provided to start Solana wallet worker.');

const data: PostMessageDataRequestSol = { address, solanaNetwork: network };

return {
start: () => {
worker.postMessage({
msg: 'startSolWalletTimer',
data
});
},
stop: () => {
worker.postMessage({
msg: 'stopSolWalletTimer'
});
},
trigger: () => {
worker.postMessage({
msg: 'triggerSolWalletTimer',
data
});
}
};
};
20 changes: 20 additions & 0 deletions src/frontend/src/sol/workers/sol-wallet.worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { PostMessage, PostMessageDataRequestSol } from '$lib/types/post-message';
import { SolWalletScheduler } from '$sol/schedulers/sol-wallet.scheduler';

const scheduler: SolWalletScheduler = new SolWalletScheduler();

onmessage = async ({ data: dataMsg }: MessageEvent<PostMessage<PostMessageDataRequestSol>>) => {
const { msg, data } = dataMsg;

switch (msg) {
case 'stopSolWalletTimer':
scheduler.stop();
return;
case 'startSolWalletTimer':
await scheduler.start(data);
return;
case 'triggerSolWalletTimer':
await scheduler.trigger(data);
return;
}
};
Loading

0 comments on commit a3eda58

Please sign in to comment.