A comprehensive Go client library for interacting with the Hiro Chainhooks API. Chainhooks is a blockchain monitoring and event triggering system for the Stacks blockchain that enables developers to register webhooks that detect and react to specific blockchain events.
- âś… Full support for all Chainhooks API endpoints
- âś… Complete type safety with Go structs
- âś… Support for 16 different blockchain event types
- âś… Flexible authentication (API Key and JWT)
- âś… Pagination support for list operations
- âś… Builder pattern for easy chainhook definition construction
- âś… Comprehensive error handling with HTTP context
- âś… Context support for cancellation and timeouts
- âś… Minimal dependencies (uses only Go standard library)
go get github.com/tony1908/chainhooks-client-gopackage main
import (
"context"
"log"
"github.com/tony1908/chainhooks-client-go"
)
func main() {
// Create a client
client := chainhooks.NewClient(chainhooks.ChainhooksBaseURLs[chainhooks.NetworkMainnet])
client.SetAPIKey("your-api-key")
// Create a simple chainhook definition
definition := &chainhooks.ChainhookDefinition{
Name: "my-stx-hook",
Version: "1",
Chain: chainhooks.ChainStacks,
Network: chainhooks.NetworkMainnet,
Filters: chainhooks.ChainhookFilters{
Events: []interface{}{
&chainhooks.STXTransferFilter{
Type: chainhooks.EventTypeSTXTransfer,
},
},
},
Action: chainhooks.ChainhookAction{
Type: "http_post",
URL: "https://example.com/webhook",
},
}
// Register the chainhook
ctx := context.Background()
hook, err := client.RegisterChainhook(ctx, definition)
if err != nil {
log.Fatal(err)
}
log.Printf("Chainhook registered: %s", hook.UUID)
}// Create a chainhook using the fluent builder API
definition, err := chainhooks.NewChainhookBuilder("stx-transfer-hook", chainhooks.NetworkMainnet).
WithWebhookURL("https://example.com/webhook").
AddSTXTransfer(
chainhooks.PrincipalStandard("SP..."),
chainhooks.PrincipalStandard("SP..."),
chainhooks.StringPtr("1000000"),
).
WithEnableOnRegistration(true).
Build()
if err != nil {
log.Fatal(err)
}
hook, err := client.RegisterChainhook(context.Background(), definition)
if err != nil {
log.Fatal(err)
}client := chainhooks.NewClient(
chainhooks.ChainhooksBaseURLs[chainhooks.NetworkMainnet],
)client := chainhooks.NewClientWithConfig(&chainhooks.ClientConfig{
BaseURL: "https://api.mainnet.hiro.so",
APIKey: chainhooks.StringPtr("your-api-key"),
JWT: chainhooks.StringPtr("your-jwt-token"),
Timeout: 30 * time.Second,
UserAgent: "my-app/1.0.0",
})client.SetAPIKey("your-api-key")
// OR
client.SetJWT("your-jwt-token")
// OR both
client.SetAPIKey("your-api-key")
client.SetJWT("your-jwt-token")definition := &chainhooks.ChainhookDefinition{
Name: "my-hook",
Version: "1",
Chain: chainhooks.ChainStacks,
Network: chainhooks.NetworkMainnet,
Filters: chainhooks.ChainhookFilters{
Events: []interface{}{
&chainhooks.STXTransferFilter{
Type: chainhooks.EventTypeSTXTransfer,
},
},
},
Action: chainhooks.ChainhookAction{
Type: "http_post",
URL: "https://example.com/webhook",
},
}
hook, err := client.RegisterChainhook(context.Background(), definition)hook, err := client.GetChainhook(context.Background(), "uuid-string")opts := &chainhooks.PaginationOptions{
Offset: 0,
Limit: 10,
}
response, err := client.GetChainhooks(context.Background(), opts)
if err != nil {
log.Fatal(err)
}
for _, hook := range response.Chainhooks {
log.Printf("Hook: %s (Status: %s)", hook.UUID, hook.Status.Status)
}Note: Update requires a complete chainhook definition with all required fields (name, version, chain, network, filters, action).
updated, err := chainhooks.NewChainhookBuilder("updated-name", chainhooks.NetworkMainnet).
WithWebhookURL("https://example.com/webhook-updated").
AddSTXTransfer(nil, nil, nil).
Build()
if err != nil {
log.Fatal(err)
}
hook, err := client.UpdateChainhook(context.Background(), "uuid-string", updated)// Enable
err := client.EnableChainhook(context.Background(), "uuid-string", true)
// Disable
err := client.EnableChainhook(context.Background(), "uuid-string", false)// By UUIDs
request := chainhooks.BulkEnableUUIDs(
true,
chainhooks.UUID("uuid-1"),
chainhooks.UUID("uuid-2"),
)
response, err := client.BulkEnableChainhooks(context.Background(), request)
// By Webhook URL
request := chainhooks.BulkEnableByWebhook(
false,
"https://example.com/webhook",
)
// By Status
request := chainhooks.BulkEnableByStatus(
true,
chainhooks.ChainhookStatusStreaming,
)err := client.DeleteChainhook(context.Background(), "uuid-string")secret, err := client.GetConsumerSecret(context.Background())
log.Println(secret.Secret)secret, err := client.RotateConsumerSecret(context.Background())
log.Println(secret.Secret)err := client.DeleteConsumerSecret(context.Background())err := client.EvaluateChainhook(context.Background(), "uuid-string", 100000)status, err := client.GetStatus(context.Background())
log.Printf("Status: %s, Version: %s", status.Status, status.Version)The client supports 16 different blockchain event types:
// Fungible Token Transfer
&chainhooks.FTTransferFilter{
Type: chainhooks.EventTypeFTTransfer,
Asset: "USDA",
Sender: chainhooks.PrincipalStandard("SP..."),
Recipient: chainhooks.PrincipalStandard("SP..."),
Amount: chainhooks.StringPtr("1000000"),
}
// Fungible Token Mint
&chainhooks.FTMintFilter{
Type: chainhooks.EventTypeFTMint,
Asset: "USDA",
Recipient: chainhooks.PrincipalStandard("SP..."),
Amount: chainhooks.StringPtr("1000000"),
}
// Fungible Token Burn
&chainhooks.FTBurnFilter{
Type: chainhooks.EventTypeFTBurn,
Asset: "USDA",
Sender: chainhooks.PrincipalStandard("SP..."),
Amount: chainhooks.StringPtr("1000000"),
}// NFT Transfer
&chainhooks.NFTTransferFilter{
Type: chainhooks.EventTypeNFTTransfer,
Asset: "nft-collection",
Sender: chainhooks.PrincipalStandard("SP..."),
Recipient: chainhooks.PrincipalStandard("SP..."),
}
// NFT Mint
&chainhooks.NFTMintFilter{
Type: chainhooks.EventTypeNFTMint,
Asset: "nft-collection",
Recipient: chainhooks.PrincipalStandard("SP..."),
}
// NFT Burn
&chainhooks.NFTBurnFilter{
Type: chainhooks.EventTypeNFTBurn,
Asset: "nft-collection",
Sender: chainhooks.PrincipalStandard("SP..."),
}// STX Transfer
&chainhooks.STXTransferFilter{
Type: chainhooks.EventTypeSTXTransfer,
Sender: chainhooks.PrincipalStandard("SP..."),
Recipient: chainhooks.PrincipalStandard("SP..."),
Amount: chainhooks.StringPtr("1000000"),
}
// STX Mint
&chainhooks.STXMintFilter{
Type: chainhooks.EventTypeSTXMint,
Recipient: chainhooks.PrincipalStandard("SP..."),
Amount: chainhooks.StringPtr("1000000"),
}
// STX Burn
&chainhooks.STXBurnFilter{
Type: chainhooks.EventTypeSTXBurn,
Sender: chainhooks.PrincipalStandard("SP..."),
Amount: chainhooks.StringPtr("1000000"),
}// Contract Deployment
&chainhooks.ContractDeployFilter{
Type: chainhooks.EventTypeContractDeploy,
DeployerPrincipal: chainhooks.PrincipalStandard("SP..."),
}
// Contract Call
&chainhooks.ContractCallFilter{
Type: chainhooks.EventTypeContractCall,
ContractIdentifier: chainhooks.StringPtr("SP...contract_name"),
Method: chainhooks.StringPtr("method_name"),
Sender: chainhooks.PrincipalStandard("SP..."),
}
// Contract Log (emit events)
&chainhooks.ContractLogFilter{
Type: chainhooks.EventTypeContractLog,
ContractIdentifier: chainhooks.StringPtr("SP...contract_name"),
}// Balance Change
&chainhooks.BalanceChangeFilter{
Type: chainhooks.EventTypeBalanceChange,
Principal: chainhooks.PrincipalStandard("SP..."),
}
// Coinbase (mining rewards)
&chainhooks.CoinbaseFilter{
Type: chainhooks.EventTypeCoinbase,
Recipient: chainhooks.PrincipalStandard("SP..."),
}
// Tenure Change
&chainhooks.TenureChangeFilter{
Type: chainhooks.EventTypeTenureChange,
}definition, err := chainhooks.NewChainhookBuilder(
"multi-filter-hook",
chainhooks.NetworkMainnet,
).
WithWebhookURL("https://example.com/webhook").
AddSTXTransfer(
chainhooks.PrincipalStandard("SP..."),
nil,
chainhooks.StringPtr("1000000"),
).
AddFTTransfer(
"USDA",
chainhooks.PrincipalStandard("SP..."),
nil,
nil,
).
AddContractCall(
chainhooks.StringPtr("SP...my_contract"),
chainhooks.StringPtr("swap"),
nil,
).
WithEnableOnRegistration(true).
WithExpireAfterOccurrences(100).
WithIncludeContractABI(true).
WithDecodeClarityValues(true).
Build()
if err != nil {
log.Fatal(err)
}options := chainhooks.NewChainhookOptionsBuilder().
EnableOnRegistration(true).
ExpireAfterEvaluations(10000).
DecodeClarityValues(true).
IncludeContractABI(true).
Build()
definition := &chainhooks.ChainhookDefinition{
Name: "my-hook",
Version: "1",
Chain: chainhooks.ChainStacks,
Network: chainhooks.NetworkMainnet,
Filters: chainhooks.ChainhookFilters{
Events: []interface{}{
&chainhooks.STXTransferFilter{
Type: chainhooks.EventTypeSTXTransfer,
},
},
},
Action: chainhooks.ChainhookAction{
Type: "http_post",
URL: "https://example.com/webhook",
},
Options: options,
}The client provides robust error handling with helpful utilities:
hook, err := client.GetChainhook(ctx, "uuid")
if err != nil {
// Check if it's an HTTP error
if chainhooks.IsHttpError(err) {
httpErr, _ := chainhooks.AsHttpError(err)
log.Printf("HTTP Error: %d %s", httpErr.StatusCode, httpErr.Body)
}
// Check specific error conditions
if chainhooks.IsNotFound(err) {
log.Println("Chainhook not found")
} else if chainhooks.IsUnauthorized(err) {
log.Println("Unauthorized - check API key")
} else if chainhooks.IsServerError(err) {
log.Println("Server error - retry later")
}
}HttpError- HTTP request/response errors with full contextValidationError- Validation errors when building requestsConfigError- Configuration errors
The client includes several utility functions for common tasks:
// Create pointers to basic types
name := chainhooks.StringPtr("my-hook")
enabled := chainhooks.BoolPtr(true)
count := chainhooks.Uint64Ptr(100)
// Create principals
standardAddr := chainhooks.PrincipalStandard("SP...")
contractAddr := chainhooks.PrincipalContract("SP...contract_name")
// Create pagination options
opts := chainhooks.NewPaginationOptions(0, 10)
// Bulk operation helpers
bulkByUUIDs := chainhooks.BulkEnableUUIDs(true, uuid1, uuid2)
bulkByWebhook := chainhooks.BulkEnableByWebhook(false, "https://example.com/webhook")
bulkByStatus := chainhooks.BulkEnableByStatus(true, chainhooks.ChainhookStatusStreaming)
// Error checking helpers
isNotFound := chainhooks.IsNotFound(err)
isUnauth := chainhooks.IsUnauthorized(err)
isServer := chainhooks.IsServerError(err)
isClient := chainhooks.IsClientError(err)When you retrieve a chainhook, the API returns a nested structure:
type Chainhook struct {
UUID UUID `json:"uuid"`
Definition *ChainhookDefinition `json:"definition"`
Status ChainhookStatusInfo `json:"status"`
}
type ChainhookStatusInfo struct {
Status ChainhookStatus `json:"status"`
Enabled bool `json:"enabled"`
CreatedAt int64 `json:"created_at"`
LastEvaluatedAt *int64 `json:"last_evaluated_at"`
LastEvaluatedBlockHeight *uint64 `json:"last_evaluated_block_height"`
LastOccurrenceAt *int64 `json:"last_occurrence_at"`
LastOccurrenceBlockHeight *uint64 `json:"last_occurrence_block_height"`
EvaluatedBlockCount uint64 `json:"evaluated_block_count"`
OccurrenceCount uint64 `json:"occurrence_count"`
}Access fields like this:
hook, err := client.GetChainhook(ctx, uuid)
if err != nil {
log.Fatal(err)
}
log.Printf("Name: %s", hook.Definition.Name)
log.Printf("Webhook URL: %s", hook.Definition.Action.URL)
log.Printf("Status: %s", hook.Status.Status)
log.Printf("Enabled: %v", hook.Status.Enabled)
log.Printf("Created: %d", hook.Status.CreatedAt)
log.Printf("Occurrence count: %d", hook.Status.OccurrenceCount)chainhooks.NetworkMainnet // "mainnet"
chainhooks.NetworkTestnet // "testnet"chainhooks.ChainhookStatusNew
chainhooks.ChainhookStatusStreaming
chainhooks.ChainhookStatusExpired
chainhooks.ChainhookStatusInterruptedSee the Event Types section above for all 16 supported event types.
All API methods support context for cancellation and timeouts:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
hook, err := client.GetChainhook(ctx, "uuid")The client is safe for concurrent use across multiple goroutines, as each request creates its own HTTP request objects.
chainhooks.ChainhooksBaseURLs[chainhooks.NetworkMainnet]
// Returns: "https://api.mainnet.hiro.so"
chainhooks.ChainhooksBaseURLs[chainhooks.NetworkTestnet]
// Returns: "https://api.testnet.hiro.so"package main
import (
"context"
"log"
"time"
"github.com/tony1908/chainhooks-client-go"
)
func main() {
// Create client
client := chainhooks.NewClientWithConfig(&chainhooks.ClientConfig{
BaseURL: chainhooks.ChainhooksBaseURLs[chainhooks.NetworkTestnet],
})
client.SetAPIKey("your-api-key")
ctx := context.Background()
// Define chainhook
definition, err := chainhooks.NewChainhookBuilder(
"testnet-stx-monitor",
chainhooks.NetworkTestnet,
).
WithWebhookURL("https://example.com/webhook").
AddSTXTransfer(nil, nil, nil).
WithEnableOnRegistration(true).
WithDecodeClarityValues(true).
Build()
if err != nil {
log.Fatal(err)
}
// Register chainhook
hook, err := client.RegisterChainhook(ctx, definition)
if err != nil {
log.Fatal(err)
}
log.Printf("Registered chainhook: %s", hook.UUID)
log.Printf("Status: %s, Enabled: %v", hook.Status.Status, hook.Status.Enabled)
// List chainhooks
response, err := client.GetChainhooks(ctx, &chainhooks.PaginationOptions{
Offset: 0,
Limit: 10,
})
if err != nil {
log.Fatal(err)
}
log.Printf("Total chainhooks: %d", response.Total)
// Get specific chainhook
time.Sleep(1 * time.Second)
hook, err = client.GetChainhook(ctx, hook.UUID)
if err != nil {
log.Fatal(err)
}
log.Printf("Latest status: %s", hook.Status.Status)
// Disable chainhook
err = client.EnableChainhook(ctx, hook.UUID, false)
if err != nil {
log.Fatal(err)
}
log.Println("Chainhook disabled")
// Clean up
err = client.DeleteChainhook(ctx, hook.UUID)
if err != nil {
log.Fatal(err)
}
log.Println("Chainhook deleted")
}Contributions are welcome! Please feel free to submit a pull request.
Apache-2.0