2.0.0 is the first stable release of the Go modules, so there is nothing to migrate. The 2.0 migration guide covers the same capability split for Rust and TypeScript.
Installation
Every backend is its own Go module, so your build and your module graph contain only the backends you import:
go get github.com/solana-foundation/solana-keychain/go/core/v2 # Interfaces, errors, helpersgo get github.com/solana-foundation/solana-keychain/go/signers/memory/v2 # Local keypair signergo get github.com/solana-foundation/solana-keychain/go/signers/vault/v2 # HashiCorp Vaultgo get github.com/solana-foundation/solana-keychain/go/signers/awskms/v2 # AWS KMSgo get github.com/solana-foundation/solana-keychain/go/signers/gcpkms/v2 # GCP KMSgo get github.com/solana-foundation/solana-keychain/go/signers/privy/v2 # Privygo get github.com/solana-foundation/solana-keychain/go/signers/turnkey/v2 # Turnkeygo get github.com/solana-foundation/solana-keychain/go/signers/fireblocks/v2 # Fireblocksgo get github.com/solana-foundation/solana-keychain/go/signers/cdp/v2 # Coinbase Developer Platformgo get github.com/solana-foundation/solana-keychain/go/signers/crossmint/v2 # Crossmintgo get github.com/solana-foundation/solana-keychain/go/signers/dfns/v2 # Dfnsgo get github.com/solana-foundation/solana-keychain/go/signers/openfort/v2 # Openfortgo get github.com/solana-foundation/solana-keychain/go/signers/para/v2 # Parago get github.com/solana-foundation/solana-keychain/go/signers/utila/v2 # Utilago get github.com/solana-foundation/solana-keychain/go/signers/fordefi/v2 # Fordefi
Modules are tagged go/<module>/v2.0.0 (for example
go/signers/memory/v2.0.0). Every module requires Go 1.25.0, except gcpkms,
which requires Go 1.25.8 because of the floor set by google.golang.org/api.
Ledger is Rust-only and has no Go module.
solana-go v2 and v1 Transactions
The modules are built on
github.com/solana-foundation/solana-go/v2
v2.0.0 and take its *solana.Transaction. Legacy, v0 and v1
(solana.MessageVersionV1) transactions are supported, except by Fireblocks
with UseProgramCall: true, which accepts legacy and v0 only.
Basic Usage
Every signer implements the base
core.SolanaSigner interface
(Pubkey, SignMessage, IsAvailable). Methods take a context.Context and
block. Each backend also implements exactly one capability interface:
core.TransactionSigner:SignTransactionsigns your transaction as given.core.ModifyingSigner:ModifyAndSignTransactionlets the provider rewrite the transaction, then signs it.core.SendingSigner:SignAndSendTransactionhas the provider sign and broadcast it.
Each backend package exposes a Config struct and a New constructor:
import ("context""fmt""github.com/solana-foundation/solana-go/v2""github.com/solana-foundation/solana-keychain/go/core/v2""github.com/solana-foundation/solana-keychain/go/signers/memory/v2")func signWithKeychain(ctx context.Context, tx *solana.Transaction) error {signer, err := memory.New(memory.Config{PrivateKeyPath: "/path/to/id.json"})if err != nil {return err}if !signer.IsAvailable(ctx) {return core.NewSignerError(core.CodeNotAvailable, "signer offline")}result, err := signer.SignTransaction(ctx, tx)if err != nil {return err}fmt.Println("signature:", result.Signature)fmt.Println("complete:", result.IsComplete())// result.EncodedTransaction is the base64 wire transactionreturn nil}
SignTransaction and ModifyAndSignTransaction return a
core.SignedTransaction (EncodedTransaction, Signature, Completeness).
Remote HTTP backends accept an optional HTTPClient and HTTPClientConfig
(timeouts). The default client enforces HTTPS; supplying your own bypasses that,
and you own its security posture.
Signer Capabilities
A backend that cannot sign a transaction carries no SignTransaction method at
all, so capability is a compile-time fact for concrete types and a type
assertion for a core.SolanaSigner:
| Backend | Capability | SignMessage |
|---|---|---|
| memory, vault, privy, turnkey, awskms, gcpkms, fireblocks, dfns, para, openfort | TransactionSigner | Yes |
| cdp | TransactionSigner | UTF-8 payloads only |
| utila | TransactionSigner | Fails with CodeSigningFailed |
fordefi black box (fordefi.BlackBoxSigner) | TransactionSigner | Yes |
fordefi native manual (fordefi.NativeManualSigner) | ModifyingSigner | Yes |
fordefi native auto (fordefi.NativeAutoSigner) | SendingSigner | Yes |
| crossmint | SendingSigner | Fails with CodeSigningFailed |
switch s := signer.(type) {case core.SendingSigner:sig, err := s.SignAndSendTransaction(ctx, tx)case core.ModifyingSigner:signed, err := s.ModifyAndSignTransaction(ctx, tx)case core.TransactionSigner:signed, err := s.SignTransaction(ctx, tx)}
Initialization
There is no separate Init step. Privy, Fireblocks, Dfns, Crossmint, Para,
Openfort and Utila resolve the wallet address inside New, which is why their
constructors take a context.Context. Always build signers through New: a
zero-value Crossmint, Para or Utila signer fails with CodeNotInitialized
rather than signing for the zero address.
Sign and Send with Any Signer
core.SignAndSendTransaction gets a transaction on chain whatever the signer's
shape. A SendingSigner broadcasts through its provider and ignores the send
function; every other shape signs and your function broadcasts the base64
result, which for a ModifyingSigner is the transaction its provider rewrote.
Core has no RPC dependency:
import "github.com/solana-foundation/solana-go/v2/rpc"client := rpc.New("https://api.devnet.solana.com")sig, err := core.SignAndSendTransaction(ctx, signer, tx,func(ctx context.Context, encoded string) (solana.Signature, error) {return client.SendEncodedTransaction(ctx, encoded)})
Batch Signing
Batch signing is provided as free helpers, concurrent with an optional per-request stagger for rate-limited APIs:
sigs, err := core.SignMessages(ctx, signer, [][]byte{msg1, msg2}, core.BatchOptions{MaxConcurrency: 4, // 0 = unboundedRequestDelay: 50 * time.Millisecond,})signed, err := core.SignTransactions(ctx, signer, txs, core.BatchOptions{})
core.SignTransactions takes a core.TransactionSigner, so the
broadcast-managed signers (Crossmint, Fordefi native auto) and Fordefi native
manual cannot be batched through it; call them one transaction at a time. A
signer whose calls create provider-side work (Fireblocks with UseProgramCall)
reports it through core.BatchServerSideEffects, and the batch then runs
sequentially.
Backend Configuration
Memory
Provide exactly one of PrivateKey (raw bytes), PrivateKeyString (base58 or a
[1,2,...] byte array) or PrivateKeyPath (Solana CLI keypair file):
import "github.com/solana-foundation/solana-keychain/go/signers/memory/v2"signer, err := memory.New(memory.Config{PrivateKeyPath: "/path/to/id.json",})
Go offers no reliable way to zero memory: the garbage collector may copy and retain key bytes (memory keypairs, and the keys Crossmint and Openfort derive locally) until collection. Treat the whole process memory as sensitive when using local-key backends.
HashiCorp Vault
import "github.com/solana-foundation/solana-keychain/go/signers/vault/v2"signer, err := vault.New(vault.Config{VaultAddr: "https://vault.example.com:8200",Token: "hvs.xxxxx",KeyName: "my-solana-key",Pubkey: "base58_public_key",})
AWS KMS
import "github.com/solana-foundation/solana-keychain/go/signers/awskms/v2"signer, err := awskms.New(ctx, awskms.Config{KeyID: "alias/my-solana-key",PublicKey: "base58_public_key",Region: "us-east-1", // optional})
GCP KMS
import "github.com/solana-foundation/solana-keychain/go/signers/gcpkms/v2"signer, err := gcpkms.New(ctx, gcpkms.Config{KeyName: "projects/my-project/locations/us-east1/keyRings/my-ring/cryptoKeys/my-key/cryptoKeyVersions/1",PublicKey: "base58_public_key",})if err != nil {return err}defer signer.Close()
Privy
import "github.com/solana-foundation/solana-keychain/go/signers/privy/v2"signer, err := privy.New(ctx, privy.Config{AppID: "app_id",AppSecret: "app_secret",WalletID: "wallet_id",})
Turnkey
import "github.com/solana-foundation/solana-keychain/go/signers/turnkey/v2"signer, err := turnkey.New(turnkey.Config{APIPublicKey: "api_public_key",APIPrivateKey: "api_private_key",OrganizationID: "org_id",PrivateKeyID: "private_key_id",PublicKey: "base58_public_key",})
Fireblocks
import "github.com/solana-foundation/solana-keychain/go/signers/fireblocks/v2"signer, err := fireblocks.New(ctx, fireblocks.Config{APIKey: "api_key",PrivateKeyPEM: "-----BEGIN RSA PRIVATE KEY-----\n...",VaultAccountID: "0",AssetID: "SOL", // or "SOL_TEST" for devnetUseProgramCall: false, // true signs via sign-only PROGRAM_CALL (legacy/v0, hot wallet)})
CDP (Coinbase Developer Platform)
import "github.com/solana-foundation/solana-keychain/go/signers/cdp/v2"signer, err := cdp.New(cdp.Config{APIKeyID: "api_key_id",APIKeySecret: "api_key_secret", // base64 Ed25519WalletSecret: "wallet_secret", // base64 PKCS#8 DERAddress: "base58_address",Network: cdp.NetworkMainnet, // or cdp.NetworkDevnet; required for address lookup tables})
CDP's SignMessage accepts UTF-8 payloads only.
Crossmint
Crossmint is sending-only: its API executes every approved transaction
server-side, so *crossmint.Signer has SignAndSendTransaction and no
SignTransaction. Your transaction is never modified; the returned signature
identifies the transaction Crossmint landed, which may differ from yours if
Crossmint sponsors gas.
import "github.com/solana-foundation/solana-keychain/go/signers/crossmint/v2"signer, err := crossmint.New(ctx, crossmint.Config{APIKey: "api_key",WalletLocator: "wallet_locator",SignerSecret: "", // optional xmsk1_ secret: auto-approves what Crossmint presents})sig, err := signer.SignAndSendTransaction(ctx, tx)
Dfns
import "github.com/solana-foundation/solana-keychain/go/signers/dfns/v2"signer, err := dfns.New(ctx, dfns.Config{AuthToken: "auth_token",CredID: "cred_id",PrivateKeyPEM: "-----BEGIN EC PRIVATE KEY-----\n...",WalletID: "wallet_id",})
Openfort
import "github.com/solana-foundation/solana-keychain/go/signers/openfort/v2"signer, err := openfort.New(ctx, openfort.Config{SecretKey: "sk_live_...",AccountID: "acc_...",WalletSecret: "wallet_secret", // base64 PKCS#8 DER or PEM})
Para
import "github.com/solana-foundation/solana-keychain/go/signers/para/v2"signer, err := para.New(ctx, para.Config{APIKey: "sk_...",WalletID: "wallet_id",})
Utila
Utila signs transactions only; SignMessage fails, and you broadcast the signed
transaction yourself.
import "github.com/solana-foundation/solana-keychain/go/signers/utila/v2"signer, err := utila.New(ctx, utila.Config{ServiceAccountEmail: "sa@example.com",ServiceAccountPrivateKeyPEM: "-----BEGIN PRIVATE KEY-----\n...",VaultID: "vault_id",WalletID: "wallet_id",Network: "networks/solana-devnet",})
Fordefi
Fordefi comes as three signer types, fixed at construction by Config.Chain and
Config.PushMode. All three sign messages:
| Config | Type | Entry point |
|---|---|---|
Chain unset | *fordefi.BlackBoxSigner | SignTransaction |
Chain set, PushMode unset or PushModeAuto | *fordefi.NativeAutoSigner | SignAndSendTransaction |
Chain set, PushMode: PushModeManual | *fordefi.NativeManualSigner | ModifyAndSignTransaction |
fordefi.New dispatches on the config and returns a core.SolanaSigner;
fordefi.NewBlackBox, fordefi.NewNativeAuto and fordefi.NewNativeManual
return the concrete type and reject the other modes' configs. API requests are
authenticated with an ECDSA P-256 request signature: provide exactly one of
PrivateKeyPEM, or a RequestSigner to keep that key in a KMS/HSM.
Native auto mode: Fordefi may replace the blockhash and fees, then signs and broadcasts. It accepts only an unsigned transaction whose sole required signer is the vault.
import "github.com/solana-foundation/solana-keychain/go/signers/fordefi/v2"signer, err := fordefi.NewNativeAuto(ctx, fordefi.Config{AccessToken: os.Getenv("FORDEFI_ACCESS_TOKEN"),VaultID: os.Getenv("FORDEFI_VAULT_ID"),PublicKey: os.Getenv("FORDEFI_PUBLIC_KEY"), // Solana vault address (base58)PrivateKeyPEM: string(pemBytes),Chain: fordefi.ChainSolanaDevnet, // or fordefi.ChainSolanaMainnetFee: &fordefi.Fee{Type: fordefi.FeeTypePriority,PriorityLevel: fordefi.PriorityMedium,},})sig, err := signer.SignAndSendTransaction(ctx, tx)
Native manual mode: set PushMode: fordefi.PushModeManual. Fordefi rewrites
the message (recent blockhash, Compute Budget fee instructions) and signs it
without broadcasting. ModifyAndSignTransaction replaces tx with the
transaction Fordefi signed: always continue from it. Fordefi must be the fee
payer and must sign before any other required signer, since the rewrite voids
existing signatures. The rewrite is not diffed, so inspect the result before
broadcasting.
signer, err := fordefi.NewNativeManual(ctx, fordefi.Config{AccessToken: os.Getenv("FORDEFI_ACCESS_TOKEN"),VaultID: os.Getenv("FORDEFI_VAULT_ID"),PublicKey: os.Getenv("FORDEFI_PUBLIC_KEY"),PrivateKeyPEM: string(pemBytes),Chain: fordefi.ChainSolanaDevnet,PushMode: fordefi.PushModeManual,})signed, err := signer.ModifyAndSignTransaction(ctx, tx)
Black box mode: leave Chain unset. Fordefi signs your exact message bytes
and does not broadcast. Use this with a Fordefi black box vault.
signer, err := fordefi.NewBlackBox(ctx, fordefi.Config{AccessToken: os.Getenv("FORDEFI_ACCESS_TOKEN"),VaultID: os.Getenv("FORDEFI_BB_VAULT_ID"),PublicKey: os.Getenv("FORDEFI_BB_PUBLIC_KEY"),PrivateKeyPEM: string(pemBytes),})
Why There Is No Umbrella Package
Rust has a Signer enum and TypeScript has createKeychainSigner; Go
deliberately has neither. Go does not dead-code-eliminate across a runtime
dispatch switch, so an umbrella would force every backend's vendor SDK (AWS, GCP
and the rest) into every consumer's build. Importing the backend package you
need is the selector. To pick a backend at runtime, write the switch in your own
code over just the backends you support, and hold the result as a
core.SolanaSigner.
Error Handling
Every error is a *core.SignerError with a stable Code (CodeConfigError,
CodeHTTPError, CodeRemoteAPIError, CodeSigningFailed,
CodeNotInitialized, CodeBroadcastUnconfirmed and others). Error() returns
only a fixed message per code, never key material or raw provider responses.
Match with errors.As, errors.Is(err, &core.SignerError{Code: ...}) or
core.CodeOf(err).
On Crossmint and Fordefi native auto mode, a failure does not mean nothing
landed. When the provider may have accepted the transaction, the code is
core.CodeBroadcastUnconfirmed ("SIGNER_BROADCAST_UNCONFIRMED").
ProviderTxID is set when the create was accepted, and IdempotencyKey carries
the key derived from the message. Resending the byte-identical transaction is
safe; a rebuilt one (for example with a new blockhash) is a new transfer.
core.SignAndSendTransaction also reports this code when your send function
fails, with the fee-payer signature in TransactionSignature. Fireblocks
PROGRAM_CALL is sign-only and sends no key; it reports this code only when a
poll shows Fireblocks broadcast anyway.
sig, err := core.SignAndSendTransaction(ctx, signer, tx, send)var se *core.SignerErrorif errors.As(err, &se) && se.Code == core.CodeBroadcastUnconfirmed {// Reconcile se.ProviderTxID, se.IdempotencyKey or se.TransactionSignature// with the provider before retrying.}
See the security model for what each signing shape guarantees, the Go README for the full module list, and pkg.go.dev for the API reference.
Adding Custom Signers
Implement core.SolanaSigner plus exactly one capability interface, and add no
method your backend cannot honor: callers detect capability by type assertion.
See the Adding Signers guide to integrate
additional key management services.
Is this page helpful?