Skip to content

feat(frontend): wire frontend to backend pow API #5889

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

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
443feb8
feat: wire frontend to backend pow API (scope 6)
DecentAgeCoder Apr 16, 2025
503c082
🤖 Apply formatting changes
github-actions[bot] Apr 16, 2025
def3055
Merge remote-tracking branch 'origin/main' into feat/frontend/pow/pro…
DecentAgeCoder Apr 17, 2025
0f19642
Adapted AllowSigningParams interface so it matches the expectation of…
DecentAgeCoder Apr 22, 2025
0f1b97f
🤖 Apply formatting changes
github-actions[bot] Apr 22, 2025
319a318
Merge branch 'main' into feat/frontend/pow/protect-allow-signing-6
DecentAgeCoder Apr 22, 2025
ddd1684
Revert "Adapted AllowSigningParams interface so it matches the expect…
DecentAgeCoder Apr 22, 2025
6a89abd
Merge branch 'refs/heads/main' into feat/frontend/pow/protect-allow-s…
DecentAgeCoder Apr 22, 2025
6330049
Adapted AllowSigningParams interface so it matches the expectation of…
DecentAgeCoder Apr 22, 2025
c9fa579
Merge branch 'main' into feat/frontend/pow/protect-allow-signing-6
DecentAgeCoder Apr 22, 2025
9e6a759
🤖 Apply formatting changes
github-actions[bot] Apr 22, 2025
65a86f1
Merge branch 'main' into feat/frontend/pow/protect-allow-signing-6
DecentAgeCoder Apr 22, 2025
62deee5
Merge branch 'main' into feat/frontend/pow/protect-allow-signing-6
DecentAgeCoder Apr 22, 2025
33d7aec
Merge remote-tracking branch 'origin/main' into feat/frontend/pow/pro…
DecentAgeCoder Apr 22, 2025
dd789be
Merge remote-tracking branch 'origin/main' into feat/frontend/pow/pro…
DecentAgeCoder Apr 22, 2025
fb6153a
Merge remote-tracking branch 'origin/main' into feat/frontend/pow/pro…
DecentAgeCoder Apr 23, 2025
339289c
🤖 Apply bindings changes
github-actions[bot] Apr 23, 2025
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
21 changes: 5 additions & 16 deletions src/frontend/src/lib/api/backend.api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {
AllowSigningResponse,
CreateChallengeResponse,
CustomToken,
PendingTransaction,
SelectedUtxosFeeResponse,
Expand All @@ -13,11 +14,9 @@ import type {
AddUserCredentialResponse,
AddUserHiddenDappIdParams,
AllowSigningParams,
AllowSigningResult,
BtcAddPendingTransactionParams,
BtcGetPendingTransactionParams,
BtcSelectUserUtxosFeeParams,
CreateChallengeResult,
GetUserProfileResponse,
SaveUserNetworksSettings,
SetUserShowTestnetsParams
Expand Down Expand Up @@ -143,9 +142,9 @@ export const selectUserUtxosFee = async ({

export const createPowChallenge = async ({
identity
}: CanisterApiFunctionParams): Promise<CreateChallengeResult> => {
const { createPowChallengeResult } = await backendCanister({ identity });
return createPowChallengeResult();
}: CanisterApiFunctionParams): Promise<CreateChallengeResponse> => {
const { createPowChallenge } = await backendCanister({ identity });
return createPowChallenge();
};

export const allowSigning = async ({
Expand All @@ -154,17 +153,7 @@ export const allowSigning = async ({
}: CanisterApiFunctionParams<AllowSigningParams>): Promise<AllowSigningResponse> => {
const { allowSigning } = await backendCanister({ identity });

return allowSigning(params);
};

export const allowSigningResult = async ({
identity,
...params
}: CanisterApiFunctionParams<AllowSigningParams>): Promise<AllowSigningResult> => {
const { allowSigningResult } = await backendCanister({ identity });

// Conditionally call allowSigning with request or provide default logic
return allowSigningResult(params);
return allowSigning(params.nonce);
};

export const addUserHiddenDappId = async ({
Expand Down
33 changes: 17 additions & 16 deletions src/frontend/src/lib/canisters/backend.canister.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
AllowSigningResponse,
_SERVICE as BackendService,
CreateChallengeResponse,
CustomToken,
PendingTransaction,
SelectedUtxosFeeResponse,
Expand All @@ -13,18 +14,16 @@ import { getAgent } from '$lib/actors/agents.ic';
import {
mapAllowSigningError,
mapBtcPendingTransactionError,
mapBtcSelectUserUtxosFeeError
mapBtcSelectUserUtxosFeeError,
mapCreateChallengeError
} from '$lib/canisters/backend.errors';
import type {
AddUserCredentialParams,
AddUserCredentialResponse,
AddUserHiddenDappIdParams,
AllowSigningParams,
AllowSigningResult,
BtcAddPendingTransactionParams,
BtcGetPendingTransactionParams,
BtcSelectUserUtxosFeeParams,
CreateChallengeResult,
GetUserProfileResponse,
SaveUserNetworksSettings,
SetUserShowTestnetsParams
Expand Down Expand Up @@ -177,27 +176,29 @@ export class BackendCanister extends Canister<BackendService> {
throw mapBtcSelectUserUtxosFeeError(response.Err);
};

// directly returning result and not the response
// TODO: check if this one is really needed because it may cause duplication of code with `allowSigningResult`
allowSigningResult = async ({ request }: AllowSigningParams): Promise<AllowSigningResult> => {
allowSigning = async (nonce?: bigint): Promise<AllowSigningResponse> => {
const { allow_signing } = this.caller({ certified: true });
return await allow_signing(toNullable(request));
};

allowSigning = async ({ request }: AllowSigningParams): Promise<AllowSigningResponse> => {
const response = await this.allowSigningResult({ request });
const result = await allow_signing(nonce !== undefined ? [{ nonce }] : []);

if ('Ok' in response) {
const { Ok } = response;
if ('Ok' in result) {
const { Ok } = result;
return Ok;
}

throw mapAllowSigningError(response.Err);
throw mapAllowSigningError(result.Err);
};

createPowChallengeResult = (): Promise<CreateChallengeResult> => {
createPowChallenge = async (): Promise<CreateChallengeResponse> => {
const { create_pow_challenge } = this.caller({ certified: true });
return create_pow_challenge();

const result = await create_pow_challenge();
if ('Ok' in result) {
const { Ok } = result;
return Ok;
}

throw mapCreateChallengeError(result.Err);
};

addUserHiddenDappId = async ({
Expand Down
21 changes: 21 additions & 0 deletions src/frontend/src/lib/canisters/backend.errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
AllowSigningError,
BtcAddPendingTransactionError,
ChallengeCompletionError,
CreateChallengeError,
SelectedUtxosFeeError
} from '$declarations/backend/backend.did';
import { CanisterInternalError } from '$lib/canisters/errors';
Expand Down Expand Up @@ -54,3 +55,23 @@ export const mapAllowSigningError = (

return new CanisterInternalError('Unknown AllowSigningError');
};

export const mapCreateChallengeError = (err: CreateChallengeError): CanisterInternalError => {
if ('ChallengeInProgress' in err) {
return new CanisterInternalError('Challenge is already in progress.');
}

if ('MissingUserProfile' in err) {
return new CanisterInternalError('User profile is missing.');
}

if ('RandomnessError' in err) {
return new CanisterInternalError(err.RandomnessError);
}

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

return new CanisterInternalError('Unknown CreateChallengeError');
};
15 changes: 3 additions & 12 deletions src/frontend/src/lib/types/api.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import type {
AddUserCredentialError,
AllowSigningError,
AllowSigningRequest,
AllowSigningResponse,
BitcoinNetwork,
CreateChallengeError,
CreateChallengeResponse,
CredentialSpec,
GetUserProfileError,
UserProfile,
Expand Down Expand Up @@ -34,9 +29,9 @@ export type AddUserCredentialResponse = { Ok: null } | { Err: AddUserCredentialE

export type GetUserProfileResponse = { Ok: UserProfile } | { Err: GetUserProfileError };

export type AllowSigningResult = { Ok: AllowSigningResponse } | { Err: AllowSigningError };

export type CreateChallengeResult = { Ok: CreateChallengeResponse } | { Err: CreateChallengeError };
export interface AllowSigningParams {
nonce?: bigint;
}

export interface BtcSelectUserUtxosFeeParams {
network: BitcoinNetwork;
Expand Down Expand Up @@ -101,7 +96,3 @@ export interface KongSwapParams {
sourceToken: Token;
payTransactionId?: TxId;
}

export interface AllowSigningParams {
request?: AllowSigningRequest;
}
117 changes: 37 additions & 80 deletions src/frontend/src/tests/lib/canisters/backend.canister.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type {
_SERVICE as BackendService,
CreateChallengeResponse,
CustomToken,
IcrcToken,
Result_2,
Expand Down Expand Up @@ -625,7 +624,7 @@ describe('backend.canister', () => {
serviceOverride: service
});

const res = await allowSigning({});
const res = await allowSigning();

expect(service.allow_signing).toHaveBeenCalledTimes(1);
expect(res).toBeDefined();
Expand All @@ -641,7 +640,7 @@ describe('backend.canister', () => {
serviceOverride: service
});

const res = allowSigning({});
const res = allowSigning();

await expect(res).rejects.toThrow(mockResponseError);
});
Expand All @@ -659,9 +658,7 @@ describe('backend.canister', () => {
serviceOverride: service
});

await expect(allowSigning({})).rejects.toThrow(
mapIcrc2ApproveError(response.Err.ApproveError)
);
await expect(allowSigning()).rejects.toThrow(mapIcrc2ApproveError(response.Err.ApproveError));
});

it('should throw a CanisterInternalError if FailedToContactCyclesLedger error is returned', async () => {
Expand All @@ -673,7 +670,7 @@ describe('backend.canister', () => {
serviceOverride: service
});

await expect(allowSigning({})).rejects.toThrow(
await expect(allowSigning()).rejects.toThrow(
new CanisterInternalError('The Cycles Ledger cannot be contacted.')
);
});
Expand All @@ -688,7 +685,7 @@ describe('backend.canister', () => {
serviceOverride: service
});

await expect(allowSigning({})).rejects.toThrow(new CanisterInternalError(errorMsg));
await expect(allowSigning()).rejects.toThrow(new CanisterInternalError(errorMsg));
});

it('should throw an unknown AllowSigningError if unrecognized error is returned', async () => {
Expand All @@ -700,120 +697,80 @@ describe('backend.canister', () => {
serviceOverride: service
});

await expect(allowSigning({})).rejects.toThrow(
await expect(allowSigning()).rejects.toThrow(
new CanisterInternalError('Unknown AllowSigningError')
);
});
});

describe('createPowChallenge', () => {
const mockPowChallengeSuccess: CreateChallengeResponse = {
const mockPowChallengeSuccess = {
start_timestamp_ms: 1_644_001_000_000n,
expiry_timestamp_ms: 1_644_001_001_200n,
difficulty: 1_000_000
};

let backendCanister: BackendCanister;

beforeEach(async () => {
backendCanister = await createBackendCanister({ serviceOverride: service });
});

it('should successfully create a PoW challenge (Ok case)', async () => {
service.create_pow_challenge.mockResolvedValue({ Ok: mockPowChallengeSuccess });

const backendCanister = await createBackendCanister({ serviceOverride: service });

const result = await backendCanister.createPowChallengeResult();
const result = await backendCanister.createPowChallenge();

expect(service.create_pow_challenge).toHaveBeenCalled();

if ('Ok' in result) {
expect(result.Ok).toEqual(mockPowChallengeSuccess);
} else {
throw new Error(`Unexpected error: ${JSON.stringify(result.Err)}`);
}
expect(result).toEqual(mockPowChallengeSuccess);
});

test('should handle challenge already in progress error', async () => {
it('should handle challenge already in progress error', async () => {
service.create_pow_challenge.mockResolvedValue({
Err: { ChallengeInProgress: null }
});

const backendCanister = await createBackendCanister({ serviceOverride: service });

const result = await backendCanister.createPowChallengeResult();
await expect(backendCanister.createPowChallenge()).rejects.toThrowError(
'Challenge is already in progress.'
);

expect(result).toEqual({ Err: { ChallengeInProgress: null } });
expect(service.create_pow_challenge).toHaveBeenCalled();
});

test('should handle randomness generation error', async () => {
it('should handle randomness generation error', async () => {
service.create_pow_challenge.mockResolvedValue({
Err: { RandomnessError: 'Failed to generate randomness' }
});

const backendCanister = await createBackendCanister({ serviceOverride: service });

const result = await backendCanister.createPowChallengeResult();
await expect(backendCanister.createPowChallenge()).rejects.toThrowError(
'Failed to generate randomness'
);

expect(result).toEqual({
Err: { RandomnessError: 'Failed to generate randomness' }
});
expect(service.create_pow_challenge).toHaveBeenCalled();
});

it('should handle missing user profile error', async () => {
service.create_pow_challenge.mockResolvedValue({ Err: { MissingUserProfile: null } });

const backendCanister = await createBackendCanister({ serviceOverride: service });

const result = await backendCanister.createPowChallengeResult();

expect(result).toEqual({ Err: { MissingUserProfile: null } });

expect(service.create_pow_challenge).toHaveBeenCalledTimes(1);
});

it('should handle unexpected errors in result', async () => {
service.create_pow_challenge.mockResolvedValue({
Err: { Other: 'Unexpected error occurred.' }
Err: { MissingUserProfile: null }
});

const backendCanister = await createBackendCanister({ serviceOverride: service });

const result = await backendCanister.createPowChallengeResult();

expect(result).toEqual({ Err: { Other: 'Unexpected error occurred.' } });

expect(service.create_pow_challenge).toHaveBeenCalledTimes(1);
});
});

describe('addUserHiddenDappId', () => {
it('should add user hidden dapp id', async () => {
const response = { Ok: null };

service.add_user_hidden_dapp_id.mockResolvedValue(response);

const { addUserHiddenDappId } = await createBackendCanister({
serviceOverride: service
});

const res = await addUserHiddenDappId({ dappId: 'test-dapp-id' });
await expect(backendCanister.createPowChallenge()).rejects.toThrowError(
'User profile is missing.'
);

expect(service.add_user_hidden_dapp_id).toHaveBeenCalledWith({
dapp_id: 'test-dapp-id',
current_user_version: []
});
expect(res).toBeUndefined();
expect(service.create_pow_challenge).toHaveBeenCalled();
});

it('should throw an error if add_user_hidden_dapp_id throws', async () => {
service.add_user_hidden_dapp_id.mockImplementation(async () => {
await Promise.resolve();
throw mockResponseError;
});

const { addUserHiddenDappId } = await createBackendCanister({
serviceOverride: service
it('should handle other unexpected errors', async () => {
service.create_pow_challenge.mockResolvedValue({
Err: { Other: 'Unexpected error occurred.' }
});

const res = addUserHiddenDappId({ dappId: 'test-dapp-id' });
await expect(backendCanister.createPowChallenge()).rejects.toThrowError(
'Unexpected error occurred.'
);

await expect(res).rejects.toThrow(mockResponseError);
expect(service.create_pow_challenge).toHaveBeenCalled();
});
});

Expand Down
Loading