diff --git a/example/webhook/main.go b/example/webhook/main.go new file mode 100644 index 0000000..e9c5069 --- /dev/null +++ b/example/webhook/main.go @@ -0,0 +1,91 @@ +// This example shows how to implement a webhook endpoint for receiving Blockfrost Webhook requests +// https://blockfrost.dev/start-building/webhooks/ +// Run using: go run example/webhook/main.go +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + + "github.com/blockfrost/blockfrost-go" +) + +const SECRET_AUTH_TOKEN string = "dc8f4b23-6c44-405f-8940-5d451222458e" + +func main() { + http.HandleFunc("/webhook", func(w http.ResponseWriter, req *http.Request) { + const MaxBodyBytes = int64(524288) + req.Body = http.MaxBytesReader(w, req.Body, MaxBodyBytes) + payload, err := io.ReadAll(req.Body) + + fmt.Printf("Received webhook request.\n") + + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading request body: %v\n", err) + w.WriteHeader(http.StatusServiceUnavailable) + return + } + + event, err := blockfrost.VerifyWebhookSignature(payload, req.Header.Get("Blockfrost-Signature"), SECRET_AUTH_TOKEN) + + if err != nil { + fmt.Fprintf(os.Stderr, "Error verifying webhook signature: %v\n", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + // Unmarshal the event data into an appropriate struct depending on its Type + switch event.Type { + case "block": + var blockEvent blockfrost.WebhookEventBlock + err := json.Unmarshal(payload, &blockEvent) + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err) + w.WriteHeader(http.StatusBadRequest) + return + } + fmt.Printf("Received block event: %v\n", blockEvent) + case "transaction": + var transactionEvent blockfrost.WebhookEventTransaction + err := json.Unmarshal(payload, &transactionEvent) + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err) + w.WriteHeader(http.StatusBadRequest) + return + } + fmt.Printf("Received tx event: %v\n", transactionEvent) + case "epoch": + var epochEvent blockfrost.WebhookEventEpoch + err := json.Unmarshal(payload, &epochEvent) + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err) + w.WriteHeader(http.StatusBadRequest) + return + } + fmt.Printf("Received epoch event: %v\n", epochEvent) + case "delegation": + var delegationEvent blockfrost.WebhookEventDelegation + err := json.Unmarshal(payload, &delegationEvent) + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err) + w.WriteHeader(http.StatusBadRequest) + return + } + fmt.Printf("Received delegation event: %v\n", delegationEvent) + default: + fmt.Fprintf(os.Stderr, "Unhandled webhook type: %s\n", event.Type) + } + + w.WriteHeader(http.StatusOK) + }) + + fmt.Println("Server is starting on port 8080...") + + err := http.ListenAndServe(":8080", nil) + if err != nil { + fmt.Fprintf(os.Stderr, "Server failed to start: %v\n", err) + } +} diff --git a/webhook.go b/webhook.go index e358070..5a82e2b 100644 --- a/webhook.go +++ b/webhook.go @@ -49,33 +49,32 @@ type WebhookEventCommon struct { WebhookID string `json:"webhook_id"` Created int `json:"created"` APIVersion int `json:"api_version,omitempty"` // omitempty because test fixtures do not include it + Type string `json:"type"` // block, transaction, delegation, epoch } type WebhookEventBlock struct { WebhookEventCommon - Type string `json:"type"` // "block" - Payload Block `json:"payload"` + Payload Block `json:"payload"` } type WebhookEventTransaction struct { WebhookEventCommon - Type string `json:"type"` // "transaction" Payload []TransactionPayload `json:"payload"` } type WebhookEventEpoch struct { WebhookEventCommon - Type string `json:"type"` // "epoch" Payload EpochPayload `json:"payload"` } type WebhookEventDelegation struct { WebhookEventCommon - Type string `json:"type"` // "delegation" Payload []StakeDelegationPayload `json:"payload"` } -type WebhookEvent interface{} +type WebhookEvent struct { + WebhookEventCommon +} const ( // Signatures older than this will be rejected by ConstructEvent @@ -152,67 +151,47 @@ func parseSignatureHeader(header string) (*signedHeader, error) { return sh, nil } -func VerifyWebhookSignature(payload []byte, header string, secret string) (WebhookEvent, error) { +func VerifyWebhookSignature(payload []byte, header string, secret string) (*WebhookEvent, error) { return VerifyWebhookSignatureWithTolerance(payload, header, secret, DefaultTolerance) } -func VerifyWebhookSignatureWithTolerance(payload []byte, header string, secret string, tolerance time.Duration) (WebhookEvent, error) { +func VerifyWebhookSignatureWithTolerance(payload []byte, header string, secret string, tolerance time.Duration) (*WebhookEvent, error) { return verifyWebhookSignature(payload, header, secret, tolerance, true) } -func VerifyWebhookSignatureIgnoringTolerance(payload []byte, header string, secret string) (WebhookEvent, error) { +func VerifyWebhookSignatureIgnoringTolerance(payload []byte, header string, secret string) (*WebhookEvent, error) { return verifyWebhookSignature(payload, header, secret, 0*time.Second, false) } -func verifyWebhookSignature(payload []byte, sigHeader string, secret string, tolerance time.Duration, enforceTolerance bool) (WebhookEvent, error) { +func verifyWebhookSignature(payload []byte, sigHeader string, secret string, tolerance time.Duration, enforceTolerance bool) (*WebhookEvent, error) { // First unmarshal into a generic map to inspect the type var genericEvent map[string]interface{} if err := json.Unmarshal(payload, &genericEvent); err != nil { - return nil, fmt.Errorf("failed to parse webhook body json: %s", err) - } - - // Determine the specific event type - eventType, ok := genericEvent["type"].(string) - if !ok { - return nil, errors.New("event type not found") + return nil, fmt.Errorf("Failed to parse webhook body json: %s", err) } var event WebhookEvent - // Unmarshal into the specific event type based on the eventType - switch eventType { - case string(WebhookEventTypeBlock): - event = new(WebhookEventBlock) - case string(WebhookEventTypeTransaction): - event = new(WebhookEventTransaction) - case string(WebhookEventTypeEpoch): - event = new(WebhookEventEpoch) - case string(WebhookEventTypeDelegation): - event = new(WebhookEventDelegation) - default: - return nil, fmt.Errorf("unknown event type: %s", eventType) - } - if err := json.Unmarshal(payload, &event); err != nil { - return nil, fmt.Errorf("failed to parse specific webhook event json: %s", err) + return nil, fmt.Errorf("Failed to parse specific webhook event json: %s", err) } header, err := parseSignatureHeader(sigHeader) if err != nil { - return event, err + return &event, err } expectedSignature := computeSignature(header.timestamp, payload, secret) expiredTimestamp := time.Since(header.timestamp) > tolerance if enforceTolerance && expiredTimestamp { - return event, ErrTooOld + return &event, ErrTooOld } for _, sig := range header.signatures { if hmac.Equal(expectedSignature, sig) { - return event, nil + return &event, nil } } - return event, ErrNoValidSignature + return &event, ErrNoValidSignature } diff --git a/webhook_test.go b/webhook_test.go index 1ce4144..c192368 100644 --- a/webhook_test.go +++ b/webhook_test.go @@ -15,13 +15,21 @@ func TestVerifyWebhookSignature(t *testing.T) { "t=1650013856,v1=abc,t=1650013856,v1=f4c3bb2a8b0c8e21fa7d5fdada2ee87c9c6f6b0b159cc22e483146917e195c3e", "59a1eb46-96f4-4f0b-8a03-b4d26e70593a") - _, ok := event.(*blockfrost.WebhookEventBlock) - if !ok { - jsonData, _ := json.Marshal(event) - t.Fatalf("Invalid webhook type %s", jsonData) + // Unmarshal the event data into an appropriate struct depending on its Type + var blockEvent blockfrost.WebhookEventBlock + switch event.Type { + case "block": + err := json.Unmarshal([]byte(validPayload), &blockEvent) + + if err != nil { + t.Fatalf("Error parsing webhook JSON: %v\n", err) + return + } + default: + t.Fatalf("Invalid webhook type: %s\n", event.Type) } - jsonData, err := json.Marshal(event) + jsonData, err := json.Marshal(blockEvent) if err != nil { t.Fatalf("Error marshaling to JSON: %s", err) }