Fireblocks Signer
The Fireblocks signer (github.com/smartcontractkit/crec-sdk/transact/signer/fireblocks) drives a Fireblocks vault account through the Fireblocks REST API. Two flows are supported:
Sign(ctx, hash): creates aRAWsigning operation. Fireblocks signs the digest opaquely.SignTypedData(ctx, td): creates aTYPED_MESSAGEoperation with the full EIP-712 typed data, so the Fireblocks policy engine and approvers can see what they are authorising.
Both return an Ethereum-canonical 65-byte (r, s, v) signature.
Prerequisites
- Fireblocks API key.
- RSA private key (PEM-encoded) for signing JWT requests to the Fireblocks API.
- A vault account ID containing the secp256k1 signing key.
- An asset ID (e.g.
ETH,ETH_TEST5for Sepolia, etc.). - IAM/policy: the API user has rights to create signing operations on the target vault account.
Construct the signer
Explicit parameters
import "github.com/smartcontractkit/crec-sdk/transact/signer/fireblocks"
privateKeyPEM := os.Getenv("FIREBLOCKS_RSA_PEM") // -----BEGIN RSA PRIVATE KEY-----...
s, err := fireblocks.NewSigner(
os.Getenv("FIREBLOCKS_API_KEY"),
privateKeyPEM,
"0", // vault account ID
"ETH", // asset ID
fireblocks.WithTimeout(60*time.Second),
fireblocks.WithPollingInterval(500*time.Millisecond),
)
From environment
s, err := fireblocks.NewSignerFromEnv()
Reads:
| Variable | Required | Notes |
|---|---|---|
FIREBLOCKS_API_KEY | yes | API key. |
FIREBLOCKS_API_SECRET | yes | Inline PEM or path to a PEM file. |
FIREBLOCKS_VAULT_ACCOUNT_ID | yes | E.g. "0". |
FIREBLOCKS_ASSET_ID | yes | E.g. ETH, ETH_TEST5. |
FIREBLOCKS_BASE_URL | no | Defaults to https://api.fireblocks.io; set to the sandbox URL during development. |
Derive the signer's address
The signer's secp256k1 address is the EVM address Fireblocks reports for that (vault account, asset) pair. Read it once from the Fireblocks console (or via the Fireblocks SDK) and add it to AllowedEcdsaSigners when you provision the wallet; see Manage Wallet Signers.
Sign with Sign (RAW)
opHash, sig, err := client.Transact.SignOperation(ctx, op, s, chainSelector)
Internally Sign(ctx, hash):
- POSTs
/v1/transactionswithoperation: "RAW"and the digest in the message body. - Polls
/v1/transactions/{id}everypollingInterval(default 500 ms) until the operation reaches a terminal status. - Extracts the
(r, s)from the signed message and reconstructs the recovery bytevso the signature recovers to the vault's public key. - Returns the 65-byte
(r ∥ s ∥ v)signature.
If the operation reaches REJECTED, CANCELLED, FAILED, or BLOCKED, Sign returns a wrapped error containing the Fireblocks status string. If timeout (default 60 s) elapses first, Sign returns context.DeadlineExceeded-style error.
Sign with SignTypedData (recommended for human-approved flows)
import "github.com/smartcontractkit/crec-sdk/transact/signer"
td := &signer.TypedData{
Types: map[string][]signer.TypedDataField{
"EIP712Domain": {
{Name: "name", Type: "string"},
{Name: "version", Type: "string"},
{Name: "chainId", Type: "uint256"},
{Name: "verifyingContract", Type: "address"},
},
"Operation": { /* ... */ },
"Transaction": { /* ... */ },
},
PrimaryType: "Operation",
Domain: signer.TypedDataDomain{
Name: "CLLSmartAccount", Version: "1", ChainID: 1,
VerifyingContract: op.Account.Hex(),
},
Message: map[string]any{ /* ... build from op.EIP712Message() ... */ },
}
sig, err := s.SignTypedData(ctx, td)
Fireblocks uses its TYPED_MESSAGE operation, so the policy engine and approvers see the full structured payload (domain, primary type, message fields), not an opaque hash. This is what you want any time a human is approving the operation.
The CRE Connect SDK's Transact.SignOperation always calls Sign(ctx, hash). To opt into SignTypedData, build a signer.TypedData document yourself (you can derive the fields from op.TypedData(chainSelector) and op.EIP712Message() in crec-sdk/transact/types, then translate them into signer.TypedData / signer.TypedDataDomain), call s.SignTypedData(ctx, td), and pass the resulting signature to Transact.SendSignedOperation.
Operational notes
- Latency. Fireblocks operations are asynchronous. Each
SignOperationpolls Fireblocks everypollingInterval(default 500ms) until the transaction reaches a terminal status; total latency is therefore set by Fireblocks itself and any approval policy / 3rd-party screening attached to your vault. - Status terminology. Fireblocks statuses (
PENDING_SIGNATURE,PENDING_AUTHORIZATION,BROADCASTING,COMPLETED, etc.) are independent from the CRE Connect operation status. The Fireblocks signer only returns control once the signing is done; it does not broadcast the resulting transaction. CRE Connect handles the on-chain submission. - Policy engine. Build allow-lists in Fireblocks for
(asset, contract address)pairs your service needs to call. Reject operations early at Fireblocks rather than at the Smart Account. - Sandbox. Use
WithBaseURL("https://sandbox-api.fireblocks.io")for the Fireblocks sandbox during development.
Next steps
- Signing Transparency: feed the same typed data into your audit log.
- Privy Signer: alternative for embedded user wallets.