[JS] @dfinity/agent version 0.7.1

Hi all, wanted to give you a heads-up that we have released a potentially breaking change to @dfinity/agent. We have removed the BigNumber package as a dependency in favor of the BigInt primitive, which will make our total package size a bit smaller and more performant.

Additionally, @dfinity/agent and @dfinity/authentication have improved Readme’s with links to some autogenerated documentation. You can install the latest versions from npm!

13 Likes

To forecast some additional upcoming changes, we’ve prioritized the following work:

  • Separate Candid / IDL logic into its own interface
  • Refactor authentication into three packages
    • @dfinity/authentication retains a library of methods, the minimum API to support IC authentication
    • @dfinity/identity, for crypto-generic identities (Ed25519, Delegation, etc)
    • @dfinity/auth-client contains code to interface with a hosted auth0 identity provider
  • Open sourcing!
5 Likes

Unable to use this version of agent with dfx 0.7.0 in typescript - generated typings are incompatible because of BigNumber.

UPD: it’s not critical, but annoying to as unknown as BigNumber.

1 Like

And also, there is a bug in agent encoding.

The code like:

await sendNat(BigInt(0));

fails with

Invalid nat argument: 0n

It works with plain zero 0 though.

1 Like

Thanks, I’ll put some pressure on the Candid team to make the types update, and I’ll look into the invalid Nat issue. Do you have a bit more context to help me reproduce and write a new test?

Hey there! Thanks for a quick response.

To reproduce:

// increment.mo
actor {
    public func inc(arg: Nat): async Nat {
        return arg + 1;
    };
}

// test.ts
await IncrementActor.inc(0 as unknown as BigNumber); // pass
await IncrementActor.inc(BigInt(1) as unknown as BigNumber); // pass
await IncrementActor.inc(BigInt(0) as unknown as BigNumber); // fail - Invalid nat argument: 0n

P.S. I’m preparing more bug reports, stay tuned.

UPD: When it’s wrapped like this

// increment.mo
public type WrappedNat = { value: Nat };

public query func incWrapped(arg: WrappedNat): async WrappedNat {
    return { value = arg.value + 1 };
};

// test.ts
await IncrementActor.incWrapped({value: BigInt(0) as unknown as BigNumber}); // fail

error message changes to

Do not know how to serialize a BigInt
        at JSON.stringify (<anonymous>)

I figured out these are more proposals than bug reports.

I. Optional types unwrap incorrectly

This code:

// increment.mo

public query func maybeInc(arg: ?Nat): async ?Nat {
    switch (arg) {
        case null { null };
        case (?arg) { ?(arg + 1) };
    }
}

would produce this type (which correctly represent what it actually returns)

'maybeInc' : (arg_0: [] | [BigNumber]) => Promise<[] | [BigNumber]>

This is misleading representation. It should returns something like

'maybeInc' : (arg_0: null | BigNumber) => Promise<null | BigNumber>

or

'maybeInc' : (arg_0?: BigNumber) => Promise<undefined | BigNumber>

The second representation is better, because it allows us to omit optional fields, which is natural in js/ts, but a little bit harder to implement, I believe.

for some reason I can’t fit it in one message…

II. Variant representation is very unsatisfying

// increment.mo

public query func incOrThrow(arg: Int): async Result.Result<Int, Text> {
    if (arg < 0) return #err("Negative!")
    else return #ok(arg + 1);
};

would produce this type

export type Result = {
    'ok' : BigNumber
  } |
  { 'err' : string };

which is very unhandy

// test.ts

const result = await IncrementActor.incOrThrow(2n as unknown as BigNumber);
if (result.hasOwnProperty('ok')) {
  console.log((result as {'ok': BigNumber}).ok);
}
else {
  console.error((result as {'err': string}).err);
}

It would be better if:

  1. For each variant kind there will be a separate generated type like
// increment.d.ts
type ResultOk = <type if ok>;
type ResultErr = <type if err>;
type Result = ResultOk | ResultErr;

this also does not fit…

  1. Express variants in js in a reflection-free way like
// increment.d.ts
type Result = {
  kind: 'ok' | 'err';
  value: ResultOk | ResultErr;
};

Thanks in advance. Let me know if such proposals are inappropriate at that moment.

Definitely not inappropriate!

We know that the interface with Candid can use some quality of life improvements. So far, our principle has been to ship the agent / types with as close of a match to the Candid concepts of types as possible, with JS developers writing a small layer around the actor methods to make things pleasant in the rest of the application.

I’d like to make some nicer inferences, but it definitely comes at a risk of introducing some deeply confusing bugs

2 Likes

with JS developers writing a small layer around the actor methods to make things pleasant

It doesn’t make sense to me, sorry.
How can I do that, for example, for our increment.mo canister? Let’s suppose I want maybeInc() to have the suggested interface (undefined instead of empty array). What exactly should I do?

You could create some small utility functions that handle conversions for you (which also allows you to maintain any future changes to the returned types in a central place), something similar to this: https://github.com/enzoh/superheroes/blob/master/src/www/utilities/idl.js

Yeah there seems to be more issues with the latest agent.

Even the hello demo example with this code:

actor {
public func greet(name : Text) : async Text {
    return "Hello, " # name # "!";
};

};

gives me this error in console: Uncaught ReferenceError: Buffer is not defined at w.toText

Thanks. This is not a problem.
The problem is that, if you want to make an interface that doesn’t force anyone to stick with some particular style or pattern, you should make it easy to use by default and flexible enough to fit in any other paradigm. Not ugly and “as close to metal as we can”.

Right now they just force anyone to write exactly the same utilities you’ve mentioned.

I do agree actually, it is a bit close to the metal, as Kyle said there’s room for improvement with all this.

If you encode opt A as null | A in TypeScript you’re losing information, because you can’t tell opt null from null for a value of type opt (opt A). I’d say losing data doesn’t meet the “flexible enough” bar. Encoding it with arrays lets you tell them apart as [null] vs [].

For the variant encoding I’d agree that a tag field might make for a nicer API.

1 Like

You’re right! But do you really want to distinct them?
Maybe it’s not right to guess here, but I can’t imagine a use-case when the distinction between opt(null) (which is null) and null could be useful. It doesn’t even lose it’s type safety.

But most of the time you want a nice ?.field access syntax, and this could be very helpful.

//canister.mo
type Rec = {a : Nat; b : ?Nat;};
public query func getSmth() : ?Rec {...}

// test.ts - good semantics
const smth = await actor.getSmth();
if (smth?.b && smth?.b > 10n) {
  doStuff();
}

// test.ts - bad semantics
const smth = await actor.getSmth();
if (smth.length > 0 && smth[0].b.length > 0 && smth[0].b[0] > 10n) {
  doStuff();
}

I think that the right path forward might be to provide some utility classes for things like Opts that can maintain full information, but can be cast to friendlier JS primitives.

In the meantime, I’ve merged a fix for the Nat bug 0n, and we should have a build of Candid without BigNumber, hopefully in time for the next dfx release

2 Likes

Thanks a lot. Great timing.

I’m running into the same issue. Did you manage to work around this somehow? -edit- I see there’s a PR that might be the fix: chore: reducing use of Buffer by krpeacock · Pull Request #457 · dfinity/agent-js · GitHub