diff --git a/.justfile b/.justfile index 8c74ce44..7df8c9ca 100644 --- a/.justfile +++ b/.justfile @@ -17,7 +17,7 @@ install-dependencies: # Start services with docker compose [working-directory: 'docker'] -docker-up $COMPOSE_PROFILES='mempool': +docker-up $COMPOSE_PROFILES='mempool-btc': #!/usr/bin/env bash set -euo pipefail IFS=',' read -ra profiles <<< "$COMPOSE_PROFILES" diff --git a/README.md b/README.md index e6688715..8152fd93 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,21 @@ npm install --workspaces 2. Start services with docker compose ```bash -cd server-backend/dev -docker compose up +cd docker +docker compose up +``` + +or + +```bash +just docker-up ``` 3. Initialize blockchain and lightning nodes ```bash -server-backend/dev/nodes-setup.sh +cd docker +nodes-setup.sh ``` 4. Build shared module @@ -60,7 +67,7 @@ npm run start:dev By sourcing [`server-backend/dev/dev-aliases.sh`](server-backend/dev/dev-aliases.sh) you can get access to some useful commands, e.g.: ```bash -source server-backend/dev/dev-aliases.sh +source docker/dev-aliases.sh # mine N blocks 40swap-bitcoin-cli -generate $N # pay lightning invoice from user node diff --git a/daemon/.golangci.yml b/daemon/.golangci.yml index d3594e82..426bafe1 100644 --- a/daemon/.golangci.yml +++ b/daemon/.golangci.yml @@ -1,64 +1,66 @@ -run: - # timeout for analysis - timeout: 10m - -issues: - # Skip autogenerated files for mobile and gRPC as well as copied code for - # internal use. - exclude-files: - - "\\.pb\\.go$" - exclude-rules: - # Exclude some linters from running on tests files. - - path: _test\.go - linters: - - gosec - # files with mock in the name are excluded from gosec - - path: .*mock.* - linters: - - gosec - +version: "2" linters: - disable-all: true + default: none enable: - # enabled by default - - errcheck - - gosimple - - govet - - ineffassign - - staticcheck - - unparam - bodyclose - - # style - dupl + - errcheck - errname - - goimports + - errorlint + - exhaustive + - gosec + - govet + - ineffassign - nlreturn + - noctx - revive + - staticcheck - tagliatelle + - unparam - whitespace - - # bugs - - errorlint - - gosec - - noctx - -linters-settings: - gofmt: - rewrite-rules: - - pattern: "interface{}" - replacement: "any" - - pattern: "a[b:len(a)]" - replacement: "a[b:]" - - tagliatelle: - case: + settings: + revive: rules: - json: camel - - - revive: + - name: duplicated-imports + severity: warning + disabled: false + tagliatelle: + case: + rules: + json: camel + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling rules: - - name: duplicated-imports - severity: warning - disabled: false \ No newline at end of file + - linters: + - gosec + path: _test\.go + - linters: + - gosec + path: .*mock.* + paths: + - \.pb\.go$ + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - goimports + settings: + gofmt: + rewrite-rules: + - pattern: interface{} + replacement: any + - pattern: a[b:len(a)] + replacement: a[b:] + exclusions: + generated: lax + paths: + - \.pb\.go$ + - third_party$ + - builtin$ + - examples$ diff --git a/daemon/README.md b/daemon/README.md index ac14cc54..0234e62f 100644 --- a/daemon/README.md +++ b/daemon/README.md @@ -3,4 +3,40 @@ The 40swap daemon (40swapd) interacts with the lightning node to manage the swap ## Docs -You can configure the 40swap daemon using environment variables or command line arguments. Check `40swapd -h` for more information. \ No newline at end of file +You can configure the 40swap daemon using environment variables or command line arguments. Check `40swapd -h` for more information. + +# Local dev environment + +## Pre-requisites +go 1.24.1 or later + +## Instructions + +1. Install all the dependencies from the root folder + +```bash +go mod tidy +``` + +2. Run the daemon + +```bash +just run-daemon +``` + +3. Create a swap in + +```bash +just user-lncli addinvoice 200000 # Copy the payreq +just run swap in --payreq + +just sendtoaddress # the amount in the response of the swap in request +just generate 3 +``` + +4. Create a swap out + +```bash +just run swap out --address bcrt1q94gs370gfjut9d75ss3j7m8l7m3phs06nlqd8n --amt 200000 +just generate 6 +``` \ No newline at end of file diff --git a/daemon/bitcoin/helpers.go b/daemon/bitcoin/helpers.go index 724af95f..8bcdfb14 100644 --- a/daemon/bitcoin/helpers.go +++ b/daemon/bitcoin/helpers.go @@ -401,7 +401,7 @@ func BuildPSBTFromOutpoint(spendingTxHex *wire.MsgTx, lockScript string, outpoin // Create the output script from the address outputScript, err := txscript.PayToAddrScript(destinationAddr) if err != nil { - return nil, fmt.Errorf("Failed to create output script: %w", err) + return nil, fmt.Errorf("failed to create output script: %w", err) } txOut := wire.NewTxOut(amount, outputScript) diff --git a/daemon/cmd/main.go b/daemon/cmd/main.go index 5070762d..39fb8f37 100644 --- a/daemon/cmd/main.go +++ b/daemon/cmd/main.go @@ -206,7 +206,7 @@ func main() { return err } - db, closeDb, err := database.New( + db, closeDb, err := database.New(ctx, c.String("db-user"), c.String("db-password"), c.String("db-name"), @@ -587,10 +587,6 @@ var bitcoin = cli.BoolFlag{ Name: "bitcoin", Usage: "Use Bitcoin chain", } -var liquid = cli.BoolFlag{ - Name: "liquid", - Usage: "Use Liquid chain", -} // Ports and hosts var grpcPort = cli.IntFlag{ diff --git a/daemon/daemon/daemon.go b/daemon/daemon/daemon.go index 1e61abc7..1b2ce92f 100644 --- a/daemon/daemon/daemon.go +++ b/daemon/daemon/daemon.go @@ -33,7 +33,7 @@ func Start(ctx context.Context, server *rpc.Server, db Repository, swaps swaps.C } go func() { - err := server.ListenAndServe() + err := server.ListenAndServe(ctx) if err != nil { log.Fatalf("couldn't start server: %v", err) } diff --git a/daemon/database/cli/main.go b/daemon/database/cli/main.go index 30d01a3c..6fe10dae 100644 --- a/daemon/database/cli/main.go +++ b/daemon/database/cli/main.go @@ -64,7 +64,7 @@ func main() { Name: "migrate", Usage: "Migrate the database", Action: func(ctx context.Context, cmd *cli.Command) error { - db, closeDb, err := StartDatabase(cmd) + db, closeDb, err := StartDatabase(ctx, cmd) if err != nil { return fmt.Errorf("❌ Could not connect to database: %w", err) } @@ -86,7 +86,7 @@ func main() { Name: "rollback", Usage: "Rollback the database", Action: func(ctx context.Context, cmd *cli.Command) error { - db, closeDb, err := StartDatabase(cmd) + db, closeDb, err := StartDatabase(ctx, cmd) if err != nil { return fmt.Errorf("❌ Could not connect to database: %w", err) } @@ -108,7 +108,7 @@ func main() { Name: "reset", Usage: "Reset the database", Action: func(ctx context.Context, cmd *cli.Command) error { - db, closeDb, err := StartDatabase(cmd) + db, closeDb, err := StartDatabase(ctx, cmd) if err != nil { return fmt.Errorf("❌ Could not connect to database: %w", err) } @@ -137,7 +137,7 @@ func main() { }, }, Action: func(ctx context.Context, cmd *cli.Command) error { - db, closeDb, err := StartDatabase(cmd) + db, closeDb, err := StartDatabase(ctx, cmd) if err != nil { return fmt.Errorf("❌ Could not connect to database: %w", err) } @@ -176,13 +176,13 @@ func main() { } } -func StartDatabase(cmd *cli.Command) (*database.Database, func() error, error) { +func StartDatabase(ctx context.Context, cmd *cli.Command) (*database.Database, func() error, error) { port, err := validatePort(cmd.Int("db-port")) if err != nil { return nil, nil, err } - db, closeDb, err := database.New( + db, closeDb, err := database.New(ctx, cmd.String("db-user"), cmd.String("db-password"), cmd.String("db-name"), diff --git a/daemon/database/database.go b/daemon/database/database.go index 8a38d156..fb4bcf4e 100644 --- a/daemon/database/database.go +++ b/daemon/database/database.go @@ -1,6 +1,7 @@ package database import ( + "context" "errors" "fmt" "os/exec" @@ -40,7 +41,7 @@ type Database struct { query *gen.Query } -func New(username, password, database string, port uint32, dataPath, host string, keepAlive bool) (*Database, func() error, error) { +func New(ctx context.Context, username, password, database string, port uint32, dataPath, host string, keepAlive bool) (*Database, func() error, error) { models.RegisterPreimageSerializer() db := Database{ @@ -66,18 +67,18 @@ func New(username, password, database string, port uint32, dataPath, host string close = func() error { if err := db.close(); err != nil { - return fmt.Errorf("Could not close database connection: %w", err) + return fmt.Errorf("could not close database connection: %w", err) } if !keepAlive { if err := postgres.Stop(); err != nil { - if errors.Is(err, embeddedpostgres.ErrServerNotStarted) && isPostgresRunning(port) { - killPostgres(port) + if errors.Is(err, embeddedpostgres.ErrServerNotStarted) && isPostgresRunning(ctx, port) { + killPostgres(ctx, port) return nil } - return fmt.Errorf("Could not stop embedded database: %w", err) + return fmt.Errorf("could not stop embedded database: %w", err) } log.Info("✅ DB stopped") } @@ -118,7 +119,7 @@ func (d *Database) GetConnectionURL() string { func (d *Database) getGorm() (*gorm.DB, error) { gormDB, err := gorm.Open(postgres.Open(d.GetConnectionURL()), &gorm.Config{}) if err != nil { - return nil, fmt.Errorf("Could not connect GORM: %w", err) + return nil, fmt.Errorf("could not connect GORM: %w", err) } log.Info("✅ DB connected") @@ -160,11 +161,11 @@ func (d *Database) Generate(path string) error { func (d *Database) close() error { db, err := d.orm.DB() if err != nil { - return fmt.Errorf("Could not get database connection: %w", err) + return fmt.Errorf("could not get database connection: %w", err) } if err := db.Close(); err != nil { - return fmt.Errorf("Could not close database connection: %w", err) + return fmt.Errorf("could not close database connection: %w", err) } return nil @@ -196,12 +197,12 @@ func newEmbeddedDatabase(username, password, database string, port uint32, dataP return postgres, nil } -func isPostgresRunning(port uint32) bool { +func isPostgresRunning(ctx context.Context, port uint32) bool { if port < 1 || port > 65535 { return false } //nolint:gosec - out, err := exec.Command("lsof", "-i", fmt.Sprintf(":%d", port), "-t").Output() + out, err := exec.CommandContext(ctx, "lsof", "-i", fmt.Sprintf(":%d", port), "-t").Output() if err != nil { return false } @@ -209,14 +210,14 @@ func isPostgresRunning(port uint32) bool { return len(out) > 0 } -func killPostgres(port uint32) { +func killPostgres(ctx context.Context, port uint32) { if port < 1 || port > 65535 { return } //nolint:gosec - out, err := exec.Command("lsof", "-i", fmt.Sprintf(":%d", port), "-t").Output() + out, err := exec.CommandContext(ctx, "lsof", "-i", fmt.Sprintf(":%d", port), "-t").Output() if err == nil { pid := strings.TrimSpace(string(out)) - _ = exec.Command("kill", "-9", pid).Run() + _ = exec.CommandContext(ctx, "kill", "-9", pid).Run() } } diff --git a/daemon/database/database_test.go b/daemon/database/database_test.go index 34d887b8..02026e15 100644 --- a/daemon/database/database_test.go +++ b/daemon/database/database_test.go @@ -49,7 +49,7 @@ func TestDatabaseOperations(t *testing.T) { os.RemoveAll(tempDir) }) - db, close, err := New("testuser", "testpass", "testdb", 5434, tempDir, "embedded", false) + db, close, err := New(t.Context(), "testuser", "testpass", "testdb", 5434, tempDir, "embedded", false) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, close()) diff --git a/daemon/lightning/lnd/lnd.go b/daemon/lightning/lnd/lnd.go index 14d75522..4e3e7393 100644 --- a/daemon/lightning/lnd/lnd.go +++ b/daemon/lightning/lnd/lnd.go @@ -144,7 +144,7 @@ func NewClient(ctx context.Context, opts ...Option) (*Client, error) { // Endpoint (host:port) options.lndEndpoint = lndConnectParams.Host + ":" + lndConnectParams.Port } else { - options.macaroonFilePath = strings.Replace(options.macaroonFilePath, "{Network}", string(options.network), -1) + options.macaroonFilePath = strings.ReplaceAll(options.macaroonFilePath, "{Network}", string(options.network)) // Read macaroon file from path @@ -293,6 +293,7 @@ func (c *Client) MonitorPaymentRequest(ctx context.Context, paymentHash string) } log.WithField("invoice", invoice).Debug("New TrackPaymentV2 event") + // nolint: exhaustive // lnrpc.Payment_UNKNOWN is deprecated, so we need to ignore either the exhaustive check or the deprecated check. switch invoice.Status { case lnrpc.Payment_SUCCEEDED: return invoice.PaymentPreimage, invoice.FeeSat, nil @@ -339,6 +340,8 @@ func (c *Client) MonitorPaymentReception(ctx context.Context, rhash []byte) (lig return hex.EncodeToString(invoice.RPreimage), nil case lnrpc.Invoice_CANCELED: return "", lightning.ErrInvoiceCanceled + case lnrpc.Invoice_OPEN, lnrpc.Invoice_ACCEPTED: + // Do nothing, we only want final status updates } } } diff --git a/daemon/rpc/server.go b/daemon/rpc/server.go index 152e636f..de27432f 100644 --- a/daemon/rpc/server.go +++ b/daemon/rpc/server.go @@ -1,6 +1,7 @@ package rpc import ( + "context" "fmt" "net" @@ -47,8 +48,9 @@ func NewRPCServer(port uint32, repository Repository, swapClient swaps.ClientInt return svr } -func (server *Server) ListenAndServe() error { - listener, err := net.Listen("tcp", fmt.Sprintf(":%d", server.Port)) +func (server *Server) ListenAndServe(ctx context.Context) error { + lc := net.ListenConfig{} + listener, err := lc.Listen(ctx, "tcp", fmt.Sprintf(":%d", server.Port)) if err != nil { return fmt.Errorf("failed to listen to port: %w", err) } diff --git a/daemon/rpc/server_test.go b/daemon/rpc/server_test.go index 700a7094..d022e676 100644 --- a/daemon/rpc/server_test.go +++ b/daemon/rpc/server_test.go @@ -15,7 +15,7 @@ func TestListenAndServe(test *testing.T) { server := NewRPCServer(50051, nil, nil, nil, nil, 1000, Network_REGTEST) errChan := make(chan error) go func() { - errChan <- server.ListenAndServe() + errChan <- server.ListenAndServe(test.Context()) }() select { case err := <-errChan: