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: get mission control certified #1098

Merged
merged 7 commits into from
Jan 16, 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
14 changes: 10 additions & 4 deletions src/frontend/src/lib/api/actors/actor.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,32 @@ import type { IDL } from '@dfinity/candid';
import { Principal } from '@dfinity/principal';
import { assertNonNullish, isNullish } from '@dfinity/utils';

type GetActorParams = {
type CreateActorParams = {
canisterId: string | Principal;
idlFactory: IDL.InterfaceFactory;
config?: Pick<ActorConfig, 'callTransform' | 'queryTransform'>;
} & GetAgentParams;

export interface GetActorParams {
certified?: boolean;
identity: OptionIdentity;
}

export class ActorApi<T = Record<string, ActorMethod>> {
#actors: Option<Record<string, ActorSubclass<T>>> = undefined;

async getActor({
identity,
canisterId,
certified = false,
...rest
}: Omit<GetActorParams, 'identity'> & { identity: OptionIdentity }): Promise<ActorSubclass<T>> {
}: Omit<CreateActorParams, 'identity'> & GetActorParams): Promise<ActorSubclass<T>> {
assertNonNullish(identity, 'No internet identity to initialize the actor.');

const identityText = identity.getPrincipal().toText();
const canisterIdText = canisterId instanceof Principal ? canisterId.toText() : canisterId;

const key = `${canisterIdText}#${identityText}`;
const key = `${canisterIdText}${certified ? '+' : '#'}${identityText}`;

if (isNullish(this.#actors) || isNullish(this.#actors[key])) {
const actor = await this.createActor({ identity, canisterId, ...rest });
Expand All @@ -46,7 +52,7 @@ export class ActorApi<T = Record<string, ActorMethod>> {
idlFactory,
config = {},
...rest
}: GetActorParams): Promise<ActorSubclass<T>> {
}: CreateActorParams): Promise<ActorSubclass<T>> {
const agent = await getAgent(rest);

// Creates an actor with using the candid interface and the HttpAgent
Expand Down
10 changes: 7 additions & 3 deletions src/frontend/src/lib/api/actors/actor.juno.api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { _SERVICE as ConsoleActor } from '$declarations/console/console.did';
import { idlFactory as idlFactoryCertifiedConsole } from '$declarations/console/console.factory.certified.did';
import { idlFactory as idlFactoryConsole } from '$declarations/console/console.factory.did';
import type { _SERVICE as MissionControlActor } from '$declarations/mission_control/mission_control.did';
import { idlFactory as idlFactoryMissionControl } from '$declarations/mission_control/mission_control.factory.did';
Expand All @@ -8,7 +9,7 @@ import type { _SERVICE as OrbiterActor } from '$declarations/orbiter/orbiter.did
import { idlFactory as idlFactoryOrbiter } from '$declarations/orbiter/orbiter.factory.did';
import type { _SERVICE as SatelliteActor } from '$declarations/satellite/satellite.did';
import { idlFactory as idlFactorySatellite } from '$declarations/satellite/satellite.factory.did';
import { ActorApi } from '$lib/api/actors/actor.api';
import { ActorApi, type GetActorParams } from '$lib/api/actors/actor.api';
import { CONSOLE_CANISTER_ID, OBSERVATORY_CANISTER_ID } from '$lib/constants/constants';
import type { OptionIdentity } from '$lib/types/itentity';
import type { MissionControlId } from '$lib/types/mission-control';
Expand All @@ -20,10 +21,13 @@ const satelliteActor = new ActorApi<SatelliteActor>();
const orbiterActor = new ActorApi<OrbiterActor>();
const missionControlActor = new ActorApi<MissionControlActor>();

export const getConsoleActor = async (identity: OptionIdentity): Promise<ConsoleActor> =>
export const getConsoleActor = async ({
identity,
certified
}: GetActorParams): Promise<ConsoleActor> =>
await consoleActor.getActor({
canisterId: CONSOLE_CANISTER_ID,
idlFactory: idlFactoryConsole,
idlFactory: certified ? idlFactoryCertifiedConsole : idlFactoryConsole,
identity
});

Expand Down
23 changes: 12 additions & 11 deletions src/frontend/src/lib/api/console.api.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import type { MissionControl } from '$declarations/console/console.did';
import type { GetActorParams } from '$lib/api/actors/actor.api';
import { getConsoleActor } from '$lib/api/actors/actor.juno.api';
import type { OptionIdentity } from '$lib/types/itentity';
import type { Principal } from '@dfinity/principal';
import { fromNullable, isNullish } from '@dfinity/utils';

export const initMissionControl = async (identity: OptionIdentity): Promise<MissionControl> => {
const actor = await getConsoleActor(identity);
const { init_user_mission_control_center } = await getConsoleActor({ identity });

const existingMissionControl: MissionControl | undefined = fromNullable<MissionControl>(
await actor.get_user_mission_control_center()
);
return await init_user_mission_control_center();
};

if (!existingMissionControl) {
return await actor.init_user_mission_control_center();
}
export const getMissionControl = async (
actorParams: GetActorParams
): Promise<MissionControl | undefined> => {
const { get_user_mission_control_center } = await getConsoleActor(actorParams);

return existingMissionControl;
return fromNullable(await get_user_mission_control_center());
};

export const getCredits = async (identity: OptionIdentity): Promise<bigint> => {
const { get_credits } = await getConsoleActor(identity);
const { get_credits } = await getConsoleActor({ identity });
const { e8s } = await get_credits();
return e8s;
};
Expand All @@ -31,7 +32,7 @@ export const getSatelliteFee = async ({
user: Principal;
identity: OptionIdentity;
}): Promise<bigint> => {
const actor = await getConsoleActor(identity);
const actor = await getConsoleActor({ identity });
const result = await actor.get_create_satellite_fee({ user });
const fee = fromNullable(result);

Expand All @@ -46,7 +47,7 @@ export const getOrbiterFee = async ({
user: Principal;
identity: OptionIdentity;
}): Promise<bigint> => {
const actor = await getConsoleActor(identity);
const actor = await getConsoleActor({ identity });
const result = await actor.get_create_orbiter_fee({ user });
const fee = fromNullable(result);

Expand Down
4 changes: 2 additions & 2 deletions src/frontend/src/lib/derived/mission-control.derived.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { missionControlIdUncertifiedStore } from '$lib/stores/mission-control.store';
import { missionControlIdCertifiedStore } from '$lib/stores/mission-control.store';
import { derived } from 'svelte/store';

// TODO: find a better name but, I don't want to use missionControlId because it would clashes with the properties called missionControlId
export const missionControlIdDerived = derived(
[missionControlIdUncertifiedStore],
[missionControlIdCertifiedStore],
([$missionControlDataStore]) => $missionControlDataStore?.data
);
1 change: 1 addition & 0 deletions src/frontend/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@
"errors": {
"no_identity": "Unexpected error. No identity provided.",
"initializing_mission_control": "Mission control center cannot be initialized.",
"mission_control_sign_out": "You have been signed out because your Mission Control could not be certified.",
"no_mission_control": "Mission control center is not initialized.",
"cli_missing_params": "Missing URL parameters. Either the redirection URL or principal is not provided.",
"cli_missing_selection": "No mission control or satellite(s) selected.",
Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/lib/i18n/zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@
"errors": {
"no_identity": "异常错误,没有提供 identity ",
"initializing_mission_control": "Mission control center cannot be initialized.",
"mission_control_sign_out": "You have been signed out because your Mission Control could not be certified.",
"no_mission_control": "Mission 控制中心没有初始化.",
"cli_missing_params": "URL参数缺失,重定向URL或者 principal没有提供.",
"cli_missing_selection": "没选择 mission control 或 satellite(s) .",
Expand Down
8 changes: 8 additions & 0 deletions src/frontend/src/lib/services/auth.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ export const signIn = async (

export const signOut = (): Promise<void> => logout({});

export const missionControlErrorSignOut = async () =>
await logout({
msg: {
text: get(i18n).errors.mission_control_sign_out,
level: 'error'
}
});

export const idleSignOut = async () =>
await logout({
msg: {
Expand Down
130 changes: 105 additions & 25 deletions src/frontend/src/lib/services/console.services.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import type { MissionControl } from '$declarations/console/console.did';
import type { Orbiter } from '$declarations/mission_control/mission_control.did';
import { initMissionControl as initMissionControlApi } from '$lib/api/console.api';
import {
getMissionControl as getMissionControlApi,
initMissionControl as initMissionControlApi
} from '$lib/api/console.api';
import { missionControlVersion } from '$lib/api/mission-control.api';
import { orbiterVersion } from '$lib/api/orbiter.api';
import { satelliteBuildVersion, satelliteVersion } from '$lib/api/satellites.api';
import { getNewestReleasesMetadata } from '$lib/rest/cdn.rest';
import { missionControlErrorSignOut } from '$lib/services/auth.services';
import { authStore } from '$lib/stores/auth.store';
import { i18n } from '$lib/stores/i18n.store';
import { missionControlIdCertifiedStore } from '$lib/stores/mission-control.store';
import { toasts } from '$lib/stores/toasts.store';
import { versionStore, type ReleaseVersionSatellite } from '$lib/stores/version.store';
import type { OptionIdentity } from '$lib/types/itentity';
import type { MissionControlId } from '$lib/types/mission-control';
import type { Option } from '$lib/types/utils';
import { container } from '$lib/utils/juno.utils';
Expand All @@ -16,63 +24,135 @@ import { assertNonNullish, fromNullable, isNullish, nonNullish } from '@dfinity/
import { satelliteBuildType } from '@junobuild/admin';
import { get } from 'svelte/store';

interface Certified {
certified: boolean;
}

type PollAndInitResult = {
missionControlId: MissionControlId;
} & Certified;

export const initMissionControl = async ({
identity,
onInitMissionControlSuccess
identity
}: {
identity: OptionIdentity;
}): Promise<{ result: 'skip' | 'success' | 'error' }> => {
// If not signed in, we are not going to init and load a mission control.
if (isNullish(identity)) {
return { result: 'skip' };
}

try {
// Poll to init mission control center
const { missionControlId, certified } = await pollAndInitMissionControl({
identity
});

missionControlIdCertifiedStore.set({
data: missionControlId,
certified
});

if (certified) {
return { result: 'success' };
}

// We deliberately do not await the promise to avoid blocking the main UX. However, if necessary, we take the required measures if Mission Control cannot be certified.
assertMissionControl({ identity });

return { result: 'success' };
} catch (err: unknown) {
toasts.error({
text: get(i18n).errors.initializing_mission_control,
detail: err
});

// There was an error so, we sign the user out otherwise skeleton and other spinners will be displayed forever
await missionControlErrorSignOut();

return { result: 'error' };
}
};

const pollAndInitMissionControl = async ({
identity
}: {
identity: Identity;
onInitMissionControlSuccess: (missionControlId: MissionControlId) => void;
// eslint-disable-next-line no-async-promise-executor, require-await
}) =>
}): Promise<PollAndInitResult> =>
// eslint-disable-next-line no-async-promise-executor, require-await
new Promise<void>(async (resolve, reject) => {
new Promise<PollAndInitResult>(async (resolve, reject) => {
try {
const { missionControlId } = await getMissionControl({
const { missionControlId, certified } = await getOrInitMissionControlId({
identity
});

// TODO: we can/should probably add a max time to not retry forever even though the user will probably close their browsers.
if (isNullish(missionControlId)) {
setTimeout(async () => {
try {
await initMissionControl({ identity, onInitMissionControlSuccess });
resolve();
const result = await pollAndInitMissionControl({ identity });
resolve(result);
} catch (err: unknown) {
reject(err);
}
}, 2000);
return;
}

onInitMissionControlSuccess(missionControlId);

resolve();
resolve({ missionControlId, certified });
} catch (err: unknown) {
reject(err);
}
});

const getMissionControl = async ({
const getOrInitMissionControlId = async (params: {
identity: Identity;
}): Promise<
{
missionControlId: MissionControlId | undefined;
} & Certified
> => {
const { missionControl, certified } = await getOrInitMissionControl(params);

const missionControlId = fromNullable(missionControl.mission_control_id);

return {
missionControlId,
certified
};
};

export const getOrInitMissionControl = async ({
identity
}: {
identity: Identity | undefined;
}): Promise<{
missionControlId: MissionControlId | undefined;
}> => {
if (isNullish(identity)) {
throw new Error('Invalid identity.');
}
identity: Identity;
}): Promise<{ missionControl: MissionControl } & Certified> => {
const existingMissionControl = await getMissionControlApi({ identity, certified: false });

const mission_control = await initMissionControlApi(identity);
if (isNullish(existingMissionControl)) {
const newMissionControl = await initMissionControlApi(identity);

const missionControlId: MissionControlId | undefined = fromNullable<Principal>(
mission_control.mission_control_id
);
return {
missionControl: newMissionControl,
certified: true
};
}

return {
missionControlId
missionControl: existingMissionControl,
certified: false
};
};

const assertMissionControl = async ({ identity }: { identity: Identity }) => {
try {
await getMissionControlApi({ identity, certified: true });
} catch (error: unknown) {
await missionControlErrorSignOut();
}
};

export const loadVersion = async ({
satelliteId,
missionControlId,
Expand Down
5 changes: 3 additions & 2 deletions src/frontend/src/lib/stores/mission-control.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import type {
MissionControlSettings,
User
} from '$declarations/mission_control/mission_control.did';
import { initCertifiedStore } from '$lib/stores/_certified.store';
import { initUncertifiedStore } from '$lib/stores/_uncertified.store';
import type { Principal } from '@dfinity/principal';
import type { MissionControlId } from '$lib/types/mission-control';

export const missionControlIdUncertifiedStore = initUncertifiedStore<Principal>();
export const missionControlIdCertifiedStore = initCertifiedStore<MissionControlId>();

export const missionControlUncertifiedStore = initUncertifiedStore<User>();

Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/lib/types/i18n.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ interface I18nCli {
interface I18nErrors {
no_identity: string;
initializing_mission_control: string;
mission_control_sign_out: string;
no_mission_control: string;
cli_missing_params: string;
cli_missing_selection: string;
Expand Down
Loading
Loading