Profiles

Profile and resolved form

The profile document, its identity, and the resolved form consumers read, field by field.

Two JSON documents carry the profile layer. The profile is what an author, a reviewer or a pack writes: annotations bound to an interface. The resolved form is what the resolver writes for consumers: one tree per method, with the effective tags at every position. Both are format version 0. The structure of the profile is profile/spec/profile-v0.schema.json; the resolved form is spec §10 and, for EVM contracts, §15.5 and §15.9. Excerpts below are the generated fixtures in profile/fixtures/, quoted unchanged apart from indentation.

The profile document#

jsonprofile/examples/tokens.profile.json
{
  "format": "candid-profile",
  "format_version": 0,
  "kind": "canister",
  "name": "tokens-example",
  "origin": "author",
  "language": "en",
  "description": "Example author profile for examples/tokens.did. Shows type: addresses (one tag on Tokens.e8s reaches every Tokens position), a method-address override, and the idempotency stamp.",
  "binds": {
    "interface_id": "candid-core:interface:v1:sha256:f8358625dd47a9b6a237a3a268ba83291fcd0dc27093e80ac27cc48a49dc1d6c"
  },
  "annotations": [
    { "at": "type:Tokens.e8s", "tag": "amount", "args": { "decimals": 8, "symbol": "TOK" } },
    { "at": "type:Tokens.e8s", "tag": "describe", "args": { "label": "Amount" } },
    { "at": "balance->(0).e8s", "tag": "describe", "args": { "label": "Balance" } },
    { "at": "send", "tag": "intent", "args": { "title": "Send TOK", "template": "Send {amount.e8s} to {to}[ with memo {memo}]" } },
    { "at": "send", "tag": "effect", "args": { "kind": "transfer", "irreversible": true } },
    { "at": "send", "tag": "idempotency", "args": { "mode": "replay_same_payload", "stamp": "created_at" } },
    { "at": "send", "tag": "outcome", "args": { "classes": { "Ok": "success", "Err.Duplicate": "already_done", "Err.Insufficient": "fail", "Err.Other": "fail" } } },
    { "at": "send(0).created_at", "tag": "timestamp", "args": { "unit": "ns", "epoch": "unix" } },
    { "at": "send(0).memo", "tag": "bytes", "args": { "encoding": "utf8" } },
    { "at": "send(0).memo", "tag": "default", "args": { "value": "null", "mode": "suggest" } }
  ]
}

Top-level fields. No other field is allowed (additionalProperties: false; anything else is EE_SCHEMA).

FieldRequiredValue
formatyes"candid-profile"
format_versionyes0
kindyes"canister", "pack" or "contract" (an EVM contract, experimental)
nameyesmatches ^[a-z0-9][a-z0-9_.-]{0,63}$
originyes"author", "third_party", "pack" or "inferred"; which ones a kind allows is below
languagenoa language tag matching ^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$; default "en"
descriptionnotext
bindsyesan object whose shape depends on kind (below)
annotationsyesan array of at least one annotation

Kinds and origins#

kindAllowed originbindsBinds by
canisterauthor, third_party, inferredcanister bindsthe interface's interface_id
packpack onlypack bindsstructure: Candid subtyping of binds.requires, or an EVM contract's selectors for binds.abi
contractauthor, third_party, inferredcontract bindsthe identity of its own binds.abi (spec §15.3). Whether it is rebound or stale is decided by its functions alone (§15.9), so one with only binds.eip712 binds no function and is never rebound.

origin is also the profile's layer when profiles are merged, highest first: author, third_party, pack, inferred. An inferred profile must be approved by a person before it is active. See Profiles, layers and binding.

binds#

Canister binds#

FieldRequiredValue
interface_idyescandid-core:interface:v1:sha256: and 64 lowercase hex digits (Content-addressed identities)
contract_idnocandid-core:contract:v1:sha256: and 64 lowercase hex digits
canister_idnoprincipal text of the canister (^[a-z0-9]{5}(-[a-z0-9]{5})*(-[a-z0-9]{1,5})?$); for a served profile it must be the serving canister (EE_SERVED)

Against an interface with another interface_id, every address is resolved again: a profile that still holds is rebound (WW_REBOUND), one that does not is stale (EE_STALE), and a stale profile's value-conversion tags (amount, timestamp, duration, account, subaccount, bytes) are left out of the resolved form.

Pack binds#

FieldRequiredValue
standardyesmatches ^[A-Z]+-[0-9]+$: "ICRC-1", "ERC-20"
spec_urlnoan https:// URL
requiresexactly one of the twoa Candid service fragment (at least 10 characters). The pack applies to a canister whose service type is a subtype of it.
abiexactly one of the twoan EVM ABI (as for a contract, below). The pack applies to a contract that has each function with the same selector, outputs and kind of call.
eip712no; only beside abithe EIP-712 messages of an EVM pack (below), experimental. The pack still matches by its binds.abi, and has no domain (spec §15.9: each token's is its own).

A pack may not use type: addresses (EE_PACK_TYPE_REF). The standard packs are on Standard packs.

Contract binds#

FieldRequiredValue
abiat least one of abi and eip712an array of at least one entry: a human-readable fragment starting with function, event, error, constructor, fallback or receive, or a JSON ABI entry with one of those as type. Only functions are read.
eip712at least one of abi and eip712the EIP-712 messages the contract verifies, with its domain's name and version (below), experimental.
deploymentsnoan array of at least one { "chain_id": integer ≥ 1, "address": "0x" and 40 hex digits }. A mixed-case address that fails its EIP-55 checksum is EE_ADDRESS_CHECKSUM; a deployment listed twice is WW_DUPLICATE. For messages, it gives the domain's chainId and verifyingContract.

binds.eip712#

Experimental

Spec §15.9, D-055: EIP-712 typed messages, the first part of the EVM binding's phase 2. See EVM contracts.

jsonprofile/packs/dai-permit.profile.json
"binds": {
  "eip712": {
    "domain": {
      "name": "Dai Stablecoin",
      "version": "1"
    },
    "types": {
      "Permit": [
        {
          "name": "holder",
          "type": "address"
        },
        {
          "name": "spender",
          "type": "address"
        },
        {
          "name": "nonce",
          "type": "uint256"
        },
        {
          "name": "expiry",
          "type": "uint256"
        },
        {
          "name": "allowed",
          "type": "bool"
        }
      ]
    }
  },
  "deployments": [
    {
      "chain_id": 1,
      "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F"
    }
  ]
},
FieldRequiredValue
typesyesan object of at least one struct, in the JSON shape of an eth_signTypedData request without EIP712Domain: a struct name (^[A-Za-z_][A-Za-z0-9_]*$) to an array of at least one { "name", "type" }. A member type matches ^[A-Za-z_][A-Za-z0-9_]*(\[([1-9][0-9]*)?\])*$: a canonical ABI type as written (uint256, never uint: the type hash hashes the text) or a struct of the same map, either with array suffixes T[] or T[k].
domainnoat least one of name and version, non-empty strings. Each must equal the request's domain value of the same name; a request with a value where the profile has none is refused too (Permit2's domain has no version). chainId and verifyingContract come from binds.deployments.

A struct name is not an ABI type (uint256, bytes32, tuple, …) and not EIP712Domain, members have unique names, and no struct contains itself. Anything else is EE_EIP712 at its place, and a struct that refers to a refused one is refused too. A type is a message when the same document tags its bare name with intent; its struct is argument 0, so its members are addressed as Permit(0).expiry (Tags). A type without an intent is a type only. A message may not share its name with a function, two profiles may not give one type name different members, and types with no message and no function describe nothing: each is EE_EIP712.

annotations#

FieldRequiredValue
atyesan address (spec §3, Addresses); which kind depends on the tag (below)
tagyesone of the 18 tags (Tags)
argsyesan object, checked against the tag's args_* definition; subaccount takes {}
notenotext
Tagsat must be
idempotency, outcome, list, namesa method address: ^[A-Za-z_][A-Za-z0-9_]*$
intent, effecta method address, or a value address (a case of a variant in the arguments, D-053). On an EVM contract the method address may be an EIP-712 type's name, which intent makes a message (§15.9).
describea value address or a method address
amount, timestamp, duration, account, subaccount, bytes, text, range, default, ui, nameda value address: METHOD(i)path, METHOD->(i)path or type:DECL path
textprofile/docs/profile-v0.md
address      = value-address / method-address
method-address = METHOD                                   ; the method itself
value-address  = METHOD "(" INDEX ")" path                ; argument INDEX
               / METHOD "->(" INDEX ")" path              ; result INDEX
               / "type:" DECL path                         ; canister profiles only
path         = *( "." label / "[]" )
label        = IDENT / "#" U32                             ; field/case name, or raw Candid id
relpath      = [ "(" INDEX ")" ] [ [ "." ] label / "[]" ] *( "." label / "[]" )   ; non-empty
METHOD, DECL, IDENT = [A-Za-z_][A-Za-z0-9_]*
U32          = decimal integer, 0 … 4294967295

grant, inside effect#

Since 0.4.0 (spec §5.3, D-056), an effect of kind transfer or approve may carry grant in its args: who may take tokens because of the call, how many and until when, as relpaths into the method's arguments. The ICRC-2 pack's:

jsonprofile/packs/icrc2.profile.json
{
  "at": "icrc2_approve",
  "tag": "effect",
  "args": {
    "kind": "approve",
    "irreversible": false,
    "grant": {
      "spender": "spender",
      "amount": "amount",
      "ends": [
        "expires_at"
      ]
    }
  }
},
Field of grantRequiredValue
spenderexactly one of spender and toa relpath: who may take the tokens through an allowance, or with a signature someone else submits
toexactly one of spender and toa relpath: who a transfer pays directly
amountyesa relpath to an amount-tagged position, or a bool
endsnoan array of 1–8 relpaths to timestamp-tagged positions

No other field is allowed (additionalProperties: false, the schema's grant definition), and the schema's oneOf takes exactly one of spender and to; anything else is EE_SCHEMA. What the positions must be, and EE_GRANT when they are not, is on Tags. A grant is part of its effect annotation, not an annotation of its own, so the validator's annotation counts do not include it; the profile's profile_id changes with it, as with any edit.

profile_id#

A profile's identity is candid-profile:v0:sha256: and the SHA-256 of the RFC 8785 (JCS) canonical JSON of the whole document, in lowercase hex. Any edit changes it, a change to description included; reordering keys does not, because JCS sorts them. The validator prints it:

bash
cd profile
node scripts/validate.mjs packs/icrc1.profile.json
text
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]

@meantjs/profile exports the same computation, and the canonical JSON it hashes:

js
import { profileId, jcs } from "@meantjs/profile";
import { readFileSync } from "node:fs";

const doc = JSON.parse(readFileSync("packs/icrc1.profile.json", "utf8"));
profileId(doc); // "candid-profile:v0:sha256:4eaa9312…"
jcs({ b: 1, a: [true, "x"] }); // '{"a":[true,"x"],"b":1}'

A profile never changes interface_id or contract_id: it is a separate document bound to them. The resolved form's profiles list carries each applied profile's profile_id, so a consumer can say which document wrote the words it shows.

The resolved form#

Consumers do not resolve addresses. They read this form, which candid-profile resolve, resolveService and validateProfile(…, { resolved: true }) write (Validate and resolve profiles). Its top level:

jsonprofile/fixtures/icrc1.resolved.json
{
  "format": "candid-profile-resolved",
  "format_version": 0,
  "interface_id": "candid-core:interface:v1:sha256:7fae0dd330b20f53ae1e4e2cb1947c1c57791da0ae20299ceb499f63228d0689",
  "profiles": [
    {
      "name": "icrc1",
      "origin": "pack",
      "profile_id": "candid-profile:v0:sha256:4eaa9312d60661248ff5627dba9906aa91240abe84cb1b1da8ca20a6fba2e3e6"
    }
  ],
  "methods": {
FieldValue
format"candid-profile-resolved"
format_version0
chain"evm" for an EVM contract's form; absent for a Candid service's. A consumer MUST refuse a form whose chain it does not know rather than read it as Candid.
interface_idthe service's candid-core:interface:v1:sha256:… (from the compiled .did, or the envelope's contract.identities.interface), or an ABI's candid-profile:evm-abi:v0:sha256:…
profilesevery applied profile, highest layer first: { name, origin, profile_id }, plus deployments when the profile binds them and eip712_domain (the domain's name and version) when it binds a domain
methodsone entry per method, keyed by name, in sorted order. An EIP-712 message is never here.
messagesEVM only, experimental: one entry per EIP-712 message, keyed by name, in sorted order (below). Absent when there is none, so a form without messages is unchanged.
diagnosticsthe diagnostics of the resolve (Profile diagnostics); a diagnostic from a multi-profile resolve may carry profile

Method entries#

FieldValue
modequery, composite_query, update or oneway. An EVM view or pure function is a query, nonpayable and payable an update.
tagsthe method tags. effect is always present: when no profile sets it, the resolver writes the §5.3 default with "origin": "default". Its args carry grant as the profile wrote it, when one is given and holds (below).
argsone node per argument
resultsone node per result
selector, signature, state_mutabilityEVM only: "0xa9059cbb", "transfer(address to,uint256 value)", "nonpayable"
jsonprofile/fixtures/icrc1.resolved.json
"icrc1_decimals": {
  "mode": "query",
  "tags": {
    "effect": {
      "args": {
        "kind": "read",
        "irreversible": false
      },
      "origin": "default"
    }
  },
  "args": [],
  "results": [
    {
      "candid": "nat8"
    }
  ]
},

A grant in the resolved form#

A grant stays inside effect.args, unchanged; the consumer reads it there and the decoded values it names (@meantjs/consent returns it as grant, see JavaScript API). From the ICRC-2 pack's fixture:

jsonprofile/fixtures/icrc2.resolved.json
"effect": {
  "args": {
    "kind": "approve",
    "irreversible": false,
    "grant": {
      "spender": "spender",
      "amount": "amount",
      "ends": [
        "expires_at"
      ]
    }
  },
  "origin": "pack",
  "profile": "icrc2"
},

A pack whose grant does not hold in the service it is applied to keeps its effect in the resolved form and loses the grant (II_PACK_ANNOTATION_SKIPPED).

Node fields#

FieldPresent whenValue
candida Candid service's forma primitive name (nat, nat64, text, principal, …), or record, variant, vec, blob, func, service
abian EVM contract's formthe canonical type of a primitive (uint256, address, bytes32), "tuple" or "array"
optionalthe position passes through an opttrue
fieldsa record, or an ABI tuple{ name → node }
casesa variant{ name → node }
elementa vec, or an ABI arraythe element's node
lengthan ABI fixed-size array T[k]k
recursivethe walk cut a cycle (or reached the depth bound) heretrue
tagsthe position has tags{ tag → { args, origin, profile } }

Tags and their provenance#

Every tag carries where it came from: origin is the layer of the profile that supplied it, and profile that profile's name. The one tag with "origin": "default" and no profile is an effect the resolver supplied. This is the idempotency stamp of icrc1_transfer, which the tool layer fills and ui.hidden keeps out of forms:

jsonprofile/fixtures/icrc1.resolved.json
"created_at_time": {
  "candid": "nat64",
  "optional": true,
  "tags": {
    "timestamp": {
      "args": {
        "unit": "ns",
        "epoch": "unix"
      },
      "origin": "pack",
      "profile": "icrc1"
    },
    "describe": {
      "args": {
        "label": "Created at",
        "help": "Set once per intent by the tool layer; enables the ledger's duplicate protection."
      },
      "origin": "pack",
      "profile": "icrc1"
    },
    "ui": {
      "args": {
        "hidden": true
      },
      "origin": "pack",
      "profile": "icrc1"
    }
  }
},

Where a node's tags come from:

  • Tags from type: addresses appear at every position reached through that edge.
  • A method-address tag at the same position wins (§8). In the tokens example, type:Tokens.e8s gives every e8s the label Amount, and balance->(0).e8s overrides it with Balance in balance's result.
  • A case node's tags may hold intent and effect (D-053). A consumer that does not read them uses the method's.
  • A stale profile supplies no value-conversion tag; its other tags stay.
jsonprofile/fixtures/tokens-example.resolved.json
"e8s": {
  "candid": "nat64",
  "tags": {
    "amount": {
      "args": {
        "decimals": 8,
        "symbol": "TOK"
      },
      "origin": "author",
      "profile": "tokens-example"
    },
    "describe": {
      "args": {
        "label": "Balance"
      },
      "origin": "author",
      "profile": "tokens-example"
    }
  }
}

Ordering#

  • Methods are sorted by name.
  • Record fields and variant cases follow canonical id order (the Candid field hash), not source order. icrc1_transfer's argument lists to, fee, memo, from_subaccount, created_at_time, amount. Use ui.order for presentation.
  • profiles is highest layer first.

Unresolved dynamic values#

A dynamic value stays as written, { "from": "query:icrc1_decimals" }: resolving it needs the network, so it is the consumer's job. The packages take the resolved values as refs, keyed by their from ({ "query:icrc1_decimals": 8 }). An EVM amount whose token names its token by a field stays as that path too; the consumer reads the token's decimals and symbol and passes them per token address (tokens in @meantjs/consent/evm, token in formatterFor). open OQ-10 (owner W1/W3): should the resolved form carry resolved dynamic values with their provenance, certified or not?

EVM contracts: chain evm#

Experimental

Spec §15, phase 1 and the first part of phase 2 (EIP-712 messages). See EVM contracts.

The resolved form of an EVM contract has "chain": "evm" at the top, an ABI identity as its interface_id, and nodes with abi where a Candid form's have candid. Each method adds selector, signature (the ERC-7730 key form, with names) and state_mutability. Its parameters are one tuple at argument index 0. From the ERC-20 pack's fixture:

jsonprofile/fixtures/erc20.resolved.json
{
  "format": "candid-profile-resolved",
  "format_version": 0,
  "chain": "evm",
  "interface_id": "candid-profile:evm-abi:v0:sha256:81471b5efd3e34c464b5dda12ccdd454d8ca6e0d0c069871c8152bc6b154be4b",
  "profiles": [
    {
      "name": "erc20",
      "origin": "pack",
      "profile_id": "candid-profile:v0:sha256:afc7d231076699406b2fc9f74da372f5da88465027ab1d46eca526e646368a81"
    }
  ],
jsonprofile/fixtures/erc20.resolved.json
"transfer": {
  "mode": "update",
  "selector": "0xa9059cbb",
  "signature": "transfer(address to,uint256 value)",
  "state_mutability": "nonpayable",
  "tags": {
    "intent": {
      "args": {
        "title": "Send tokens",
        "template": "Send {value} to {to}"
      },
      "origin": "pack",
      "profile": "erc20"
    },
    "effect": {
      "args": {
        "kind": "transfer",
        "irreversible": true,
        "grant": {
          "to": "to",
          "amount": "value"
        }
      },
      "origin": "pack",
      "profile": "erc20"
    }
  },
  "args": [
    {
      "abi": "tuple",
      "fields": {
        "to": {
          "abi": "address",
          "tags": {
            "describe": {
              "args": {
                "label": "Recipient"
              },
              "origin": "pack",
              "profile": "erc20"
            }
          }
        },
        "value": {
          "abi": "uint256",
          "tags": {
            "amount": {
              "args": {
                "decimals": {
                  "from": "query:decimals"
                },
                "symbol": {
                  "from": "query:symbol"
                }
              },
              "origin": "pack",
              "profile": "erc20"
            },
            "describe": {
              "args": {
                "label": "Amount"
              },
              "origin": "pack",
              "profile": "erc20"
            }
          }
        }
      }
    }
  ],
  "results": [
    {
      "abi": "bool"
    }
  ]
},

@meantjs/consent refuses this form at its main entry, and @meantjs/tools refuses it outright: EVM tools are a later phase.

EIP-712 messages#

A form whose profiles bind EIP-712 messages lists them under a top-level messages map, never under methods (spec §15.5, §15.9). Each entry has encode_type (EIP-712's encodeType), type_hash (its Keccak-256), tags (intent and effect) and args: one tuple node, the struct, with the same node fields as a function's argument. It has no results, mode or selector. A consumer matches a signing request to a message by type_hash, never by its name. Without an effect tag, a message's default is write, irreversible, as for an update. From the EIP-2612 pack's fixture, with three of its five members left out:

jsonprofile/fixtures/erc2612.resolved.json
"messages": {
  "Permit": {
    "encode_type": "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)",
    "type_hash": "0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9",
    "tags": {
      "intent": {
        "args": {
          "title": "Permit token spending",
          "template": "Allow {spender} to spend up to {value} from {owner}. Signature expires: {deadline}."
        },
        "origin": "pack",
        "profile": "erc2612"
      },
      "effect": {
        "args": {
          "kind": "approve",
          "irreversible": false,
          "grant": {
            "spender": "spender",
            "amount": "value"
          }
        },
        "origin": "pack",
        "profile": "erc2612"
      }
    },
    "args": [
      {
        "abi": "tuple",
        "fields": {
          …
          "value": {
            "abi": "uint256",
            "tags": {
              "amount": {
                "args": {
                  "decimals": {
                    "from": "query:decimals"
                  },
                  "symbol": {
                    "from": "query:symbol"
                  },
                  "threshold": "115792089237316195423570985008687907853269984665640564039457584007913129639935"
                },
                "origin": "pack",
                "profile": "erc2612"
              },
              …
            }
          },
          …
          "deadline": {
            "abi": "uint256",
            "tags": {
              "timestamp": {
                "args": {
                  "unit": "s",
                  "epoch": "unix",
                  "threshold": "115792089237316195423570985008687907853269984665640564039457584007913129639935",
                  "message": "Never"
                },
                "origin": "pack",
                "profile": "erc2612"
              },
              …

Each entry of profiles that binds a domain lists its name and version as eip712_domain, beside its deployments. From the Permit2 profile's fixture, whose domain has a name and no version:

jsonprofile/fixtures/permit2.resolved.json
"profiles": [
  {
    "name": "permit2",
    "origin": "third_party",
    "profile_id": "candid-profile:v0:sha256:d85261a93989a3f9cdaf7b8f3a93a3a508a388c9d8e808696bf867ace920b0c6",
    "deployments": [
      {
        "chain_id": 1,
        "address": "0x000000000022D473030F116dDEE9F6B43aC78BA3"
      },
      {
        "chain_id": 42161,
        "address": "0x000000000022D473030F116dDEE9F6B43aC78BA3"
      }
    ],
    "eip712_domain": {
      "name": "Permit2"
    }
  }
],
"methods": {},
"messages": {
  "PermitBatch": {
    "encode_type": "PermitBatch(PermitDetails[] details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)",
    "type_hash": "0xaf1b0d30d2cab0380e68f0689007e3254993c596f2fdd0aaa7f4d04f79440863",

Permit2's profile binds no function, so its methods is empty. A struct a message refers to (PermitDetails) is part of its encode_type, after the primary type, sorted by name; it is no message of its own, because nothing tags its name with intent.

The interface identity#

The ABI identity is candid-profile:evm-abi:v0:sha256: and the SHA-256 of the JCS array of the functions' canonical fragments and, when the form has EIP-712 types, one struct <Name>(<type> <name>,…) line per type, sorted (spec §15.1, §15.9). With no types it is §15.1's, so the ERC-20 pack's form keeps its identity. The array hashed for the EIP-2612 pack, five functions and one struct:

json
[
 "function DOMAIN_SEPARATOR() view returns (bytes32)",
 "function decimals() view returns (uint8)",
 "function nonces(address owner) view returns (uint256)",
 "function permit(address owner,address spender,uint256 value,uint256 deadline,uint8 v,bytes32 r,bytes32 s)",
 "function symbol() view returns (string)",
 "struct Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
]

Its SHA-256 gives the fixture's interface_idcandid-profile:evm-abi:v0:sha256:73c9721a3b8abd52830805c1ac5f848f1ef8918e9cf681b5d4e134333d0955ee; the five functions alone give interface_idcandid-profile:evm-abi:v0:sha256:caf7f6c5bf79a00460009c26ebb9b4169ae67841375e1658fbb3ff456e65f4e5 (both computed with abiInterfaceId of @meantjs/profile/evm, with and without the types). A pack's messages join the contract it matches, so they are part of the merged form's identity; whether a contract profile is rebound or stale is decided by its functions alone, so the messages a pack adds never make it stale.