From 91f5adc9d5c3c347c310fd446c3689ac5e8d78e1 Mon Sep 17 00:00:00 2001 From: Alexander Gerk Date: Wed, 8 Oct 2025 16:43:21 +0200 Subject: [PATCH 1/2] feature: Blockfrost Webhooks Dart - Add example for using of the blockfrost-dart SDK and the validation of the incoming webhooks --- .../webhooks/using-webhooks.mdx | 123 ++++++++++++++++++ .../webhooks/webhooks-signatures.mdx | 94 +++++++++++++ docusaurus.config.js | 3 + 3 files changed, 220 insertions(+) diff --git a/docs/start-building/webhooks/using-webhooks.mdx b/docs/start-building/webhooks/using-webhooks.mdx index eff0599..19a1d7c 100644 --- a/docs/start-building/webhooks/using-webhooks.mdx +++ b/docs/start-building/webhooks/using-webhooks.mdx @@ -315,6 +315,129 @@ func main() { ``` + + + + +```dart showLineNumbers +import 'dart:io'; +import 'dart:async'; +import 'dart:convert'; + +import 'package:blockfrost_api/blockfrost_api.dart'; +import 'package:blockfrost_secure_webhooks/webhook_handler.dart'; +import 'package:shelf/shelf.dart'; +import 'package:shelf/shelf_io.dart' as io; +import 'package:shelf_router/shelf_router.dart'; + +// You will find your webhook secret auth token in your webhook settings in the Blockfrost Dashboard +// Pass it as environment variable when starting the server: +// BLOCKFROST_TOKEN='WEBHOOK-AUTH-TOKEN' dart run bin/server.dart +const String secretAuthEnvToken = 'BLOCKFROST_TOKEN'; +const int port = 8080; + +// Main Method +void main() async { + final secretToken = Platform.environment[secretAuthEnvToken]; + if (secretToken == null || secretToken.isEmpty) { + print('FATAL: Missing environment variable $secretAuthEnvToken'); + exit(1); + } + + // path mapping with router configuration + final router = Router() + ..post( + '/webhook', + (Request request) => handleWebhook( + request: request, + secretToken: secretToken, + validator: + BlockfrostSignatureValidator())) // Map POST requests to /webhook + ..get('/status', (_) => Response.ok('ok')); // Simple test route + + // Configure middleware (optional, but good practice for logging) + final handler = + Pipeline().addMiddleware(logRequests()).addHandler(router.call); + + // Start the server + try { + final server = await io.serve(handler, InternetAddress.anyIPv4, port); + print('Server running on http://${server.address.host}:${server.port}'); + print('Ready to receive webhooks at: http://localhost:$port/webhook'); + } catch (e) { + print('Failed to start server: $e'); + } +} + +/// Request handler which handles the incoming webhook +Future handleWebhook( + {required Request request, + required String secretToken, + required SignatureValidator validator}) async { + final String requestPayload = await request.readAsString(); + final signatureHeader = request.headers['blockfrost-signature']; + + // Verify request data is available + if (signatureHeader == null || signatureHeader.isEmpty) { + return Response(400, body: 'Missing signature header.'); + } + if (requestPayload.isEmpty) { + return Response(400, body: 'Empty requestPayload.'); + } + + // Validate using the SignatureValidator + if (!validator.validate( + signatureHeader: signatureHeader, + requestPayload: requestPayload, + secretAuthToken: secretToken, + )) { + return Response(400, body: 'Signature validation failed!'); + } + + // Parsing requestPayload JSON + try { + Map data = + jsonDecode(requestPayload) as Map; + final String? type = data['type'] as String?; + final dynamic payload = data['payload']; + + if (type == null || payload == null) { + throw Exception("Error: Payload or type is missing."); + } + + // process event types (transaction, block, delegation, epoch) + switch (type) { + case "transaction": + final List transactions = payload as List; + print("Received ${transactions.length} transactions"); + for (final transaction in transactions) { + final Map txData = + transaction as Map; + final String? txHash = + (txData['tx'] as Map?)?['hash'] as String?; + print("Transaction $txHash"); + } + break; + case "block": + final Map blockData = payload as Map; + final String? blockHash = blockData['hash'] as String?; + print("Received block hash $blockHash"); + break; + // ...other types (delegation, epoch) + default: + throw Exception("Unexpected type $type"); + } + // Signature is valid + return Response.ok( + jsonEncode({'status': 'Webhook received successfully ✅'}), + headers: {'content-type': 'application/json'}, + ); + } catch (e) { + return Response.internalServerError(body: 'Failed to process JSON.'); + } +} +``` + ### Retries diff --git a/docs/start-building/webhooks/webhooks-signatures.mdx b/docs/start-building/webhooks/webhooks-signatures.mdx index c5b7742..017a991 100644 --- a/docs/start-building/webhooks/webhooks-signatures.mdx +++ b/docs/start-building/webhooks/webhooks-signatures.mdx @@ -142,6 +142,100 @@ func main() { + + +```dart showLineNumbers +import 'dart:io'; +import 'dart:async'; +import 'dart:convert'; + +import 'package:blockfrost_api/blockfrost_api.dart'; +import 'package:blockfrost_secure_webhooks/webhook_handler.dart'; +import 'package:shelf/shelf.dart'; +import 'package:shelf/shelf_io.dart' as io; +import 'package:shelf_router/shelf_router.dart'; + +// You will find your webhook secret auth token in your webhook settings in the Blockfrost Dashboard +// Pass it as environment variable when starting the server: +// BLOCKFROST_TOKEN='WEBHOOK-AUTH-TOKEN' dart run bin/server.dart +const String secretAuthEnvToken = 'BLOCKFROST_TOKEN'; +const int port = 8080; + +// Main Method +void main() async { + final secretToken = Platform.environment[secretAuthEnvToken]; + if (secretToken == null || secretToken.isEmpty) { + print('FATAL: Missing environment variable $secretAuthEnvToken'); + exit(1); // Exit if secret is not set + } + + // path mapping with router configuration + final router = Router() + ..post( + '/webhook', + (Request request) => handleWebhook( + request: request, + secretToken: secretToken, + validator: + BlockfrostSignatureValidator())) // Map POST requests to /webhook + ..get('/status', (_) => Response.ok('ok')); // Simple test route + + // Configure middleware (optional, but good practice for logging) + final handler = + Pipeline().addMiddleware(logRequests()).addHandler(router.call); + + // Start the server + try { + final server = await io.serve(handler, InternetAddress.anyIPv4, port); + print('Server running on http://${server.address.host}:${server.port}'); + print('Ready to receive webhooks at: http://localhost:$port/webhook'); + } catch (e) { + print('Failed to start server: $e'); + } +} + +/// Request handler which handles the incoming webhook +Future handleWebhook( + {required Request request, + required String secretToken, + required SignatureValidator validator}) async { + final String requestPayload = await request.readAsString(); + final signatureHeader = request.headers['blockfrost-signature']; + + // Verify request data is available + if (signatureHeader == null || signatureHeader.isEmpty) { + return Response(400, body: 'Missing signature header.'); + } + if (requestPayload.isEmpty) { + return Response(400, body: 'Empty requestPayload.'); + } + + // Validate using the SignatureValidator + if (!validator.validate( + signatureHeader: signatureHeader, + requestPayload: requestPayload, + secretAuthToken: secretToken, + )) { + return Response(400, body: 'Signature validation failed!'); + } + + // Try to parse payload JSON + try { + jsonDecode(requestPayload) as Map; + } catch (e) { + return Response.internalServerError(body: 'Failed to process JSON.'); + } + + // Signature is valid + return Response.ok( + jsonEncode({'status': 'Webhook received successfully ✅'}), + headers: {'content-type': 'application/json'}, + ); +} +``` + + + ## Verifying the signature manually diff --git a/docusaurus.config.js b/docusaurus.config.js index f4f990b..2ca822c 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -117,6 +117,9 @@ module.exports = { ], copyright: `Copyright © ${new Date().getFullYear()} Blockfrost.io`, }, + prism: { + additionalLanguages: ['dart'], + }, }, presets: [ [ From 6d66e6f3197886c3ac7971905300ff6f356b8aef Mon Sep 17 00:00:00 2001 From: Alexander Gerk Date: Wed, 22 Oct 2025 11:17:17 +0200 Subject: [PATCH 2/2] feature: Blockfrost Webhooks Dart - Add SignatureValidationException handling --- .../start-building/webhooks/using-webhooks.mdx | 18 ++++++++++++------ .../webhooks/webhooks-signatures.mdx | 18 ++++++++++++------ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/docs/start-building/webhooks/using-webhooks.mdx b/docs/start-building/webhooks/using-webhooks.mdx index 19a1d7c..898d599 100644 --- a/docs/start-building/webhooks/using-webhooks.mdx +++ b/docs/start-building/webhooks/using-webhooks.mdx @@ -386,12 +386,18 @@ Future handleWebhook( } // Validate using the SignatureValidator - if (!validator.validate( - signatureHeader: signatureHeader, - requestPayload: requestPayload, - secretAuthToken: secretToken, - )) { - return Response(400, body: 'Signature validation failed!'); + try { + validator.validate( + requestPayload: requestPayload, + signatureHeader: signatureHeader, + secretAuthToken: secretToken); + } on SignatureValidationException catch (e) { + return Response( + 400, + body: 'Signature validation failed! ${e.toString()}', + ); + } catch (e) { + return Response.internalServerError(body: 'Signature validation failed!'); } // Parsing requestPayload JSON diff --git a/docs/start-building/webhooks/webhooks-signatures.mdx b/docs/start-building/webhooks/webhooks-signatures.mdx index 017a991..21a6eb2 100644 --- a/docs/start-building/webhooks/webhooks-signatures.mdx +++ b/docs/start-building/webhooks/webhooks-signatures.mdx @@ -211,12 +211,18 @@ Future handleWebhook( } // Validate using the SignatureValidator - if (!validator.validate( - signatureHeader: signatureHeader, - requestPayload: requestPayload, - secretAuthToken: secretToken, - )) { - return Response(400, body: 'Signature validation failed!'); + try { + validator.validate( + requestPayload: requestPayload, + signatureHeader: signatureHeader, + secretAuthToken: secretToken); + } on SignatureValidationException catch (e) { + return Response( + 400, + body: 'Signature validation failed! ${e.toString()}', + ); + } catch (e) { + return Response.internalServerError(body: 'Signature validation failed!'); } // Try to parse payload JSON