initial commit
This commit is contained in:
commit
fa44705673
13 changed files with 1908 additions and 0 deletions
7
.env-example
Normal file
7
.env-example
Normal file
|
@ -0,0 +1,7 @@
|
|||
NEO4J_URI=bolt://localhost:7687
|
||||
NEO4J_USERNAME=
|
||||
NEO4J_PASSWORD=
|
||||
LND_ADDRESS=<HOST>:10009
|
||||
LND_NETWORK=mainnet
|
||||
LND_MACAROON_PATH=/path/to/readonly.macaroon
|
||||
LND_TLS_CERT_PATH=/path/to/tls.cert
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
.DS_Store
|
||||
.env
|
||||
.idea/
|
56
README.md
Normal file
56
README.md
Normal file
|
@ -0,0 +1,56 @@
|
|||
# ln-stream
|
||||
|
||||
> Stream graph updates from LND into Memgraph.
|
||||
|
||||
## Setup and Installation
|
||||
|
||||
This guide assumes that you already have [Go](https://go.dev/) and [Memgraph Lab](https://memgraph.com/download#individual) installed.
|
||||
To get started, follow these steps:
|
||||
|
||||
1. Clone this repository to your local machine:
|
||||
|
||||
```
|
||||
git clone https://github.com/.../ln-stream.git
|
||||
cd ln-stream
|
||||
```
|
||||
|
||||
2. Initialize the Go module and download dependencies:
|
||||
|
||||
```
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
3. Configure the environment variables:
|
||||
|
||||
- Copy the example file `.env-example` to `.env`:
|
||||
|
||||
```
|
||||
cp .env-example .env
|
||||
```
|
||||
|
||||
- Open the `.env` file and populate it with the relevant values:
|
||||
|
||||
```
|
||||
NEO4J_URI=bolt://localhost:7687
|
||||
NEO4J_USERNAME=
|
||||
NEO4J_PASSWORD=
|
||||
LND_ADDRESS=<HOST>:10009
|
||||
LND_NETWORK=mainnet
|
||||
LND_MACAROON_PATH=/path/to/readonly.macaroon
|
||||
LND_TLS_CERT_PATH=/path/to/tls.cert
|
||||
```
|
||||
|
||||
4. Start Memgraph using Docker Compose:
|
||||
|
||||
```
|
||||
docker compose up
|
||||
```
|
||||
|
||||
5. Start the Control Panel:
|
||||
|
||||
```
|
||||
go run main.go
|
||||
```
|
||||
|
||||
From here, you should have an instance of memgraph up receiving updates from LND.
|
||||
There is also a control panel running at http://localhost:8080 that lets you pause graph updates and reset the graph.
|
7
docker-compose.yml
Normal file
7
docker-compose.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
version: '3'
|
||||
services:
|
||||
memgraph-mage:
|
||||
image: memgraph/memgraph-mage
|
||||
ports:
|
||||
- '7687:7687'
|
||||
- '7444:7444'
|
159
go.mod
Normal file
159
go.mod
Normal file
|
@ -0,0 +1,159 @@
|
|||
module ln-stream
|
||||
|
||||
go 1.21.5
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/lightninglabs/lndclient v0.16.0-0
|
||||
github.com/neo4j/neo4j-go-driver/v4 v4.4.7
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
|
||||
github.com/aead/siphash v1.0.1 // indirect
|
||||
github.com/andybalholm/brotli v1.0.3 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/btcsuite/btcd v0.23.1 // indirect
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
|
||||
github.com/btcsuite/btcd/btcutil v1.1.1 // indirect
|
||||
github.com/btcsuite/btcd/btcutil/psbt v1.1.4 // indirect
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
||||
github.com/btcsuite/btcwallet v0.15.1 // indirect
|
||||
github.com/btcsuite/btcwallet/wallet/txauthor v1.2.3 // indirect
|
||||
github.com/btcsuite/btcwallet/wallet/txrules v1.2.0 // indirect
|
||||
github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0 // indirect
|
||||
github.com/btcsuite/btcwallet/walletdb v1.4.0 // indirect
|
||||
github.com/btcsuite/btcwallet/wtxmgr v1.5.0 // indirect
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
|
||||
github.com/btcsuite/winsvc v1.0.0 // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||
github.com/decred/dcrd/lru v1.0.0 // indirect
|
||||
github.com/dsnet/compress v0.0.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20210602112143-b1f3d6f4ef4e // indirect
|
||||
github.com/fergusstrange/embedded-postgres v1.10.0 // indirect
|
||||
github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-errors/errors v1.0.1 // 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.14.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0 // indirect
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.10.0 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.1.1 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||
github.com/jackc/pgtype v1.8.1 // indirect
|
||||
github.com/jackc/pgx/v4 v4.13.0 // indirect
|
||||
github.com/jessevdk/go-flags v1.4.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/jrick/logrotate v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/juju/loggo v0.0.0-20210728185423-eebad3a902c4 // indirect
|
||||
github.com/kkdai/bstream v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.13.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/klauspost/pgzip v1.2.5 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/lib/pq v1.10.3 // indirect
|
||||
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
|
||||
github.com/lightninglabs/neutrino v0.14.2 // indirect
|
||||
github.com/lightningnetwork/lightning-onion v1.0.2-0.20220211021909-bb84a1ccb0c5 // indirect
|
||||
github.com/lightningnetwork/lnd v0.15.0-beta.rc6.0.20220714125147-af97b8f877c2 // indirect
|
||||
github.com/lightningnetwork/lnd/clock v1.1.0 // indirect
|
||||
github.com/lightningnetwork/lnd/healthcheck v1.2.2 // indirect
|
||||
github.com/lightningnetwork/lnd/kvdb v1.3.1 // indirect
|
||||
github.com/lightningnetwork/lnd/queue v1.1.0 // indirect
|
||||
github.com/lightningnetwork/lnd/ticker v1.1.0 // indirect
|
||||
github.com/lightningnetwork/lnd/tlv v1.0.3 // indirect
|
||||
github.com/lightningnetwork/lnd/tor v1.0.1 // indirect
|
||||
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mholt/archiver/v3 v3.5.0 // indirect
|
||||
github.com/miekg/dns v1.1.43 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/nwaples/rardecode v1.1.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.8 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.11.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.26.0 // indirect
|
||||
github.com/prometheus/procfs v0.6.0 // indirect
|
||||
github.com/rogpeppe/fastuuid v1.2.0 // indirect
|
||||
github.com/sirupsen/logrus v1.7.0 // indirect
|
||||
github.com/soheilhy/cmux v0.1.5 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/testify v1.8.3 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/ulikunitz/xz v0.5.10 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/client/v2 v2.305.0 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/pkg/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/raft/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/server/v3 v3.5.0 // indirect
|
||||
go.opentelemetry.io/contrib v0.20.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/export/metric v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v0.20.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.7.0 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/zap v1.17.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.9.0 // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
golang.org/x/term v0.8.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
|
||||
google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced // indirect
|
||||
google.golang.org/grpc v1.38.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/errgo.v1 v1.0.1 // indirect
|
||||
gopkg.in/macaroon-bakery.v2 v2.0.1 // indirect
|
||||
gopkg.in/macaroon.v2 v2.1.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
sigs.k8s.io/yaml v1.2.0 // indirect
|
||||
)
|
13
index.html
Normal file
13
index.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Graph Control Panel</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<button id="resetButton">Reset Graph in Neo4j</button>
|
||||
<button id="toggleButton">Toggle Graph Updates</button>
|
||||
<span id="indicator"></span>
|
||||
<script src="/static/script.js"></script>
|
||||
</body>
|
||||
</html>
|
81
lnd/lnd.go
Normal file
81
lnd/lnd.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package lnd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/neo4j/neo4j-go-driver/v4/neo4j"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func convertChannelIDToString(channelID uint64) string {
|
||||
blockHeight := channelID >> 40
|
||||
blockIndex := (channelID >> 16) & ((1 << 24) - 1)
|
||||
outputIndex := channelID & ((1 << 16) - 1)
|
||||
return fmt.Sprintf("%d:%d:%d", blockHeight, blockIndex, outputIndex)
|
||||
}
|
||||
|
||||
func ConnectToLND() (*lndclient.GrpcLndServices, error) {
|
||||
config := lndclient.LndServicesConfig{
|
||||
LndAddress: os.Getenv("LND_ADDRESS"),
|
||||
Network: lndclient.Network(os.Getenv("LND_NETWORK")),
|
||||
CustomMacaroonPath: os.Getenv("LND_MACAROON_PATH"),
|
||||
TLSPath: os.Getenv("LND_TLS_CERT_PATH"),
|
||||
}
|
||||
return lndclient.NewLndServices(&config)
|
||||
}
|
||||
|
||||
func writeNodesToMemgraph(session neo4j.Session, nodes []lndclient.Node) {
|
||||
for _, node := range nodes {
|
||||
query := "MERGE (n:Node {pub_key: $pubKey, alias: $alias})"
|
||||
params := map[string]interface{}{
|
||||
"pubKey": node.PubKey.String(),
|
||||
"alias": node.Alias,
|
||||
}
|
||||
_, err := session.Run(query, params)
|
||||
if err != nil {
|
||||
log.Printf("Failed to execute node query: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeChannelsToMemgraph(session neo4j.Session, edges []lndclient.ChannelEdge) {
|
||||
for _, edge := range edges {
|
||||
chanID := convertChannelIDToString(edge.ChannelID) // Convert uint64 to string format
|
||||
if edge.Node1Policy != nil && !edge.Node1Policy.Disabled {
|
||||
writeChannelPolicyToMemgraph(session, &edge, edge.Node1Policy, edge.Node1.String(), edge.Node2.String(), chanID)
|
||||
}
|
||||
if edge.Node2Policy != nil && !edge.Node2Policy.Disabled {
|
||||
writeChannelPolicyToMemgraph(session, &edge, edge.Node2Policy, edge.Node2.String(), edge.Node1.String(), chanID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeChannelPolicyToMemgraph(session neo4j.Session, edge *lndclient.ChannelEdge, policy *lndclient.RoutingPolicy, node1PubKey, node2PubKey, chanID string) {
|
||||
if policy != nil {
|
||||
query := fmt.Sprintf(`
|
||||
MATCH (a:Node {pub_key: '%s'}), (b:Node {pub_key: '%s'})
|
||||
MERGE (a)-[r:CHANNEL {channel_id: '%s', capacity: %d}]->(b)
|
||||
SET r.fee_base_msat = %d, r.fee_rate_milli_msat = %d, r.time_lock_delta = %d, r.disabled = %v
|
||||
`, node1PubKey, node2PubKey, chanID, edge.Capacity,
|
||||
policy.FeeBaseMsat, policy.FeeRateMilliMsat, policy.TimeLockDelta, policy.Disabled)
|
||||
_, err := session.Run(query, nil)
|
||||
if err != nil {
|
||||
log.Printf("Failed to execute channel policy query: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WriteGraphToMemgraph(lndServices *lndclient.GrpcLndServices, neo4jDriver neo4j.Driver) {
|
||||
session := neo4jDriver.NewSession(neo4j.SessionConfig{})
|
||||
defer session.Close()
|
||||
|
||||
graph, err := lndServices.Client.DescribeGraph(context.Background(), false)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to retrieve graph: %v", err)
|
||||
}
|
||||
|
||||
writeNodesToMemgraph(session, graph.Nodes)
|
||||
writeChannelsToMemgraph(session, graph.Edges)
|
||||
}
|
56
main.go
Normal file
56
main.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/joho/godotenv"
|
||||
"ln-stream/lnd"
|
||||
"ln-stream/memgraph"
|
||||
"ln-stream/routes"
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
err = godotenv.Load(".env")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load .env file: %v", err)
|
||||
}
|
||||
|
||||
// Connect to Neo4j instance and drop the database
|
||||
routes.Driver, err = memgraph.ConnectNeo4j()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to Neo4j: %v", err)
|
||||
}
|
||||
defer memgraph.CloseDriver(routes.Driver)
|
||||
|
||||
// Connect to lnd
|
||||
routes.LndServices, err = lnd.ConnectToLND()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create new lnd services: %v", err)
|
||||
}
|
||||
defer routes.LndServices.Close()
|
||||
|
||||
router := gin.Default()
|
||||
// Handle the button click
|
||||
router.GET("/reset-graph", routes.ResetGraphHandler)
|
||||
|
||||
// Handle the button click to toggle the routine
|
||||
router.GET("/toggle-updates", routes.ToggleUpdatesHandler)
|
||||
|
||||
router.GET("/get-status", routes.GetStatusHandler)
|
||||
|
||||
// Serve static files (HTML, CSS, JS)
|
||||
router.StaticFile("/static/script.js", "./static/script.js")
|
||||
router.StaticFile("/static/style.css", "./static/style.css")
|
||||
router.StaticFile("/", "./index.html")
|
||||
|
||||
// Subscribe to graph topology updates
|
||||
//go routes.SubscribeToGraphUpdates()
|
||||
routes.IsRoutineRunning = false
|
||||
|
||||
// Start the server
|
||||
fmt.Println("Server started at http://localhost:8080")
|
||||
router.Run(":8080")
|
||||
|
||||
}
|
130
memgraph/memgraph.go
Normal file
130
memgraph/memgraph.go
Normal file
|
@ -0,0 +1,130 @@
|
|||
package memgraph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/neo4j/neo4j-go-driver/v4/neo4j"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// ConnectNeo4j creates a new Neo4j driver instance and establishes a connection
|
||||
func ConnectNeo4j() (neo4j.Driver, error) {
|
||||
uri := os.Getenv("NEO4J_URI")
|
||||
username := os.Getenv("NEO4J_USERNAME")
|
||||
password := os.Getenv("NEO4J_PASSWORD")
|
||||
|
||||
driver, err := neo4j.NewDriver(uri, neo4j.BasicAuth(username, password, ""))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Neo4j driver: %v", err)
|
||||
}
|
||||
|
||||
return driver, nil
|
||||
}
|
||||
|
||||
// CloseDriver closes the Neo4j driver connection
|
||||
func CloseDriver(driver neo4j.Driver) {
|
||||
driver.Close()
|
||||
}
|
||||
|
||||
// DropDatabase drops all nodes/relationships from the database.
|
||||
func DropDatabase(neo4jDriver neo4j.Driver) {
|
||||
session := neo4jDriver.NewSession(neo4j.SessionConfig{})
|
||||
defer session.Close()
|
||||
_, err := session.Run("MATCH (n) DETACH DELETE n", nil)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create index on pub_key property: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// CommitQuery commits a query to Neo4j with parameters
|
||||
func CommitQuery(driver neo4j.Driver, query string, params map[string]interface{}) (neo4j.Result, error) {
|
||||
session := driver.NewSession(neo4j.SessionConfig{})
|
||||
defer session.Close()
|
||||
result, err := session.Run(query, params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute query: %v", err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ProcessNodeUpdate converts node updates to Memgraph queries
|
||||
func ProcessNodeUpdate(nodeUpdate lndclient.NodeUpdate) (string, map[string]interface{}) {
|
||||
nodeQuery := "MERGE (n:Node {pub_key: $pubKey})\nSET n.alias = $alias"
|
||||
params := map[string]interface{}{
|
||||
"pubKey": nodeUpdate.IdentityKey.String(),
|
||||
"alias": nodeUpdate.Alias,
|
||||
}
|
||||
return nodeQuery, params
|
||||
}
|
||||
|
||||
// ProcessEdgeUpdate converts channel edge updates to Memgraph queries
|
||||
func ProcessEdgeUpdate(edgeUpdate lndclient.ChannelEdgeUpdate) (string, map[string]interface{}) {
|
||||
var (
|
||||
edgeQuery string
|
||||
params map[string]interface{}
|
||||
)
|
||||
if edgeUpdate.RoutingPolicy.Disabled {
|
||||
edgeQuery = "MATCH ()-[r:CHANNEL {channel_id: $channelID}]->()\nWHERE r.disabled = true\nDELETE r"
|
||||
params = map[string]interface{}{
|
||||
"channelID": edgeUpdate.ChannelID.String(),
|
||||
}
|
||||
} else {
|
||||
edgeQuery = "MERGE (n1:Node {pub_key: $advertisingNode})\nMERGE (n2:Node {pub_key: $connectingNode})\n" +
|
||||
"MERGE (n1)-[r:CHANNEL {channel_id: $channelID}]->(n2)\n" +
|
||||
"SET r.fee_base_msat = $fee_base_msat, r.fee_rate_milli_msat = $fee_rate_milli_msat, r.time_lock_delta = $time_lock_delta, r.disabled = $disabled"
|
||||
params = map[string]interface{}{
|
||||
"advertisingNode": edgeUpdate.AdvertisingNode.String(),
|
||||
"connectingNode": edgeUpdate.ConnectingNode.String(),
|
||||
"channelID": edgeUpdate.ChannelID.String(),
|
||||
"capacity": edgeUpdate.Capacity,
|
||||
"fee_base_msat": edgeUpdate.RoutingPolicy.FeeBaseMsat,
|
||||
"fee_rate_milli_msat": edgeUpdate.RoutingPolicy.FeeRateMilliMsat,
|
||||
"time_lock_delta": edgeUpdate.RoutingPolicy.TimeLockDelta,
|
||||
"disabled": edgeUpdate.RoutingPolicy.Disabled,
|
||||
}
|
||||
}
|
||||
return edgeQuery, params
|
||||
}
|
||||
|
||||
// ProcessCloseUpdate converts channel close updates to Memgraph queries
|
||||
func ProcessCloseUpdate(closeUpdate lndclient.ChannelCloseUpdate) (string, map[string]interface{}) {
|
||||
closeQuery := "MATCH ()-[r:CHANNEL {channel_id: $channelID}]->()\nWHERE r.disabled = true\nDELETE r"
|
||||
params := map[string]interface{}{
|
||||
"channelID": closeUpdate.ChannelID.String(),
|
||||
}
|
||||
return closeQuery, params
|
||||
}
|
||||
|
||||
// ProcessUpdates calls the corresponding update function for each type of update
|
||||
func ProcessUpdates(driver neo4j.Driver, update *lndclient.GraphTopologyUpdate) {
|
||||
// Process node updates
|
||||
for _, nodeUpdate := range update.NodeUpdates {
|
||||
nodeQuery, nodeParams := ProcessNodeUpdate(nodeUpdate)
|
||||
fmt.Println(nodeQuery)
|
||||
_, err := CommitQuery(driver, nodeQuery, nodeParams)
|
||||
if err != nil {
|
||||
log.Printf("Failed to commit node query: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Process channel edge updates
|
||||
for _, edgeUpdate := range update.ChannelEdgeUpdates {
|
||||
edgeQuery, edgeParams := ProcessEdgeUpdate(edgeUpdate)
|
||||
fmt.Println(edgeQuery)
|
||||
_, err := CommitQuery(driver, edgeQuery, edgeParams)
|
||||
if err != nil {
|
||||
log.Printf("Failed to commit edge query: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Process channel close updates
|
||||
for _, closeUpdate := range update.ChannelCloseUpdates {
|
||||
closeQuery, closeParams := ProcessCloseUpdate(closeUpdate)
|
||||
fmt.Println(closeQuery)
|
||||
_, err := CommitQuery(driver, closeQuery, closeParams)
|
||||
if err != nil {
|
||||
log.Printf("Failed to commit close query: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
92
routes/routes.go
Normal file
92
routes/routes.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/neo4j/neo4j-go-driver/v4/neo4j"
|
||||
"ln-stream/lnd"
|
||||
"ln-stream/memgraph"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
IsRoutineRunning = true
|
||||
LndServices *lndclient.GrpcLndServices
|
||||
Driver neo4j.Driver
|
||||
StopChannel chan bool
|
||||
GraphUpdates <-chan *lndclient.GraphTopologyUpdate
|
||||
Errors <-chan error
|
||||
)
|
||||
|
||||
func ToggleUpdatesHandler(c *gin.Context) {
|
||||
var err error
|
||||
// Subscribe to graph topology updates
|
||||
GraphUpdates, Errors, err = LndServices.Client.SubscribeGraph(context.Background())
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to subscribe to graph updates: %v", err)
|
||||
}
|
||||
|
||||
if !IsRoutineRunning {
|
||||
IsRoutineRunning = true
|
||||
StopChannel = make(chan bool)
|
||||
go SubscribeToGraphUpdates()
|
||||
c.JSON(http.StatusOK, gin.H{"isRoutineRunning": true,
|
||||
"message": "Routine started."})
|
||||
} else {
|
||||
IsRoutineRunning = false
|
||||
StopChannel <- true
|
||||
close(StopChannel)
|
||||
c.JSON(http.StatusOK, gin.H{"isRoutineRunning": false,
|
||||
"message": "Routine stopped."})
|
||||
}
|
||||
}
|
||||
|
||||
func ResetGraphHandler(c *gin.Context) {
|
||||
fmt.Println("Graph update initiated...")
|
||||
|
||||
if IsRoutineRunning {
|
||||
IsRoutineRunning = false
|
||||
StopChannel <- true
|
||||
close(StopChannel)
|
||||
}
|
||||
|
||||
memgraph.DropDatabase(Driver)
|
||||
lnd.WriteGraphToMemgraph(LndServices, Driver)
|
||||
|
||||
IsRoutineRunning = true
|
||||
StopChannel = make(chan bool)
|
||||
//go SubscribeToGraphUpdates()
|
||||
|
||||
c.String(http.StatusOK, "Graph update started.")
|
||||
}
|
||||
|
||||
func GetStatusHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"isRoutineRunning": IsRoutineRunning,
|
||||
"message": "Routine stopped."})
|
||||
}
|
||||
|
||||
func SubscribeToGraphUpdates() {
|
||||
var err error
|
||||
GraphUpdates, Errors, err = LndServices.Client.SubscribeGraph(context.Background())
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to subscribe to graph updates: %v", err)
|
||||
}
|
||||
IsRoutineRunning = true
|
||||
StopChannel = make(chan bool)
|
||||
fmt.Println("Routine started.")
|
||||
fmt.Println("Subscribed to graph topology updates. Waiting for updates...")
|
||||
for {
|
||||
select {
|
||||
case update := <-GraphUpdates:
|
||||
memgraph.ProcessUpdates(Driver, update)
|
||||
case err := <-Errors:
|
||||
log.Printf("Error receiving graph update: %v\n", err)
|
||||
case <-StopChannel:
|
||||
fmt.Println("Stopping graph update loop.")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
92
static/script.js
Normal file
92
static/script.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
var toggleStatus = true; // Initial value of the toggle status
|
||||
|
||||
// Initialize the indicator and toggleStatus values using an API request
|
||||
fetch('/get-status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
updateIndicator(data.isRoutineRunning);
|
||||
toggleStatus = data.isRoutineRunning;
|
||||
updateToggleButton();
|
||||
})
|
||||
.catch(error => {
|
||||
// Handle error if the request fails
|
||||
console.error("Failed to retrieve graph update status:", error);
|
||||
});
|
||||
|
||||
function fetchToggleUpdates() {
|
||||
// Fetch action based on the toggle status
|
||||
var action = toggleStatus ? 'enable' : 'disable';
|
||||
|
||||
fetch('/toggle-updates')
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
toggleStatus = !toggleStatus; // Reverse the toggle status
|
||||
updateToggleButton();
|
||||
if (toggleStatus) {
|
||||
alert("Graph updates enabled.");
|
||||
} else {
|
||||
alert("Graph updates disabled.");
|
||||
}
|
||||
} else {
|
||||
alert("Failed to toggle graph updates.");
|
||||
}
|
||||
return response.json(); // Parse the response as JSON
|
||||
})
|
||||
.then(data => {
|
||||
toggleStatus = data.isRoutineRunning;
|
||||
updateIndicator(data.isRoutineRunning); // Update the indicator based on the returned value
|
||||
});
|
||||
}
|
||||
|
||||
function updateToggleButton() {
|
||||
var toggleButton = document.getElementById("toggleButton");
|
||||
|
||||
if (toggleStatus) {
|
||||
toggleButton.textContent = "Disable Graph Updates";
|
||||
toggleButton.style.backgroundColor = "#007bff";
|
||||
} else {
|
||||
toggleButton.textContent = "Enable Graph Updates";
|
||||
toggleButton.style.backgroundColor = "#dc3545";
|
||||
}
|
||||
}
|
||||
|
||||
function updateIndicator(isRunning) {
|
||||
var indicator = document.getElementById("indicator");
|
||||
|
||||
if (isRunning) {
|
||||
indicator.classList.add("green");
|
||||
indicator.classList.remove("red");
|
||||
} else {
|
||||
indicator.classList.add("red");
|
||||
indicator.classList.remove("green");
|
||||
}
|
||||
}
|
||||
|
||||
function disableButtons() {
|
||||
document.getElementById("resetButton").disabled = true;
|
||||
document.getElementById("toggleButton").disabled = true;
|
||||
}
|
||||
|
||||
function enableButtons() {
|
||||
document.getElementById("resetButton").disabled = false;
|
||||
document.getElementById("toggleButton").disabled = false;
|
||||
}
|
||||
|
||||
document.getElementById("resetButton").onclick = function () {
|
||||
alert("Graph reset initiated.");
|
||||
disableButtons(); // Disable buttons when the request is initiated
|
||||
fetch('/reset-graph').then(response => {
|
||||
if (response.ok) {
|
||||
alert("Graph reset OK.");
|
||||
} else {
|
||||
alert("Failed to initiate graph reset.");
|
||||
}
|
||||
enableButtons(); // Enable buttons when the request is completed
|
||||
});
|
||||
};
|
||||
|
||||
document.getElementById("toggleButton").onclick = function () {
|
||||
fetchToggleUpdates();
|
||||
};
|
||||
|
||||
updateToggleButton();
|
54
static/style.css
Normal file
54
static/style.css
Normal file
|
@ -0,0 +1,54 @@
|
|||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
background-color: #007bff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
#indicator {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#indicator.green {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
#indicator.red {
|
||||
background-color: red;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue