Skip to content
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
2 changes: 1 addition & 1 deletion nix/sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"motoko": {
"ref": "master",
"repo": "ssh://git@github.com/dfinity-lab/motoko",
"rev": "788395608cdf75ecdff3c904bef52f2478467d5f",
"rev": "849cfba0b8dead6c6e5de0905491f9e33631eb32",
"type": "git"
},
"napalm": {
Expand Down
9 changes: 6 additions & 3 deletions src/userlib/js/bootstrap/candid/idl-ui.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IDL } from '@internet-computer/userlib';
import { CanisterId, IDL } from '@internet-computer/userlib';
import BigNumber from 'bignumber.js';

// tslint:disable:max-classes-per-file
Expand All @@ -7,7 +7,7 @@ class Render extends IDL.Visitor<null, InputBox> {
public visitPrimitive<T>(t: IDL.PrimitiveType<T>, d: null): InputBox {
return new InputBox(t, null);
}
public visitUnit(t: IDL.UnitClass, d: null): InputBox {
public visitNull(t: IDL.NullClass, d: null): InputBox {
const input = new InputBox(t, null);
input.input.type = 'hidden';
return input;
Expand All @@ -34,7 +34,7 @@ class Render extends IDL.Visitor<null, InputBox> {
}

class Parse extends IDL.Visitor<string, any> {
public visitUnit(t: IDL.UnitClass, v: string): null {
public visitNull(t: IDL.NullClass, v: string): null {
return null;
}
public visitBool(t: IDL.BoolClass, v: string): boolean {
Expand All @@ -61,6 +61,9 @@ class Parse extends IDL.Visitor<string, any> {
public visitFixedNat(t: IDL.FixedNatClass, v: string): BigNumber {
return new BigNumber(v);
}
public visitPrincipal(t: IDL.PrincipalClass, v: string): CanisterId {
return CanisterId.fromText(v);
}
}

export function renderInput(t: IDL.Type): InputBox {
Expand Down
20 changes: 14 additions & 6 deletions src/userlib/js/src/canisterId.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
// Canister IDs are represented as an array of bytes in the HTTP handler of the client.
export class CanisterId {
public static fromText(hex: string): CanisterId {
if (hex.startsWith('ic:')) {
// Remove the checksum from the hexadecimal.
// TODO: validate the checksum.
return this.fromHex(hex.slice(3, -2));
public static fromText(text: string): CanisterId {
if (text.startsWith('ic:')) {
const hex = text.slice(3);
if (hex.length % 2 === 0 && /^[0-9A-F]+$/.test(hex)) {
// Remove the checksum from the hexadecimal.
// TODO: validate the checksum.
return this.fromHex(hex.slice(0, -2));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't fromHex fail here actually? (I thought it was failing.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fromHex just stores the string. It doesn't do any checks.

Copy link
Contributor

@eftychis eftychis Mar 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I can swear I had added checks in encode decode functions somewhere...

carry on 😉

} else {
throw new Error('Cannot parse canister id: ' + text);
}
} else {
throw new Error('CanisterId not a "ic:" url: ' + hex);
throw new Error('CanisterId not a "ic:" url: ' + text);
}
}

Expand All @@ -19,4 +24,7 @@ export class CanisterId {
public toHex(): string {
return this._idHex;
}
public toText(): string {
return 'ic:' + this.toHex() + '00';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don’t forget a // TODO here :-)

}
}
26 changes: 21 additions & 5 deletions src/userlib/js/src/idl.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// tslint:disable
import BigNumber from 'bignumber.js';
import { CanisterId } from './canisterId';
import * as IDL from './idl';
import { Buffer } from 'buffer/';

Expand Down Expand Up @@ -37,9 +38,9 @@ test('IDL encoding (empty)', () => {
);
});

test('IDL encoding (unit)', () => {
test('IDL encoding (null)', () => {
// Null
test_(IDL.Unit, null, '4449444c00017f', 'Null value');
test_(IDL.Null, null, '4449444c00017f', 'Null value');
});

test('IDL encoding (text)', () => {
Expand Down Expand Up @@ -151,7 +152,7 @@ test('IDL encoding (array + tuples)', () => {

// Nested Tuples
test_(
IDL.Tuple(IDL.Tuple(IDL.Tuple(IDL.Tuple(IDL.Unit)))),
IDL.Tuple(IDL.Tuple(IDL.Tuple(IDL.Tuple(IDL.Null)))),
[[[[null]]]],
'4449444c046c01007f6c0100006c0100016c0100020103',
'Nested Tuples',
Expand Down Expand Up @@ -212,6 +213,21 @@ test('IDL encoding (bool)', () => {
expect(() => IDL.encode([IDL.Bool], ['false'])).toThrow(/Invalid bool argument/);
});

test('IDL encoding (principal)', () => {
// Principal
test_(IDL.Principal, CanisterId.fromText('ic:CAFFEE00'), '4449444c0001680103caffee', 'principal');
test_(
IDL.Principal,
CanisterId.fromText('ic:000000000000000107'),
'4449444c00016801080000000000000001',
'principal',
);
expect(() => IDL.encode([IDL.Principal], ['ic:CAFFEE00'])).toThrow(/Invalid principal argument/);
expect(() => IDL.decode([IDL.Principal], Buffer.from('4449444c00016803caffee', 'hex'))).toThrow(
/Cannot decode principal/,
);
});

test('IDL encoding (variants)', () => {
// Variants
const Result = IDL.Variant({ ok: IDL.Text, err: IDL.Text });
Expand All @@ -224,7 +240,7 @@ test('IDL encoding (variants)', () => {

// Test that nullary constructors work as expected
test_(
IDL.Variant({ foo: IDL.Unit }),
IDL.Variant({ foo: IDL.Null }),
{ foo: null },
'4449444c016b01868eb7027f010000',
'Nullary constructor in variant',
Expand All @@ -250,7 +266,7 @@ test('IDL encoding (variants)', () => {
'4449444c026e7d6e000101010101',
'Nested option',
);
test_(IDL.Opt(IDL.Opt(IDL.Unit)), [[null]], '4449444c026e7f6e0001010101', 'Null option');
test_(IDL.Opt(IDL.Opt(IDL.Null)), [[null]], '4449444c026e7f6e0001010101', 'Null option');

// Type description sharing
test_(
Expand Down
67 changes: 59 additions & 8 deletions src/userlib/js/src/idl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import BigNumber from 'bignumber.js';
import Pipe = require('buffer-pipe');
import { Buffer } from 'buffer/';
import { CanisterId } from './canisterId';
import { JsonValue } from './types';
import { idlLabelToId } from './utils/hash';
import { lebDecode, lebEncode, slebDecode, slebEncode } from './utils/leb128';
Expand All @@ -26,6 +27,9 @@ const enum IDLTypeIds {
Vector = -19,
Record = -20,
Variant = -21,
Func = -22,
Service = -23,
Principal = -24,
}

const magicNumber = 'DIDL';
Expand Down Expand Up @@ -95,7 +99,7 @@ export abstract class Visitor<D, R> {
public visitBool(t: BoolClass, data: D): R {
return this.visitPrimitive(t, data);
}
public visitUnit(t: UnitClass, data: D): R {
public visitNull(t: NullClass, data: D): R {
return this.visitPrimitive(t, data);
}
public visitText(t: TextClass, data: D): R {
Expand All @@ -113,6 +117,9 @@ export abstract class Visitor<D, R> {
public visitFixedNat(t: FixedNatClass, data: D): R {
return this.visitPrimitive(t, data);
}
public visitPrincipal(t: PrincipalClass, data: D): R {
return this.visitPrimitive(t, data);
}

public visitConstruct<T>(t: ConstructType<T>, data: D): R {
return this.visitType(t, data);
Expand Down Expand Up @@ -265,9 +272,9 @@ export class BoolClass extends PrimitiveType<boolean> {
/**
* Represents an IDL Null
*/
export class UnitClass extends PrimitiveType<null> {
export class NullClass extends PrimitiveType<null> {
public accept<D, R>(v: Visitor<D, R>, d: D): R {
return v.visitUnit(this, d);
return v.visitNull(this, d);
}

public covariant(x: any): x is null {
Expand Down Expand Up @@ -873,7 +880,52 @@ export class RecClass<T = any> extends ConstructType<T> {
}

/**
* Represents an async function which can return data.
* Represents an IDL principal reference
*/
export class PrincipalClass extends PrimitiveType<CanisterId> {
public accept<D, R>(v: Visitor<D, R>, d: D): R {
return v.visitPrincipal(this, d);
}

public covariant(x: any): x is CanisterId {
return x instanceof CanisterId;
}

public encodeValue(x: CanisterId): Buffer {
const hex = x.toHex();
const buf = Buffer.from(hex, 'hex');
const len = lebEncode(buf.length);
return Buffer.concat([Buffer.from([1]), len, buf]);
}

public encodeType() {
return slebEncode(IDLTypeIds.Principal);
}

public decodeValue(b: Pipe): CanisterId {
const x = b.read(1).toString('hex');
if (x !== '01') {
throw new Error('Cannot decode principal');
}
const len = lebDecode(b).toNumber();
const hex = b
.read(len)
.toString('hex')
.toUpperCase();
// TODO implement checksum
return CanisterId.fromText('ic:' + hex + '00');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good, but perhaps we need to centralize all this in a IC identifier library?? I am going to add senders for instance which is more of the same logic. And this is canister id specific. (No change asked for this PR, just asking, as I want to do something similar.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Same applies to Rust. canisterId is defined inside the ic_http_agent crate. Probably separate that out as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I have been pondering that. Not sure which option is the best.

}

get name() {
return 'principal';
}
public valueToString(x: CanisterId) {
return x.toText();
}
}

/**
* Represents an IDL function reference.
* @param argTypes Argument types.
* @param retTypes Return types.
* @param annotations Function annotations.
Expand Down Expand Up @@ -994,16 +1046,13 @@ export function decode(retTypes: Type[], bytes: Buffer): JsonValue[] {
* @param {Object} [fields] - a map of function names to IDL function signatures
*/
export class ActorInterface {
protected _id: Blob | null = null;
protected _batch: boolean = false;

constructor(public _fields: Record<string, FuncClass>) {}
}

// Export Types instances.
export const Empty = new EmptyClass();
export const Bool = new BoolClass();
export const Unit = new UnitClass();
export const Null = new NullClass();
export const Text = new TextClass();
export const Int = new IntClass();
export const Nat = new NatClass();
Expand All @@ -1018,6 +1067,8 @@ export const Nat16 = new FixedNatClass(16);
export const Nat32 = new FixedNatClass(32);
export const Nat64 = new FixedNatClass(64);

export const Principal = new PrincipalClass();

export function Tuple<T extends any[]>(...types: T): TupleClass<T> {
return new TupleClass(types);
}
Expand Down