Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: wallet idb #1101

Merged
merged 2 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/frontend/src/lib/services/auth.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
cyclesIdbStore,
exchangeIdbStore,
monitoringIdbStore,
statusesIdbStore
statusesIdbStore,
walletIdbStore
} from '$lib/stores/idb.store';
import { toasts } from '$lib/stores/toasts.store';
import type { ToastLevel, ToastMsg } from '$lib/types/toast';
Expand Down Expand Up @@ -77,6 +78,7 @@ const logout = async ({
clear(statusesIdbStore),
clear(monitoringIdbStore),
clear(exchangeIdbStore),
clear(walletIdbStore),
resetSnapshots(),
resetSubnets()
]);
Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/lib/stores/idb.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const monitoringIdbStore = createStore('juno-monitoring', 'juno-monitorin
*/
export const statusesIdbStore = createStore('juno-statuses', 'juno-statuses-store');
export const exchangeIdbStore = createStore('juno-exchange', 'juno-exchange-store');
export const walletIdbStore = createStore('juno-wallet', 'juno-wallet-store');

// Loaded and set on the UI side
export const snapshotsIdbStore = createStore('juno-snapshot', 'juno-snapshot-store');
Expand Down
87 changes: 87 additions & 0 deletions src/frontend/src/lib/workers/_stores/wallet-worker.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { ICP_LEDGER_CANISTER_ID } from '$lib/constants/constants';
import { walletIdbStore } from '$lib/stores/idb.store';
import type { CertifiedData } from '$lib/types/store';
import type { TransactionWithId } from '@dfinity/ledger-icp';
import { get, set } from 'idb-keyval';

export type IndexedTransactions = Record<string, CertifiedData<TransactionWithId>>;

// Not reactive, only used to hold values imperatively.
interface WalletState {
balance: CertifiedData<bigint> | undefined;
transactions: IndexedTransactions;
}

export class WalletStore {
private static EMPTY_STORE: WalletState = {
balance: undefined,
transactions: {}
};

#store: WalletState;

private constructor(state: WalletState) {
this.#store = state ?? WalletStore.EMPTY_STORE;
}

get balance(): CertifiedData<bigint> | undefined {
return this.#store.balance;
}

get transactions(): IndexedTransactions {
return this.#store.transactions;
}

update({
balance,
newTransactions,
certified
}: {
balance: bigint;
newTransactions: TransactionWithId[];
certified: boolean;
}): void {
this.#store = {
balance: { data: balance, certified },
transactions: {
...this.#store.transactions,
...newTransactions.reduce(
(acc: Record<string, CertifiedData<TransactionWithId>>, { id, transaction }) => ({
...acc,
[`${id}`]: {
data: {
id,
transaction
},
certified
}
}),
{}
)
}
};
}

clean(certifiedTransactions: IndexedTransactions) {
this.#store = {
...this.#store,
transactions: {
...certifiedTransactions
}
};
}

reset() {
this.#store = WalletStore.EMPTY_STORE;
}

async save(): Promise<void> {
// Save information to improve UX when application is reloaded or returning users.
await set(ICP_LEDGER_CANISTER_ID, this.#store, walletIdbStore);
}

static async init(): Promise<WalletStore> {
const state = await get(ICP_LEDGER_CANISTER_ID, walletIdbStore);
return new WalletStore(state);
}
}
108 changes: 50 additions & 58 deletions src/frontend/src/lib/workers/wallet.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,11 @@ import type {
PostMessageDataResponseWalletCleanUp,
PostMessageRequest
} from '$lib/types/post-message';
import type { CertifiedData } from '$lib/types/store';
import { mapIcpTransaction } from '$lib/utils/icp-transactions.utils';
import { loadIdentity } from '$lib/utils/worker.utils';
import { type IndexedTransactions, WalletStore } from '$lib/workers/_stores/wallet-worker.store';
import type { Identity } from '@dfinity/agent';
import type {
GetAccountIdentifierTransactionsResponse,
TransactionWithId
} from '@dfinity/ledger-icp';
import type { GetAccountIdentifierTransactionsResponse } from '@dfinity/ledger-icp';
import { Principal } from '@dfinity/principal';
import { isNullish, jsonReplacer } from '@dfinity/utils';

Expand Down Expand Up @@ -62,7 +59,11 @@ const startTimer = async ({ data: { missionControlId } }: { data: PostMessageDat
return;
}

const sync = async () => await syncWallet({ missionControlId, identity });
const store = await WalletStore.init();

emitSavedWallet({ store, identity });

const sync = async () => await syncWallet({ missionControlId, identity, store });

// We sync the cycles now but also schedule the update afterwards
await sync();
Expand All @@ -74,25 +75,14 @@ let syncing = false;

let initialized = false;

// Not reactive, only used to hold values imperatively.
interface IcWalletStore {
balance: CertifiedData<bigint> | undefined;
transactions: IndexedTransactions;
}

type IndexedTransactions = Record<string, CertifiedData<TransactionWithId>>;

let store: IcWalletStore = {
balance: undefined,
transactions: {}
};

const syncWallet = async ({
missionControlId,
identity
identity,
store
}: {
missionControlId: string;
identity: Identity;
store: WalletStore;
}) => {
// We avoid to relaunch a sync while previous sync is not finished
if (syncing) {
Expand All @@ -118,14 +108,15 @@ const syncWallet = async ({
certified,
...rest
}) => {
syncTransactions({ certified, identity, ...rest });
cleanTransactions({ certified });
syncTransactions({ certified, identity, store, ...rest });
cleanTransactions({ certified, store });
};

const onCertifiedError: QueryAndUpdateOnCertifiedError = ({ error }) => {
store.reset();

postMessageWalletError(error);

console.error(error);
stopTimer();
};

Expand All @@ -137,15 +128,16 @@ const syncWallet = async ({
resolution: 'all_settled'
});

await store.save();

syncing = false;
};

const postMessageWallet = ({
certified,
balance,
transactions: newTransactions,
...rest
}: Omit<GetAccountIdentifierTransactionsResponse, 'transactions'> & {
transactions: newTransactions
}: Pick<GetAccountIdentifierTransactionsResponse, 'balance'> & {
transactions: IcTransactionUi[];
} & {
certified: boolean;
Expand All @@ -158,11 +150,7 @@ const postMessageWallet = ({
data: balance,
certified
},
...rest,
newTransactions: JSON.stringify(
Object.entries(certifiedTransactions).map(([_id, transaction]) => transaction),
jsonReplacer
)
newTransactions: JSON.stringify(certifiedTransactions, jsonReplacer)
}
};

Expand All @@ -175,11 +163,13 @@ const postMessageWallet = ({
const syncTransactions = ({
response: { transactions: fetchedTransactions, balance, ...rest },
certified,
identity
identity,
store
}: {
response: GetAccountIdentifierTransactionsResponse;
certified: boolean;
identity: Identity;
store: WalletStore;
}) => {
// Is there any new transactions unknown so far or which has become certified
const newTransactions = fetchedTransactions.filter(
Expand Down Expand Up @@ -208,25 +198,7 @@ const syncTransactions = ({
return;
}

store = {
balance: { data: balance, certified },
transactions: {
...store.transactions,
...newTransactions.reduce(
(acc: Record<string, CertifiedData<TransactionWithId>>, { id, transaction }) => ({
...acc,
[`${id}`]: {
data: {
id,
transaction
},
certified
}
}),
{}
)
}
};
store.update({ balance, newTransactions, certified });

const newUiTransactions = newTransactions.map((transaction) =>
mapIcpTransaction({ transaction, identity })
Expand All @@ -247,7 +219,7 @@ const syncTransactions = ({
* For security reason, everytime we get an update results we check if there are remaining transactions not certified in memory.
* If we find some, we prune those. Given that we are fetching transactions every X seconds, there should not be any query in memory when update calls have been resolved.
*/
const cleanTransactions = ({ certified }: { certified: boolean }) => {
const cleanTransactions = ({ certified, store }: { certified: boolean; store: WalletStore }) => {
if (!certified) {
return;
}
Expand Down Expand Up @@ -275,12 +247,7 @@ const cleanTransactions = ({ certified }: { certified: boolean }) => {

postMessageWalletCleanUp(notCertifiedTransactions);

store = {
...store,
transactions: {
...certifiedTransactions
}
};
store.clean(certifiedTransactions);
};

const postMessageWalletCleanUp = (transactions: IndexedTransactions) => {
Expand All @@ -304,3 +271,28 @@ const postMessageWalletError = (error: unknown) => {
data
});
};

const emitSavedWallet = ({ store, identity }: { store: WalletStore; identity: Identity }) => {
if (isNullish(store.balance)) {
return;
}

const uiTransactions = Object.values(store.transactions)
.sort(({ data: { id: idA } }, { data: { id: idB } }) => Number(idB) - Number(idA))
.map(({ certified, data: transaction }) => ({
certified,
data: mapIcpTransaction({ transaction, identity })
}));

const data: PostMessageDataResponseWallet = {
wallet: {
balance: store.balance,
newTransactions: JSON.stringify(uiTransactions, jsonReplacer)
}
};

postMessage({
msg: 'syncWallet',
data
});
};
Loading