Go

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, helpers
go get github.com/solana-foundation/solana-keychain/go/signers/memory/v2 # Local keypair signer
go get github.com/solana-foundation/solana-keychain/go/signers/vault/v2 # HashiCorp Vault
go get github.com/solana-foundation/solana-keychain/go/signers/awskms/v2 # AWS KMS
go get github.com/solana-foundation/solana-keychain/go/signers/gcpkms/v2 # GCP KMS
go get github.com/solana-foundation/solana-keychain/go/signers/privy/v2 # Privy
go get github.com/solana-foundation/solana-keychain/go/signers/turnkey/v2 # Turnkey
go get github.com/solana-foundation/solana-keychain/go/signers/fireblocks/v2 # Fireblocks
go get github.com/solana-foundation/solana-keychain/go/signers/cdp/v2 # Coinbase Developer Platform
go get github.com/solana-foundation/solana-keychain/go/signers/crossmint/v2 # Crossmint
go get github.com/solana-foundation/solana-keychain/go/signers/dfns/v2 # Dfns
go get github.com/solana-foundation/solana-keychain/go/signers/openfort/v2 # Openfort
go get github.com/solana-foundation/solana-keychain/go/signers/para/v2 # Para
go get github.com/solana-foundation/solana-keychain/go/signers/utila/v2 # Utila
go 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: SignTransaction signs your transaction as given.
  • core.ModifyingSigner: ModifyAndSignTransaction lets the provider rewrite the transaction, then signs it.
  • core.SendingSigner: SignAndSendTransaction has 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 transaction
return 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:

BackendCapabilitySignMessage
memory, vault, privy, turnkey, awskms, gcpkms, fireblocks, dfns, para, openfortTransactionSignerYes
cdpTransactionSignerUTF-8 payloads only
utilaTransactionSignerFails with CodeSigningFailed
fordefi black box (fordefi.BlackBoxSigner)TransactionSignerYes
fordefi native manual (fordefi.NativeManualSigner)ModifyingSignerYes
fordefi native auto (fordefi.NativeAutoSigner)SendingSignerYes
crossmintSendingSignerFails 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 = unbounded
RequestDelay: 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 devnet
UseProgramCall: 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 Ed25519
WalletSecret: "wallet_secret", // base64 PKCS#8 DER
Address: "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:

ConfigTypeEntry point
Chain unset*fordefi.BlackBoxSignerSignTransaction
Chain set, PushMode unset or PushModeAuto*fordefi.NativeAutoSignerSignAndSendTransaction
Chain set, PushMode: PushModeManual*fordefi.NativeManualSignerModifyAndSignTransaction

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.ChainSolanaMainnet
Fee: &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.SignerError
if 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?

© 2026 Solana Foundation. Tutti i diritti riservati.
Go | Solana