Profiles

The candid-profile command

Validate a profile, resolve a service with its packs, and write the resolved form, from @meantjs/profile.

bash
npm install --save-exact @meantjs/profile@0.5.0
npx candid-profile resolve ledger.did --packs > ledger.resolved.json

candid-profile is the command @meantjs/profile installs. It has one command, resolve: it reads an interface, applies the profiles you give it and the packs that match, prints the resolved form on stdout and a summary on stderr. It needs Node 22 or later. Its source is src/cli.mjs, started by bin/candid-profile.mjs.

Usage#

bash
npx candid-profile --help
text
usage: candid-profile resolve <service.did> [options] > resolved.json
       candid-profile resolve --envelope <file> [options] > resolved.json
       candid-profile resolve --abi <file> [options] > resolved.json

  --envelope <file>   the service's candid-core ContractEnvelope (as `candid-core-cli gen` writes it), instead of
                      compiling a .did here
  --abi <file>        an EVM contract's ABI, a JSON array of human-readable fragments or JSON ABI entries
                      (experimental, spec section 15)
  --profile <file>    a profile to apply (repeatable). A pack given this way must match the service.
  --packs             try every pack in the packs directory; apply those that match
  --packs-dir <dir>   packs directory (default: the standard packs this package ships)
  --verbose           also list info diagnostics on stderr
  --quiet             no summary on stderr
  -h, --help          this text

The help text goes to stderr. candid-profile --help, -h, and candid-profile resolve --help print it and exit with status 0; candid-profile with no arguments prints it and exits with status 2.

Inputs#

Give exactly one interface:

InputWhat it reads
<service.did>Candid source, compiled with the @candid-core/cli the package pins (0.4.0). One file: an import is not followed, and fails with EE_CANDID (did_source_not_found).
--envelope <file>The service's candid-core ContractEnvelope, as candid-core-cli gen writes it beside a generated module. The service is not compiled again, and the resolved form is the same as from the .did (decision D-039).
--abi <file>An EVM contract's ABI: a JSON array of human-readable fragments or JSON ABI entries. Experimental (spec §15).

None of them is a usage error (missing <service.did> (or --envelope <file>, or --abi <file>)), and so is more than one (give only one of <service.did>, --envelope and --abi), or a second positional argument (unexpected argument).

Options#

OptionEffect
--profile <file>A profile to apply, read as JSON. Repeat it for several; they merge by layer (Profiles, layers and binding). A pack given this way has to match the service, or it is EE_PACK_NOT_MATCHED. Diagnostics name the profile by its name.
--packsTry every pack in the packs directory and apply the ones that match. The others get II_PACK_NOT_MATCHED; a pack for the other kind of interface is skipped silently.
--packs-dir <dir>Where --packs looks: every *.profile.json of kind: "pack" in the directory, sorted by file name. Default: the ICRC-1, ICRC-2, ERC-20 and EIP-2612 packs the package ships (the Permit2 and DAI permit files beside them are contract profiles, not packs). Without --packs it does nothing. A directory with no pack in it is a usage error.
--verboseList info diagnostics in the summary too. They are always in the resolved form's diagnostics.
--quietPrint no summary. Usage errors are still printed.
-h, --helpPrint the usage text and exit with status 0.

A value option takes the next argument or an inline value: --profile mine.profile.json and --profile=mine.profile.json are the same. A next argument that starts with -- is not taken as a value (--profile needs a value). Options and the input may come in any order. Any other argument that starts with - is unknown option.

Output#

stdout holds the resolved form, as JSON indented by two spaces with a final newline, whenever the interface could be read, including when diagnostics hold errors. It is empty when the input does not compile (EE_CANDID), the envelope cannot be read (EE_ENVELOPE), the ABI cannot be read (EE_ABI), or on a usage error. Profile and resolved form describes every field.

stderr holds the summary, unless --quiet:

text
candid-profile: ledger.did  candid-core:interface:v1:sha256:22eb4787e1a755acb9aabd0ed2121d754098bcd1cedb5d0ed8b5c4effb921846  [26 methods]
  applied     local-ledger     author       candid-profile:v0:sha256:3ab093c4e8583fc5e2efb4e7c8ce595457450bd38c7be6959998c0d17eab3b84
  applied     icrc1            pack         candid-profile:v0:sha256:4eaa9312d60661248ff5627dba9906aa91240abe84cb1b1da8ca20a6fba2e3e6
  applied     icrc2            pack         candid-profile:v0:sha256:f9d75e8bf5ff2edb0b167988feee494eeb54c386e6f62252c4bd96ac56346567
  0 error, 1 warning, 3 info
  warning W_OVERRIDDEN               [icrc1] icrc1_transfer(0).memo  bytes from profile "icrc1" (pack) is overridden by profile "local-ledger" (author) at "icrc1_transfer(0).memo" [icrc1_transfer(0).memo]
  1. The input as you named it, the interface_id, and the number of methods. Absent when the interface could not be read.
  2. One line per profile: its status (applied, rebound, stale, not_matched or invalid), name, origin and profile_id. Applied profiles come first, highest layer first.
  3. The count of errors, warnings and infos.
  4. One line per error and warning, and per info with --verbose: severity, code, the profile it is about in brackets, the address or document path, and the message.

A usage or I/O problem prints candid-profile: and the reason, then the usage text, on stderr.

Exit status#

StatusWhen
0No diagnostic is an error. Warnings and infos do not change it. Also --help.
1At least one diagnostic is an error. The resolved form is still on stdout if the interface could be read.
2A usage or I/O problem: no arguments, an unknown command or option, a missing value, a file that cannot be read or is not JSON, a packs directory that cannot be read or holds no pack. Also any unexpected exception.

Examples#

The standard packs, with infos#

bash
npx candid-profile resolve ledger.did --packs --verbose > ledger.resolved.json
text
candid-profile: ledger.did  candid-core:interface:v1:sha256:22eb4787e1a755acb9aabd0ed2121d754098bcd1cedb5d0ed8b5c4effb921846  [26 methods]
  applied     icrc1            pack         candid-profile:v0:sha256:4eaa9312d60661248ff5627dba9906aa91240abe84cb1b1da8ca20a6fba2e3e6
  applied     icrc2            pack         candid-profile:v0:sha256:f9d75e8bf5ff2edb0b167988feee494eeb54c386e6f62252c4bd96ac56346567
  0 error, 0 warning, 3 info
  info    I_PACK_MATCHED             [icrc1] icrc1  the service is a subtype of the pack's binds.requires
  info    I_PACK_MATCHED             [icrc2] icrc2  the service is a subtype of the pack's binds.requires
  info    I_UNTAGGED_NUMERIC         icrc21_canister_call_consent_message(0).user_preferences.metadata.utc_offset_minutes  int16 argument without amount/timestamp/duration/describe

ledger.did is the ICRC ledger interface of the profiles quickstart. Exit status 0.

A profile and its interface#

bash
npx candid-profile resolve tokens.did --profile tokens.profile.json > tokens.resolved.json
text
candid-profile: tokens.did  candid-core:interface:v1:sha256:f8358625dd47a9b6a237a3a268ba83291fcd0dc27093e80ac27cc48a49dc1d6c  [2 methods]
  applied     tokens-example   author       candid-profile:v0:sha256:70abc8cbce701650b6adafeabbf636a10b9c80e620bfcb6cc2d8793ee027745c
  0 error, 0 warning, 0 info

This is how you validate a profile you wrote: the files are the repository's profile/examples/tokens.did and tokens.profile.json (Write a profile). Run it without --profile to print an interface's interface_id before you write the profile that binds to it.

An explicit pack that does not match#

bash
npx candid-profile resolve tokens.did --profile node_modules/@meantjs/profile/data/packs/icrc1.profile.json
text
candid-profile: tokens.did  candid-core:interface:v1:sha256:f8358625dd47a9b6a237a3a268ba83291fcd0dc27093e80ac27cc48a49dc1d6c  [2 methods]
  not_matched icrc1            pack         candid-profile:v0:sha256:4eaa9312d60661248ff5627dba9906aa91240abe84cb1b1da8ca20a6fba2e3e6
  1 error, 0 warning, 2 info
  error   E_PACK_NOT_MATCHED         [icrc1] icrc1  the service is not a subtype of the pack's binds.requires: icrc1_balance_of: method "icrc1_balance_of" is missing from the canister; icrc1_decimals: method "icrc1_decimals" is missing from the canister; icrc1_fee: method "icrc1_fee" is missing from the canister; and 2 more

Exit status 1, and the resolved form on stdout lists no profile. With --packs instead, the same pack is only II_PACK_NOT_MATCHED and the status is 0: a pack you name has to apply, a pack the search tries does not.

A .did that does not compile#

bash
npx candid-profile resolve bad.did --packs > bad.json
text
  1 error, 0 warning, 0 info
  error   E_CANDID                   bad.did  did_parse_error: Candid parser error: Unexpected EOF at bytes 20..21

bad.did holds service : { oops : (. Exit status 1, and bad.json is empty.

From an envelope#

bash
npx candid-core-cli gen tokens.did -o gen
npx candid-profile resolve --envelope gen/tokens.envelope.json --profile tokens.profile.json > tokens.resolved.json
text
contract:  candid-core:contract:v1:sha256:9f5c81035f14d348d05279bf3683ef288679897e69dcb752b74250afc558ae67
interface: candid-core:interface:v1:sha256:f8358625dd47a9b6a237a3a268ba83291fcd0dc27093e80ac27cc48a49dc1d6c
wrote gen/tokens.ts
wrote gen/tokens.envelope.json
candid-profile: gen/tokens.envelope.json  candid-core:interface:v1:sha256:f8358625dd47a9b6a237a3a268ba83291fcd0dc27093e80ac27cc48a49dc1d6c  [2 methods]
  applied     tokens-example   author       candid-profile:v0:sha256:70abc8cbce701650b6adafeabbf636a10b9c80e620bfcb6cc2d8793ee027745c
  0 error, 0 warning, 0 info

The resolved form is byte for byte the one resolve tokens.did writes. An envelope the resolver cannot read is EE_ENVELOPE, with nothing on stdout. Validate and resolve profiles says what is checked.

Your own packs directory#

bash
mkdir my-packs && cp node_modules/@meantjs/profile/data/packs/icrc1.profile.json my-packs/
npx candid-profile resolve ledger.did --packs --packs-dir my-packs > ledger.resolved.json
text
candid-profile: ledger.did  candid-core:interface:v1:sha256:22eb4787e1a755acb9aabd0ed2121d754098bcd1cedb5d0ed8b5c4effb921846  [26 methods]
  applied     icrc1            pack         candid-profile:v0:sha256:4eaa9312d60661248ff5627dba9906aa91240abe84cb1b1da8ca20a6fba2e3e6
  0 error, 0 warning, 10 info

Only the packs in my-packs are tried. The ICRC-2 methods now carry no tags, so eight of their numeric arguments are reported as II_UNTAGGED_NUMERIC: 10 infos instead of 3.

An EVM contract#

jsontoken.abi.json
[
  "function name() view returns (string)",
  "function symbol() view returns (string)",
  "function decimals() view returns (uint8)",
  "function totalSupply() view returns (uint256)",
  "function balanceOf(address owner) view returns (uint256)",
  "function allowance(address owner, address spender) view returns (uint256)",
  "function transfer(address to, uint256 value) returns (bool)",
  "function approve(address spender, uint256 value) returns (bool)",
  "function transferFrom(address from, address to, uint256 value) returns (bool)"
]
bash
npx candid-profile resolve --abi token.abi.json --packs > token.resolved.json
text
candid-profile: token.abi.json  candid-profile:evm-abi:v0:sha256:64c9e8e85fd0f3cdfd86c278b8f0d9f137cf2b872c228bd349b9e73fb85ff6d4  [9 methods]
  applied     erc20            pack         candid-profile:v0:sha256:afc7d231076699406b2fc9f74da372f5da88465027ab1d46eca526e646368a81
  0 error, 0 warning, 1 info

Experimental. The ERC-20 pack applies by selector, and the resolved form says "chain": "evm"; transfer's intent template reads Send {value} to {to}, in this contract's parameter names. An EVM interface gets no agent tools, outcome classes or idempotency yet; EVM contracts says what phase 1 covers.

From code#

js
import { runCli } from "@meantjs/profile";

let resolved = "";
let summary = "";
const code = await runCli(["resolve", "tokens.did", "--profile", "tokens.profile.json"], {
  stdout: { write: (text) => { resolved += text; } },
  stderr: { write: (text) => { summary += text; } },
});
console.log(code, JSON.parse(resolved).interface_id);

runCli(argv, io) is the command as a function: argv starts with the command ("resolve"), io takes the two channels, and it resolves to the exit status, 0, 1 or 2. The help text is exported as USAGE. The functions underneath, without the file handling, are on Validate and resolve profiles.

scripts/validate.mjs#

bash
cd profile
node scripts/validate.mjs
text
PASS packs/erc20.profile.json  (0 error, 0 warning, 0 info)
     profile_id   candid-profile:v0:sha256:afc7d231076699406b2fc9f74da372f5da88465027ab1d46eca526e646368a81
     interface_id candid-profile:evm-abi:v0:sha256:81471b5efd3e34c464b5dda12ccdd454d8ca6e0d0c069871c8152bc6b154be4b  [7 methods, 23 annotations]
PASS packs/icrc1.profile.json  (0 error, 0 warning, 0 info)
     profile_id   candid-profile:v0:sha256:4eaa9312d60661248ff5627dba9906aa91240abe84cb1b1da8ca20a6fba2e3e6
     interface_id candid-core:interface:v1:sha256:7fae0dd330b20f53ae1e4e2cb1947c1c57791da0ae20299ceb499f63228d0689  [5 methods, 29 annotations]
PASS packs/icrc2.profile.json  (0 error, 0 warning, 0 info)
     profile_id   candid-profile:v0:sha256:f9d75e8bf5ff2edb0b167988feee494eeb54c386e6f62252c4bd96ac56346567
     interface_id candid-core:interface:v1:sha256:c7e94a82863faf90ec6906e7b35a1b917b94a34664bb3a54cc3fca979b3bda3f  [5 methods, 56 annotations]

profile/scripts/validate.mjs is a script in the repository, not part of any published package; run it from profile/ after npm ci. It is a thin wrapper over validateProfile, and it checks one profile at a time: a pack against its own binds.requires (so the interface_id above is the pack fragment's, not a canister's), a canister profile against the .did you give. It never merges layers or searches for packs; that is candid-profile resolve. It reads the schema from the repository's spec/ rather than the copy the package ships.

InvocationDoes
node scripts/validate.mjsEvery packs/*.profile.json.
node scripts/validate.mjs <file>… [--did <service.did>]The files named. --did is the interface for canister profiles; without it a canister profile gets the schema check and WW_NO_INTERFACE.
--resolvedPrint the resolved form of exactly one profile. The repository's fixtures/*.resolved.json are generated this way.
--jsonPrint { ok, results }, one result per file with its diagnostics, instead of the report.

It prints PASS or FAIL per file, with its profile_id, its interface_id and every diagnostic, and exits with status 0 when every file passes, 1 when one fails, and 2 on an unexpected exception.