Skip to content

Using the Agent/Actor

πŸ“‘ Making Calls to Canisters with Plug

Once connected, you can use Plug to make proxied calls to a canister on behalf of your users. This is the main way your app will be able to call actions on behalf of users, like calling an update method on your app's BE canister to make a post (if it is a social media), or interact with an NFT collection's canister, etc.

  1. First, connect to Plug and pass a whitelist of canisters you need to interact to.
  2. The user will approve the connection, giving you access to the createActor method.
  3. Use the createActor method to make safe calls to canisters on behalf of users.

Important

It is key to use the createActor to talk to a canister on behalf of users. Using the agent on its own, and creating an Actor on your end (without using Plug's method) will show users a warning when they need to approve update calls that affect their assets. This is considered an unsafe practice because it isn't fully transparent to users on the parameters you pass.

createActor() - Making Safe Calls

createActor() is an asynchronous method that creates an Actor to interact with the Internet Computer. Returns an Actor for the provided Canister Id and interface factory (Candid or IDL).

The createActor expects that the Agent is initialized beforehand by calling the requestConnect method with the whitelist (canister ID string array) and the host (string).

As mentioned above, on instantiation the Agent is assigned to the window Plug object, as window.ic.plug.agent. Creating an Actor allows you to securely interact with a canister’s interface on behalf of the user.

(async () => {
  // NNS Canister Id as an example
  const nnsCanisterId = 'qoctq-giaaa-aaaaa-aaaea-cai'
  const whitelist = [nnsCanisterId];

  // Initialise Agent, expects no return value
  await window?.ic?.plug?.requestConnect({
    whitelist,
  });

  // A partial Interface factory
  // for the NNS Canister UI
  // Check the `plug authentication - nns` for more
  const nnsPartialInterfaceFactory = ({ IDL }) => {
    const BlockHeight = IDL.Nat64;
    const Stats = IDL.Record({
      'latest_transaction_block_height' : BlockHeight,
      'seconds_since_last_ledger_sync' : IDL.Nat64,
      'sub_accounts_count' : IDL.Nat64,
      'hardware_wallet_accounts_count' : IDL.Nat64,
      'accounts_count' : IDL.Nat64,
      'earliest_transaction_block_height' : BlockHeight,
      'transactions_count' : IDL.Nat64,
      'block_height_synced_up_to' : IDL.Opt(IDL.Nat64),
      'latest_transaction_timestamp_nanos' : IDL.Nat64,
      'earliest_transaction_timestamp_nanos' : IDL.Nat64,
    });
    return IDL.Service({
      'get_stats' : IDL.Func([], [Stats], ['query']),
    });
  };

  // Create an actor to interact with the NNS Canister
  // we pass the NNS Canister id and the interface factory
  const NNSUiActor = await window.ic.plug.createActor({
    canisterId: nnsCanisterId,
    interfaceFactory: nnsPartialInterfaceFactory,
  });

  // We can use any method described in the Candid (IDL)
  // for example the get_stats()
  // See https://github.com/dfinity/nns-dapp/blob/cd755b8/canisters/nns_ui/nns_ui.did
  const stats = await NNSUiActor.get_stats();
  console.log('NNS stats', stats);
})()

Rebuilding Actors Across Account Switches

We can use requestConnect's callback parameter onConnectionUpdate to make sure that our actors are rebuilt across user identity switches.

Here's an example doing so with a partial Sonic interface:

(async () => {

  // Add the Sonic mainnet canister to whitelist
  const sonicCanisterId = '3xwpq-ziaaa-aaaah-qcn4a-cai';
  const whitelist = [sonicCanisterId];

  // Create an interface factory from a canister's IDL
  const sonicPartialInterfaceFactory = ({ IDL }) => {
      const TokenInfoExt = IDL.Record({
        'id' : IDL.Text,
        'fee' : IDL.Nat,
        'decimals' : IDL.Nat8,
        'name' : IDL.Text,
        'totalSupply' : IDL.Nat,
        'symbol' : IDL.Text,
      });
      const PairInfoExt = IDL.Record({
        'id' : IDL.Text,
        'price0CumulativeLast' : IDL.Nat,
        'creator' : IDL.Principal,
        'reserve0' : IDL.Nat,
        'reserve1' : IDL.Nat,
        'lptoken' : IDL.Text,
        'totalSupply' : IDL.Nat,
        'token0' : IDL.Text,
        'token1' : IDL.Text,
        'price1CumulativeLast' : IDL.Nat,
        'kLast' : IDL.Nat,
        'blockTimestampLast' : IDL.Int,
      });
      const SwapInfo = IDL.Record({
        'owner' : IDL.Principal,
        'cycles' : IDL.Nat,
        'tokens' : IDL.Vec(TokenInfoExt),
        'pairs' : IDL.Vec(PairInfoExt),
      });
      return IDL.Service({
        'getSwapInfo' : IDL.Func([], [SwapInfo], ['query'])
      })
    }

  // requestConnect callback function
  const onConnectionUpdate = async () => {

    // rebuild actor and test by getting Sonic info
    const sonicActor = await window.ic.plug.createActor({
      canisterId: sonicCanisterId,
      interfaceFactory: sonicPartialInterfaceFactory,
    });

    // use our actors getSwapInfo method
    const swapInfo = await sonicActor.getSwapInfo();
    console.log('Sonic Swap Info: ', swapInfo);
  }

  // Request a connection
  // Will fire onConnectionUpdate on account switch
  await window?.ic?.plug?.requestConnect({
    whitelist,
    onConnectionUpdate,
  });
})()

⚠️ How NOT to Make Canister Calls

Making calls on behalf of the user directly through the Plug Agent is not a suggested way to make calls. That is why Plug has an exposed createActor method shown above.

If you bypass the createActor method and use the Agent to create an actor on your side; when that actor is sent to be signed by Plug it will show the user a warning, because Plug is not able to read all the arguments passed to that call, and considers it risky for the user to accept the action.

To learn more about what happens on both the user and developer side if you try to bypass the createActor( ) flow, you can read this section of our documentation.

Plug Agent (.agent)

On instantiation (requestConnect with whitelist) the Agent is assigned to the window Plug object as:

window.ic.plug.agent

The agent field is an instance of the HttpAgent class from the @dfinity/agent library, that allow us to interact with the Internet Computer.

Important

It's important to note that we are going to deprecate direct access to the agent in the near future, as developers should be using the CREATE ACTOR method to make calls to canisters through Plug. You can check the correspondent commit version for the latest interface, in accordance to the dfinity/agent version in use.

Here's an example, of getting the user principal id:

(async () => {
  // Canister Ids
  const nnsCanisterId = 'qoctq-giaaa-aaaaa-aaaea-cai'

  // Whitelist
  const whitelist = [
    nnsCanisterId,
  ];

  // Make the request
  const isConnected = await window.ic.plug.requestConnect({
    whitelist,
  });

  // Get the user principal id
  const principalId = await window.ic.plug.agent.getPrincipal();

  console.log(`Plug's user principal Id is ${principalId}`);
})();