Skip to content

Commit

Permalink
feat(wallet): use didc for candid parsing (#402)
Browse files Browse the repository at this point in the history
Adds candid encoding/decoding to external canister method calls through
the recently published
[@dfinity/didc](https://www.npmjs.com/package/@dfinity/didc) lib from
the candid [PR](dfinity/candid#577).
  • Loading branch information
keplervital authored Oct 25, 2024
1 parent 3f6eb10 commit c76d527
Show file tree
Hide file tree
Showing 19 changed files with 607 additions and 270 deletions.
8 changes: 7 additions & 1 deletion apps/wallet/build/main.build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import vue from '@vitejs/plugin-vue';
import { basename, dirname, resolve } from 'path';
import { defineConfig } from 'vite';
import vuetify from 'vite-plugin-vuetify';
import wasm from 'vite-plugin-wasm';
import {
ENV,
MODE,
Expand Down Expand Up @@ -34,13 +35,18 @@ export default defineConfig(_ => {
appType: 'spa',
server: {
open: false,
fs: {
// required to be able to test packages locally with pnpm link
strict: false,
},
},
preview: {
open: true,
},
// Vite automatically loads .env files from the root of the project if they are prefixed with the envPrefix.
envPrefix: 'APP_',
plugins: [
wasm(),
vue(),
vuetify({ autoImport: true }),
withCanisterIds({ isProduction }),
Expand All @@ -49,7 +55,7 @@ export default defineConfig(_ => {
withIcAssetsFile(isProduction && MODE !== 'localhost'),
],
build: {
target: 'es2020',
target: 'es2022',
sourcemap: !optimized,
minify: optimized,
chunkSizeWarningLimit: 500,
Expand Down
6 changes: 4 additions & 2 deletions apps/wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@dfinity/agent": "1.4.0",
"@dfinity/auth-client": "1.4.0",
"@dfinity/candid": "1.4.0",
"@dfinity/didc": "0.0.2",
"@dfinity/identity": "1.4.0",
"@dfinity/principal": "1.4.0",
"@mdi/font": "7.4.47",
Expand All @@ -43,13 +44,14 @@
"devDependencies": {
"@pinia/testing": "0.1.3",
"@types/validator": "13.11.10",
"@vitejs/plugin-vue": "5.0.4",
"@vitejs/plugin-vue": "5.1.4",
"@vue/test-utils": "2.4.6",
"cheerio": "1.0.0-rc.12",
"eslint-plugin-vue": "9.26.0",
"jsdom": "24.0.0",
"resize-observer-polyfill": "1.5.1",
"sass": "1.77.1",
"vite-plugin-vuetify": "2.0.3"
"vite-plugin-vuetify": "2.0.4",
"vite-plugin-wasm": "3.3.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
:hide="{ canisterId: !!canisterCallModel.canisterId }"
:allowed-methods="props.allowedMethods"
:allow-any-method="props.allowAnyMethod"
:candid-idl="props.canisterCandidIdl"
@submitting="canClose = !$event"
@submitted="open = false"
>
Expand Down Expand Up @@ -59,13 +60,15 @@ const props = withDefaults(
allowedMethods?: CanisterAllowedMethod[];
allowAnyMethod?: boolean;
dialogMaxWidth?: number;
canisterCandidIdl?: string;
title?: string;
}>(),
{
open: false,
allowedMethods: () => [],
allowAnyMethod: false,
dialogMaxWidth: 800,
canisterCandidIdl: undefined,
title: undefined,
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,14 @@
/>
</VCol>
<VCol cols="12" class="pb-0 px-6">
<CanisterArgumentField v-model="model.arg" name="argument" :readonly="readonly" />
<CanisterArgumentField
v-model="model.arg"
name="argument"
:readonly="readonly"
:candid="
props.candidIdl ? { idl: props.candidIdl, method: model.methodName } : undefined
"
/>
</VCol>
<VCol v-if="hasConfiguredValidationMethods" cols="12" class="px-0 px-6">
<VSelect
Expand Down Expand Up @@ -134,13 +141,16 @@ import { SelectItem } from '~/types/helper.types';
import { ValidationMethodResourceTarget } from '~/generated/station/station.did';
import { useI18n } from 'vue-i18n';
import { isApiError } from '~/utils/app.utils';
import { getServiceMethods } from '~/utils/didc.utils';
import logger from '~/core/logger.core';
const props = withDefaults(
defineProps<{
modelValue: CanisterCallModel;
allowedMethods?: CanisterAllowedMethod[];
allowAnyMethod?: boolean;
readonly?: boolean;
candidIdl?: string;
hide?: {
canisterId?: boolean;
};
Expand All @@ -149,6 +159,7 @@ const props = withDefaults(
readonly: false,
allowedMethods: () => [],
allowAnyMethod: false,
candidIdl: undefined,
hide: () => ({
canisterId: false,
}),
Expand Down Expand Up @@ -242,10 +253,20 @@ watch(
);
const availableExecutionMethods = computed<string[]>(() => {
const allowAnyMethod =
props.allowAnyMethod || props.allowedMethods.some(method => method.methodName === '*');
const configuredMethods = props.allowedMethods
.map(method => method.methodName)
.filter(methodName => !!methodName && methodName.trim().length && methodName !== '*');
if (allowAnyMethod && props.candidIdl) {
try {
configuredMethods.push(...getServiceMethods(props.candidIdl));
} catch (e) {
logger.warn('Failed to get service methods from candid idl', e);
}
}
return Array.from(new Set(configuredMethods)).sort();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

<CanisterConfigureMethodCallForm
v-model="canisterConfigureMethodCallModel"
:candid-idl="props.canisterCandidIdl"
@submitting="canClose = !$event"
@submitted="open = false"
>
Expand Down Expand Up @@ -70,6 +71,7 @@ const props = withDefaults(
alreadyConfiguredMethods?: CanisterConfiguredMethodCall[];
dialogMaxWidth?: number;
title?: string;
canisterCandidIdl?: string;
}>(),
{
open: false,
Expand All @@ -79,6 +81,7 @@ const props = withDefaults(
alreadyConfiguredMethods: () => [],
dialogMaxWidth: 800,
title: undefined,
canisterCandidIdl: undefined,
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
<VContainer>
<VRow>
<VCol cols="12" class="pb-0 px-6">
<VTextField
<VCombobox
v-model="model.methodName"
density="comfortable"
:prepend-inner-icon="mdiCodeBraces"
:readonly="readonly"
:label="$t('external_canisters.call_configuration.method_name')"
:hint="$t('external_canisters.call_configuration.method_name_hint')"
:rules="[requiredRule]"
:items="availableServiceMethods"
name="method_name"
/>
</VCol>
Expand Down Expand Up @@ -180,13 +181,17 @@ import { assertAndReturn } from '~/utils/helper.utils';
import AllowInput from '../inputs/AllowInput.vue';
import CanisterIdField from '../inputs/CanisterIdField.vue';
import { CanisterMethodCallConfigurationModel } from './external-canisters.types';
import { getServiceMethods } from '~/utils/didc.utils';
import logger from '~/core/logger.core';
const props = withDefaults(
defineProps<{
modelValue: CanisterMethodCallConfigurationModel;
candidIdl?: string;
readonly?: boolean;
}>(),
{
candidIdl: undefined,
readonly: false,
},
);
Expand Down Expand Up @@ -222,6 +227,19 @@ const createValidationTarget = (
}
: { No: null };
const availableServiceMethods = computed(() => {
if (!props.candidIdl) {
return [];
}
try {
return getServiceMethods(props.candidIdl);
} catch (error) {
logger.warn('Failed to parse the Candid IDL', error);
return [];
}
});
const { submit, edited, initialModel, additionalFieldErrors, submitting, valid, submitted } =
useForm({
model,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
:canister-id="props.canisterId"
:method="canisterConfigureMethodCallDialog.method"
:already-configured-methods="canisterConfigureMethodCallDialog.alreadyConfiguredMethods"
:canister-candid-idl="props.canisterCandidIdl"
@update:open="canisterConfigureMethodCallDialog.open = $event"
/>
<header class="d-flex flex-md-row flex-column ga-2">
Expand Down Expand Up @@ -149,11 +150,13 @@ const props = withDefaults(
requestPolicies?: ExternalCanisterRequestPolicies['calls'];
permissions?: ExternalCanisterPermissions['calls'];
readonly?: boolean;
canisterCandidIdl?: string;
}>(),
{
requestPolicies: () => [],
permissions: () => [],
readonly: false,
canisterCandidIdl: undefined,
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
v-model="canisterInstallModel"
v-model:trigger-submit="triggerFormSubmit"
:display="{ canisterId: props.canisterId === undefined }"
:candid-idl="props.canisterCandidIdl"
@submit="submit"
@valid="valid = $event"
/>
Expand Down Expand Up @@ -102,13 +103,15 @@ const props = withDefaults(
open?: boolean;
canisterId?: Principal;
canisterModuleHash?: string;
canisterCandidIdl?: string;
dialogMaxWidth?: number;
title?: string;
}>(),
{
open: false,
canisterId: undefined,
canisterModuleHash: undefined,
canisterCandidIdl: undefined,
dialogMaxWidth: 800,
title: undefined,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<CanisterArgumentField
v-model="model.wasmInstallArg"
:readonly="props.readonly"
:candid="props.candidIdl ? { idl: props.candidIdl } : undefined"
name="argument"
/>
</VCol>
Expand All @@ -51,13 +52,15 @@ const props = withDefaults(
modelValue: CanisterInstallModel;
triggerSubmit?: boolean;
readonly?: boolean;
candidIdl?: string;
display?: {
canisterId: boolean;
};
}>(),
{
readonly: false,
triggerSubmit: false,
candidIdl: undefined,
display: () => ({
canisterId: true,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,10 @@ export interface CanisterCallReviewContext {
canisterId: Principal;
methodName: string;
arg?: Uint8Array;
argHex?: string;
argChecksum?: string;
argValidationRendering?: string;
cycles?: bigint;
validationMethod?: CanisterMethod;
reply?: Uint8Array;
replyHex?: string;
candidIdl?: string;
}
35 changes: 27 additions & 8 deletions apps/wallet/src/components/inputs/CanisterArgumentField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,20 @@ import { useI18n } from 'vue-i18n';
import { VBtn, VBtnToggle, VIcon, VTextarea } from 'vuetify/components';
import { SelectItem } from '~/types/helper.types';
import { hexStringToArrayBuffer } from '~/utils/crypto.utils';
import { encode } from '~/utils/didc.utils';
import { isHexRule, requiredRule } from '~/utils/form.utils';
import { assertAndReturn } from '~/utils/helper.utils';
const props = withDefaults(
defineProps<{
modelValue?: Uint8Array;
candidIdl?: string;
/**
* Enables the argument to be written in Candid textual representation.
*/
candid?: {
idl: string;
method?: string;
};
readonly?: boolean;
required?: boolean;
label?: string;
Expand All @@ -54,7 +62,7 @@ const props = withDefaults(
readonly: false,
required: false,
label: undefined,
candidIdl: undefined,
candid: undefined,
density: 'comfortable',
variant: 'filled',
},
Expand All @@ -81,16 +89,16 @@ const label = computed(() => {
});
const argument = ref<string>();
const selectedParseFormat = ref<string>(props.candidIdl ? 'candid' : 'hex');
const selectedParseFormat = ref<string>(props.candid ? 'candid' : 'hex');
const availableParseFormats = computed<SelectItem[]>(() => {
const items: SelectItem[] = [
{ text: i18n.t('external_canisters.wasm_args_formats.hex'), value: 'hex' },
];
const items: SelectItem[] = [];
if (props.candidIdl) {
if (props.candid) {
items.push({ text: i18n.t('external_canisters.wasm_args_formats.candid'), value: 'candid' });
}
items.push({ text: i18n.t('external_canisters.wasm_args_formats.hex'), value: 'hex' });
return items;
});
Expand Down Expand Up @@ -127,7 +135,18 @@ const parseArgumentRule = async (value: unknown): Promise<string | boolean> => {
parsedArgument = new Uint8Array(hexStringToArrayBuffer(rawArgument));
break;
}
case 'candid':
case 'candid': {
const candid = assertAndReturn(props.candid, 'Candid definition is expected');
const hexString = encode({
idl: candid.idl,
serviceMethod: candid.method,
input: rawArgument,
targetFormat: 'hex',
});
parsedArgument = new Uint8Array(hexStringToArrayBuffer(hexString));
break;
}
default:
throw new Error('Not implemented');
}
Expand Down
Loading

0 comments on commit c76d527

Please sign in to comment.