Skip to content

Commit fe321c8

Browse files
Merge pull request #77 from blockfrost/chore/webhook-impr
chore: add common webhook fields to WebhookEvent
2 parents ae3b268 + e21e666 commit fe321c8

File tree

3 files changed

+119
-41
lines changed

3 files changed

+119
-41
lines changed

example/webhook/main.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// This example shows how to implement a webhook endpoint for receiving Blockfrost Webhook requests
2+
// https://blockfrost.dev/start-building/webhooks/
3+
// Run using: go run example/webhook/main.go
4+
package main
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"io"
10+
"net/http"
11+
"os"
12+
13+
"github.com/blockfrost/blockfrost-go"
14+
)
15+
16+
const SECRET_AUTH_TOKEN string = "dc8f4b23-6c44-405f-8940-5d451222458e"
17+
18+
func main() {
19+
http.HandleFunc("/webhook", func(w http.ResponseWriter, req *http.Request) {
20+
const MaxBodyBytes = int64(524288)
21+
req.Body = http.MaxBytesReader(w, req.Body, MaxBodyBytes)
22+
payload, err := io.ReadAll(req.Body)
23+
24+
fmt.Printf("Received webhook request.\n")
25+
26+
if err != nil {
27+
fmt.Fprintf(os.Stderr, "Error reading request body: %v\n", err)
28+
w.WriteHeader(http.StatusServiceUnavailable)
29+
return
30+
}
31+
32+
event, err := blockfrost.VerifyWebhookSignature(payload, req.Header.Get("Blockfrost-Signature"), SECRET_AUTH_TOKEN)
33+
34+
if err != nil {
35+
fmt.Fprintf(os.Stderr, "Error verifying webhook signature: %v\n", err)
36+
w.WriteHeader(http.StatusBadRequest)
37+
return
38+
}
39+
40+
// Unmarshal the event data into an appropriate struct depending on its Type
41+
switch event.Type {
42+
case "block":
43+
var blockEvent blockfrost.WebhookEventBlock
44+
err := json.Unmarshal(payload, &blockEvent)
45+
if err != nil {
46+
fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err)
47+
w.WriteHeader(http.StatusBadRequest)
48+
return
49+
}
50+
fmt.Printf("Received block event: %v\n", blockEvent)
51+
case "transaction":
52+
var transactionEvent blockfrost.WebhookEventTransaction
53+
err := json.Unmarshal(payload, &transactionEvent)
54+
if err != nil {
55+
fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err)
56+
w.WriteHeader(http.StatusBadRequest)
57+
return
58+
}
59+
fmt.Printf("Received tx event: %v\n", transactionEvent)
60+
case "epoch":
61+
var epochEvent blockfrost.WebhookEventEpoch
62+
err := json.Unmarshal(payload, &epochEvent)
63+
if err != nil {
64+
fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err)
65+
w.WriteHeader(http.StatusBadRequest)
66+
return
67+
}
68+
fmt.Printf("Received epoch event: %v\n", epochEvent)
69+
case "delegation":
70+
var delegationEvent blockfrost.WebhookEventDelegation
71+
err := json.Unmarshal(payload, &delegationEvent)
72+
if err != nil {
73+
fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err)
74+
w.WriteHeader(http.StatusBadRequest)
75+
return
76+
}
77+
fmt.Printf("Received delegation event: %v\n", delegationEvent)
78+
default:
79+
fmt.Fprintf(os.Stderr, "Unhandled webhook type: %s\n", event.Type)
80+
}
81+
82+
w.WriteHeader(http.StatusOK)
83+
})
84+
85+
fmt.Println("Server is starting on port 8080...")
86+
87+
err := http.ListenAndServe(":8080", nil)
88+
if err != nil {
89+
fmt.Fprintf(os.Stderr, "Server failed to start: %v\n", err)
90+
}
91+
}

webhook.go

Lines changed: 15 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -49,33 +49,32 @@ type WebhookEventCommon struct {
4949
WebhookID string `json:"webhook_id"`
5050
Created int `json:"created"`
5151
APIVersion int `json:"api_version,omitempty"` // omitempty because test fixtures do not include it
52+
Type string `json:"type"` // block, transaction, delegation, epoch
5253
}
5354

5455
type WebhookEventBlock struct {
5556
WebhookEventCommon
56-
Type string `json:"type"` // "block"
57-
Payload Block `json:"payload"`
57+
Payload Block `json:"payload"`
5858
}
5959

6060
type WebhookEventTransaction struct {
6161
WebhookEventCommon
62-
Type string `json:"type"` // "transaction"
6362
Payload []TransactionPayload `json:"payload"`
6463
}
6564

6665
type WebhookEventEpoch struct {
6766
WebhookEventCommon
68-
Type string `json:"type"` // "epoch"
6967
Payload EpochPayload `json:"payload"`
7068
}
7169

7270
type WebhookEventDelegation struct {
7371
WebhookEventCommon
74-
Type string `json:"type"` // "delegation"
7572
Payload []StakeDelegationPayload `json:"payload"`
7673
}
7774

78-
type WebhookEvent interface{}
75+
type WebhookEvent struct {
76+
WebhookEventCommon
77+
}
7978

8079
const (
8180
// Signatures older than this will be rejected by ConstructEvent
@@ -152,67 +151,47 @@ func parseSignatureHeader(header string) (*signedHeader, error) {
152151
return sh, nil
153152
}
154153

155-
func VerifyWebhookSignature(payload []byte, header string, secret string) (WebhookEvent, error) {
154+
func VerifyWebhookSignature(payload []byte, header string, secret string) (*WebhookEvent, error) {
156155
return VerifyWebhookSignatureWithTolerance(payload, header, secret, DefaultTolerance)
157156
}
158157

159-
func VerifyWebhookSignatureWithTolerance(payload []byte, header string, secret string, tolerance time.Duration) (WebhookEvent, error) {
158+
func VerifyWebhookSignatureWithTolerance(payload []byte, header string, secret string, tolerance time.Duration) (*WebhookEvent, error) {
160159
return verifyWebhookSignature(payload, header, secret, tolerance, true)
161160
}
162161

163-
func VerifyWebhookSignatureIgnoringTolerance(payload []byte, header string, secret string) (WebhookEvent, error) {
162+
func VerifyWebhookSignatureIgnoringTolerance(payload []byte, header string, secret string) (*WebhookEvent, error) {
164163
return verifyWebhookSignature(payload, header, secret, 0*time.Second, false)
165164
}
166165

167-
func verifyWebhookSignature(payload []byte, sigHeader string, secret string, tolerance time.Duration, enforceTolerance bool) (WebhookEvent, error) {
166+
func verifyWebhookSignature(payload []byte, sigHeader string, secret string, tolerance time.Duration, enforceTolerance bool) (*WebhookEvent, error) {
168167
// First unmarshal into a generic map to inspect the type
169168
var genericEvent map[string]interface{}
170169
if err := json.Unmarshal(payload, &genericEvent); err != nil {
171-
return nil, fmt.Errorf("failed to parse webhook body json: %s", err)
172-
}
173-
174-
// Determine the specific event type
175-
eventType, ok := genericEvent["type"].(string)
176-
if !ok {
177-
return nil, errors.New("event type not found")
170+
return nil, fmt.Errorf("Failed to parse webhook body json: %s", err)
178171
}
179172

180173
var event WebhookEvent
181174

182-
// Unmarshal into the specific event type based on the eventType
183-
switch eventType {
184-
case string(WebhookEventTypeBlock):
185-
event = new(WebhookEventBlock)
186-
case string(WebhookEventTypeTransaction):
187-
event = new(WebhookEventTransaction)
188-
case string(WebhookEventTypeEpoch):
189-
event = new(WebhookEventEpoch)
190-
case string(WebhookEventTypeDelegation):
191-
event = new(WebhookEventDelegation)
192-
default:
193-
return nil, fmt.Errorf("unknown event type: %s", eventType)
194-
}
195-
196175
if err := json.Unmarshal(payload, &event); err != nil {
197-
return nil, fmt.Errorf("failed to parse specific webhook event json: %s", err)
176+
return nil, fmt.Errorf("Failed to parse specific webhook event json: %s", err)
198177
}
199178

200179
header, err := parseSignatureHeader(sigHeader)
201180
if err != nil {
202-
return event, err
181+
return &event, err
203182
}
204183

205184
expectedSignature := computeSignature(header.timestamp, payload, secret)
206185
expiredTimestamp := time.Since(header.timestamp) > tolerance
207186
if enforceTolerance && expiredTimestamp {
208-
return event, ErrTooOld
187+
return &event, ErrTooOld
209188
}
210189

211190
for _, sig := range header.signatures {
212191
if hmac.Equal(expectedSignature, sig) {
213-
return event, nil
192+
return &event, nil
214193
}
215194
}
216195

217-
return event, ErrNoValidSignature
196+
return &event, ErrNoValidSignature
218197
}

webhook_test.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,21 @@ func TestVerifyWebhookSignature(t *testing.T) {
1515
"t=1650013856,v1=abc,t=1650013856,v1=f4c3bb2a8b0c8e21fa7d5fdada2ee87c9c6f6b0b159cc22e483146917e195c3e",
1616
"59a1eb46-96f4-4f0b-8a03-b4d26e70593a")
1717

18-
_, ok := event.(*blockfrost.WebhookEventBlock)
19-
if !ok {
20-
jsonData, _ := json.Marshal(event)
21-
t.Fatalf("Invalid webhook type %s", jsonData)
18+
// Unmarshal the event data into an appropriate struct depending on its Type
19+
var blockEvent blockfrost.WebhookEventBlock
20+
switch event.Type {
21+
case "block":
22+
err := json.Unmarshal([]byte(validPayload), &blockEvent)
23+
24+
if err != nil {
25+
t.Fatalf("Error parsing webhook JSON: %v\n", err)
26+
return
27+
}
28+
default:
29+
t.Fatalf("Invalid webhook type: %s\n", event.Type)
2230
}
2331

24-
jsonData, err := json.Marshal(event)
32+
jsonData, err := json.Marshal(blockEvent)
2533
if err != nil {
2634
t.Fatalf("Error marshaling to JSON: %s", err)
2735
}

0 commit comments

Comments
 (0)