From 79f9135638afe11ed2076db0de721595958180a6 Mon Sep 17 00:00:00 2001 From: Aurora Gaffney Date: Mon, 15 Apr 2024 17:01:52 -0500 Subject: [PATCH] feat: chainsync API support This creates a /chainsync/sync API websocket endpoint that will provide block and rollback events from the specified point on the chain Fixes #26 --- cmd/cardano-node-api/main.go | 2 +- docs/docs.go | 50 +++++++++++- docs/swagger.json | 45 +++++++++++ docs/swagger.yaml | 29 +++++++ go.mod | 14 ++-- go.sum | 35 ++++++--- internal/api/api.go | 23 +++--- internal/api/chainsync.go | 132 ++++++++++++++++++++++++++++++++ internal/api/localstatequery.go | 12 +-- internal/api/localtxmonitor.go | 6 +- internal/node/chainsync.go | 111 +++++++++++++++------------ internal/node/node.go | 17 +++- internal/utxorpc/build.go | 2 +- 13 files changed, 383 insertions(+), 95 deletions(-) create mode 100644 internal/api/chainsync.go diff --git a/cmd/cardano-node-api/main.go b/cmd/cardano-node-api/main.go index e4a6cd5..5f4e468 100644 --- a/cmd/cardano-node-api/main.go +++ b/cmd/cardano-node-api/main.go @@ -62,7 +62,7 @@ func main() { }() // Test node connection - if oConn, err := node.GetConnection(); err != nil { + if oConn, err := node.GetConnection(nil); err != nil { logger.Fatalf("failed to connect to node: %s", err) } else { oConn.Close() diff --git a/docs/docs.go b/docs/docs.go index cc27f7c..a0c3c04 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1,5 +1,4 @@ -// Code generated by swaggo/swag. DO NOT EDIT. - +// Package docs Code generated by swaggo/swag. DO NOT EDIT package docs import "github.com/swaggo/swag" @@ -24,6 +23,51 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/chainsync/sync": { + "get": { + "tags": [ + "chainsync" + ], + "summary": "Start a chain-sync using a websocket for events", + "parameters": [ + { + "type": "boolean", + "description": "whether to start from the current tip", + "name": "tip", + "in": "query" + }, + { + "type": "integer", + "description": "slot to start sync at, should match hash", + "name": "slot", + "in": "query" + }, + { + "type": "string", + "description": "block hash to start sync at, should match slot", + "name": "hash", + "in": "query" + } + ], + "responses": { + "101": { + "description": "Switching Protocols" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.responseApiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.responseApiError" + } + } + } + } + }, "/localstatequery/current-era": { "get": { "produces": [ @@ -423,6 +467,8 @@ var SwaggerInfo = &swag.Spec{ Description: "Cardano Node API", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", } func init() { diff --git a/docs/swagger.json b/docs/swagger.json index 67129cd..459dd82 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -20,6 +20,51 @@ "host": "localhost", "basePath": "/api", "paths": { + "/chainsync/sync": { + "get": { + "tags": [ + "chainsync" + ], + "summary": "Start a chain-sync using a websocket for events", + "parameters": [ + { + "type": "boolean", + "description": "whether to start from the current tip", + "name": "tip", + "in": "query" + }, + { + "type": "integer", + "description": "slot to start sync at, should match hash", + "name": "slot", + "in": "query" + }, + { + "type": "string", + "description": "block hash to start sync at, should match slot", + "name": "hash", + "in": "query" + } + ], + "responses": { + "101": { + "description": "Switching Protocols" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.responseApiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.responseApiError" + } + } + } + } + }, "/localstatequery/current-era": { "get": { "produces": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b3d085f..57af58a 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -79,6 +79,35 @@ info: title: cardano-node-api version: "1.0" paths: + /chainsync/sync: + get: + parameters: + - description: whether to start from the current tip + in: query + name: tip + type: boolean + - description: slot to start sync at, should match hash + in: query + name: slot + type: integer + - description: block hash to start sync at, should match slot + in: query + name: hash + type: string + responses: + "101": + description: Switching Protocols + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.responseApiError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.responseApiError' + summary: Start a chain-sync using a websocket for events + tags: + - chainsync /localstatequery/current-era: get: produces: diff --git a/go.mod b/go.mod index 2c2094e..7cf5c85 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,11 @@ toolchain go1.21.6 require ( connectrpc.com/connect v1.16.0 github.com/blinklabs-io/gouroboros v0.78.0 + github.com/blinklabs-io/snek v0.17.4 github.com/blinklabs-io/tx-submit-api v0.16.2 github.com/gin-contrib/zap v1.1.1 github.com/gin-gonic/gin v1.9.1 + github.com/gorilla/websocket v1.5.1 github.com/kelseyhightower/envconfig v1.4.0 github.com/penglongli/gin-metrics v0.1.10 github.com/swaggo/files v1.0.1 @@ -32,15 +34,15 @@ require ( github.com/fxamacker/cbor/v2 v2.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.20.0 // indirect - github.com/go-openapi/spec v0.20.7 // indirect - github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-openapi/jsonpointer v0.20.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/spec v0.20.9 // indirect + github.com/go-openapi/swag v0.22.4 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.19.0 // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/jinzhu/copier v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -65,7 +67,7 @@ require ( golang.org/x/crypto v0.22.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.7.0 // indirect + golang.org/x/tools v0.14.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c3c6f0e..7396fde 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,8 @@ github.com/blinklabs-io/gouroboros v0.78.0 h1:m/qI5WhN4ZpF96p+dQteiqWP0w+jn1vpL2 github.com/blinklabs-io/gouroboros v0.78.0/go.mod h1:xlSqLRuMknQWY73AJCx2HRWo49BfyGnreYT1t3/riVo= github.com/blinklabs-io/ouroboros-mock v0.3.0 h1:6VRWyhAv0k7nQEgzFpuqhS/n8OM+OAaLN/sCT5K2Hbc= github.com/blinklabs-io/ouroboros-mock v0.3.0/go.mod h1:0dzTNEk/Kvqa7qYHDy7/Nn3OTt+EOosMknB37FRzI1k= +github.com/blinklabs-io/snek v0.17.4 h1:/BoWqfJTZXSQsRkVRS3tLFhlrvjme82Xu6jYkEBtMJ4= +github.com/blinklabs-io/snek v0.17.4/go.mod h1:EeH+8u94Jekws0WnDAKY4SeHD0ftp/t1UixLcu9qSts= github.com/blinklabs-io/tx-submit-api v0.16.2 h1:DQpsJxa8+RkrEKlqcFqpRQ1JrQoHURuQbpLb2DiBhww= github.com/blinklabs-io/tx-submit-api v0.16.2/go.mod h1:qAETl1MuvaAexBhFul1ztSJIT0ObH2orGLv8+v/zR1Y= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= @@ -105,16 +107,20 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= +github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= -github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI= -github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= +github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= @@ -157,8 +163,9 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -185,6 +192,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -213,8 +222,10 @@ github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgSh github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -275,6 +286,8 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= @@ -368,8 +381,8 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -521,8 +534,8 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/api/api.go b/internal/api/api.go index 876125e..f8d45d1 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -30,18 +30,18 @@ import ( ginSwagger "github.com/swaggo/gin-swagger" // gin-swagger middleware ) -// @title cardano-node-api -// @version 1.0 -// @description Cardano Node API -// @host localhost -// @Schemes http -// @BasePath /api -// @contact.name Blink Labs -// @contact.url https://blinklabs.io -// @contact.email support@blinklabs.io +// @title cardano-node-api +// @version 1.0 +// @description Cardano Node API +// @host localhost +// @Schemes http +// @BasePath /api +// @contact.name Blink Labs +// @contact.url https://blinklabs.io +// @contact.email support@blinklabs.io // -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html func Start(cfg *config.Config) error { // Disable gin debug and color output gin.SetMode(gin.ReleaseMode) @@ -74,6 +74,7 @@ func Start(cfg *config.Config) error { // Configure API routes apiGroup := router.Group("/api") + configureChainSyncRoutes(apiGroup) configureLocalStateQueryRoutes(apiGroup) configureLocalTxMonitorRoutes(apiGroup) configureLocalTxSubmissionRoutes(apiGroup) diff --git a/internal/api/chainsync.go b/internal/api/chainsync.go new file mode 100644 index 0000000..7153ac2 --- /dev/null +++ b/internal/api/chainsync.go @@ -0,0 +1,132 @@ +// Copyright 2024 Blink Labs Software +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "encoding/hex" + "net/http" + + "github.com/blinklabs-io/cardano-node-api/internal/node" + + ocommon "github.com/blinklabs-io/gouroboros/protocol/common" + "github.com/blinklabs-io/snek/event" + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +func configureChainSyncRoutes(apiGroup *gin.RouterGroup) { + group := apiGroup.Group("/chainsync") + group.GET("/sync", handleChainSyncSync) +} + +type requestChainSyncSync struct { + Slot uint64 `form:"slot"` + Hash string `form:"hash"` + Tip bool `form:"tip"` +} + +// handleChainSyncSync godoc +// +// @Summary Start a chain-sync using a websocket for events +// @Tags chainsync +// @Success 101 +// @Failure 400 {object} responseApiError +// @Failure 500 {object} responseApiError +// @Param tip query bool false "whether to start from the current tip" +// @Param slot query int false "slot to start sync at, should match hash" +// @Param hash query string false "block hash to start sync at, should match slot" +// @Router /chainsync/sync [get] +func handleChainSyncSync(c *gin.Context) { + // Get parameters + var req requestChainSyncSync + if err := c.ShouldBindQuery(&req); err != nil { + c.JSON(http.StatusBadRequest, apiError(err.Error())) + return + } + if !req.Tip && (req.Slot == 0 || req.Hash == "") { + c.JSON(http.StatusBadRequest, apiError("you must provide the 'slot' and 'hash' parameters or set 'tip' to True")) + return + } + // Setup event channel + eventChan := make(chan event.Event, 10) + connCfg := node.ConnectionConfig{ + ChainSyncEventChan: eventChan, + } + // Connect to node + oConn, err := node.GetConnection(&connCfg) + if err != nil { + c.JSON(500, apiError(err.Error())) + return + } + // Async error handler + go func() { + err, ok := <-oConn.ErrorChan() + if !ok { + return + } + c.JSON(500, apiError(err.Error())) + }() + defer func() { + // Close Ouroboros connection + oConn.Close() + }() + var intersectPoints []ocommon.Point + if req.Tip { + tip, err := oConn.ChainSync().Client.GetCurrentTip() + if err != nil { + c.JSON(500, apiError(err.Error())) + return + } + intersectPoints = []ocommon.Point{ + tip.Point, + } + } else { + hashBytes, err := hex.DecodeString(req.Hash) + if err != nil { + c.JSON(500, apiError(err.Error())) + return + } + intersectPoints = []ocommon.Point{ + ocommon.NewPoint(req.Slot, hashBytes), + } + } + // Start the sync with the node + if err := oConn.ChainSync().Client.Sync(intersectPoints); err != nil { + c.JSON(500, apiError(err.Error())) + return + } + // Upgrade the connection + webConn, err := upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + return + } + defer webConn.Close() + // Wait for events + for { + evt, ok := <-eventChan + if !ok { + return + } + if err := webConn.WriteJSON(evt); err != nil { + c.JSON(500, apiError(err.Error())) + return + } + } +} diff --git a/internal/api/localstatequery.go b/internal/api/localstatequery.go index 506f7f7..e5f2563 100644 --- a/internal/api/localstatequery.go +++ b/internal/api/localstatequery.go @@ -50,7 +50,7 @@ type responseLocalStateQueryCurrentEra struct { // @Router /localstatequery/current-era [get] func handleLocalStateQueryCurrentEra(c *gin.Context) { // Connect to node - oConn, err := node.GetConnection() + oConn, err := node.GetConnection(nil) if err != nil { c.JSON(500, apiError(err.Error())) return @@ -102,7 +102,7 @@ type responseLocalStateQuerySystemStart struct { // @Router /localstatequery/system-start [get] func handleLocalStateQuerySystemStart(c *gin.Context) { // Connect to node - oConn, err := node.GetConnection() + oConn, err := node.GetConnection(nil) if err != nil { c.JSON(500, apiError(err.Error())) return @@ -156,7 +156,7 @@ type responseLocalStateQueryTip struct { // @Router /localstatequery/tip [get] func handleLocalStateQueryTip(c *gin.Context) { // Connect to node - oConn, err := node.GetConnection() + oConn, err := node.GetConnection(nil) if err != nil { c.JSON(500, apiError(err.Error())) return @@ -232,7 +232,7 @@ type responseLocalStateQueryEraHistory struct { // @Router /localstatequery/era-history [get] func handleLocalStateQueryEraHistory(c *gin.Context) { // Connect to node - oConn, err := node.GetConnection() + oConn, err := node.GetConnection(nil) if err != nil { c.JSON(500, apiError(err.Error())) return @@ -281,7 +281,7 @@ type responseLocalStateQueryProtocolParams struct { // @Router /localstatequery/protocol-params [get] func handleLocalStateQueryProtocolParams(c *gin.Context) { // Connect to node - oConn, err := node.GetConnection() + oConn, err := node.GetConnection(nil) if err != nil { c.JSON(500, apiError(err.Error())) return @@ -332,7 +332,7 @@ type responseLocalStateQueryGenesisConfig struct { //nolint:unused func handleLocalStateQueryGenesisConfig(c *gin.Context) { // Connect to node - oConn, err := node.GetConnection() + oConn, err := node.GetConnection(nil) if err != nil { c.JSON(500, apiError(err.Error())) return diff --git a/internal/api/localtxmonitor.go b/internal/api/localtxmonitor.go index e261460..aad3306 100644 --- a/internal/api/localtxmonitor.go +++ b/internal/api/localtxmonitor.go @@ -48,7 +48,7 @@ type responseLocalTxMonitorSizes struct { // @Router /localtxmonitor/sizes [get] func handleLocalTxMonitorSizes(c *gin.Context) { // Connect to node - oConn, err := node.GetConnection() + oConn, err := node.GetConnection(nil) if err != nil { c.JSON(500, apiError(err.Error())) return @@ -107,7 +107,7 @@ func handleLocalTxMonitorHasTx(c *gin.Context) { return } // Connect to node - oConn, err := node.GetConnection() + oConn, err := node.GetConnection(nil) if err != nil { c.JSON(500, apiError(err.Error())) return @@ -160,7 +160,7 @@ type responseLocalTxMonitorTxs struct { // @Router /localtxmonitor/txs [get] func handleLocalTxMonitorTxs(c *gin.Context) { // Connect to node - oConn, err := node.GetConnection() + oConn, err := node.GetConnection(nil) if err != nil { c.JSON(500, apiError(err.Error())) return diff --git a/internal/node/chainsync.go b/internal/node/chainsync.go index aa3d4e7..36826e9 100644 --- a/internal/node/chainsync.go +++ b/internal/node/chainsync.go @@ -15,88 +15,99 @@ package node import ( - "encoding/hex" "fmt" - "log" "time" + "github.com/blinklabs-io/cardano-node-api/internal/config" + "github.com/blinklabs-io/gouroboros/ledger" "github.com/blinklabs-io/gouroboros/protocol/chainsync" "github.com/blinklabs-io/gouroboros/protocol/common" - - "github.com/blinklabs-io/cardano-node-api/internal/config" + "github.com/blinklabs-io/snek/event" + input_chainsync "github.com/blinklabs-io/snek/input/chainsync" ) -func buildChainSyncConfig() chainsync.Config { +func buildChainSyncConfig(connCfg ConnectionConfig) chainsync.Config { cfg := config.GetConfig() return chainsync.NewConfig( chainsync.WithBlockTimeout( time.Duration(cfg.Node.QueryTimeout)*time.Second, ), - chainsync.WithRollBackwardFunc(chainSyncRollBackwardHandler), - chainsync.WithRollForwardFunc(chainSyncRollForwardHandler), + // We wrap the handler funcs to include our ConnectionConfig + chainsync.WithRollBackwardFunc( + func(connCfg ConnectionConfig) chainsync.RollBackwardFunc { + return func(ctx chainsync.CallbackContext, point common.Point, tip chainsync.Tip) error { + return chainSyncRollBackwardHandler( + ctx, connCfg, point, tip, + ) + } + }(connCfg), + ), + chainsync.WithRollForwardFunc( + func(connCfg ConnectionConfig) chainsync.RollForwardFunc { + return func(ctx chainsync.CallbackContext, blockType uint, blockData any, tip chainsync.Tip) error { + return chainSyncRollForwardHandler( + ctx, connCfg, blockType, blockData, tip, + ) + } + }(connCfg), + ), ) } func chainSyncRollBackwardHandler( ctx chainsync.CallbackContext, + connCfg ConnectionConfig, point common.Point, tip chainsync.Tip, ) error { - log.Printf("roll backward: point = %#v, tip = %#v\n", point, tip) + if connCfg.ChainSyncEventChan != nil { + evt := event.New( + "chainsync.rollback", + time.Now(), + nil, + input_chainsync.NewRollbackEvent(point), + ) + connCfg.ChainSyncEventChan <- evt + } return nil } func chainSyncRollForwardHandler( ctx chainsync.CallbackContext, + connCfg ConnectionConfig, blockType uint, blockData interface{}, tip chainsync.Tip, ) error { - var block ledger.Block - switch v := blockData.(type) { - case ledger.Block: - block = v - case ledger.BlockHeader: - blockSlot := v.SlotNumber() - blockHash, _ := hex.DecodeString(v.Hash()) - oConn, err := GetConnection() - if err != nil { - return err - } - block, err = oConn.BlockFetch().Client.GetBlock(common.NewPoint(blockSlot, blockHash)) - if err != nil { - return err + if connCfg.ChainSyncEventChan != nil { + var block ledger.Block + switch v := blockData.(type) { + case ledger.Block: + block = v + /* + case ledger.BlockHeader: + blockSlot := v.SlotNumber() + blockHash, _ := hex.DecodeString(v.Hash()) + oConn, err := GetConnection() + if err != nil { + return err + } + block, err = oConn.BlockFetch().Client.GetBlock(common.NewPoint(blockSlot, blockHash)) + if err != nil { + return err + } + */ + default: + return fmt.Errorf("unknown block data") } - default: - return fmt.Errorf("unknown block data") - } - // Display block info - switch blockType { - case ledger.BlockTypeByronEbb: - byronEbbBlock := block.(*ledger.ByronEpochBoundaryBlock) - log.Printf( - "era = Byron (EBB), epoch = %d, slot = %d, id = %s\n", - byronEbbBlock.Header.ConsensusData.Epoch, - byronEbbBlock.SlotNumber(), - byronEbbBlock.Hash(), - ) - case ledger.BlockTypeByronMain: - byronBlock := block.(*ledger.ByronMainBlock) - log.Printf( - "era = Byron, epoch = %d, slot = %d, id = %s\n", - byronBlock.Header.ConsensusData.SlotId.Epoch, - byronBlock.SlotNumber(), - byronBlock.Hash(), - ) - default: - log.Printf( - "era = %s, slot = %d, block_no = %d, id = %s\n", - block.Era().Name, - block.SlotNumber(), - block.BlockNumber(), - block.Hash(), + evt := event.New( + "chainsync.block", + time.Now(), + nil, + input_chainsync.NewBlockEvent(block, false), ) + connCfg.ChainSyncEventChan <- evt } return nil } diff --git a/internal/node/node.go b/internal/node/node.go index b0010eb..27e6b62 100644 --- a/internal/node/node.go +++ b/internal/node/node.go @@ -18,19 +18,28 @@ import ( "fmt" "os" - "github.com/blinklabs-io/gouroboros" - "github.com/blinklabs-io/cardano-node-api/internal/config" + + ouroboros "github.com/blinklabs-io/gouroboros" + "github.com/blinklabs-io/snek/event" ) -func GetConnection() (*ouroboros.Connection, error) { +type ConnectionConfig struct { + ChainSyncEventChan chan event.Event +} + +func GetConnection(connCfg *ConnectionConfig) (*ouroboros.Connection, error) { + // Make sure we always have a ConnectionConfig object + if connCfg == nil { + connCfg = &ConnectionConfig{} + } cfg := config.GetConfig() // Connect to cardano-node oConn, err := ouroboros.NewConnection( ouroboros.WithNetworkMagic(uint32(cfg.Node.NetworkMagic)), ouroboros.WithNodeToNode(false), ouroboros.WithKeepAlive(true), - ouroboros.WithChainSyncConfig(buildChainSyncConfig()), + ouroboros.WithChainSyncConfig(buildChainSyncConfig(*connCfg)), ouroboros.WithLocalTxMonitorConfig(buildLocalTxMonitorConfig()), ouroboros.WithLocalStateQueryConfig(buildLocalStateQueryConfig()), ouroboros.WithLocalTxSubmissionConfig(buildLocalTxSubmissionConfig()), diff --git a/internal/utxorpc/build.go b/internal/utxorpc/build.go index 9105eb4..b1fd933 100644 --- a/internal/utxorpc/build.go +++ b/internal/utxorpc/build.go @@ -38,7 +38,7 @@ func (s *ledgerStateServiceServer) GetChainTip( log.Printf("Got a GetChainTip request") // Connect to node - oConn, err := node.GetConnection() + oConn, err := node.GetConnection(nil) if err != nil { return nil, err }