diff --git a/cmd/bolt-agent/main.go b/cmd/bolt-agent/main.go index 6ea53ed..04b90fb 100644 --- a/cmd/bolt-agent/main.go +++ b/cmd/bolt-agent/main.go @@ -410,6 +410,14 @@ func shouldCrash(status int, body string) { } func nodeDataCallback(ctx context.Context, report *agent_entities.NodeDataReport) bool { + if report == nil { + return true + } + if report.NodeDetails != nil { + // a bit cumbersome but we don't want to put GitRevision into the checker + report.NodeDetails.AgentVersion = fmt.Sprintf("bolt-agent/%s", GitRevision) + } + rep, err := json.Marshal(report) if err != nil { glog.Warningf("Error marshalling report: %v", err) @@ -430,7 +438,7 @@ func nodeDataCallback(ctx context.Context, report *agent_entities.NodeDataReport return false } - req.Header.Set("User-Agent", fmt.Sprintf("boltobserver-agent/%s", GitRevision)) + req.Header.Set("User-Agent", fmt.Sprintf("bolt-agent/%s", GitRevision)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey)) req.Header.Set("Content-Type", "application/json") @@ -582,8 +590,10 @@ func runAgent(cmdCtx *cli.Context) error { g, gctx := errgroup.WithContext(ct) + var nodeDataChecker *nodedata.NodeData + if periodicSend { - nodeDataChecker := nodedata.NewDefaultNodeData(ct, cmdCtx.Duration("keepalive"), cmdCtx.Bool("smooth"), cmdCtx.Bool("checkgraph"), nodedata.NewNopNodeDataMonitoring("nodedata checker")) + nodeDataChecker = nodedata.NewDefaultNodeData(ct, cmdCtx.Duration("keepalive"), cmdCtx.Bool("smooth"), cmdCtx.Bool("checkgraph"), nodedata.NewNopNodeDataMonitoring("nodedata checker")) settings := agent_entities.ReportingSettings{PollInterval: interval, AllowedEntropy: cmdCtx.Int("allowedentropy"), AllowPrivateChannels: private, Filter: f} if settings.PollInterval == agent_entities.ManualRequest { @@ -619,7 +629,14 @@ func runAgent(cmdCtx *cli.Context) error { if cmdCtx.Bool("actions") { fn := mkGetLndAPI(cmdCtx) if !cmdCtx.Bool("noplugins") { - plugins.InitPlugins(fn, f, cmdCtx) + // Need this due to https://stackoverflow.com/questions/43059653/golang-interfacenil-is-nil-or-not + var invalidatable agent_entities.Invalidatable + if nodeDataChecker == nil { + invalidatable = nil + } else { + invalidatable = agent_entities.Invalidatable(nodeDataChecker) + } + plugins.InitPlugins(fn, f, cmdCtx, invalidatable) } g.Go(func() error { ac := &actions.Connector{ diff --git a/cmd/bolt-agent/main_test.go b/cmd/bolt-agent/main_test.go index 2babc55..708b36d 100644 --- a/cmd/bolt-agent/main_test.go +++ b/cmd/bolt-agent/main_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "github.com/bolt-observer/agent/entities" "github.com/stretchr/testify/assert" ) @@ -34,3 +35,17 @@ func TestConvertTimeSetting(t *testing.T) { assert.Equal(t, false, s.useLatestTimeFromServer) assert.Equal(t, now, s.time) } + +func TestNilInterface(t *testing.T) { + //var nodeDataChecker *nodedata.NodeData + //x := entities.Invalidatable(nodeDataChecker) + var y entities.Invalidatable + if y == nil { + t.Logf("NIL\n") + return + } + + t.Logf("NOT NIL %v\n", y) + + t.Fail() +} diff --git a/entities/common.go b/entities/common.go index ced0bbd..c74780e 100644 --- a/entities/common.go +++ b/entities/common.go @@ -62,3 +62,8 @@ func (r *ReentrancyBlock) Release(id string) { sem.Release(1) } + +// Invalidatable interface. +type Invalidatable interface { + Invalidate() error +} diff --git a/entities/nodedetails.go b/entities/nodedetails.go index 715c5dd..c24c45d 100644 --- a/entities/nodedetails.go +++ b/entities/nodedetails.go @@ -6,9 +6,11 @@ import ( // NodeDetails struct type NodeDetails struct { - NodeVersion string `json:"node_version"` - IsSyncedToChain bool `json:"is_synced_to_chain"` - IsSyncedToGraph bool `json:"is_synced_to_graph"` + NodeVersion string `json:"node_version"` + IsSyncedToChain bool `json:"is_synced_to_chain"` + IsSyncedToGraph bool `json:"is_synced_to_graph"` + AgentVersion string `json:"agent_version"` // will be filled before sending + TotalOnChainBalance uint64 `json:"total_onchain_balance"` api.NodeInfoAPIExtended } diff --git a/nodedata/nodedata.go b/nodedata/nodedata.go index fa95191..bf8062c 100644 --- a/nodedata/nodedata.go +++ b/nodedata/nodedata.go @@ -74,6 +74,20 @@ func NewNodeData(ctx context.Context, cache ChannelCache, keepAlive time.Duratio } } +// Invalidate when data was last reported +func (c *NodeData) Invalidate() error { + c.perNodeSettings.mutex.Lock() + defer c.perNodeSettings.mutex.Unlock() + + for _, one := range c.perNodeSettings.data { + one.lastCheck = time.Time{} + one.lastReport = time.Time{} + one.lastNodeReport = time.Time{} + } + + return nil +} + // IsSubscribed - check if we are subscribed for a certain public key func (c *NodeData) IsSubscribed(pubKey, uniqueID string) bool { return utils.Contains(c.perNodeSettings.GetKeys(), pubKey+uniqueID) @@ -540,6 +554,12 @@ func (c *NodeData) checkOne( return nil, 0, fmt.Errorf("pubkey and reported pubkey are not the same") } + funds, err := api.GetOnChainFunds(c.ctx) + if err != nil { + c.monitoring.MetricsReport("checkone", "failure", map[string]string{"pubkey": pubkey}) + return nil, 0, fmt.Errorf("failed to get info: %v", err) + } + identifier.Identifier = info.IdentityPubkey channelList, set, err := c.getChannelList(api, info, settings.AllowedEntropy, settings.AllowPrivateChannels, settings.Filter) @@ -578,9 +598,10 @@ func (c *NodeData) checkOne( } nodeInfoFull := &entities.NodeDetails{ - NodeVersion: info.Version, - IsSyncedToChain: info.IsSyncedToChain, - IsSyncedToGraph: info.IsSyncedToGraph, + NodeVersion: info.Version, + IsSyncedToChain: info.IsSyncedToChain, + IsSyncedToGraph: info.IsSyncedToGraph, + TotalOnChainBalance: uint64(funds.TotalBalance), } nodeInfoFull.NodeInfoAPIExtended = *nodeInfo diff --git a/nodedata/nodedata_test.go b/nodedata/nodedata_test.go index 7893e0d..2ab8021 100644 --- a/nodedata/nodedata_test.go +++ b/nodedata/nodedata_test.go @@ -40,6 +40,10 @@ func getNodeInfoJSON(pubKey string) string { `, pubKey) } +func getBalanceJSON() string { + return `{"total_balance":"89476363","confirmed_balance":"89476363","reserved_balance_anchor_chan":"50000","account_balance":{"default":{"confirmed_balance":89476363}}}` +} + func getChanInfo(url string) string { s := strings.ReplaceAll(url, "/v1/graph/edge/", "") id, err := strconv.Atoi(s) @@ -208,6 +212,8 @@ func TestBasicFlow(t *testing.T) { contents = getChanInfo(req.URL.Path) } else if strings.Contains(req.URL.Path, "v1/graph/node") { contents = getNodeInfoJSON("02b67e55fb850d7f7d77eb71038362bc0ed0abd5b7ee72cc4f90b16786c69b9256") + } else if strings.Contains(req.URL.Path, "v1/balance/blockchain") { + contents = getBalanceJSON() } r := io.NopCloser(bytes.NewReader([]byte(contents))) @@ -270,6 +276,8 @@ func TestContextCanBeNil(t *testing.T) { contents = getChanInfo(req.URL.Path) } else if strings.Contains(req.URL.Path, "v1/graph/node") { contents = getNodeInfoJSON("02b67e55fb850d7f7d77eb71038362bc0ed0abd5b7ee72cc4f90b16786c69b9256") + } else if strings.Contains(req.URL.Path, "v1/balance/blockchain") { + contents = getBalanceJSON() } r := io.NopCloser(bytes.NewReader([]byte(contents))) @@ -329,6 +337,8 @@ func TestGetState(t *testing.T) { contents = getChanInfo(req.URL.Path) } else if strings.Contains(req.URL.Path, "v1/graph/node") { contents = getNodeInfoJSON("02b67e55fb850d7f7d77eb71038362bc0ed0abd5b7ee72cc4f90b16786c69b9256") + } else if strings.Contains(req.URL.Path, "v1/balance/blockchain") { + contents = getBalanceJSON() } r := io.NopCloser(bytes.NewReader([]byte(contents))) @@ -379,6 +389,8 @@ func TestGetStateCallback(t *testing.T) { contents = getChanInfo(req.URL.Path) } else if strings.Contains(req.URL.Path, "v1/graph/node") { contents = getNodeInfoJSON("02b67e55fb850d7f7d77eb71038362bc0ed0abd5b7ee72cc4f90b16786c69b9256") + } else if strings.Contains(req.URL.Path, "v1/balance/blockchain") { + contents = getBalanceJSON() } r := io.NopCloser(bytes.NewReader([]byte(contents))) @@ -533,6 +545,8 @@ func TestPrivateChannelsExcluded(t *testing.T) { contents = getChannelJSON(1337, true, true) } else if strings.Contains(req.URL.Path, "v1/graph/node") { contents = getNodeInfoJSON("02b67e55fb850d7f7d77eb71038362bc0ed0abd5b7ee72cc4f90b16786c69b9256") + } else if strings.Contains(req.URL.Path, "v1/balance/blockchain") { + contents = getBalanceJSON() } r := io.NopCloser(bytes.NewReader([]byte(contents))) @@ -602,6 +616,8 @@ func TestInactiveFlow(t *testing.T) { contents = getNodeInfoJSON("02b67e55fb850d7f7d77eb71038362bc0ed0abd5b7ee72cc4f90b16786c69b9256") } else if strings.Contains(req.URL.Path, "v1/graph/edge") { contents = getChanInfo(req.URL.Path) + } else if strings.Contains(req.URL.Path, "v1/balance/blockchain") { + contents = getBalanceJSON() } r := io.NopCloser(bytes.NewReader([]byte(contents))) @@ -691,6 +707,8 @@ func TestChange(t *testing.T) { contents = getChanInfo(req.URL.Path) } else if strings.Contains(req.URL.Path, "v1/graph/node") { contents = getNodeInfoJSON("02b67e55fb850d7f7d77eb71038362bc0ed0abd5b7ee72cc4f90b16786c69b9256") + } else if strings.Contains(req.URL.Path, "v1/balance/blockchain") { + contents = getBalanceJSON() } r := io.NopCloser(bytes.NewReader([]byte(contents))) @@ -821,6 +839,8 @@ func TestKeepAliveIsSent(t *testing.T) { contents = getChanInfo(req.URL.Path) } else if strings.Contains(req.URL.Path, "v1/graph/node") { contents = getNodeInfoJSON("02b67e55fb850d7f7d77eb71038362bc0ed0abd5b7ee72cc4f90b16786c69b9256") + } else if strings.Contains(req.URL.Path, "v1/balance/blockchain") { + contents = getBalanceJSON() } r := io.NopCloser(bytes.NewReader([]byte(contents))) @@ -907,6 +927,8 @@ func TestKeepAliveIsNotSentWhenError(t *testing.T) { contents = getChanInfo(req.URL.Path) } else if strings.Contains(req.URL.Path, "v1/graph/node") { contents = getNodeInfoJSON("02b67e55fb850d7f7d77eb71038362bc0ed0abd5b7ee72cc4f90b16786c69b9256") + } else if strings.Contains(req.URL.Path, "v1/balance/blockchain") { + contents = getBalanceJSON() } r := io.NopCloser(bytes.NewReader([]byte(contents))) @@ -987,6 +1009,8 @@ func TestChangeIsCachedWhenCallbackFails(t *testing.T) { contents = getChanInfo(req.URL.Path) } else if strings.Contains(req.URL.Path, "v1/graph/node") { contents = getNodeInfoJSON("02b67e55fb850d7f7d77eb71038362bc0ed0abd5b7ee72cc4f90b16786c69b9256") + } else if strings.Contains(req.URL.Path, "v1/balance/blockchain") { + contents = getBalanceJSON() } r := io.NopCloser(bytes.NewReader([]byte(contents))) @@ -1077,6 +1101,8 @@ func TestGraphIsRequested(t *testing.T) { {"nodes":[{"last_update":1659296984,"pub_key":"020003b9499a97c8dfbbab6b196319db37ba9c37bccb60477f3c867175f417988e","alias":"BJCR_BTCPayServer","addresses":[{"network":"tcp","addr":"95.217.192.209:9735"}],"color":"#3399ff","features":{"0":{"name":"data-loss-protect","is_required":true,"is_known":true},"12":{"name":"static-remote-key","is_required":true,"is_known":true},"14":{"name":"payment-addr","is_required":true,"is_known":true},"17":{"name":"multi-path-payments","is_known":true},"2023":{"name":"script-enforced-lease","is_known":true},"23":{"name":"anchors-zero-fee-htlc-tx","is_known":true},"31":{"name":"amp","is_known":true},"45":{"name":"explicit-commitment-type","is_known":true},"5":{"name":"upfront-shutdown-script","is_known":true},"7":{"name":"gossip-queries","is_known":true},"9":{"name":"tlv-onion","is_known":true}}},{"last_update":1657199384,"pub_key":"0200072fd301cb4a680f26d87c28b705ccd6a1d5b00f1b5efd7fe5f998f1bbb1f1","alias":"OutaSpace 🚀","addresses":[{"network":"tcp","addr":"176.28.11.68:9760"},{"network":"tcp","addr":"nzslu33ecbokyn32teza2peiiiuye43ftom7jvnuhsxdbg3vhw7w3aqd.onion:9760"}],"color":"#123456","features":{"1":{"name":"data-loss-protect","is_known":true},"11":{"name":"unknown"},"13":{"name":"static-remote-key","is_known":true},"14":{"name":"payment-addr","is_required":true,"is_known":true},"17":{"name":"multi-path-payments","is_known":true},"27":{"name":"unknown"},"5":{"name":"upfront-shutdown-script","is_known":true},"55":{"name":"unknown"},"7":{"name":"gossip-queries","is_known":true},"8":{"name":"tlv-onion","is_required":true,"is_known":true}}},{"last_update":1618162974,"pub_key":"0200081eaa41b5661d3b512f5aae9d6abfb11ba1497a354e9217d9a18fbaa1e76b","alias":"0200081eaa41b5661d3b","addresses":[{"network":"tcp","addr":"lm63zodngkzqbol6lgadijh5p5xm6ltbekfxlbofvmnbkvi5cnzrzdid.onion:9735"}],"color":"#3399ff","features":{"0":{"name":"data-loss-protect","is_required":true,"is_known":true},"12":{"name":"static-remote-key","is_required":true,"is_known":true},"14":{"name":"payment-addr","is_required":true,"is_known":true},"17":{"name":"multi-path-payments","is_known":true},"5":{"name":"upfront-shutdown-script","is_known":true},"7":{"name":"gossip-queries","is_known":true},"9":{"name":"tlv-onion","is_known":true}}},{"last_update":1660845145,"pub_key":"020016201d389a44840f1f33be29288952f67c8ef6b3f98726fda180b4185ca6e2","alias":"AlasPoorYorick","addresses":[{"network":"tcp","addr":"7vuykfnmgkarlk4xjew4ea6lj7qwbbggbox4b72abupu7sn24geajzyd.onion:9735"}],"color":"#604bee","features":{"0":{"name":"data-loss-protect","is_required":true,"is_known":true},"12":{"name":"static-remote-key","is_required":true,"is_known":true},"14":{"name":"payment-addr","is_required":true,"is_known":true},"17":{"name":"multi-path-payments","is_known":true},"2023":{"name":"script-enforced-lease","is_known":true},"23":{"name":"anchors-zero-fee-htlc-tx","is_known":true},"31":{"name":"amp","is_known":true},"45":{"name":"explicit-commitment-type","is_known":true},"5":{"name":"upfront-shutdown-script","is_known":true},"7":{"name":"gossip-queries","is_known":true},"9":{"name":"tlv-onion","is_known":true}}},{"last_update":1660753871,"pub_key":"02001828ca7eb8e44d4d78b5c1ea609cd3744be823c22cd69d895eff2f9345892d","alias":"nodl-lnd-s010-042","addresses":[{"network":"tcp","addr":"185.150.160.210:4042"}],"color":"#000000","features":{"0":{"name":"data-loss-protect","is_required":true,"is_known":true},"12":{"name":"static-remote-key","is_required":true,"is_known":true},"14":{"name":"payment-addr","is_required":true,"is_known":true},"17":{"name":"multi-path-payments","is_known":true},"2023":{"name":"script-enforced-lease","is_known":true},"23":{"name":"anchors-zero-fee-htlc-tx","is_known":true},"31":{"name":"amp","is_known":true},"45":{"name":"explicit-commitment-type","is_known":true},"5":{"name":"upfront-shutdown-script","is_known":true},"7":{"name":"gossip-queries","is_known":true},"9":{"name":"tlv-onion","is_known":true}}}],"edges":[{"channel_id":"553951550347608065","capacity":"37200","chan_point":"ede04f9cfc1bb5373fd07d8af9c9b8b5a85cfe5e323b7796eb0a4d0dce5d5058:1","node1_pub":"03bd3466efd4a7306b539e2314e69efc6b1eaee29734fcedd78cf81b1dde9fedf8","node2_pub":"03c3d14714b78f03fd6ea4997c2b540a4139258249ea1d625c03b68bb82f85d0ea"},{"channel_id":"554317687705305088","capacity":"1000000","chan_point":"cfd0ae79fc150c2c3c4068ceca74bc26652bb2691624379aba9e28b197a78d6a:0","node1_pub":"02eccebd9ed98f6d267080a58194dbe554a2b33d976eb95bb7c116d00fd64c4a13","node2_pub":"02ee4469f2b686d5d02422917ac199602ce4c366a7bfaac1099e3ade377677064d"},{"channel_id":"554460624201252865","capacity":"1000000","chan_point":"c0a8d3428f562c232d86be399eb4497934e7e0390fa79e6860bcb65e7b0dd4fe:1","node1_pub":"02eccebd9ed98f6d267080a58194dbe554a2b33d976eb95bb7c116d00fd64c4a13","node2_pub":"02ee4469f2b686d5d02422917ac199602ce4c366a7bfaac1099e3ade377677064d"},{"channel_id":"554494709160148993","capacity":"200000","chan_point":"06bbac25ed610feb1d07316d1be8b8ba6850ee1dd96cc1d5439159bfe992be5a:1","node1_pub":"03bd3466efd4a7306b539e2314e69efc6b1eaee29734fcedd78cf81b1dde9fedf8","node2_pub":"03cbf298b068300be33f06c947b9d3f00a0f0e8089da3233f5db37e81d3a596fe1"},{"channel_id":"554495808645955584","capacity":"2000000","chan_point":"2392c45431c064269e4eaeccb0476ac32e56485d84e104064636aea896d1e439:0","node1_pub":"022e74ed3ddd3f590fd6492e60b20dcad7303f17e1ffd882fb33bb3f6c88f64398","node2_pub":"02ee4469f2b686d5d02422917ac199602ce4c366a7bfaac1099e3ade377677064d"}]} ` success = true + } else if strings.Contains(req.URL.Path, "v1/balance/blockchain") { + contents = getBalanceJSON() } r := io.NopCloser(bytes.NewReader([]byte(contents))) @@ -1143,6 +1169,8 @@ func TestBasicFlowRedis(t *testing.T) { contents = getChannelJSON(1337, false, true) } else if strings.Contains(req.URL.Path, "v1/graph/node") { contents = getNodeInfoJSON("02b67e55fb850d7f7d77eb71038362bc0ed0abd5b7ee72cc4f90b16786c69b9256") + } else if strings.Contains(req.URL.Path, "v1/balance/blockchain") { + contents = getBalanceJSON() } r := io.NopCloser(bytes.NewReader([]byte(contents))) @@ -1212,6 +1240,8 @@ func TestBaseFeePolicyChange(t *testing.T) { } else if step == 1 { contents = getChanInfoWithPolicyBaseFee(req.URL.Path, 1100) } + } else if strings.Contains(req.URL.Path, "v1/balance/blockchain") { + contents = getBalanceJSON() } r := io.NopCloser(bytes.NewReader([]byte(contents))) @@ -1289,6 +1319,8 @@ func TestBasicFlowFilterOne(t *testing.T) { contents = getChanInfo(req.URL.Path) } else if strings.Contains(req.URL.Path, "v1/graph/node") { contents = getNodeInfoJSON("02b67e55fb850d7f7d77eb71038362bc0ed0abd5b7ee72cc4f90b16786c69b9256") + } else if strings.Contains(req.URL.Path, "v1/balance/blockchain") { + contents = getBalanceJSON() } r := io.NopCloser(bytes.NewReader([]byte(contents))) @@ -1356,6 +1388,8 @@ func TestBasicFlowFilterTwo(t *testing.T) { contents = getChanInfo(req.URL.Path) } else if strings.Contains(req.URL.Path, "v1/graph/node") { contents = getNodeInfoJSON("02b67e55fb850d7f7d77eb71038362bc0ed0abd5b7ee72cc4f90b16786c69b9256") + } else if strings.Contains(req.URL.Path, "v1/balance/blockchain") { + contents = getBalanceJSON() } r := io.NopCloser(bytes.NewReader([]byte(contents))) diff --git a/plugins/boltz/connection_test.go b/plugins/boltz/connection_test.go index 0a19b8e..5555251 100644 --- a/plugins/boltz/connection_test.go +++ b/plugins/boltz/connection_test.go @@ -90,7 +90,7 @@ func TestEnsureConnected(t *testing.T) { f, err := filter.NewAllowAllFilter() require.NoError(t, err) - b, err := NewPlugin(getAPI(t, "fixture.secret", api.LndRest), f, getMockCliCtx("", "", "regtest")) + b, err := NewPlugin(getAPI(t, "fixture.secret", api.LndRest), f, getMockCliCtx("", "", "regtest"), nil) if b == nil || b.LnAPI == nil { if FailNoCredsBoltz { t.Fail() diff --git a/plugins/boltz/db_test.go b/plugins/boltz/db_test.go index 3d9c39b..3bcaf10 100644 --- a/plugins/boltz/db_test.go +++ b/plugins/boltz/db_test.go @@ -7,6 +7,7 @@ import ( "os" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -16,6 +17,11 @@ type Dummy struct { Name string } +type ExtendedJobData struct { + Burek string + JobData +} + func TestBoltzDB(t *testing.T) { os.Remove(dbpath) db := &BoltzDB{} @@ -46,4 +52,22 @@ func TestBoltzDB(t *testing.T) { err := db.Get("notfound", &d) require.EqualError(t, err, "No data found for this key") }) + t.Run("Extended entities", func(t *testing.T) { + jd := &JobData{ID: 1337} + err = db.Insert(1337, jd) + assert.NoError(t, err) + + var jd2 JobData + err = db.Get(1337, &jd2) + assert.NoError(t, err) + + assert.Equal(t, int32(1337), jd.ID) + + ejd := &ExtendedJobData{Burek: "mesni", JobData: *jd} + err = db.Insert(1337, ejd) + assert.NoError(t, err) + + //err = db.Get(1337, &jd2) + //assert.NoError(t, err) + }) } diff --git a/plugins/boltz/liquidity_test.go b/plugins/boltz/liquidity_test.go index 969df86..0bc6719 100644 --- a/plugins/boltz/liquidity_test.go +++ b/plugins/boltz/liquidity_test.go @@ -16,7 +16,7 @@ func TestGetNodeLiquidity(t *testing.T) { f, err := filter.NewAllowAllFilter() assert.NoError(t, err) - b, err := NewPlugin(getAPI(t, "fixture.secret", api.LndGrpc), f, getMockCliCtx("", "", "regtest")) + b, err := NewPlugin(getAPI(t, "fixture.secret", api.LndGrpc), f, getMockCliCtx("", "", "regtest"), nil) if b == nil || b.LnAPI == nil { if FailNoCredsBoltz { t.Fail() @@ -33,7 +33,7 @@ func TestGetByDescendingOutboundLiqudity(t *testing.T) { f, err := filter.NewAllowAllFilter() assert.NoError(t, err) - b, err := NewPlugin(getAPI(t, "fixture.secret", api.LndGrpc), f, getMockCliCtx("", "", "regtest")) + b, err := NewPlugin(getAPI(t, "fixture.secret", api.LndGrpc), f, getMockCliCtx("", "", "regtest"), nil) if b == nil || b.LnAPI == nil { if FailNoCredsBoltz { t.Fail() diff --git a/plugins/boltz/main.go b/plugins/boltz/main.go index a736f04..e295d1b 100644 --- a/plugins/boltz/main.go +++ b/plugins/boltz/main.go @@ -69,8 +69,8 @@ func init() { plugins.AllPluginFlags = append(plugins.AllPluginFlags, PluginFlags...) plugins.RegisteredPlugins = append(plugins.RegisteredPlugins, plugins.PluginData{ Name: Name, - Init: func(lnAPI agent_entities.NewAPICall, filter filter.FilteringInterface, cmdCtx *cli.Context) (agent_entities.Plugin, error) { - r, err := NewPlugin(lnAPI, filter, cmdCtx) + Init: func(lnAPI agent_entities.NewAPICall, filter filter.FilteringInterface, cmdCtx *cli.Context, nodeDataInvalidator agent_entities.Invalidatable) (agent_entities.Plugin, error) { + r, err := NewPlugin(lnAPI, filter, cmdCtx, nodeDataInvalidator) return agent_entities.Plugin(r), err }, }) @@ -78,20 +78,21 @@ func init() { // Plugin can save its data here type Plugin struct { - BoltzAPI *boltz.Boltz - ChainParams *chaincfg.Params - LnAPI agent_entities.NewAPICall - Filter filter.FilteringInterface - MaxFeePercentage float64 - CryptoAPI *CryptoAPI - SwapMachine *SwapMachine - Redeemer *Redeemer[FsmIn] - ReverseRedeemer *Redeemer[FsmIn] - Limits *SwapLimits - isDryRun bool - db DB - jobs map[int32]interface{} - mutex sync.Mutex + BoltzAPI *boltz.Boltz + ChainParams *chaincfg.Params + LnAPI agent_entities.NewAPICall + Filter filter.FilteringInterface + MaxFeePercentage float64 + CryptoAPI *CryptoAPI + SwapMachine *SwapMachine + Redeemer *Redeemer[FsmIn] + ReverseRedeemer *Redeemer[FsmIn] + Limits *SwapLimits + NodeDataInvalidator agent_entities.Invalidatable + isDryRun bool + db DB + jobs map[int32]interface{} + mutex sync.Mutex agent_entities.Plugin } @@ -112,7 +113,7 @@ type SwapLimits struct { } // NewPlugin creates new instance -func NewPlugin(lnAPI agent_entities.NewAPICall, filter filter.FilteringInterface, cmdCtx *cli.Context) (*Plugin, error) { +func NewPlugin(lnAPI agent_entities.NewAPICall, filter filter.FilteringInterface, cmdCtx *cli.Context, nodeDataInvalidator agent_entities.Invalidatable) (*Plugin, error) { if lnAPI == nil { return nil, ErrInvalidArguments } @@ -140,12 +141,13 @@ func NewPlugin(lnAPI agent_entities.NewAPICall, filter filter.FilteringInterface BoltzAPI: &boltz.Boltz{ URL: cmdCtx.String("boltzurl"), }, - CryptoAPI: NewCryptoAPI(entropy), - Filter: filter, - LnAPI: lnAPI, - db: db, - jobs: make(map[int32]interface{}), - isDryRun: cmdCtx.Bool("dryrun"), + CryptoAPI: NewCryptoAPI(entropy), + Filter: filter, + LnAPI: lnAPI, + NodeDataInvalidator: nodeDataInvalidator, + db: db, + jobs: make(map[int32]interface{}), + isDryRun: cmdCtx.Bool("dryrun"), } resp.MaxFeePercentage = cmdCtx.Float64("maxfeepercentage") resp.BoltzAPI.Init(Btc) // required @@ -159,7 +161,7 @@ func NewPlugin(lnAPI agent_entities.NewAPICall, filter filter.FilteringInterface resp.Limits = limits // Swap machine is the finite state machine for doing the swap - resp.SwapMachine = NewSwapMachine(resp) + resp.SwapMachine = NewSwapMachine(resp, nodeDataInvalidator) // Currently there is just one redeemer instance (perhaps split it) resp.Redeemer = NewRedeemer(context.Background(), (RedeemForward | RedeemReverse), resp.ChainParams, resp.BoltzAPI, resp.LnAPI, @@ -202,6 +204,7 @@ func (b *Plugin) Execute(jobID int32, data []byte, msgCallback agent_entities.Me if err != nil { return ErrCouldNotParseJobData } + jd.ID = jobID data, err := b.jobDataToSwapData(ctx, b.Limits, jd, msgCallback) if err != nil { @@ -234,8 +237,8 @@ func (b *Plugin) Execute(jobID int32, data []byte, msgCallback agent_entities.Me // start or continue running job func (b *Plugin) runJob(jobID int32, jd *SwapData, msgCallback agent_entities.MessageCallback) { in := FsmIn{ - SwapData: jd, - MsgCallback: msgCallback, + SwapData: jd, + MsgCallback: msgCallback, } // Running the job just means going through the state machine starting with jd.State @@ -360,16 +363,16 @@ func (b *Plugin) convertInboundLiqudityChanPercent(ctx context.Context, jobData IsFinished: true, }) } - return nil, fmt.Errorf("could not get liquidity") + return nil, fmt.Errorf("could not get liquidity %v", err) } ratio := float64(liquidity.Capacity) / float64(total) if ratio*100 > jobData.Amount || jobData.Amount < 0 || jobData.Amount > 100 { - glog.Infof("[Boltz] [%v] No need to do anything - current inbound liquidity %v %%", jobData.ID, ratio*100) + glog.Infof("[Boltz] [%v] No need to do anything - current inbound liquidity %v %% for channel %v", jobData.ID, ratio*100, jobData.ChannelId) if msgCallback != nil { msgCallback(agent_entities.PluginMessage{ JobID: int32(jobData.ID), - Message: fmt.Sprintf("No need to do anything - current inbound liquidity %v %%", ratio*100), + Message: fmt.Sprintf("No need to do anything - current inbound liquidity %v %% for channel %v", ratio*100, jobData.ChannelId), IsError: false, IsFinished: true, }) diff --git a/plugins/boltz/redeemer.go b/plugins/boltz/redeemer.go index 78fceae..40aab49 100644 --- a/plugins/boltz/redeemer.go +++ b/plugins/boltz/redeemer.go @@ -163,6 +163,8 @@ func (r *Redeemer[T]) redeem() bool { } delete(r.Entries, one) } + } else { + glog.Warningf("Redeeming failed due to %v\n", err) } return true diff --git a/plugins/boltz/swap_test.go b/plugins/boltz/swap_test.go index 137ea49..65d0514 100644 --- a/plugins/boltz/swap_test.go +++ b/plugins/boltz/swap_test.go @@ -197,7 +197,7 @@ func newPlugin(t *testing.T, ln agent_entities.NewAPICall, dbName string, boltzU f, err := filter.NewAllowAllFilter() assert.NoError(t, err) - p, err := NewPlugin(ln, f, getMockCliCtx(boltzUrl, dbName, network)) + p, err := NewPlugin(ln, f, getMockCliCtx(boltzUrl, dbName, network), nil) assert.NoError(t, err) _, err = p.BoltzAPI.GetNodes() assert.NoError(t, err) @@ -365,3 +365,37 @@ func TestInboundTestnet(t *testing.T) { t.Logf("timed out") t.Fail() } + +func TestPayInvoice(t *testing.T) { + return + + ctx := context.Background() + + lnA := getLocalLndByName(t, "A") + lnC := getLocalLndByName(t, "C") + + lnAPI1, err := lnA() + assert.NotNil(t, lnAPI1) + assert.NoError(t, err) + + lnAPI2, err := lnC() + assert.NotNil(t, lnAPI2) + assert.NoError(t, err) + + _, err = lnAPI1.GetInfo(ctx) + assert.NoError(t, err) + _, err = lnAPI2.GetInfo(ctx) + assert.NoError(t, err) + + invoice, err := lnAPI2.CreateInvoice(ctx, 3000000, "", "", 24*time.Hour) + assert.NoError(t, err) + + t.Logf("Invoice %v\n", invoice.PaymentRequest) + + // 128642860515328 + resp, err := lnAPI1.PayInvoice(ctx, invoice.PaymentRequest, 0, []uint64{1337, 128642860515328, 1338}) + assert.NoError(t, err) + t.Logf("Burek %v", resp) + + t.Fail() +} diff --git a/plugins/boltz/swapmachine.go b/plugins/boltz/swapmachine.go index 2b2b8c0..13af6db 100644 --- a/plugins/boltz/swapmachine.go +++ b/plugins/boltz/swapmachine.go @@ -76,6 +76,9 @@ func (s *SwapMachine) FsmSwapFailed(in FsmIn) FsmOut { IsFinished: true, }) } + if s.NodeDataInvalidator != nil { + s.NodeDataInvalidator.Invalidate() + } return FsmOut{} } @@ -92,6 +95,9 @@ func (s *SwapMachine) FsmSwapSuccess(in FsmIn) FsmOut { IsFinished: true, }) } + if s.NodeDataInvalidator != nil { + s.NodeDataInvalidator.Invalidate() + } return FsmOut{} } @@ -191,14 +197,16 @@ func (s *State) isFinal() bool { // Swapmachine is a finite state machine used for swaps. type SwapMachine struct { - Machine *Fsm[FsmIn, FsmOut, State] - BoltzPlugin *Plugin + Machine *Fsm[FsmIn, FsmOut, State] + BoltzPlugin *Plugin + NodeDataInvalidator entities.Invalidatable } -func NewSwapMachine(plugin *Plugin) *SwapMachine { +func NewSwapMachine(plugin *Plugin, nodeDataInvalidator entities.Invalidatable) *SwapMachine { s := &SwapMachine{Machine: &Fsm[FsmIn, FsmOut, State]{States: make(map[State]func(data FsmIn) FsmOut)}, // TODO: instead of BoltzPlugin this should be a bit more granular - BoltzPlugin: plugin, + BoltzPlugin: plugin, + NodeDataInvalidator: nodeDataInvalidator, } s.Machine.States[SwapFailed] = FsmWrap(s.FsmSwapFailed, plugin) diff --git a/plugins/boltz/swapmachine_reverse.go b/plugins/boltz/swapmachine_reverse.go index 0b8b12a..48ddf47 100644 --- a/plugins/boltz/swapmachine_reverse.go +++ b/plugins/boltz/swapmachine_reverse.go @@ -143,7 +143,7 @@ func (s *SwapMachine) FsmReverseSwapCreated(in FsmIn) FsmOut { } if !paid { - log(in, fmt.Sprintf("Paying invoice %v", in.SwapData.ReverseInvoice)) + log(in, fmt.Sprintf("Paying invoice %v %+v", in.SwapData.ReverseInvoice, in.SwapData.ChanIdsToUse)) _, err = lnConnection.PayInvoice(ctx, in.SwapData.ReverseInvoice, 0, in.SwapData.ChanIdsToUse) if err != nil { @@ -169,6 +169,8 @@ func (s *SwapMachine) FsmReverseSwapCreated(in FsmIn) FsmOut { } status := boltz.ParseEvent(s.Status) + log(in, fmt.Sprintf("Swap status is: %v", status)) + if (in.SwapData.AllowZeroConf && status == boltz.TransactionMempool) || status == boltz.TransactionConfirmed { return FsmOut{NextState: ClaimReverseFunds} } @@ -199,6 +201,9 @@ func (s *SwapMachine) FsmClaimReverseFunds(in FsmIn) FsmOut { return FsmOut{Error: fmt.Errorf("invalid state boltzID not set")} } + // debug + log(in, fmt.Sprintf("Adding entry %+v to redeem locked funds", in.SwapData)) + s.BoltzPlugin.ReverseRedeemer.AddEntry(in) return FsmOut{} } diff --git a/plugins/main.go b/plugins/main.go index df9ff0e..40bbad6 100644 --- a/plugins/main.go +++ b/plugins/main.go @@ -27,7 +27,7 @@ var ( ) // InitPluginFn signature of init plugin function -type InitPluginFn func(lnAPI agent_entities.NewAPICall, filter filter.FilteringInterface, cmdCtx *cli.Context) (agent_entities.Plugin, error) +type InitPluginFn func(lnAPI agent_entities.NewAPICall, filter filter.FilteringInterface, cmdCtx *cli.Context, nodeDataInvalidator agent_entities.Invalidatable) (agent_entities.Plugin, error) // PluginData structure type PluginData struct { @@ -35,11 +35,11 @@ type PluginData struct { Init InitPluginFn } -func InitPlugins(lnAPI agent_entities.NewAPICall, filter filter.FilteringInterface, cmdCtx *cli.Context) error { +func InitPlugins(lnAPI agent_entities.NewAPICall, filter filter.FilteringInterface, cmdCtx *cli.Context, nodeDataInvalidator agent_entities.Invalidatable) error { Plugins = make(map[string]agent_entities.Plugin) for _, p := range RegisteredPlugins { - plugin, err := p.Init(lnAPI, filter, cmdCtx) + plugin, err := p.Init(lnAPI, filter, cmdCtx, nodeDataInvalidator) if err != nil { glog.Warningf("Plugin %s had an error %v\n", p.Name, err) return err