The integration of Internet Identity (II for short) needs to be distinguished between the development environment and the main network environment.
The principals obtained through the II authentication of the main network environment cannot be used in the development environment, and the principals authenticated through the II of the development environment cannot be used in the main network environment.
Software Installation II of the IDE requires the following software to be downloaded and installed.
dfx: sh -ci "$(curl -fsSL https://smartcontracts.org/install.sh)"
Rust: install via the command "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
.
NodeJS: installed with the command "apt install nodejs; apt install npm; npm install -g n; n lts"
.
CMake: install with the command "apt install cmake"
.
Start the dfx development chain and deploy II canister on it by executing the following command:
$git clone https://github.com/dfinity/internet-identity.git
$cd internet-identity
$npm install
$dfx start --clean --background
$II_ENV=development dfx deploy --no-wallet --argument '(null)'
$dfx canister id internet_identity
Note: You need to write down the II_Canister_ID of the II canister obtained by "dfx canister id internet_identity"
.
The following is an example of code to perform II authentication.
import { AuthClient } from "@dfinity/auth-client";
let identityProvider = 'http://lm5fh-ayaaa-aaaah-aafua-cai.localhost:8000';
let identity;
try {
const authClient = await AuthClient.create();
if (await authClient.isAuthenticated()) {
identity = authClient.getIdentity();
} else {
identity = await new Promise((resolve, reject) => {
let timer = setTimeout(() => {
timer = null;
reject('do II auth timeout!');
}, 30 * 1000);
authClient.login({
identityProvider,
maxTimeToLive: BigInt(60_000_000_000),
onSuccess: () => {
if (timer != null) {
clearTimeout(timer);
timer = null;
resolve(authClient.getIdentity());
}
},
onError: (err) => {
if (timer != null) {
clearTimeout(timer);
timer = null;
reject(err);
}
},
});
});
}
} catch (e) {
console.log(e);
}
console.log(identity);
identityProvider
specifies the url path to the II authentication service. If not specified, then the default is identity.ic0.app for the main network. localII path is provided here.
AuthClient.create()
creates the auth client and will restore the identity from local storage if the Internet Identity has been done and has not expired.
authClient.getIdentity()
is used to retrieve the identity, which may be II-authenticated or anonymously generated.
authClient.isAuthenticated()
is used to check if the current identity is II-authenticated.
authClient.login(opt) is used to open a new window for II authentication. opt has the following options.
identityProvider
Provides the url path to the authentication service, the default is identity.ic0.app.
maxTimeToLive
provides the valid duration of the delegate proxy identity in ns.
onSuccess Specifies the callback for successful authentication. onError Specifies the callback for failed authentication.
Note: If the user closes the window without authentication, the onError callback is not generated.
When the II authentication is successfully completed, the II authenticated identity can be obtained through authClient.getIdentity().
The remaining valid hours of the II-authenticated identity can be obtained by:
const nextExpiration = identity.getDelegation().delegations
.map(d => d.delegation.expiration)
.reduce((current, next) => next < current ? next : current);
const expirationDuration = nextExpiration - BigInt(Date.now()) * BigInt(1000_000);
Requests can be sent to canister by:
import {Actor, HttpAgent} from "@dfinity/agent";
const agent = new HttpAgent({identity}); //identity is an identity authenticated by II, if it is null, it defaults to an anonymous identity.
await agent.fetchRootKey();
const idlFactory = ({ IDL }) =>
IDL.Service({
whoami: IDL.Func([], [IDL.Principal], ['query']),
});
const canisterId = "lm5fh-ayaaa-aaaah-aafua-cai";
const actor = Actor.createActor(idlFactory, {agent, canisterId});
const principal = await actor.whoami();
new HttpAgent({identity})
Generate agent for proxy requests. The agent uses the specified identity as the identity subject of the request, or an anonymous identity if not specified.
agent.fetchRootKey()
is used to pull the rootkey, because the built-in rootkey is for the main network environment. The code can only be used in the development environment, and must not be used in the main network environment.
idlFactory
defines the interface of canister. canisterId defines the ID of canister.
Actor.createActor
will create the actor.
actor.whoami is used to send requests to the canister.
The main difference between the main network environment and the development environment is the difference between II authentication and identity agent requests.
The following is a code example to perform II authentication.
import { AuthClient } from "@dfinity/auth-client";
let identityProvider = null;
let identity;
try {
const authClient = await AuthClient.create();
if (await authClient.isAuthenticated()) {
identity = authClient.getIdentity();
} else {
identity = await new Promise((resolve, reject) => {
let timer = setTimeout(() => {
timer = null;
reject('do II auth timeout!');
}, 30 * 1000);
authClient.login({
identityProvider,
maxTimeToLive: BigInt(60_000_000_000),
onSuccess: () => {
if (timer != null) {
clearTimeout(timer);
timer = null;
resolve(authClient.getIdentity());
}
},
onError: (err) => {
if (timer != null) {
clearTimeout(timer);
timer = null;
reject(err);
}
},
});
});
}
} catch (e) {
console.log(e);
}
console.log(identity);
identityProvider specifies the url path to the II authentication service. If not specified, then the default is identity.ic0.app for the main network.
AuthClient.create() creates the auth client and will restore the identity from local storage if the Internet Identity has been done and has not expired.
authClient.getIdentity() is used to retrieve the identity, which may be II authenticated or anonymously generated.
authClient.isAuthenticated() is used to check if the current identity is II-authenticated.
authClient.login(opt) is used to open a new window for II authentication. opt has the following options.
identityProvider Provides the url path to the authentication service, the default is identity.ic0.app.
maxTimeToLive provides the valid duration of the delegate proxy identity in ns.
onSuccess Specifies the callback for successful authentication.
onError Specifies the callback for failed authentication.
Note: If the user closes the window without authentication, the onError callback is not generated.
When the II authentication is successfully completed, the II authenticated identity can be obtained through authClient.getIdentity().
The remaining valid duration of the II-authenticated identity can be obtained by:
const nextExpiration = identity.getDelegation().delegations
.map(d => d.delegation.expiration)
.reduce((current, next) => next < current ? next : current);
const expirationDuration = nextExpiration - BigInt(Date.now()) * BigInt(1000_000);
Requests can be sent to canister by:
import {Actor, HttpAgent} from "@dfinity/agent";
const agent = new HttpAgent({identity}); // identity is an identity authenticated by II, if it is null, it defaults to an anonymous identity.
// await agent.fetchRootKey(); // The main network environment cannot pull the rootkey, otherwise it may lead to man-in-the-middle attack.
const idlFactory = ({ IDL }) =>
IDL.Service({
whoami: IDL.Func([], [IDL.Principal], ['query']),
});
const canisterId = "lm5fh-ayaaa-aaaah-aafua-cai";
const actor = Actor.createActor(idlFactory, {agent, canisterId});
const principal = await actor.whoami();
new HttpAgent({identity})
Generates an agent for proxy requests that uses the specified identity as the identity subject of the request, or an anonymous identity if it is not specified.
The idlFactory defines the interface of the canister. canisterId defines the ID of the canister. actor.createActor will create the actor.
actor.whoami is used to send requests to canister.
There are two cases of sending requests to a canister: the canister of this project and the canister of other projects.
Canister of this project
For referencing this project's Canister, the steps are as follows.
{
"canisters": {
"Nomos": {
"main": "src/Nomos/main.mo",
"type": "motoko"
},
"Nomos_assets": {
"dependencies": [
"Nomos"
],
"frontend": {
"entrypoint": "src/Nomos_assets/src/index.html"
},
"source": [
"src/Nomos_assets/assets",
"dist/Nomos_assets/"
],
"type": "assets"
}
},
...
}
Then import the IDL definition of canister as well as the canister ID. e.g. import {idlFactory as customNomosIDL, canisterId as customNomosID} from "dfx-generated/Nomos";
where Nomos is the name of the canister.
The specific IDL definition of canister and canister ID can be found in <project_root>/.dfx/local/canisters/<canister_name>/<canister_name>.js.
An Actor can be created once the IDL definition of the canister and the canister ID are available. e.g.
const nomosActor = Actor.createActor(customNomosIDL, {agent, canisterId: customNomosID});
Where agent is the request agent, see Identity Agent Request for details.
Once the actor is successfully created you can send a request like canister. For example.
let userInfo = await nomosActor.userInfo()
The difference between sending a request to a canister of another project and this project is that there is no need to add dependencies, but you need to manually specify the IDL definition and ID of the canister.
First specify the IDL definition of the canister and the canister ID. e.g.
const canisterId = Principal.fromText(canisterIdEl.value);
const idlFactory = ({ IDL }) =>
IDL.Service({
whoami: IDL.Func([], [IDL.Principal], ['query']),
});
An Actor can be created when the IDL definition of canister and canister ID are available. e.g.
const whoamiActor = Actor.createActor(idlFactory, {agent, canisterId});
Where agent is the request agent, see Identity Agent Request for details.
Once the actor is successfully created you can send a request like canister. For example.
let principal = await actor.whoami()
IDL definition is a service declaration generation function. For example.
({ IDL }) =>
IDL.Service({
whoami: IDL.Func([], [IDL.Principal], ['query']),
register : IDL.Func([IDL.Text, IDL.Text],[IDL.Bool, IDL.Bool, IDL.Nat],[],),
});
Raw HTTP requests can be processed in canister. Note that the /api and /_ routes are not processed by the HTTP middleware processor.
If a canister needs to handle HTTP requests, it needs to provide a service with the following candid definition:
type HttpResponse =
record {
body: blob;
headers: vec HeaderField;
status_code: nat16;
};
type HttpRequest =
record {
body: blob;
headers: vec HeaderField;
method: text;
url: text;
};
type HeaderField =
record {
text;
text;
};
service : {
“http_request”: (HttpRequest) -> (HttpResponse) query;
}
import Text "mo:base/Text";
actor {
type HeaderField = (Text, Text);
type HttpRequest = {
method: Text;
url: Text;
headers: [HeaderField];
body: Blob;
};
type HttpResponse = {
status_code: Nat16;
headers: [HeaderField];
body: Blob;
};
public query func http_request(req: HttpRequest) : async HttpResponse {
return {
status_code=200;
headers= [("content-type","text/plain")];
body=Text.encodeUtf8("hello boy!");
};
};
};
use ic_cdk::export::{candid::{CandidType, Deserialize}};
use ic_cdk_macros::*;
use serde_bytes::{ByteBuf};
type HeaderField = (String, String);
#[derive(Clone, Debug, CandidType, Deserialize)]
struct HttpRequest {
method: String,
url: String,
headers: Vec<(String, String)>,
body: ByteBuf,
}
#[derive(Clone, Debug, CandidType, Deserialize)]
struct HttpResponse {
status_code: u16,
headers: Vec<HeaderField>,
body: Vec<u8>,
}
#[query]
async fn http_request(_req: HttpRequest) -> HttpResponse {
let mut headers: Vec<HeaderField> = Vec::new();
headers.push(("content-type".to_string(), "text/plain".to_string()));
return HttpResponse {
status_code: 200,
headers,
body: "hello boy!".as_bytes().to_vec(),
}
}
IC provides a virtual canister for all canister management, call it canister manager, the canister ID is "aaaaaa-aa". IC canister manager does not actually exist as a container (with isolated state, Wasm code, etc.).
The canister manager's did file is described as follows:
type canister_id = principal;
type user_id = principal;
type wasm_module = blob;
type canister_settings = record {
controllers : opt vec principal;
compute_allocation : opt nat;
memory_allocation : opt nat;
freezing_threshold : opt nat;
};
type definite_canister_settings = record {
controllers : vec principal;
compute_allocation : nat;
memory_allocation : nat;
freezing_threshold : nat;
};
service ic : {
create_canister : (record {
settings : opt canister_settings
}) -> (record {canister_id : canister_id});
update_settings : (record {
canister_id : principal;
settings : canister_settings
}) -> ();
install_code : (record {
mode : variant {install; reinstall; upgrade};
canister_id : canister_id;
wasm_module : wasm_module;
arg : blob;
}) -> ();
uninstall_code : (record {canister_id : canister_id}) -> ();
start_canister : (record {canister_id : canister_id}) -> ();
stop_canister : (record {canister_id : canister_id}) -> ();
canister_status : (record {canister_id : canister_id}) -> (record {
status : variant { running; stopping; stopped };
settings: definite_canister_settings;
module_hash: opt blob;
memory_size: nat;
cycles: nat;
});
delete_canister : (record {canister_id : canister_id}) -> ();
deposit_cycles : (record {canister_id : canister_id}) -> ();
raw_rand : () -> (blob);
// provisional interfaces for the pre-ledger world
provisional_create_canister_with_cycles : (record {
amount: opt nat;
settings : opt canister_settings
}) -> (record {canister_id : canister_id});
provisional_top_up_canister :
(record { canister_id: canister_id; amount: nat }) -> ();
}
create_canister : (record {settings : opt canister_settings}) -> (record {canister_id : canister_id});
Before deploying a container, the administrator of the container first has to register it in the system, get a container ID (being an empty container) and then install the code separately.
The optional settings parameter can be used to make the following settings.
controllers (vec principal): list of principals. Size must be between 0 and 10. Default value: contains only the caller of create_canister. This value is assigned to the controller attribute of the container.
compute_allocation (nat)
: must be a number between 0 and 100, including 0 and 100, with a default value of 0. It indicates how much compute power should be guaranteed for this container, representing the percentage of the maximum compute power that can be allocated for a single container. If the system is unable to provide the requested allocation, for example because it is overbooked, the call will be rejected.
memory_allocation (nat)
: must be a number between 0 and 2^48 (i.e. 256TB), inclusive, with a default value of 0. It indicates how much memory the container is allowed to use in total. Any attempt to increase memory usage beyond this allocation will fail. If the system is unable to provide the requested allocation, for example because it is overbooked, the call will be rejected. If set to 0, the container's memory will grow as best it can and be limited by the memory available on the network.
freezing_threshold (nat)
: must be a number between 0 and 2^64-1 inclusive, and indicates the length of time (in seconds), default value: 2592000 (approximately 30 days). Considering the current size of the container and the current storage cost of the system, the container is considered frozen when the system estimates that the container will run out of cycles after freeze_threshold seconds.
Note: Additional cycles need to be added for injection into the new canister when creating_canister is executed.
update_settings : (record {canister_id : principal; settings : canister_settings}) -> ();
Only controllers of canister can update the settings.
canister_id
specifies the id of the canister whose settings need to be updated.
settings
is the same as settings in create_canister, if a field is not included in settings, it means that the field is not changed.
install_code : (record {mode : variant {install; reinstall; upgrade}; canister_id : canister_id; wasm_module : wasm_module; arg : blob;}) -> ();
This method installs the code into the container. Only the controllers of the container can install the code.
For different modes, the situation is different.
If mode = install, the container must previously be empty. This instantiates the container module and calls its canister_init system method (if present) and passes arg to that method.
If mode = reinstall, if the container is not empty, its existing code and state are removed before doing mode = install. Note that this is different from the uninstall_code followed by install_code, as this forces rejection of all unresponsive calls.
If mode = upgrade, this will perform an upgrade of the non-empty container, passing arg to the canister_post_upgrade system method of the new instance.
Note: This call is invalid if the response to this request is reject.
uninstall_code : (record {canister_id : canister_id}) -> ();
This method removes the container's code and state, emptying the container again. Only the container's controllers can unload the code.
Uninstall will reject all calls to the container that have not yet been responded to, and remove the container's code and state. Outstanding responses to the container are not processed, even if they arrive after the code has been installed again.
canister
is now empty. In particular, any incoming or queued calls will be rejected.
The unloaded container retains its cycle count, controllers,, status and allocations.
canister_status : (record {canister_id : canister_id}) -> (record {
status : variant { running; stopping; stopped }; settings: definite_canister_settings;
module_hash: opt blob; memory_size: nat; cycles: nat;});
Indicates various information about the canister. Only the controller of the container can request its status. It contains status.
status: It can be one of running, stopped or stopped.
SHA256 hash: The SHA256 hash of the module installed on the container.
If the container is empty, it is null.
controller: list of controllers
allocations: size of occupied memory, number of cycles.
stop_canister : (record {canister_id : canister_id}) -> ();
The controller of a container can stop the container (for example, to prepare for a container upgrade).
Stopping the canister is not an atomic operation. The immediate effect is that the state of the container changes to being stopped (unless the container is already stopped). The system will reject all calls to the container that is being stopped, indicating that the container is being stopped. Responses to the stopped canister are processed as usual. After all outstanding responses are processed (so there is no open call context), the container state is changed to stopped and the caller who managed the container response to the stop_canister request.
start_canister : (record {canister_id : canister_id}) -> ();
A container can be started by its controller.
If the container state has been stopped or is being stopped, the container state is set to running only. In the latter case, all stop_canister calls that are being processed fail (and are rejected).
If the container is already running, the state remains unchanged.
delete_canister : (record {canister_id : canister_id}) -> ();
This method deletes a canister from the IC. only the controllers of the container can delete it, and the container must have been stopped.
Deleting a container cannot be undone, any state stored on the container is permanently deleted and its cycle is discarded. once a container is deleted, its ID cannot be used again.
deposit_cycles : (record {canister_id : canister_id}) -> ();
This method stores the cycle contained in this call in the specified container.
There is no controller restriction on who can call this method.
raw_rand
raw_rand : () -> (blob);
This method accepts no input and returns 32 pseudo-random bytes to the caller. The return value is not known to any part of the IC at the time this call is submitted. A new return value is generated each time this method is called.
Currently motoko cannot generate canisters directly from within a canister, so canisters need to be deployed externally and then the ID of the canister being called is passed to the caller.
Currently inter-canister calls cannot be made using query.
Note: It may be possible to create a canister using the IC_MANAGER line canister.
Example:
actor Counterer {
type CounterIn = actor {
get : shared query () -> async Nat;
set : (n: Nat) -> async ();
inc : () -> async ();
};
var counter : ?CounterIn = null;
// set counter canister ID.
public func init(c : Text) {
counter := ?actor(c);
};
// Get the value of the counter.
public func get() : async Nat {
switch counter {
case (?c) { await c.get();};
case null {0};
};
};
// Set the value of the counter.
public func set(n: Nat) {
switch counter {
case (?c) {await c.set(n);};
case null {()};
};
};
// Increment the value of the counter.
public func inc() {
switch counter {
case (?c) {await c.inc();};
case null {()};
};
};
};
By implementing the following did interface, canister is able to be notified when it receives an ICP:
type TransactionNotification =
record {
amount: ICPTs;
block_height: BlockHeight;
from: principal;
from_subaccount: opt SubAccount;
memo: Memo;
to: principal;
to_subaccount: opt SubAccount;
};
type SubAccount = vec nat8;
type Result =
variant {
Err: text;
Ok;
};
type Memo = nat64;
type ICPTs = record {e8s: nat64;};
type BlockHeight = nat64;
service : {
transaction_notification: (TransactionNotification) -> (Result);
}
Also published on: https://nnsdao.medium.com/ic-programming-best-practices-cf7228b39074