EthSafeMessage

concept
safeprotocol-kitsignaturesdata-structuremultisig

EthSafeMessage is the Safe Protocol Kit class that represents a Safe message and its accumulating owner signatures. It is the message-signing counterpart to EthSafeTransaction and is returned by protocolKit.createMessage(message).

Shape

class EthSafeMessage implements SafeMessage {
  data: EIP712TypedData | string
  signatures: Map<string, SafeSignature> = new Map()
  // Additional methods: getSignature, addSignature, encodedSignatures, ...
}

Two fields matter:

  • data — the message being signed. Either a plain string (for EIP-191 eth_sign) or an EIP-712 typed-data object with types, primaryType, domain, and message fields. The type distinction is purely nominal; the SDK routes both through hashSafeMessage to produce the canonical Safe-wrapped hash.
  • signatures — a map from owner address to SafeSignature. It starts empty and is populated as each owner signs. The map is the heart of the class: it is the signature accumulator that lets a multi-step multisig signing workflow coexist with the synchronous-looking signMessage API.

Why a mutable accumulator

Multisig signing is fundamentally asynchronous and distributed. Four owners of a 3/4 Safe sit at four different wallets, possibly in four different processes, possibly across hours or days. No single process can “sign a message” in one shot — each process contributes one signature, hands the accumulator off (or stores it somewhere shared), and waits for the others.

EthSafeMessage models this directly. protocolKit.signMessage(safeMessage, method) does not return a new immutable object; it mutates (or returns an updated copy of) the same safeMessage, adding one entry to the signatures map. The caller holds a single object that grows until it carries enough signatures to meet the Safe’s threshold.

This is the same pattern Safe uses for transactions (EthSafeTransaction with its own signatures map). Messages and transactions are structurally unified as “a payload plus an accumulating set of owner signatures”.

Key methods

  • getSignature(address): SafeSignature | undefined — look up a specific owner’s signature. Used when you need to extract one signature for API submission, e.g. safeMessage.getSignature(OWNER_1_ADDRESS) to post it to the Safe Transaction Service via apiKit.addMessage.
  • addSignature(signature: SafeSignature) — manually insert a signature, used when a signature was produced outside the Protocol Kit (e.g. received from a counterparty over the network).
  • encodedSignatures(): string — concatenate all signatures in the map into the canonical bytes blob that Safe’s checkSignatures expects. Used when calling protocolKit.isValidSignature(messageHash, encodedSignatures) for off-chain validation.

Relationship to the signature bytes Safe expects

Safe’s on-chain checkSignatures function takes a single bytes argument containing all owner signatures concatenated in owner-address order. EthSafeMessage.encodedSignatures() produces exactly that blob from the accumulated map, sorting by address as Safe requires. The blob includes both ECDSA signatures (65 bytes each, v ∈ {27, 28}) and contract signatures (65 bytes of static data with v = 0, plus a dynamic tail containing the nested signer’s contract signature bytes).

See Safe Nested Contract Signatures for how contract signatures are produced before being added to the map, and Safe Protocol Kit Message Signatures for where in the flow encodedSignatures is called.

Connections