ChainCtx & Curves
The keyless ChainCtx view (publicKey/vm/network/assets/_signBytes) a dialect signs through, and the DigestSigner, MessageEncoding, CurveName and RecoveredSecpSig contracts it implements against.
A ChainDialect never touches the private key. Its signing methods operate through a ChainCtx — a keyless view of an Address — and sign by calling one raw-sign handle, _signBytes. This page covers the ChainCtx shape, that handle's DigestSigner contract, and the MessageEncoding, CurveName, and RecoveredSecpSig types a dialect works against.
ChainCtx
ChainCtx is the object each signing method receives. At runtime it is a dedicated keyless object the Address builds — not the Address itself, which is what keeps the private key unreachable: the dialect sees only these members, never the internal key custody.
interface ChainCtx extends DigestSigner {
readonly publicKey: EvmAddress | SvmAddress | SuiAddress;
readonly vm: VmToken;
readonly network: Network;
readonly assets: AssetCollection;
}| Member | Type | Purpose |
|---|---|---|
publicKey | branded EvmAddress / SvmAddress / SuiAddress | The address this ctx signs for. |
vm | VmToken | The vm token — the same one the dialect declares. |
network | Network | The target network the address is on. |
assets | AssetCollection | The account's tracked assets on that network. |
_signBytes | inherited from DigestSigner | The raw-sign handle (below). |
DigestSigner and _signBytes
_signBytes is the only signing primitive a dialect is given. It routes to the Address's key custody and exposes only the raw sign — never a key getter, never the private key itself.
interface DigestSigner {
_signBytes(input: string | Uint8Array): RecoveredSecpSig | Uint8Array;
}It is underscore-named on purpose: a public raw-digest signer would be a signing oracle over caller-chosen digests — broader than the guarded public methods — so the name marks it as internal-facing even though a dialect calls it.
What you pass and get back depends on the declared curve:
- secp256k1 — pass a 32-byte digest as a
0x-hex string (no prehash); you receive aRecoveredSecpSig. The built-in EVM dialect formats it into Ethereum's 65-byte0x{r}{s}{v}. - ed25519 — pass the raw message bytes as a
Uint8Array(no prehash); you receive the signature as aUint8Array. The built-in SVM dialect base58-encodes it.
A dialect signs by calling _signBytes and formatting the result into its own encoding:
signMessage(ctx, message) {
const raw = ctx._signBytes(new TextEncoder().encode(`xvm:${message}`));
return `xvm-sig:${Buffer.from(raw).toString("hex")}`;
}MessageEncoding
The encodings signMessageEncoded may be asked to serve:
type MessageEncoding = "personal_sign" | "raw" | "ed25519";| Value | Meaning |
|---|---|
personal_sign | EIP-191 personal-sign over a UTF-8 message (EVM). |
raw | Sign keccak256 of an even-length 0x-hex string, with no prefix (EVM). |
ed25519 | Sign the raw message bytes (SVM). |
Address matches the requested encoding to the vm before delegating, so the built-in EVM dialect only ever sees personal_sign / raw and the SVM dialect only sees ed25519. A custom dialect receives whichever value the caller asked for.
CurveName
The signing curve a dialect declares through its curve member:
type CurveName = "secp256k1" | "ed25519";| Value | Signs | Prehash |
|---|---|---|
secp256k1 | A 32-byte digest handed in as hex, lowS-canonical. | None. |
ed25519 | The raw message bytes; the key is a base58 secret. | None. |
The declared curve selects the raw signing primitive the key custody uses through an internal, per-curve table, so a hex key can never be cross-fed to ed25519 nor a base58 secret to secp256k1. It is a closed union of the two curves that table implements — a custom dialect reuses one of them (the xvm example declares ed25519).
A vm with no registered dialect (for example the sui placeholder) has no declared curve, so _signBytes refuses it with a WativeError("UNSUPPORTED_OP").
RecoveredSecpSig
The raw secp256k1 signature the underlying curve library returns for a secp256k1 dialect:
interface RecoveredSecpSig {
recovery: number;
toCompactHex(): string;
}It carries the recovery id and its own canonical compact serializer, so a dialect formats it into its chain's signature form byte-for-byte. The built-in EVM dialect builds Ethereum's 65-byte 0x{r}{s}{v} from it, with v = 27 + recovery.