Skip to content

Commit

Permalink
feat(js): Add error handling during IDL parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
holykol committed May 23, 2024
1 parent fbe22b4 commit f1a1f45
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 72 deletions.
39 changes: 32 additions & 7 deletions idl-parser/src/ffi/ast/mod.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,43 @@
use crate::ast;
use std::{slice, str};
use std::{
error::Error,
ffi::{c_char, CString},
slice, str,
};

pub mod visitor;

#[repr(C)]
pub enum ParseResult {
Success(*mut Program),
Error(*const c_char),
}

/// # Safety
///
/// See the safety documentation of [`slice::from_raw_parts`].
#[no_mangle]
pub unsafe extern "C" fn parse_idl(idl_ptr: *const u8, idl_len: u32) -> *mut Program {
let idl = unsafe { slice::from_raw_parts(idl_ptr, idl_len.try_into().unwrap()) };
let idl = str::from_utf8(idl).unwrap();
let program = ast::parse_idl(idl).unwrap();
let program = Box::new(program);
Box::into_raw(program)
pub unsafe extern "C" fn parse_idl(idl_ptr: *const u8, idl_len: u32) -> *mut ParseResult {
let idl_slice = unsafe { slice::from_raw_parts(idl_ptr, idl_len as usize) };
let idl_str = match str::from_utf8(idl_slice) {
Ok(s) => s,
Err(e) => return create_error(e, "validate idl_str"),
};

let program = match ast::parse_idl(idl_str) {
Ok(p) => p,
Err(e) => return create_error(e, "parse IDL"),
};

let program_box = Box::new(program);
let result = Box::new(ParseResult::Success(Box::into_raw(program_box)));
Box::into_raw(result)
}

fn create_error(e: impl Error, context: &str) -> *mut ParseResult {
let err_str = CString::new(format!("{}: {}", context, e)).unwrap();
let result = Box::new(ParseResult::Error(err_str.into_raw()));
Box::into_raw(result)
}

/// # Safety
Expand Down
130 changes: 72 additions & 58 deletions js/example/src/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@ export type Part =
| { slot: SlotPart };

export interface FixedPart {
z: number | string | null;
z: number | null;
metadata_uri: string;
}

export interface SlotPart {
equippable: Array<`0x${string}` | Uint8Array>;
z: number | string | null;
equippable: Array<string>;
z: number | null;
metadata_uri: string;
}

export class RmrkCatalog {
private registry: TypeRegistry;
public readonly registry: TypeRegistry;
public readonly service: Service;

constructor(public api: GearApi, public programId?: `0x${string}`) {
const types: Record<string, any> = {
Error: {"_enum":["PartIdCantBeZero","BadConfig","PartAlreadyExists","ZeroLengthPassed","PartDoesNotExist","WrongPartFormat","NotAllowedToCall"]},
Expand All @@ -32,6 +34,8 @@ export class RmrkCatalog {
this.registry = new TypeRegistry();
this.registry.setKnownTypes({ types });
this.registry.register(types);

this.service = new Service(this);
}

newCtorFromCode(code: Uint8Array | Buffer): TransactionBuilder<null> {
Expand Down Expand Up @@ -63,104 +67,114 @@ export class RmrkCatalog {
this.programId = builder.programId;
return builder;
}
}

public addEquippables(part_id: number | string, collection_ids: Array<`0x${string}` | Uint8Array>): TransactionBuilder<{ ok: [number | string, Array<`0x${string}` | Uint8Array>] } | { err: Error }> {
return new TransactionBuilder<{ ok: [number | string, Array<`0x${string}` | Uint8Array>] } | { err: Error }>(
this.api,
this.registry,
export class Service {
constructor(private _program: RmrkCatalog) {}

public addEquippables(part_id: number, collection_ids: Array<string>): TransactionBuilder<{ ok: [number, Array<string>] } | { err: Error }> {
if (!this._program.programId) throw new Error('Program ID is not set');
return new TransactionBuilder<{ ok: [number, Array<string>] } | { err: Error }>(
this._program.api,
this._program.registry,
'send_message',
['AddEquippables', part_id, collection_ids],
'(String, u32, Vec<[u8;32]>)',
['Service', 'AddEquippables', part_id, collection_ids],
'(String, String, u32, Vec<[u8;32]>)',
'Result<(u32, Vec<[u8;32]>), Error>',
this.programId
this._program.programId
);
}

public addParts(parts: Record<number | string, Part>): TransactionBuilder<{ ok: Record<number | string, Part> } | { err: Error }> {
return new TransactionBuilder<{ ok: Record<number | string, Part> } | { err: Error }>(
this.api,
this.registry,
public addParts(parts: Record<number, Part>): TransactionBuilder<{ ok: Record<number, Part> } | { err: Error }> {
if (!this._program.programId) throw new Error('Program ID is not set');
return new TransactionBuilder<{ ok: Record<number, Part> } | { err: Error }>(
this._program.api,
this._program.registry,
'send_message',
['AddParts', parts],
'(String, BTreeMap<u32, Part>)',
['Service', 'AddParts', parts],
'(String, String, BTreeMap<u32, Part>)',
'Result<BTreeMap<u32, Part>, Error>',
this.programId
this._program.programId
);
}

public removeEquippable(part_id: number | string, collection_id: `0x${string}` | Uint8Array): TransactionBuilder<{ ok: [number | string, `0x${string}` | Uint8Array] } | { err: Error }> {
return new TransactionBuilder<{ ok: [number | string, `0x${string}` | Uint8Array] } | { err: Error }>(
this.api,
this.registry,
public removeEquippable(part_id: number, collection_id: string): TransactionBuilder<{ ok: [number, string] } | { err: Error }> {
if (!this._program.programId) throw new Error('Program ID is not set');
return new TransactionBuilder<{ ok: [number, string] } | { err: Error }>(
this._program.api,
this._program.registry,
'send_message',
['RemoveEquippable', part_id, collection_id],
'(String, u32, [u8;32])',
['Service', 'RemoveEquippable', part_id, collection_id],
'(String, String, u32, [u8;32])',
'Result<(u32, [u8;32]), Error>',
this.programId
this._program.programId
);
}

public removeParts(part_ids: Array<number | string>): TransactionBuilder<{ ok: Array<number | string> } | { err: Error }> {
return new TransactionBuilder<{ ok: Array<number | string> } | { err: Error }>(
this.api,
this.registry,
public removeParts(part_ids: Array<number>): TransactionBuilder<{ ok: Array<number> } | { err: Error }> {
if (!this._program.programId) throw new Error('Program ID is not set');
return new TransactionBuilder<{ ok: Array<number> } | { err: Error }>(
this._program.api,
this._program.registry,
'send_message',
['RemoveParts', part_ids],
'(String, Vec<u32>)',
['Service', 'RemoveParts', part_ids],
'(String, String, Vec<u32>)',
'Result<Vec<u32>, Error>',
this.programId
this._program.programId
);
}

public resetEquippables(part_id: number | string): TransactionBuilder<{ ok: null } | { err: Error }> {
public resetEquippables(part_id: number): TransactionBuilder<{ ok: null } | { err: Error }> {
if (!this._program.programId) throw new Error('Program ID is not set');
return new TransactionBuilder<{ ok: null } | { err: Error }>(
this.api,
this.registry,
this._program.api,
this._program.registry,
'send_message',
['ResetEquippables', part_id],
'(String, u32)',
['Service', 'ResetEquippables', part_id],
'(String, String, u32)',
'Result<Null, Error>',
this.programId
this._program.programId
);
}

public setEquippablesToAll(part_id: number | string): TransactionBuilder<{ ok: null } | { err: Error }> {
public setEquippablesToAll(part_id: number): TransactionBuilder<{ ok: null } | { err: Error }> {
if (!this._program.programId) throw new Error('Program ID is not set');
return new TransactionBuilder<{ ok: null } | { err: Error }>(
this.api,
this.registry,
this._program.api,
this._program.registry,
'send_message',
['SetEquippablesToAll', part_id],
'(String, u32)',
['Service', 'SetEquippablesToAll', part_id],
'(String, String, u32)',
'Result<Null, Error>',
this.programId
this._program.programId
);
}

public async equippable(part_id: number | string, collection_id: `0x${string}` | Uint8Array, originAddress: string, value?: number | string | bigint, atBlock?: `0x${string}`): Promise<{ ok: boolean } | { err: Error }> {
const payload = this.registry.createType('(String, u32, [u8;32])', ['Equippable', part_id, collection_id]).toU8a();
const reply = await this.api.message.calculateReply({
destination: this.programId,
public async equippable(part_id: number, collection_id: string, originAddress: string, value?: number | string | bigint, atBlock?: `0x${string}`): Promise<{ ok: boolean } | { err: Error }> {
const payload = this._program.registry.createType('(String, String, u32, [u8;32])', ['Service', 'Equippable', part_id, collection_id]).toHex();
const reply = await this._program.api.message.calculateReply({
destination: this._program.programId,
origin: decodeAddress(originAddress),
payload,
value: value || 0,
gasLimit: this.api.blockGasLimit.toBigInt(),
gasLimit: this._program.api.blockGasLimit.toBigInt(),
at: atBlock || null,
});
const result = this.registry.createType('(String, Result<bool, Error>)', reply.payload);
return result[1].toJSON() as unknown as { ok: boolean } | { err: Error };
const result = this._program.registry.createType('(String, String, Result<bool, Error>)', reply.payload);
return result[2].toJSON() as unknown as { ok: boolean } | { err: Error };
}

public async part(part_id: number | string, originAddress: string, value?: number | string | bigint, atBlock?: `0x${string}`): Promise<Part | null> {
const payload = this.registry.createType('(String, u32)', ['Part', part_id]).toU8a();
const reply = await this.api.message.calculateReply({
destination: this.programId,
public async part(part_id: number, originAddress: string, value?: number | string | bigint, atBlock?: `0x${string}`): Promise<Part | null> {
const payload = this._program.registry.createType('(String, String, u32)', ['Service', 'Part', part_id]).toHex();
const reply = await this._program.api.message.calculateReply({
destination: this._program.programId,
origin: decodeAddress(originAddress),
payload,
value: value || 0,
gasLimit: this.api.blockGasLimit.toBigInt(),
gasLimit: this._program.api.blockGasLimit.toBigInt(),
at: atBlock || null,
});
const result = this.registry.createType('(String, Option<Part>)', reply.payload);
return result[1].toJSON() as unknown as Part | null;
const result = this._program.registry.createType('(String, String, Option<Part>)', reply.payload);
return result[2].toJSON() as unknown as Part | null;
}
}
12 changes: 6 additions & 6 deletions js/example/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -514,10 +514,10 @@ bn.js@^5.2.1:
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70"
integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==

commander@12.0.0:
version "12.0.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-12.0.0.tgz#b929db6df8546080adfd004ab215ed48cf6f2592"
integrity sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==
commander@12.1.0:
version "12.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3"
integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==

create-require@^1.1.0:
version "1.1.1"
Expand Down Expand Up @@ -617,9 +617,9 @@ rxjs@^7.8.1:
tslib "^2.1.0"

"sails-js@file:../lib":
version "0.0.7"
version "0.1.4"
dependencies:
commander "12.0.0"
commander "12.1.0"

scale-ts@^1.6.0:
version "1.6.0"
Expand Down
1 change: 1 addition & 0 deletions js/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const handler = async (path: string, out: string, name: string) => {
generate(sails.parseIdl(idl), dir, file, name);
} catch (e) {
console.log(e.message, e.stack);
process.exit(1);
}
};

Expand Down
23 changes: 22 additions & 1 deletion js/src/parser/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,31 @@ export class WasmParser {
this._idlLen = null;
}

private readCString = (ptr: number): string => {
const view = new DataView(this._memory.buffer);
let len = 0;
while (view.getUint8(ptr + len) !== 0) {
len++;
}
const buf = new Uint8Array(this._memory.buffer, ptr, len);
return new TextDecoder().decode(buf);
};

public parse(idl: string): Program {
this.fillMemory(idl);

const programPtr = this._instance.exports.parse_idl(this._memPtr, this._idlLen);
const resultPtr = this._instance.exports.parse_idl(this._memPtr, this._idlLen);

const view = new DataView(this._memory.buffer);
if (view.getUint32(resultPtr, true) == 1) { // Read ParseResult enum discriminant
// Error
const errorPtr = view.getUint32(resultPtr + 4, true);
const error = this.readCString(errorPtr);

throw new Error(error);
}

const programPtr = view.getUint32(resultPtr + 4, true);

this._program = new Program();
this._instance.exports.accept_program(programPtr, 0);
Expand Down

0 comments on commit f1a1f45

Please sign in to comment.