diff --git a/docs/start-building/webhooks/using-webhooks.mdx b/docs/start-building/webhooks/using-webhooks.mdx index eff0599..898d599 100644 --- a/docs/start-building/webhooks/using-webhooks.mdx +++ b/docs/start-building/webhooks/using-webhooks.mdx @@ -315,6 +315,135 @@ 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 + 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 + 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..21a6eb2 100644 --- a/docs/start-building/webhooks/webhooks-signatures.mdx +++ b/docs/start-building/webhooks/webhooks-signatures.mdx @@ -142,6 +142,106 @@ 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 + 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 + 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: [ [