• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 11216766535

07 Oct 2024 01:37PM UTC coverage: 57.817% (-1.0%) from 58.817%
11216766535

Pull #9148

github

ProofOfKeags
lnwire: remove kickoff feerate from propose/commit
Pull Request #9148: DynComms [2/n]: lnwire: add authenticated wire messages for Dyn*

571 of 879 new or added lines in 16 files covered. (64.96%)

23253 existing lines in 251 files now uncovered.

99022 of 171268 relevant lines covered (57.82%)

38420.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/subrpcserver_config.go
1
package lnd
2

3
import (
4
        "fmt"
5
        "net"
6
        "reflect"
7

8
        "github.com/btcsuite/btcd/chaincfg"
9
        "github.com/btcsuite/btclog"
10
        "github.com/lightningnetwork/lnd/aliasmgr"
11
        "github.com/lightningnetwork/lnd/autopilot"
12
        "github.com/lightningnetwork/lnd/chainreg"
13
        "github.com/lightningnetwork/lnd/channeldb"
14
        "github.com/lightningnetwork/lnd/fn"
15
        "github.com/lightningnetwork/lnd/htlcswitch"
16
        "github.com/lightningnetwork/lnd/invoices"
17
        "github.com/lightningnetwork/lnd/lncfg"
18
        "github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
19
        "github.com/lightningnetwork/lnd/lnrpc/chainrpc"
20
        "github.com/lightningnetwork/lnd/lnrpc/devrpc"
21
        "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
22
        "github.com/lightningnetwork/lnd/lnrpc/neutrinorpc"
23
        "github.com/lightningnetwork/lnd/lnrpc/peersrpc"
24
        "github.com/lightningnetwork/lnd/lnrpc/routerrpc"
25
        "github.com/lightningnetwork/lnd/lnrpc/signrpc"
26
        "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
27
        "github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc"
28
        "github.com/lightningnetwork/lnd/lnrpc/wtclientrpc"
29
        "github.com/lightningnetwork/lnd/lnwire"
30
        "github.com/lightningnetwork/lnd/macaroons"
31
        "github.com/lightningnetwork/lnd/netann"
32
        "github.com/lightningnetwork/lnd/routing"
33
        "github.com/lightningnetwork/lnd/sweep"
34
        "github.com/lightningnetwork/lnd/watchtower"
35
        "github.com/lightningnetwork/lnd/watchtower/wtclient"
36
        "google.golang.org/protobuf/proto"
37
)
38

39
// subRPCServerConfigs is special sub-config in the main configuration that
40
// houses the configuration for the optional sub-servers. These sub-RPC servers
41
// are meant to house experimental new features that may eventually make it
42
// into the main RPC server that lnd exposes. Special methods are present on
43
// this struct to allow the main RPC server to create and manipulate the
44
// sub-RPC servers in a generalized manner.
45
type subRPCServerConfigs struct {
46
        // SignRPC is a sub-RPC server that exposes signing of arbitrary inputs
47
        // as a gRPC service.
48
        SignRPC *signrpc.Config `group:"signrpc" namespace:"signrpc"`
49

50
        // WalletKitRPC is a sub-RPC server that exposes functionality allowing
51
        // a client to send transactions through a wallet, publish them, and
52
        // also requests keys and addresses under control of the backing
53
        // wallet.
54
        WalletKitRPC *walletrpc.Config `group:"walletrpc" namespace:"walletrpc"`
55

56
        // AutopilotRPC is a sub-RPC server that exposes methods on the running
57
        // autopilot as a gRPC service.
58
        AutopilotRPC *autopilotrpc.Config `group:"autopilotrpc" namespace:"autopilotrpc"`
59

60
        // ChainRPC is a sub-RPC server that exposes functionality allowing a
61
        // client to be notified of certain on-chain events (new blocks,
62
        // confirmations, spends).
63
        ChainRPC *chainrpc.Config `group:"chainrpc" namespace:"chainrpc"`
64

65
        // InvoicesRPC is a sub-RPC server that exposes invoice related methods
66
        // as a gRPC service.
67
        InvoicesRPC *invoicesrpc.Config `group:"invoicesrpc" namespace:"invoicesrpc"`
68

69
        // PeersRPC is a sub-RPC server that exposes peer related methods
70
        // as a gRPC service.
71
        PeersRPC *peersrpc.Config `group:"peersrpc" namespace:"peersrpc"`
72

73
        // NeutrinoKitRPC is a sub-RPC server that exposes functionality allowing
74
        // a client to interact with a running neutrino node.
75
        NeutrinoKitRPC *neutrinorpc.Config `group:"neutrinorpc" namespace:"neutrinorpc"`
76

77
        // RouterRPC is a sub-RPC server the exposes functionality that allows
78
        // clients to send payments on the network, and perform Lightning
79
        // payment related queries such as requests for estimates of off-chain
80
        // fees.
81
        RouterRPC *routerrpc.Config `group:"routerrpc" namespace:"routerrpc"`
82

83
        // WatchtowerRPC is a sub-RPC server that exposes functionality allowing
84
        // clients to monitor and control their embedded watchtower.
85
        WatchtowerRPC *watchtowerrpc.Config `group:"watchtowerrpc" namespace:"watchtowerrpc"`
86

87
        // WatchtowerClientRPC is a sub-RPC server that exposes functionality
88
        // that allows clients to interact with the active watchtower client
89
        // instance within lnd in order to add, remove, list registered client
90
        // towers, etc.
91
        WatchtowerClientRPC *wtclientrpc.Config `group:"wtclientrpc" namespace:"wtclientrpc"`
92

93
        // DevRPC is a sub-RPC server that exposes functionality that allows
94
        // developers manipulate LND state that is normally not possible.
95
        // Should only be used for development purposes.
96
        DevRPC *devrpc.Config `group:"devrpc" namespace:"devrpc"`
97
}
98

99
// PopulateDependencies attempts to iterate through all the sub-server configs
100
// within this struct, and populate the items it requires based on the main
101
// configuration file, and the chain control.
102
//
103
// NOTE: This MUST be called before any callers are permitted to execute the
104
// FetchConfig method.
105
func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config,
106
        cc *chainreg.ChainControl,
107
        networkDir string, macService *macaroons.Service,
108
        atpl *autopilot.Manager,
109
        invoiceRegistry *invoices.InvoiceRegistry,
110
        htlcSwitch *htlcswitch.Switch,
111
        activeNetParams *chaincfg.Params,
112
        chanRouter *routing.ChannelRouter,
113
        routerBackend *routerrpc.RouterBackend,
114
        nodeSigner *netann.NodeSigner,
115
        graphDB *channeldb.ChannelGraph,
116
        chanStateDB *channeldb.ChannelStateDB,
117
        sweeper *sweep.UtxoSweeper,
118
        tower *watchtower.Standalone,
119
        towerClientMgr *wtclient.Manager,
120
        tcpResolver lncfg.TCPResolver,
121
        genInvoiceFeatures func() *lnwire.FeatureVector,
122
        genAmpInvoiceFeatures func() *lnwire.FeatureVector,
123
        getNodeAnnouncement func() lnwire.NodeAnnouncement,
124
        updateNodeAnnouncement func(features *lnwire.RawFeatureVector,
125
                modifiers ...netann.NodeAnnModifier) error,
126
        parseAddr func(addr string) (net.Addr, error),
127
        rpcLogger btclog.Logger, aliasMgr *aliasmgr.Manager,
128
        auxDataParser fn.Option[AuxDataParser],
UNCOV
129
        invoiceHtlcModifier *invoices.HtlcModificationInterceptor) error {
×
UNCOV
130

×
UNCOV
131
        // First, we'll use reflect to obtain a version of the config struct
×
UNCOV
132
        // that allows us to programmatically inspect its fields.
×
UNCOV
133
        selfVal := extractReflectValue(s)
×
UNCOV
134
        selfType := selfVal.Type()
×
UNCOV
135

×
UNCOV
136
        numFields := selfVal.NumField()
×
UNCOV
137
        for i := 0; i < numFields; i++ {
×
UNCOV
138
                field := selfVal.Field(i)
×
UNCOV
139
                fieldElem := field.Elem()
×
UNCOV
140
                fieldName := selfType.Field(i).Name
×
UNCOV
141

×
UNCOV
142
                ltndLog.Debugf("Populating dependencies for sub RPC "+
×
UNCOV
143
                        "server: %v", fieldName)
×
UNCOV
144

×
UNCOV
145
                // If this sub-config doesn't actually have any fields, then we
×
UNCOV
146
                // can skip it, as the build tag for it is likely off.
×
UNCOV
147
                if fieldElem.NumField() == 0 {
×
148
                        continue
×
149
                }
UNCOV
150
                if !fieldElem.CanSet() {
×
151
                        continue
×
152
                }
153

UNCOV
154
                switch subCfg := field.Interface().(type) {
×
UNCOV
155
                case *signrpc.Config:
×
UNCOV
156
                        subCfgValue := extractReflectValue(subCfg)
×
UNCOV
157

×
UNCOV
158
                        subCfgValue.FieldByName("MacService").Set(
×
UNCOV
159
                                reflect.ValueOf(macService),
×
UNCOV
160
                        )
×
UNCOV
161
                        subCfgValue.FieldByName("NetworkDir").Set(
×
UNCOV
162
                                reflect.ValueOf(networkDir),
×
UNCOV
163
                        )
×
UNCOV
164
                        subCfgValue.FieldByName("Signer").Set(
×
UNCOV
165
                                reflect.ValueOf(cc.Signer),
×
UNCOV
166
                        )
×
UNCOV
167
                        subCfgValue.FieldByName("KeyRing").Set(
×
UNCOV
168
                                reflect.ValueOf(cc.KeyRing),
×
UNCOV
169
                        )
×
170

UNCOV
171
                case *walletrpc.Config:
×
UNCOV
172
                        subCfgValue := extractReflectValue(subCfg)
×
UNCOV
173

×
UNCOV
174
                        subCfgValue.FieldByName("NetworkDir").Set(
×
UNCOV
175
                                reflect.ValueOf(networkDir),
×
UNCOV
176
                        )
×
UNCOV
177
                        subCfgValue.FieldByName("MacService").Set(
×
UNCOV
178
                                reflect.ValueOf(macService),
×
UNCOV
179
                        )
×
UNCOV
180
                        subCfgValue.FieldByName("FeeEstimator").Set(
×
UNCOV
181
                                reflect.ValueOf(cc.FeeEstimator),
×
UNCOV
182
                        )
×
UNCOV
183
                        subCfgValue.FieldByName("Wallet").Set(
×
UNCOV
184
                                reflect.ValueOf(cc.Wallet),
×
UNCOV
185
                        )
×
UNCOV
186
                        subCfgValue.FieldByName("CoinSelectionLocker").Set(
×
UNCOV
187
                                reflect.ValueOf(cc.Wallet),
×
UNCOV
188
                        )
×
UNCOV
189
                        subCfgValue.FieldByName("KeyRing").Set(
×
UNCOV
190
                                reflect.ValueOf(cc.KeyRing),
×
UNCOV
191
                        )
×
UNCOV
192
                        subCfgValue.FieldByName("Sweeper").Set(
×
UNCOV
193
                                reflect.ValueOf(sweeper),
×
UNCOV
194
                        )
×
UNCOV
195
                        subCfgValue.FieldByName("Chain").Set(
×
UNCOV
196
                                reflect.ValueOf(cc.ChainIO),
×
UNCOV
197
                        )
×
UNCOV
198
                        subCfgValue.FieldByName("ChainParams").Set(
×
UNCOV
199
                                reflect.ValueOf(activeNetParams),
×
UNCOV
200
                        )
×
UNCOV
201
                        subCfgValue.FieldByName("CurrentNumAnchorChans").Set(
×
UNCOV
202
                                reflect.ValueOf(cc.Wallet.CurrentNumAnchorChans),
×
UNCOV
203
                        )
×
UNCOV
204
                        subCfgValue.FieldByName("CoinSelectionStrategy").Set(
×
UNCOV
205
                                reflect.ValueOf(
×
UNCOV
206
                                        cc.Wallet.Cfg.CoinSelectionStrategy,
×
UNCOV
207
                                ),
×
UNCOV
208
                        )
×
UNCOV
209
                        subCfgValue.FieldByName("ChanStateDB").Set(
×
UNCOV
210
                                reflect.ValueOf(chanStateDB),
×
UNCOV
211
                        )
×
212

UNCOV
213
                case *autopilotrpc.Config:
×
UNCOV
214
                        subCfgValue := extractReflectValue(subCfg)
×
UNCOV
215

×
UNCOV
216
                        subCfgValue.FieldByName("Manager").Set(
×
UNCOV
217
                                reflect.ValueOf(atpl),
×
UNCOV
218
                        )
×
219

UNCOV
220
                case *chainrpc.Config:
×
UNCOV
221
                        subCfgValue := extractReflectValue(subCfg)
×
UNCOV
222

×
UNCOV
223
                        subCfgValue.FieldByName("NetworkDir").Set(
×
UNCOV
224
                                reflect.ValueOf(networkDir),
×
UNCOV
225
                        )
×
UNCOV
226
                        subCfgValue.FieldByName("MacService").Set(
×
UNCOV
227
                                reflect.ValueOf(macService),
×
UNCOV
228
                        )
×
UNCOV
229
                        subCfgValue.FieldByName("ChainNotifier").Set(
×
UNCOV
230
                                reflect.ValueOf(cc.ChainNotifier),
×
UNCOV
231
                        )
×
UNCOV
232
                        subCfgValue.FieldByName("Chain").Set(
×
UNCOV
233
                                reflect.ValueOf(cc.ChainIO),
×
UNCOV
234
                        )
×
235

UNCOV
236
                case *invoicesrpc.Config:
×
UNCOV
237
                        subCfgValue := extractReflectValue(subCfg)
×
UNCOV
238

×
UNCOV
239
                        subCfgValue.FieldByName("NetworkDir").Set(
×
UNCOV
240
                                reflect.ValueOf(networkDir),
×
UNCOV
241
                        )
×
UNCOV
242
                        subCfgValue.FieldByName("MacService").Set(
×
UNCOV
243
                                reflect.ValueOf(macService),
×
UNCOV
244
                        )
×
UNCOV
245
                        subCfgValue.FieldByName("InvoiceRegistry").Set(
×
UNCOV
246
                                reflect.ValueOf(invoiceRegistry),
×
UNCOV
247
                        )
×
UNCOV
248
                        subCfgValue.FieldByName("HtlcModifier").Set(
×
UNCOV
249
                                reflect.ValueOf(invoiceHtlcModifier),
×
UNCOV
250
                        )
×
UNCOV
251
                        subCfgValue.FieldByName("IsChannelActive").Set(
×
UNCOV
252
                                reflect.ValueOf(htlcSwitch.HasActiveLink),
×
UNCOV
253
                        )
×
UNCOV
254
                        subCfgValue.FieldByName("ChainParams").Set(
×
UNCOV
255
                                reflect.ValueOf(activeNetParams),
×
UNCOV
256
                        )
×
UNCOV
257
                        subCfgValue.FieldByName("NodeSigner").Set(
×
UNCOV
258
                                reflect.ValueOf(nodeSigner),
×
UNCOV
259
                        )
×
UNCOV
260
                        defaultDelta := cfg.Bitcoin.TimeLockDelta
×
UNCOV
261
                        subCfgValue.FieldByName("DefaultCLTVExpiry").Set(
×
UNCOV
262
                                reflect.ValueOf(defaultDelta),
×
UNCOV
263
                        )
×
UNCOV
264
                        subCfgValue.FieldByName("GraphDB").Set(
×
UNCOV
265
                                reflect.ValueOf(graphDB),
×
UNCOV
266
                        )
×
UNCOV
267
                        subCfgValue.FieldByName("ChanStateDB").Set(
×
UNCOV
268
                                reflect.ValueOf(chanStateDB),
×
UNCOV
269
                        )
×
UNCOV
270
                        subCfgValue.FieldByName("GenInvoiceFeatures").Set(
×
UNCOV
271
                                reflect.ValueOf(genInvoiceFeatures),
×
UNCOV
272
                        )
×
UNCOV
273
                        subCfgValue.FieldByName("GenAmpInvoiceFeatures").Set(
×
UNCOV
274
                                reflect.ValueOf(genAmpInvoiceFeatures),
×
UNCOV
275
                        )
×
UNCOV
276
                        subCfgValue.FieldByName("GetAlias").Set(
×
UNCOV
277
                                reflect.ValueOf(aliasMgr.GetPeerAlias),
×
UNCOV
278
                        )
×
UNCOV
279

×
UNCOV
280
                        parseAuxData := func(m proto.Message) error {
×
UNCOV
281
                                return fn.MapOptionZ(
×
UNCOV
282
                                        auxDataParser,
×
UNCOV
283
                                        func(p AuxDataParser) error {
×
284
                                                return p.InlineParseCustomData(
×
285
                                                        m,
×
286
                                                )
×
287
                                        },
×
288
                                )
289
                        }
UNCOV
290
                        subCfgValue.FieldByName("ParseAuxData").Set(
×
UNCOV
291
                                reflect.ValueOf(parseAuxData),
×
UNCOV
292
                        )
×
293

UNCOV
294
                case *neutrinorpc.Config:
×
UNCOV
295
                        subCfgValue := extractReflectValue(subCfg)
×
UNCOV
296

×
UNCOV
297
                        subCfgValue.FieldByName("NeutrinoCS").Set(
×
UNCOV
298
                                reflect.ValueOf(cc.Cfg.NeutrinoCS),
×
UNCOV
299
                        )
×
300

301
                // RouterRPC isn't conditionally compiled and doesn't need to be
302
                // populated using reflection.
UNCOV
303
                case *routerrpc.Config:
×
UNCOV
304
                        subCfgValue := extractReflectValue(subCfg)
×
UNCOV
305

×
UNCOV
306
                        subCfgValue.FieldByName("AliasMgr").Set(
×
UNCOV
307
                                reflect.ValueOf(aliasMgr),
×
UNCOV
308
                        )
×
309

UNCOV
310
                case *watchtowerrpc.Config:
×
UNCOV
311
                        subCfgValue := extractReflectValue(subCfg)
×
UNCOV
312

×
UNCOV
313
                        subCfgValue.FieldByName("Active").Set(
×
UNCOV
314
                                reflect.ValueOf(tower != nil),
×
UNCOV
315
                        )
×
UNCOV
316
                        subCfgValue.FieldByName("Tower").Set(
×
UNCOV
317
                                reflect.ValueOf(tower),
×
UNCOV
318
                        )
×
319

UNCOV
320
                case *wtclientrpc.Config:
×
UNCOV
321
                        subCfgValue := extractReflectValue(subCfg)
×
UNCOV
322

×
UNCOV
323
                        if towerClientMgr != nil {
×
UNCOV
324
                                subCfgValue.FieldByName("Active").Set(
×
UNCOV
325
                                        reflect.ValueOf(towerClientMgr != nil),
×
UNCOV
326
                                )
×
UNCOV
327
                                subCfgValue.FieldByName("ClientMgr").Set(
×
UNCOV
328
                                        reflect.ValueOf(towerClientMgr),
×
UNCOV
329
                                )
×
UNCOV
330
                        }
×
UNCOV
331
                        subCfgValue.FieldByName("Resolver").Set(
×
UNCOV
332
                                reflect.ValueOf(tcpResolver),
×
UNCOV
333
                        )
×
UNCOV
334
                        subCfgValue.FieldByName("Log").Set(
×
UNCOV
335
                                reflect.ValueOf(rpcLogger),
×
UNCOV
336
                        )
×
337

UNCOV
338
                case *devrpc.Config:
×
UNCOV
339
                        subCfgValue := extractReflectValue(subCfg)
×
UNCOV
340

×
UNCOV
341
                        subCfgValue.FieldByName("ActiveNetParams").Set(
×
UNCOV
342
                                reflect.ValueOf(activeNetParams),
×
UNCOV
343
                        )
×
UNCOV
344

×
UNCOV
345
                        subCfgValue.FieldByName("GraphDB").Set(
×
UNCOV
346
                                reflect.ValueOf(graphDB),
×
UNCOV
347
                        )
×
UNCOV
348

×
NEW
349
                        subCfgValue.FieldByName("Switch").Set(
×
NEW
350
                                reflect.ValueOf(htlcSwitch),
×
NEW
351
                        )
×
352

UNCOV
353
                case *peersrpc.Config:
×
UNCOV
354
                        subCfgValue := extractReflectValue(subCfg)
×
UNCOV
355

×
UNCOV
356
                        subCfgValue.FieldByName("GetNodeAnnouncement").Set(
×
UNCOV
357
                                reflect.ValueOf(getNodeAnnouncement),
×
UNCOV
358
                        )
×
UNCOV
359

×
UNCOV
360
                        subCfgValue.FieldByName("ParseAddr").Set(
×
UNCOV
361
                                reflect.ValueOf(parseAddr),
×
UNCOV
362
                        )
×
UNCOV
363

×
UNCOV
364
                        subCfgValue.FieldByName("UpdateNodeAnnouncement").Set(
×
UNCOV
365
                                reflect.ValueOf(updateNodeAnnouncement),
×
UNCOV
366
                        )
×
367

368
                default:
×
369
                        return fmt.Errorf("unknown field: %v, %T", fieldName,
×
370
                                cfg)
×
371
                }
372
        }
373

374
        // Populate routerrpc dependencies.
UNCOV
375
        s.RouterRPC.NetworkDir = networkDir
×
UNCOV
376
        s.RouterRPC.MacService = macService
×
UNCOV
377
        s.RouterRPC.Router = chanRouter
×
UNCOV
378
        s.RouterRPC.RouterBackend = routerBackend
×
UNCOV
379

×
UNCOV
380
        return nil
×
381
}
382

383
// FetchConfig attempts to locate an existing configuration file mapped to the
384
// target sub-server. If we're unable to find a config file matching the
385
// subServerName name, then false will be returned for the second parameter.
386
//
387
// NOTE: Part of the lnrpc.SubServerConfigDispatcher interface.
UNCOV
388
func (s *subRPCServerConfigs) FetchConfig(subServerName string) (interface{}, bool) {
×
UNCOV
389
        // First, we'll use reflect to obtain a version of the config struct
×
UNCOV
390
        // that allows us to programmatically inspect its fields.
×
UNCOV
391
        selfVal := extractReflectValue(s)
×
UNCOV
392

×
UNCOV
393
        // Now that we have the value of the struct, we can check to see if it
×
UNCOV
394
        // has an attribute with the same name as the subServerName.
×
UNCOV
395
        configVal := selfVal.FieldByName(subServerName)
×
UNCOV
396

×
UNCOV
397
        // We'll now ensure that this field actually exists in this value. If
×
UNCOV
398
        // not, then we'll return false for the ok value to indicate to the
×
UNCOV
399
        // caller that this field doesn't actually exist.
×
UNCOV
400
        if !configVal.IsValid() {
×
401
                return nil, false
×
402
        }
×
403

UNCOV
404
        configValElem := configVal.Elem()
×
UNCOV
405

×
UNCOV
406
        // If a config of this type is found, it doesn't have any fields, then
×
UNCOV
407
        // it's the same as if it wasn't present. This can happen if the build
×
UNCOV
408
        // tag for the sub-server is inactive.
×
UNCOV
409
        if configValElem.NumField() == 0 {
×
410
                return nil, false
×
411
        }
×
412

413
        // At this pint, we know that the field is actually present in the
414
        // config struct, so we can return it directly.
UNCOV
415
        return configVal.Interface(), true
×
416
}
417

418
// extractReflectValue attempts to extract the value from an interface using
419
// the reflect package. The resulting reflect.Value allows the caller to
420
// programmatically examine and manipulate the underlying value.
UNCOV
421
func extractReflectValue(instance interface{}) reflect.Value {
×
UNCOV
422
        var val reflect.Value
×
UNCOV
423

×
UNCOV
424
        // If the type of the instance is a pointer, then we need to deference
×
UNCOV
425
        // the pointer one level to get its value. Otherwise, we can access the
×
UNCOV
426
        // value directly.
×
UNCOV
427
        if reflect.TypeOf(instance).Kind() == reflect.Ptr {
×
UNCOV
428
                val = reflect.ValueOf(instance).Elem()
×
UNCOV
429
        } else {
×
430
                val = reflect.ValueOf(instance)
×
431
        }
×
432

UNCOV
433
        return val
×
434
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc