Skip to content

feat(didc-js): encode and validate specific types and service args #602

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
64 changes: 61 additions & 3 deletions tools/didc-js/wasm-package/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @dfinity/didc

A multi-purpose Candid tool for JavaScript projects, including encoding and decoding Candid values.
A multi-purpose Candid tool for JavaScript projects, including encoding and decoding Candid values.

## Usage

Expand All @@ -16,7 +16,16 @@ export const IDL = `
number : nat64;
};

service : {
type InitArgs = record {
name : text;
};

type SomeType = record {
field1 : text;
field2 : nat64;
};

service : (InitArgs) -> {
store_number : (input : StoreNumberInput) -> ();
get_number : () -> (nat64) query;
};
Expand All @@ -33,7 +42,23 @@ const methods = getServiceMethods(IDL);
const encoded = encode({
idl: IDL,
input: "(record { number=90; })",
serviceMethod: "store_number",
withType: { methodParams: "store_number" },
targetFormat: "hex",
});

// Encodes a specific type - encode values according to a named type
const encoded = encode({
idl: IDL,
input: '(record { field1="Hello"; field2=42 })',
withType: { type: "SomeType" },
targetFormat: "hex",
});

// Encodes service constructor parameters
const encoded = encode({
idl: IDL,
input: '(record { name="MyCanister" })',
withType: { serviceParams: null },
targetFormat: "hex",
});

Expand All @@ -48,3 +73,36 @@ const decoded = decode({
targetFormat: "candid",
});
```

## API Reference

### getServiceMethods(idl: string): string[]

Returns a list of method names available in the service defined in the IDL.

### encode(args: EncodeArgs): string

Encodes Candid text format to hex or blob format.

Parameters:

- `idl`: The Candid IDL to encode against
- `input`: The Candid text value to encode
- `withType` (optional): Type specifier for encoding:
- `{ methodParams: "method_name" }`: Uses the parameters of the specified method
- `{ type: "type_name" }`: Uses the specified type
- `{ serviceParams: null }`: Uses the service constructor parameters
- `targetFormat` (optional): Output format, either 'hex' (default) or 'blob'

### decode(args: DecodeArgs): string

Decodes hex or blob format to Candid text format.

Parameters:

- `idl`: The Candid IDL to decode against
- `input`: The hex or blob value to decode
- `serviceMethod` (optional): Method to use for type information
- `useServiceMethodReturnType` (optional): Whether to use return types (true) or parameter types (false)
- `inputFormat` (optional): Input format, either 'hex' (default) or 'blob'
- `targetFormat` (optional): Output format, only 'candid' is supported currently
4 changes: 2 additions & 2 deletions tools/didc-js/wasm-package/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@dfinity/didc",
"description": "Utility tools for candid.",
"version": "0.0.2",
"version": "0.0.3",
"author": "DFINITY Stiftung",
"license": "Apache-2.0",
"repository": "github:dfinity/candid",
Expand Down Expand Up @@ -36,4 +36,4 @@
"devDependencies": {
"wasm-pack": "0.13.0"
}
}
}
216 changes: 211 additions & 5 deletions tools/didc-js/wasm-package/src/core.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use crate::validation::Validate;
use candid::IDLArgs;
use crate::{types::EncodeType, validation::Validate};
use candid::{
types::{Type, TypeInner},
IDLArgs,
};

use crate::{
errors::LibraryError,
Expand Down Expand Up @@ -37,8 +40,8 @@ pub fn encode(args: EncodeArgs) -> Result<String, LibraryError> {
let idl = parse_idl(&args.idl)?;
let idl_args = parse_idl_args(&args.input)?;

let args_bytes = match (args.service_method, &idl.actor) {
(Some(method), Some(actor)) => {
let args_bytes = match (args.with_type, &idl.actor) {
(Some(EncodeType::MethodParams(method)), Some(actor)) => {
let method = idl.env.get_method(actor, &method).map_err(|_| {
LibraryError::IdlMethodNotFound {
method: method.clone(),
Expand All @@ -51,7 +54,53 @@ pub fn encode(args: EncodeArgs) -> Result<String, LibraryError> {
reason: format!("Could not encode args to bytes {}", e),
})?
}
(Some(method), None) => Err(LibraryError::IdlMethodNotFound { method })?,
(Some(EncodeType::Type(type_name)), Some(_)) => {
let type_def =
idl.env
.find_type(&type_name)
.map_err(|_| LibraryError::TypeNotFound {
type_name: type_name.clone(),
})?;

idl_args
.to_bytes_with_types(&idl.env, &[type_def.clone()])
.map_err(|e| LibraryError::IdlArgsToBytesFailed {
reason: format!("Could not encode input to bytes {}", e),
})?
}
(Some(EncodeType::ServiceParams), Some(actor)) => match actor {
Type(inner) => match &**inner {
TypeInner::Service(_) => {
if !idl_args.args.is_empty() {
return Err(LibraryError::UnexpectedServiceParams);
}

idl_args.to_bytes_with_types(&idl.env, &[]).map_err(|e| {
LibraryError::IdlArgsToBytesFailed {
reason: format!("Could not encode input to bytes {}", e),
}
})?
}

TypeInner::Class(ctor_args, _) => {
if ctor_args.is_empty() && !idl_args.args.is_empty() {
return Err(LibraryError::UnexpectedServiceParams);
}

idl_args
.to_bytes_with_types(&idl.env, ctor_args)
.map_err(|e| LibraryError::IdlArgsToBytesFailed {
reason: format!("Could not encode input to bytes {}", e),
})?
}
_ => {
return Err(LibraryError::IdlArgsToBytesFailed {
reason: "Service parameters are not supported for this type".to_string(),
})
}
},
},
(Some(_), None) => Err(LibraryError::IdlNotFound)?,
(None, _) => idl_args
.to_bytes()
.map_err(|e| LibraryError::IdlArgsToBytesFailed {
Expand Down Expand Up @@ -118,3 +167,160 @@ pub fn decode(args: DecodeArgs) -> Result<String, LibraryError> {

Ok(decoded_idl_args)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_encode_service_params() {
let idl = r#"
type Payload = record {
field1: text;
field2: nat;
};

type Init = variant {
One : Payload;
Two;
};

service : (opt Init) -> {}
"#;

encode(EncodeArgs {
idl: idl.to_owned(),
target_format: EncodeFormat::Hex,
input: r#"(opt variant { One = record { field1 = "Hello"; field_invalid = 1 } })"#
.to_owned(),
with_type: Some(EncodeType::ServiceParams),
})
.expect_err("Should fail to encode with bad args");

encode(EncodeArgs {
idl: idl.to_owned(),
target_format: EncodeFormat::Hex,
input: r#"(opt variant { One = record { field1 = "Hello"; field2 = 1 } })"#.to_owned(),
with_type: Some(EncodeType::ServiceParams),
})
.expect("Failed to encode");
}

#[test]
fn test_encode_service_without_params() {
encode(EncodeArgs {
idl: r#"service : {}"#.to_owned(),
target_format: EncodeFormat::Hex,
input: r#"()"#.to_owned(),
with_type: Some(EncodeType::ServiceParams),
})
.expect("Failed to encode");

encode(EncodeArgs {
idl: r#"service : {}"#.to_owned(),
target_format: EncodeFormat::Hex,
input: r#"(1)"#.to_owned(),
with_type: Some(EncodeType::ServiceParams),
})
.expect_err("Should fail to encode with args");

encode(EncodeArgs {
idl: r#"service : () -> {}"#.to_owned(),
target_format: EncodeFormat::Hex,
input: r#"(1)"#.to_owned(),
with_type: Some(EncodeType::ServiceParams),
})
.expect_err("Should fail to encode with args");

encode(EncodeArgs {
idl: r#"service : () -> {}"#.to_owned(),
target_format: EncodeFormat::Hex,
input: r#"()"#.to_owned(),
with_type: Some(EncodeType::ServiceParams),
})
.expect("Failed to encode");
}

#[test]
fn test_encode_value() {
let idl = r#"
type Payload = record {
field1: text;
field2: nat;
};

type Args = variant {
One : Payload;
Two;
};

service : {
test_method : (opt Args) -> () query;
}
"#;

encode(EncodeArgs {
idl: idl.to_owned(),
target_format: EncodeFormat::Hex,
input: r#"(variant { One = record { field1 = "Hello"; field_invalid = 1 } })"#
.to_owned(),
with_type: Some(EncodeType::Type("Args".to_string())),
})
.expect_err("Should fail to encode with bad args");

encode(EncodeArgs {
idl: idl.to_owned(),
target_format: EncodeFormat::Hex,
input: r#"(variant { One = record { field1 = "Hello"; field2 = 1 } })"#.to_owned(),
with_type: Some(EncodeType::Type("Args".to_string())),
})
.expect("Failed to encode");
}

#[test]
fn test_encode_method_args() {
let idl = r#"
type Payload = record {
field1: text;
field2: nat;
};

type Args = variant {
One : Payload;
Two;
};

service : {
test_method : (opt Args, nat) -> () query;
}
"#;

encode(EncodeArgs {
idl: idl.to_owned(),
target_format: EncodeFormat::Hex,

input: r#"(opt variant { One = record { field1 = "Hello"; field2 = 1 } })"#.to_owned(),
with_type: Some(EncodeType::MethodParams("test_method".to_string())),
})
.expect_err("Should fail to encode with missing second argument");

encode(EncodeArgs {
idl: idl.to_owned(),
target_format: EncodeFormat::Hex,

input: r#"(opt variant { One = record { field1 = "Hello"; field_invalid = 1 } }, 1)"#
.to_owned(),
with_type: Some(EncodeType::MethodParams("test_method".to_string())),
})
.expect_err("Should fail to encode with invalid argument");

encode(EncodeArgs {
idl: idl.to_owned(),
target_format: EncodeFormat::Hex,
input: r#"(opt variant { One = record { field1 = "Hello"; field2 = 1 } }, 1)"#
.to_owned(),
with_type: Some(EncodeType::MethodParams("test_method".to_string())),
})
.expect("Failed to encode");
}
}
9 changes: 9 additions & 0 deletions tools/didc-js/wasm-package/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ pub enum LibraryError {
/// Service method not found.
#[error(r#"Service method not found `{method}`."#)]
IdlMethodNotFound { method: String },
/// IDL not specified.
#[error(r#"IDL not specified."#)]
IdlNotFound,
/// Type not found.
#[error(r#"Type not found `{type_name}`."#)]
TypeNotFound { type_name: String },
/// Unexpected service parameters.
#[error(r#"Unexpected service parameters."#)]
UnexpectedServiceParams,
/// Failed to parse arguments.
#[error(r#"Failed to parse arguments `{reason}`."#)]
IdlArgsParsingFailed { reason: String },
Expand Down
Loading
Loading