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(frontend): init signer allowance before getting addresses #2632

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions src/frontend/src/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ declare namespace svelteHTML {
'on:oisyCkBtcMinterInfoStatus'?: (event: CustomEvent<any>) => void;
'on:oisyCkEthMinterInfoStatus'?: (event: CustomEvent<any>) => void;
'on:oisyCkEthereumPendingTransactions'?: (event: CustomEvent<any>) => void;
'on:oisyValidateAddresses'?: (event: CustomEvent<any>) => void;
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/frontend/src/lib/api/backend.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
AddUserCredentialResponse,
GetUserProfileResponse
} from '$lib/types/api';
import type { AllowSigningResponse } from '$lib/types/backend';
import type { CanisterApiFunctionParams } from '$lib/types/canister';
import { Principal } from '@dfinity/principal';
import { assertNonNullish, isNullish, type QueryParams } from '@dfinity/utils';
Expand Down Expand Up @@ -88,6 +89,7 @@ export const getUserProfile = async ({

return getUserProfile({ certified });
};

export const addUserCredential = async ({
identity,
...params
Expand All @@ -97,6 +99,14 @@ export const addUserCredential = async ({
return addUserCredential(params);
};

export const allowSigning = async ({
identity
}: CanisterApiFunctionParams): Promise<AllowSigningResponse> => {
const { allowSigning } = await backendCanister({ identity });

return allowSigning();
};

const backendCanister = async ({
identity,
nullishIdentityErrorMessage,
Expand Down
7 changes: 7 additions & 0 deletions src/frontend/src/lib/canisters/backend.canister.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
AddUserCredentialResponse,
GetUserProfileResponse
} from '$lib/types/api';
import type { AllowSigningResponse } from '$lib/types/backend';
import type { CreateCanisterOptions } from '$lib/types/canister';
import { Canister, createServices, toNullable, type QueryParams } from '@dfinity/utils';

Expand Down Expand Up @@ -94,4 +95,10 @@ export class BackendCanister extends Canister<BackendService> {
credential_spec: credentialSpec
});
};

allowSigning = async (): Promise<AllowSigningResponse> => {
const { allow_signing } = this.caller({ certified: true });

return allow_signing();
};
}
13 changes: 13 additions & 0 deletions src/frontend/src/lib/components/core/Loader.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
loadIdbAddresses
} from '$lib/services/address.services';
import { signOut } from '$lib/services/auth.services';
import { initSignerAllowance } from '$lib/services/loader.services';
import { i18n } from '$lib/stores/i18n.store';
import { loading } from '$lib/stores/loader.store';
import { emit } from '$lib/utils/events.utils';

let progressStep: string = ProgressStepsLoader.ADDRESSES;

Expand Down Expand Up @@ -83,6 +85,8 @@
}
}

const validateAddresses = () => emit({ message: 'oisyValidateAddresses' });

onMount(async () => {
const { success: addressIdbSuccess, err } = await loadIdbAddresses();

Expand All @@ -91,12 +95,21 @@

await progressAndLoad();

validateAddresses();

return;
}

// We are loading the addresses from the backend. Consequently, we aim to animate this operation and offer the user an explanation of what is happening. To achieve this, we will present this information within a modal.
progressModal = true;

const { success: initSignerAllowanceSuccess } = await initSignerAllowance();

if (!initSignerAllowanceSuccess) {
await signOut();
return;
}

const { success: addressSuccess } = await loadAddresses(
err?.map(({ tokenId }) => tokenId) ?? []
);
Expand Down
36 changes: 30 additions & 6 deletions src/frontend/src/lib/components/guard/AddressGuard.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
<script lang="ts">
import { NETWORK_BITCOIN_ENABLED } from '$env/networks.btc.env';
import { validateBtcAddressMainnet, validateEthAddress } from '$lib/services/address.services';
import { signOut } from '$lib/services/auth.services';
import { initSignerAllowance } from '$lib/services/loader.services';
import { btcAddressMainnetStore, ethAddressStore } from '$lib/stores/address.store';

$: $btcAddressMainnetStore,
(async () =>
NETWORK_BITCOIN_ENABLED
? await validateBtcAddressMainnet($btcAddressMainnetStore)
: await Promise.resolve())();
let signerAllowanceLoaded = false;

$: $ethAddressStore, (async () => await validateEthAddress($ethAddressStore))();
const loadSignerAllowanceAndValidateAddresses = async () => {
const { success: initSignerAllowanceSuccess } = await initSignerAllowance();

if (!initSignerAllowanceSuccess) {
await signOut();
return;
}

signerAllowanceLoaded = true;

await validateAddresses();
};

const validateAddresses = async () => {
if (!signerAllowanceLoaded) {
return;
}

await Promise.allSettled([
validateEthAddress($ethAddressStore),
...(NETWORK_BITCOIN_ENABLED ? [validateBtcAddressMainnet($btcAddressMainnetStore)] : [])
]);
};

$: $btcAddressMainnetStore, $ethAddressStore, (async () => await validateAddresses())();
</script>

<svelte:window on:oisyValidateAddresses={loadSignerAllowanceAndValidateAddresses} />

<slot />
61 changes: 61 additions & 0 deletions src/frontend/src/lib/services/loader.services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { AllowSigningError } from '$declarations/backend/backend.did';
import { allowSigning } from '$lib/api/backend.api';
import { authStore } from '$lib/stores/auth.store';
import { i18n } from '$lib/stores/i18n.store';
import { toastsError } from '$lib/stores/toasts.store';
import type { ResultSuccess } from '$lib/types/utils';
import { mapIcrc2ApproveError } from '@dfinity/ledger-icp';
import { get } from 'svelte/store';

/**
* `allow_signing` needs to be called before get eth address etc. It is currently enough to call it once at boot time.
* It provides a cycles budget that should be big enough for any reasonable number of calls to the signer.
* "reasonable" is currently defined as 30 calls, so one call to allow should enable the user to get their eth+btc addresses and then make say 28 transactions.
*/
export const initSignerAllowance = async (): Promise<ResultSuccess> => {
try {
const { identity } = get(authStore);

const result = await allowSigning({ identity });

// TODO: maybe we do not even need a toast given that the user is signed out anyway?

if ('Err' in result) {
const mapErr = (err: AllowSigningError): Error => {
if ('ApproveError' in err) {
return mapIcrc2ApproveError(err.ApproveError);
}

if ('FailedToContactCyclesLedger' in err) {
return new Error('TODO_FailedToContactCyclesLedger');
}

if ('Other' in err) {
return new Error(err.Other);
}

return new Error();
};

const err = mapErr(result.Err);

toastsError({
msg: { text: get(i18n).send.error.unexpected },
err
});

return { success: false };
}
} catch (err: unknown) {
toastsError({
msg: {
text: get(i18n).init.error.loading_address
},
err
});

return { success: false };
}

return { success: true };
};
3 changes: 3 additions & 0 deletions src/frontend/src/lib/types/backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { Result_1 } from '$declarations/backend/backend.did';

export type AllowSigningResponse = Result_1;