Custom ChainsChainCtx & Curves

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.

chain-ctx.ts
interface ChainCtx extends DigestSigner {
  readonly publicKey: EvmAddress | SvmAddress | SuiAddress;
  readonly vm: VmToken;
  readonly network: Network;
  readonly assets: AssetCollection;
}
MemberTypePurpose
publicKeybranded EvmAddress / SvmAddress / SuiAddressThe address this ctx signs for.
vmVmTokenThe vm token — the same one the dialect declares.
networkNetworkThe target network the address is on.
assetsAssetCollectionThe account's tracked assets on that network.
_signBytesinherited from DigestSignerThe 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.

digest-signer.ts
interface DigestSigner {
  _signBytes(input: string | Uint8Array): RecoveredSecpSig | Uint8Array;
}

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 a RecoveredSecpSig. The built-in EVM dialect formats it into Ethereum's 65-byte 0x{r}{s}{v}.
  • ed25519 — pass the raw message bytes as a Uint8Array (no prehash); you receive the signature as a Uint8Array. The built-in SVM dialect base58-encodes it.

A dialect signs by calling _signBytes and formatting the result into its own encoding:

sign-through-ctx.ts
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";
ValueMeaning
personal_signEIP-191 personal-sign over a UTF-8 message (EVM).
rawSign keccak256 of an even-length 0x-hex string, with no prefix (EVM).
ed25519Sign 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";
ValueSignsPrehash
secp256k1A 32-byte digest handed in as hex, lowS-canonical.None.
ed25519The 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).

RecoveredSecpSig

The raw secp256k1 signature the underlying curve library returns for a secp256k1 dialect:

recovered-secp-sig.ts
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.

Last updated on