diff --git a/README.md b/README.md index 78a7b79f..fb213131 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ go run ./integrationtest ### 🏁 Prerequisites - A **PostgreSQL database**, bfgd expects the sql scripts in `./database/bfgd/scripts/` to be run to set up your schema. -- A **connection to an ElectrumX node** on the proper Bitcoin network (testnet or mainnet). +- A **connection to an Electrs node** on the proper Bitcoin network (testnet or mainnet). ## ▶️ Running bssd diff --git a/cmd/bfgd/bfgd.go b/cmd/bfgd/bfgd.go index 94b17c62..cadeac73 100644 --- a/cmd/bfgd/bfgd.go +++ b/cmd/bfgd/bfgd.go @@ -34,19 +34,19 @@ var ( "BFG_EXBTC_ADDRESS": config.Config{ Value: &cfg.EXBTCAddress, DefaultValue: "localhost:18001", - Help: "electrumx endpoint", + Help: "electrs endpoint", Print: config.PrintAll, }, "BFG_EXBTC_INITIAL_CONNECTIONS": config.Config{ Value: &cfg.EXBTCInitialConns, DefaultValue: 5, - Help: "electrumx initial connections", + Help: "electrs initial connections", Print: config.PrintAll, }, "BFG_EXBTC_MAX_CONNECTIONS": config.Config{ Value: &cfg.EXBTCMaxConns, DefaultValue: 100, - Help: "electrumx max connections", + Help: "electrs max connections", Print: config.PrintAll, }, "BFG_PUBLIC_KEY_AUTH": config.Config{ diff --git a/cmd/extool/extool.go b/cmd/extool/extool.go index 9480965b..e826f9ab 100644 --- a/cmd/extool/extool.go +++ b/cmd/extool/extool.go @@ -16,7 +16,7 @@ import ( btcchainhash "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/hemilabs/heminetwork/hemi/electrumx" + "github.com/hemilabs/heminetwork/hemi/electrs" "github.com/hemilabs/heminetwork/version" ) @@ -37,12 +37,12 @@ func main() { log.Fatal("No address specified") } - c, err := electrumx.NewClient(address, &electrumx.ClientOptions{ + c, err := electrs.NewClient(address, &electrs.ClientOptions{ InitialConnections: 1, MaxConnections: 1, }) if err != nil { - log.Fatalf("Failed to create electrumx client: %v", err) + log.Fatalf("Failed to create electrs client: %v", err) } ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second) diff --git a/e2e/cookie b/e2e/cookie new file mode 100644 index 00000000..89a29521 --- /dev/null +++ b/e2e/cookie @@ -0,0 +1 @@ +user:password \ No newline at end of file diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index 1197770b..8f5a6322 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -33,7 +33,7 @@ services: - "-rpcport=18443" - "-rpcconnect=bitcoind" - "generatetoaddress" - - "3000" # need to generate a lot for greater chance to not spend coinbase + - "1000" # need to generate a lot for greater chance to not spend coinbase - "$BTC_ADDRESS" restart: on-failure @@ -55,19 +55,28 @@ services: - "1" - "$BTC_ADDRESS" - electrumx: - image: "lukechilds/electrumx@sha256:2949784536f8f85af229004e12e5b5c3a1d7428918a492f77b4e958035c2ae2a" + electrs: + build: + context: https://github.com/romanz/electrs.git#1d02f10ec38edbc3b7df6b16bb8989d9bc0aaa0f depends_on: - "bitcoind" - ports: - - "50001:8000" - environment: - DAEMON_URL: "http://user:password@bitcoind:18443" - COIN: "Bitcoin" - COST_HARD_LIMIT: "0" - COST_SOFT_LIMIT: "0" - MAX_SEND: "8388608" - NET: "regtest" + command: + - electrs + - --electrum-rpc-addr + - '0.0.0.0:50001' + - --daemon-rpc-addr + - "bitcoind:18443" + - --daemon-p2p-addr + - "bitcoind:18444" + - --network + - regtest + - --cookie-file + - "/tmp/.cookie" + volumes: + - ./cookie:/tmp/.cookie + deploy: + restart_policy: + condition: "on-failure" bfgd-postgres: build: @@ -96,13 +105,14 @@ services: condition: "any" depends_on: - "bfgd-postgres" + - "electrs" ports: - "8080:8080" - "8383:8383" environment: BFG_POSTGRES_URI: "postgres://postgres@bfgd-postgres:5432/bfg?sslmode=disable" BFG_BTC_START_HEIGHT: "1" - BFG_EXBTC_ADDRESS: "electrumx:50001" + BFG_EXBTC_ADDRESS: "electrs:50001" BFG_LOG_LEVEL: "INFO" BFG_PUBLIC_ADDRESS: ":8383" BFG_PRIVATE_ADDRESS: ":8080" diff --git a/e2e/e2e_ext_test.go b/e2e/e2e_ext_test.go index c62398e7..cef5a630 100644 --- a/e2e/e2e_ext_test.go +++ b/e2e/e2e_ext_test.go @@ -53,19 +53,19 @@ import ( "github.com/hemilabs/heminetwork/database/bfgd" "github.com/hemilabs/heminetwork/database/bfgd/postgres" "github.com/hemilabs/heminetwork/hemi" - "github.com/hemilabs/heminetwork/hemi/electrumx" + "github.com/hemilabs/heminetwork/hemi/electrs" "github.com/hemilabs/heminetwork/hemi/pop" "github.com/hemilabs/heminetwork/service/bfg" "github.com/hemilabs/heminetwork/service/bss" ) const ( - testDBPrefix = "e2e_ext_test_db_" - mockEncodedBlockHeader = "\"0000c02048cd664586152c3dcf356d010cbb9216fdeb3b1aeae256d59a0700000000000086182c855545356ec11d94972cf31b97ef01ae7c9887f4349ad3f0caf2d3c0b118e77665efdf2819367881fb\"" - mockTxHash = "7fe9c3262f8fe26764b01955b4c996296f7c0c72945af1556038a084fcb37dbb" - mockTxPos = 3 - mockTxheight = 2 - mockElectrumxConnectTimeout = 3 * time.Second + testDBPrefix = "e2e_ext_test_db_" + mockEncodedBlockHeader = "\"0000c02048cd664586152c3dcf356d010cbb9216fdeb3b1aeae256d59a0700000000000086182c855545356ec11d94972cf31b97ef01ae7c9887f4349ad3f0caf2d3c0b118e77665efdf2819367881fb\"" + mockTxHash = "7fe9c3262f8fe26764b01955b4c996296f7c0c72945af1556038a084fcb37dbb" + mockTxPos = 3 + mockTxheight = 2 + mockElectrsConnectTimeout = 3 * time.Second ) var mockMerkleHashes = []string{ @@ -282,7 +282,7 @@ func nextPort(ctx context.Context, t *testing.T) int { } } -func createBfgServerWithAuth(ctx context.Context, t *testing.T, pgUri string, electrumxAddr string, btcStartHeight uint64, auth bool) (*bfg.Server, string, string, string) { +func createBfgServerWithAuth(ctx context.Context, t *testing.T, pgUri string, electrsAddr string, btcStartHeight uint64, auth bool) (*bfg.Server, string, string, string) { bfgPrivateListenAddress := fmt.Sprintf(":%d", nextPort(ctx, t)) bfgPublicListenAddress := fmt.Sprintf(":%d", nextPort(ctx, t)) @@ -290,7 +290,7 @@ func createBfgServerWithAuth(ctx context.Context, t *testing.T, pgUri string, el PrivateListenAddress: bfgPrivateListenAddress, PublicListenAddress: bfgPublicListenAddress, PgURI: pgUri, - EXBTCAddress: electrumxAddr, + EXBTCAddress: electrsAddr, BTCStartHeight: btcStartHeight, PublicKeyAuth: auth, RequestLimit: bfgapi.DefaultRequestLimit, @@ -321,8 +321,8 @@ func createBfgServerWithAuth(ctx context.Context, t *testing.T, pgUri string, el return bfgServer, bfgPrivateListenAddress, bfgWsPrivateUrl, bfgWsPublicUrl } -func createBfgServer(ctx context.Context, t *testing.T, pgUri string, electrumxAddr string, btcStartHeight uint64) (*bfg.Server, string, string, string) { - return createBfgServerWithAuth(ctx, t, pgUri, electrumxAddr, btcStartHeight, false) +func createBfgServer(ctx context.Context, t *testing.T, pgUri string, electrsAddr string, btcStartHeight uint64) (*bfg.Server, string, string, string) { + return createBfgServerWithAuth(ctx, t, pgUri, electrsAddr, btcStartHeight, false) } func createBssServer(ctx context.Context, t *testing.T, bfgWsurl string) (*bss.Server, string, string) { @@ -361,7 +361,7 @@ func reverseAndEncodeEncodedHash(encodedHash string) string { return hex.EncodeToString(rev) } -func createMockElectrumxServer(ctx context.Context, t *testing.T, l2Keystone *hemi.L2Keystone, btx []byte) (string, func()) { +func createMockElectrsServer(ctx context.Context, t *testing.T, l2Keystone *hemi.L2Keystone, btx []byte) (string, func()) { addr := fmt.Sprintf("localhost:%d", nextPort(ctx, t)) listener, err := net.Listen("tcp", addr) @@ -391,14 +391,14 @@ func createMockElectrumxServer(ctx context.Context, t *testing.T, l2Keystone *he panic(err) } - go handleMockElectrumxConnection(ctx, t, conn, btx) + go handleMockElectrsConnection(ctx, t, conn, btx) } }() return addr, cleanup } -func handleMockElectrumxConnection(ctx context.Context, t *testing.T, conn net.Conn, btx []byte) { +func handleMockElectrsConnection(ctx context.Context, t *testing.T, conn net.Conn, btx []byte) { mb := wire.MsgTx{} if err := mb.Deserialize(bytes.NewBuffer(btx)); err != nil { panic(fmt.Sprintf("failed to deserialize tx: %v", err)) @@ -426,13 +426,13 @@ func handleMockElectrumxConnection(ctx context.Context, t *testing.T, conn net.C return } - req := electrumx.JSONRPCRequest{} + req := electrs.JSONRPCRequest{} err = json.Unmarshal(buf[:n], &req) if err != nil { panic(err) } - res := electrumx.JSONRPCResponse{} + res := electrs.JSONRPCResponse{} if req.Method == "blockchain.transaction.broadcast" { res.ID = req.ID res.Error = nil @@ -442,7 +442,7 @@ func handleMockElectrumxConnection(ctx context.Context, t *testing.T, conn net.C if req.Method == "blockchain.headers.subscribe" { res.ID = req.ID res.Error = nil - headerNotification := electrumx.HeaderNotification{ + headerNotification := electrs.HeaderNotification{ Height: mockTxheight, BinaryHeader: "aaaa", } @@ -464,11 +464,8 @@ func handleMockElectrumxConnection(ctx context.Context, t *testing.T, conn net.C if req.Method == "blockchain.transaction.id_from_pos" { res.ID = req.ID res.Error = nil - params := struct { - Height uint64 `json:"height"` - TXPos uint64 `json:"tx_pos"` - Merkle bool `json:"merkle"` - }{} + + params := []any{} err := json.Unmarshal(req.Params, ¶ms) if err != nil { @@ -476,21 +473,21 @@ func handleMockElectrumxConnection(ctx context.Context, t *testing.T, conn net.C } result := struct { - TXHash string `json:"tx_hash"` + TXHash string `json:"tx_id"` Merkle []string `json:"merkle"` }{} - t.Logf("checking height %d, pos %d", params.Height, params.TXPos) + t.Logf("checking height %d, pos %d", params[0], params[1]) - if params.TXPos == mockTxPos && params.Height == mockTxheight { + if params[0].(float64) == mockTxheight && params[1].(float64) == mockTxPos { result.TXHash = reverseAndEncodeEncodedHash(mb.TxID()) result.Merkle = mockMerkleHashes } // pretend that there are no transactions past mockTxHeight // and mockTxPos - if params.Height >= mockTxheight && params.TXPos > mockTxPos { - res.Error = electrumx.NewJSONRPCError(1, "no tx at pos") + if params[0].(float64) >= mockTxheight && params[1].(float64) > mockTxPos { + res.Error = electrs.NewJSONRPCError(1, "no tx at position") } b, err := json.Marshal(&result) @@ -505,17 +502,14 @@ func handleMockElectrumxConnection(ctx context.Context, t *testing.T, conn net.C res.ID = req.ID res.Error = nil - params := struct { - TXHash string `json:"tx_hash"` - Verbose bool `json:"verbose"` - }{} + params := []any{} err := json.Unmarshal(req.Params, ¶ms) if err != nil { panic(err) } - if params.TXHash == reverseAndEncodeEncodedHash(mb.TxID()) { + if params[0] == reverseAndEncodeEncodedHash(mb.TxID()) { j, err := json.Marshal(hex.EncodeToString(btx)) if err != nil { panic(err) @@ -527,7 +521,7 @@ func handleMockElectrumxConnection(ctx context.Context, t *testing.T, conn net.C if req.Method == "blockchain.scripthash.get_balance" { res.ID = req.ID res.Error = nil - j, err := json.Marshal(electrumx.Balance{ + j, err := json.Marshal(electrs.Balance{ Confirmed: 1, Unconfirmed: 2, }) @@ -541,7 +535,7 @@ func handleMockElectrumxConnection(ctx context.Context, t *testing.T, conn net.C if req.Method == "blockchain.headers.subscribe" { res.ID = req.ID res.Error = nil - j, err := json.Marshal(electrumx.HeaderNotification{ + j, err := json.Marshal(electrs.HeaderNotification{ Height: 10, }) if err != nil { @@ -568,6 +562,7 @@ func handleMockElectrumxConnection(ctx context.Context, t *testing.T, conn net.C Index: 9999, Value: 999999, }} + b, err := json.Marshal(j) if err != nil { panic(err) @@ -937,14 +932,14 @@ func TestBitcoinBalance(t *testing.T) { btx := createBtcTx(t, 199, &l2Keystone, minerPrivateKeyBytes) - electrumxAddr, cleanupE := createMockElectrumxServer(ctx, t, nil, btx) + electrsAddr, cleanupE := createMockElectrsServer(ctx, t, nil, btx) defer cleanupE() - err := EnsureCanConnectTCP(t, electrumxAddr, mockElectrumxConnectTimeout) + err := EnsureCanConnectTCP(t, electrsAddr, mockElectrsConnectTimeout) if err != nil { t.Fatal(err) } - _, _, _, bfgPublicWsUrl := createBfgServer(ctx, t, pgUri, electrumxAddr, 1) + _, _, _, bfgPublicWsUrl := createBfgServer(ctx, t, pgUri, electrsAddr, 1) c, _, err := websocket.Dial(ctx, bfgPublicWsUrl, nil) if err != nil { @@ -1009,7 +1004,7 @@ func TestBFGPublicErrorCases(t *testing.T) { name string expectedError string requests any - electrumx bool + electrs bool skip bool } @@ -1018,7 +1013,7 @@ func TestBFGPublicErrorCases(t *testing.T) { name: "bitcoin balance error", expectedError: "internal error", requests: []bfgapi.BitcoinBalanceRequest{}, - electrumx: false, + electrs: false, }, { name: "bitcoin broadcast deserialize error", @@ -1028,18 +1023,18 @@ func TestBFGPublicErrorCases(t *testing.T) { Transaction: []byte("invalid..."), }, }, - electrumx: false, + electrs: false, }, { - name: "bitcoin broadcast electrumx error", + name: "bitcoin broadcast electrs error", expectedError: "internal error", requests: []bfgapi.BitcoinBroadcastRequest{ { Transaction: btx, }, }, - electrumx: false, - skip: true, + electrs: false, + skip: true, }, { name: "bitcoin broadcast database error", @@ -1052,25 +1047,25 @@ func TestBFGPublicErrorCases(t *testing.T) { Transaction: btx, }, }, - skip: true, - electrumx: true, + skip: true, + electrs: true, }, { - name: "bitcoin info electrumx error", + name: "bitcoin info electrs error", expectedError: "internal error", requests: []bfgapi.BitcoinInfoRequest{ {}, }, - electrumx: false, - skip: true, + electrs: false, + skip: true, }, { - name: "bitcoin utxos electrumx error", + name: "bitcoin utxos electrs error", expectedError: "internal error", requests: []bfgapi.BitcoinUTXOsRequest{ {}, }, - electrumx: false, + electrs: false, }, } @@ -1089,10 +1084,10 @@ func TestBFGPublicErrorCases(t *testing.T) { ctx, cancel := defaultTestContext() defer cancel() - electrumxAddr := "" + electrsAddr := "" var cleanupE func() - if tti.electrumx { + if tti.electrs { l2Keystone := hemi.L2Keystone{ Version: 1, L1BlockNumber: 5, @@ -1105,15 +1100,15 @@ func TestBFGPublicErrorCases(t *testing.T) { btx := createBtcTx(t, 199, &l2Keystone, minerPrivateKeyBytes) - electrumxAddr, cleanupE = createMockElectrumxServer(ctx, t, nil, btx) + electrsAddr, cleanupE = createMockElectrsServer(ctx, t, nil, btx) defer cleanupE() - err := EnsureCanConnectTCP(t, electrumxAddr, mockElectrumxConnectTimeout) + err := EnsureCanConnectTCP(t, electrsAddr, mockElectrsConnectTimeout) if err != nil { t.Fatal(err) } } - _, _, _, bfgPublicWsUrl := createBfgServer(ctx, t, pgUri, electrumxAddr, 1) + _, _, _, bfgPublicWsUrl := createBfgServer(ctx, t, pgUri, electrsAddr, 1) c, _, err := websocket.Dial(ctx, bfgPublicWsUrl, nil) if err != nil { @@ -1299,14 +1294,14 @@ func TestBitcoinInfo(t *testing.T) { btx := createBtcTx(t, 199, &l2Keystone, minerPrivateKeyBytes) - electrumxAddr, cleanupE := createMockElectrumxServer(ctx, t, nil, btx) + electrsAddr, cleanupE := createMockElectrsServer(ctx, t, nil, btx) defer cleanupE() - err := EnsureCanConnectTCP(t, electrumxAddr, mockElectrumxConnectTimeout) + err := EnsureCanConnectTCP(t, electrsAddr, mockElectrsConnectTimeout) if err != nil { t.Fatal(err) } - _, _, _, bfgPublicWsUrl := createBfgServer(ctx, t, pgUri, electrumxAddr, 1) + _, _, _, bfgPublicWsUrl := createBfgServer(ctx, t, pgUri, electrsAddr, 1) c, _, err := websocket.Dial(ctx, bfgPublicWsUrl, nil) if err != nil { @@ -1383,14 +1378,14 @@ func TestBitcoinUTXOs(t *testing.T) { btx := createBtcTx(t, 199, &l2Keystone, minerPrivateKeyBytes) - electrumxAddr, cleanupE := createMockElectrumxServer(ctx, t, nil, btx) + electrsAddr, cleanupE := createMockElectrsServer(ctx, t, nil, btx) defer cleanupE() - err := EnsureCanConnectTCP(t, electrumxAddr, mockElectrumxConnectTimeout) + err := EnsureCanConnectTCP(t, electrsAddr, mockElectrsConnectTimeout) if err != nil { t.Fatal(err) } - _, _, _, bfgPublicWsUrl := createBfgServer(ctx, t, pgUri, electrumxAddr, 1) + _, _, _, bfgPublicWsUrl := createBfgServer(ctx, t, pgUri, electrsAddr, 1) c, _, err := websocket.Dial(ctx, bfgPublicWsUrl, nil) if err != nil { @@ -1475,14 +1470,14 @@ func TestBitcoinBroadcast(t *testing.T) { // 1 btx := createBtcTx(t, 800, &l2Keystone, minerPrivateKeyBytes) - electrumxAddr, cleanupE := createMockElectrumxServer(ctx, t, nil, btx) + electrsAddr, cleanupE := createMockElectrsServer(ctx, t, nil, btx) defer cleanupE() - err := EnsureCanConnectTCP(t, electrumxAddr, mockElectrumxConnectTimeout) + err := EnsureCanConnectTCP(t, electrsAddr, mockElectrsConnectTimeout) if err != nil { t.Fatal(err) } - _, _, _, bfgPublicWsUrl := createBfgServer(ctx, t, pgUri, electrumxAddr, 1) + _, _, _, bfgPublicWsUrl := createBfgServer(ctx, t, pgUri, electrsAddr, 1) minerPrivateKeyBytes := []byte{1, 2, 3, 4, 5, 6, 7, 199} @@ -1575,14 +1570,14 @@ func TestBitcoinBroadcastDuplicate(t *testing.T) { // 1 btx := createBtcTx(t, 800, &l2Keystone, minerPrivateKeyBytes) - electrumxAddr, cleanupE := createMockElectrumxServer(ctx, t, nil, btx) + electrsAddr, cleanupE := createMockElectrsServer(ctx, t, nil, btx) defer cleanupE() - err := EnsureCanConnectTCP(t, electrumxAddr, mockElectrumxConnectTimeout) + err := EnsureCanConnectTCP(t, electrsAddr, mockElectrsConnectTimeout) if err != nil { t.Fatal(err) } - _, _, _, bfgPublicWsUrl := createBfgServer(ctx, t, pgUri, electrumxAddr, 1) + _, _, _, bfgPublicWsUrl := createBfgServer(ctx, t, pgUri, electrsAddr, 1) minerPrivateKeyBytes := []byte{1, 2, 3, 4, 5, 6, 7, 199} @@ -1708,9 +1703,9 @@ func TestBitcoinBroadcastDuplicate(t *testing.T) { // } } -// TestProcessBitcoinBlockNewBtcBlock mocks a btc block response from electrumx +// TestProcessBitcoinBlockNewBtcBlock mocks a btc block response from electrs // server and ensures that the btc block was inserted correctly -// 1 create mock electrumx server, by default it will send a mock btc block +// 1 create mock electrs server, by default it will send a mock btc block // when blockchain.block.header is called // 2 ensure that btc_block is inserted with correct values. this is checked on // an internal timer, so give this a timeout @@ -1738,14 +1733,14 @@ func TestProcessBitcoinBlockNewBtcBlock(t *testing.T) { btx := createBtcTx(t, 800, &l2Keystone, minerPrivateKeyBytes) // 1 - electrumxAddr, cleanupE := createMockElectrumxServer(ctx, t, &l2Keystone, btx) + electrsAddr, cleanupE := createMockElectrsServer(ctx, t, &l2Keystone, btx) defer cleanupE() - err := EnsureCanConnectTCP(t, electrumxAddr, mockElectrumxConnectTimeout) + err := EnsureCanConnectTCP(t, electrsAddr, mockElectrsConnectTimeout) if err != nil { t.Fatal(err) } - createBfgServer(ctx, t, pgUri, electrumxAddr, 1) + createBfgServer(ctx, t, pgUri, electrsAddr, 1) expectedBtcBlockHeader, err := hex.DecodeString(strings.Replace(mockEncodedBlockHeader, "\"", "", 2)) if err != nil { @@ -1759,7 +1754,7 @@ func TestProcessBitcoinBlockNewBtcBlock(t *testing.T) { // 2 // wait a max of 10 seconds (with a resolution of 1 second) for the // btc_block to be inserted into the db. this happens on a timer - // when checking electrumx + // when checking electrs lctx, lcancel := context.WithTimeout(ctx, 10*time.Second) defer lcancel() var btcBlockHeader *bfgd.BtcBlock @@ -1792,10 +1787,10 @@ loop: } // TestProcessBitcoinBlockNewFullPopBasis takes a full btc tx from the mock -// electrumx server and ensures that a new full pop_basis was inserted into the +// electrs server and ensures that a new full pop_basis was inserted into the // db // 1 create btc tx -// 2 run mock electrumx, instructing it to use the created btc tx +// 2 run mock electrs, instructing it to use the created btc tx // 3 query database for newly created pop_basis, this happens on a timer // 4 ensure pop_basis was inserted and filled out with correct fields func TestProcessBitcoinBlockNewFullPopBasis(t *testing.T) { @@ -1823,19 +1818,19 @@ func TestProcessBitcoinBlockNewFullPopBasis(t *testing.T) { btx := createBtcTx(t, 199, &l2Keystone, []byte{1, 2, 3}) // 2 - electrumxAddr, cleanupE := createMockElectrumxServer(ctx, t, &l2Keystone, btx) + electrsAddr, cleanupE := createMockElectrsServer(ctx, t, &l2Keystone, btx) defer cleanupE() - err := EnsureCanConnectTCP(t, electrumxAddr, mockElectrumxConnectTimeout) + err := EnsureCanConnectTCP(t, electrsAddr, mockElectrsConnectTimeout) if err != nil { t.Fatal(err) } - createBfgServer(ctx, t, pgUri, electrumxAddr, 1) + createBfgServer(ctx, t, pgUri, electrsAddr, 1) // 3 // wait a max of 10 seconds (with a resolution of 1 second) for the // btc_block to be inserted into the db. this happens on a timer - // when checking electrumx + // when checking electrs lctx, lcancel := context.WithTimeout(ctx, 10*time.Second) defer lcancel() var popBases []bfgd.PopBasis @@ -1905,11 +1900,11 @@ loop: } // TestBitcoinBroadcastThenUpdate will insert a pop_basis record from -// BitcoinBroadcast RPC call to BFG. Then wait for electrumx to send full +// BitcoinBroadcast RPC call to BFG. Then wait for electrs to send full // information about that pop_basis from a pop tx. then assert that the // pop_basis was filled out correctly // 1 create a btc tx -// 2 create a mock electrumx server that will return that btc tx +// 2 create a mock electrs server that will return that btc tx // 3 call BitcoinBroadcast RPC call // 4 wait for full pop_basis to be in database // 5 assert the pop_basis fields are correct @@ -1942,14 +1937,14 @@ func TestBitcoinBroadcastThenUpdate(t *testing.T) { } // 2 - electrumxAddr, cleanupE := createMockElectrumxServer(ctx, t, &l2Keystone, btx) + electrsAddr, cleanupE := createMockElectrsServer(ctx, t, &l2Keystone, btx) defer cleanupE() - err := EnsureCanConnectTCP(t, electrumxAddr, mockElectrumxConnectTimeout) + err := EnsureCanConnectTCP(t, electrsAddr, mockElectrsConnectTimeout) if err != nil { t.Fatal(err) } - _, _, _, bfgPublicWsUrl := createBfgServer(ctx, t, pgUri, electrumxAddr, 1) + _, _, _, bfgPublicWsUrl := createBfgServer(ctx, t, pgUri, electrsAddr, 1) c, _, err := websocket.Dial(ctx, bfgPublicWsUrl, nil) if err != nil { @@ -2005,7 +2000,7 @@ func TestBitcoinBroadcastThenUpdate(t *testing.T) { // 4 // wait a max of 10 seconds (with a resolution of 1 second) for the // btc_block to be inserted into the db. this happens on a timer - // when checking electrumx + // when checking electrs lctx, lcancel := context.WithTimeout(ctx, 10*time.Second) defer lcancel() var popBases []bfgd.PopBasis @@ -2754,7 +2749,7 @@ func TestGetFinalitiesByL2KeystoneBFG(t *testing.T) { } // TestNotifyOnNewBtcBlockBFGClients tests that upon getting a new btc block, -// in this case from (mock) electrumx, that a new btc block +// in this case from (mock) electrs, that a new btc block // notification will be sent to all clients connected to BFG // 1. connect client // 2. wait for notification @@ -2781,17 +2776,17 @@ func TestNotifyOnNewBtcBlockBFGClients(t *testing.T) { btx := createBtcTx(t, 199, &l2Keystone, minerPrivateKeyBytes) - electrumxAddr, cleanupE := createMockElectrumxServer(ctx, t, &l2Keystone, btx) + electrsAddr, cleanupE := createMockElectrsServer(ctx, t, &l2Keystone, btx) defer cleanupE() if err := EnsureCanConnectTCP( t, - electrumxAddr, - mockElectrumxConnectTimeout, + electrsAddr, + mockElectrsConnectTimeout, ); err != nil { t.Fatal(err) } - _, _, bfgWsurl, _ := createBfgServer(ctx, t, pgUri, electrumxAddr, 1) + _, _, bfgWsurl, _ := createBfgServer(ctx, t, pgUri, electrsAddr, 1) // 1 c, _, err := websocket.Dial(ctx, bfgWsurl, nil) @@ -2824,7 +2819,7 @@ func TestNotifyOnNewBtcBlockBFGClients(t *testing.T) { } // TestNotifyOnNewBtcFinalityBFGClients tests that upon getting a new btc block, -// in this case from (mock) electrumx, that a finality notification will be sent +// in this case from (mock) electrs, that a finality notification will be sent // to all clients connected to BFG // 1. connect client // 2. wait for notification @@ -2851,17 +2846,17 @@ func TestNotifyOnNewBtcFinalityBFGClients(t *testing.T) { btx := createBtcTx(t, 199, &l2Keystone, minerPrivateKeyBytes) - electrumxAddr, cleanupE := createMockElectrumxServer(ctx, t, &l2Keystone, btx) + electrsAddr, cleanupE := createMockElectrsServer(ctx, t, &l2Keystone, btx) defer cleanupE() if err := EnsureCanConnectTCP( t, - electrumxAddr, - mockElectrumxConnectTimeout, + electrsAddr, + mockElectrsConnectTimeout, ); err != nil { t.Fatal(err) } - _, _, bfgWsurl, _ := createBfgServer(ctx, t, pgUri, electrumxAddr, 1) + _, _, bfgWsurl, _ := createBfgServer(ctx, t, pgUri, electrsAddr, 1) // 1 c, _, err := websocket.Dial(ctx, bfgWsurl, nil) @@ -2955,7 +2950,7 @@ func TestNotifyOnL2KeystonesBFGClients(t *testing.T) { } // TestNotifyOnNewBtcBlockBSSClients tests that upon getting a new btc block, -// in this case from (mock) electrumx, that a new btc notification +// in this case from (mock) electrs, that a new btc notification // will be sent to all clients connected to BSS // 1. connect client // 2. wait for notification @@ -2982,17 +2977,17 @@ func TestNotifyOnNewBtcBlockBSSClients(t *testing.T) { btx := createBtcTx(t, 199, &l2Keystone, minerPrivateKeyBytes) - electrumxAddr, cleanupE := createMockElectrumxServer(ctx, t, &l2Keystone, btx) + electrsAddr, cleanupE := createMockElectrsServer(ctx, t, &l2Keystone, btx) defer cleanupE() if err := EnsureCanConnectTCP( t, - electrumxAddr, - mockElectrumxConnectTimeout, + electrsAddr, + mockElectrsConnectTimeout, ); err != nil { t.Fatal(err) } - _, _, bfgWsurl, _ := createBfgServer(ctx, t, pgUri, electrumxAddr, 1) + _, _, bfgWsurl, _ := createBfgServer(ctx, t, pgUri, electrsAddr, 1) _, _, bssWsurl := createBssServer(ctx, t, bfgWsurl) // 1 @@ -3025,7 +3020,7 @@ func TestNotifyOnNewBtcBlockBSSClients(t *testing.T) { } // TestNotifyOnNewBtcFinalityBSSClients tests that upon getting a new btc block, -// in this case from (mock) electrumx, that a new finality notification +// in this case from (mock) electrs, that a new finality notification // will be sent to all clients connected to BSS // 1. connect client // 2. wait for notification @@ -3052,17 +3047,17 @@ func TestNotifyOnNewBtcFinalityBSSClients(t *testing.T) { btx := createBtcTx(t, 199, &l2Keystone, minerPrivateKeyBytes) - electrumxAddr, cleanupE := createMockElectrumxServer(ctx, t, &l2Keystone, btx) + electrsAddr, cleanupE := createMockElectrsServer(ctx, t, &l2Keystone, btx) defer cleanupE() if err := EnsureCanConnectTCP( t, - electrumxAddr, - mockElectrumxConnectTimeout, + electrsAddr, + mockElectrsConnectTimeout, ); err != nil { t.Fatal(err) } - _, _, bfgWsurl, _ := createBfgServer(ctx, t, pgUri, electrumxAddr, 1) + _, _, bfgWsurl, _ := createBfgServer(ctx, t, pgUri, electrsAddr, 1) _, _, bssWsurl := createBssServer(ctx, t, bfgWsurl) // 1 @@ -3117,17 +3112,17 @@ func TestNotifyMultipleBFGClients(t *testing.T) { btx := createBtcTx(t, 199, &l2Keystone, minerPrivateKeyBytes) - electrumxAddr, cleanupE := createMockElectrumxServer(ctx, t, &l2Keystone, btx) + electrsAddr, cleanupE := createMockElectrsServer(ctx, t, &l2Keystone, btx) defer cleanupE() if err := EnsureCanConnectTCP( t, - electrumxAddr, - mockElectrumxConnectTimeout, + electrsAddr, + mockElectrsConnectTimeout, ); err != nil { t.Fatal(err) } - _, _, bfgWsurl, _ := createBfgServer(ctx, t, pgUri, electrumxAddr, 1) + _, _, bfgWsurl, _ := createBfgServer(ctx, t, pgUri, electrsAddr, 1) wg := sync.WaitGroup{} @@ -3188,17 +3183,17 @@ func TestNotifyMultipleBSSClients(t *testing.T) { btx := createBtcTx(t, 199, &l2Keystone, minerPrivateKeyBytes) - electrumxAddr, cleanupE := createMockElectrumxServer(ctx, t, &l2Keystone, btx) + electrsAddr, cleanupE := createMockElectrsServer(ctx, t, &l2Keystone, btx) defer cleanupE() if err := EnsureCanConnectTCP( t, - electrumxAddr, - mockElectrumxConnectTimeout, + electrsAddr, + mockElectrsConnectTimeout, ); err != nil { t.Fatal(err) } - _, _, bfgWsurl, _ := createBfgServer(ctx, t, pgUri, electrumxAddr, 1) + _, _, bfgWsurl, _ := createBfgServer(ctx, t, pgUri, electrsAddr, 1) _, _, bssWsurl := createBssServer(ctx, t, bfgWsurl) wg := sync.WaitGroup{} diff --git a/e2e/monitor/main_test.go b/e2e/monitor/main_test.go index fada4a97..33c8d4a9 100644 --- a/e2e/monitor/main_test.go +++ b/e2e/monitor/main_test.go @@ -16,7 +16,7 @@ import ( // after 5 minutes and check that it has progressed at least to a certain // point func TestMonitor(t *testing.T) { - ms := 1000 * 60 * 5 // dump after 5 minutes + ms := (1000 * 60 * 5) + 25*1000 // dump after 5 minutes + 25 seconds for cushion (1 keystone) output := monitor(uint(ms)) t.Log(output) @@ -29,6 +29,7 @@ func TestMonitor(t *testing.T) { // each keystone is 25 seconds, so there are 4 keystones per 100 seconds, // we expect the number of pop txs to be at least once every 25 seconds // for the time we waited + // add 25 seconds for cushion seconds := ms / 1000 popTxsPer100Seconds := 4 expectedPopTxs := popTxsPer100Seconds * (seconds / 100) diff --git a/hemi/electrumx/conn.go b/hemi/electrs/conn.go similarity index 95% rename from hemi/electrumx/conn.go rename to hemi/electrs/conn.go index b9c74e54..3d6394ce 100644 --- a/hemi/electrumx/conn.go +++ b/hemi/electrs/conn.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the MIT License, // which can be found in the LICENSE file. -package electrumx +package electrs import ( "bufio" @@ -24,7 +24,7 @@ const ( connPingTimeout = 5 * time.Second ) -// clientConn is a connection with an ElectrumX server. +// clientConn is a connection with an Electrs server. type clientConn struct { mx sync.Mutex conn net.Conn @@ -147,7 +147,7 @@ func readResponse(ctx context.Context, r io.Reader, reqID uint64) (*JSONRPCRespo if res.ID != reqID { if res.ID == 0 { - // ElectrumX may have sent a request, ignore it and try again. + // Electrs may have sent a request, ignore it and try again. // TODO(joshuasing): We should probably handle incoming requests by // having a separate goroutine that handles reading. select { @@ -155,7 +155,7 @@ func readResponse(ctx context.Context, r io.Reader, reqID uint64) (*JSONRPCRespo return nil, ctx.Err() default: } - log.Debugf("Received a response from ElectrumX with ID 0, retrying read response...") + log.Debugf("Received a response from Electrs with ID 0, retrying read response...") return readResponse(ctx, r, reqID) } return nil, fmt.Errorf("response ID differs from request ID (%d != %d)", res.ID, reqID) diff --git a/hemi/electrumx/conn_pool.go b/hemi/electrs/conn_pool.go similarity index 98% rename from hemi/electrumx/conn_pool.go rename to hemi/electrs/conn_pool.go index 6440a69e..7a831e46 100644 --- a/hemi/electrumx/conn_pool.go +++ b/hemi/electrs/conn_pool.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the MIT License, // which can be found in the LICENSE file. -package electrumx +package electrs import ( "errors" @@ -12,7 +12,7 @@ import ( "sync" ) -// connPool represents an ElectrumX connection pool. +// connPool represents an electrs connection pool. type connPool struct { network string address string diff --git a/hemi/electrumx/conn_pool_test.go b/hemi/electrs/conn_pool_test.go similarity index 99% rename from hemi/electrumx/conn_pool_test.go rename to hemi/electrs/conn_pool_test.go index 710b918c..b031fb6d 100644 --- a/hemi/electrumx/conn_pool_test.go +++ b/hemi/electrs/conn_pool_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the MIT License, // which can be found in the LICENSE file. -package electrumx +package electrs import ( "context" diff --git a/hemi/electrumx/conn_test.go b/hemi/electrs/conn_test.go similarity index 99% rename from hemi/electrumx/conn_test.go rename to hemi/electrs/conn_test.go index fd4f1aaf..526712ed 100644 --- a/hemi/electrumx/conn_test.go +++ b/hemi/electrs/conn_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the MIT License, // which can be found in the LICENSE file. -package electrumx +package electrs import ( "bufio" diff --git a/hemi/electrumx/electrumx.go b/hemi/electrs/electrs.go similarity index 85% rename from hemi/electrumx/electrumx.go rename to hemi/electrs/electrs.go index 6570676c..92e7d25b 100644 --- a/hemi/electrumx/electrumx.go +++ b/hemi/electrs/electrs.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the MIT License, // which can be found in the LICENSE file. -package electrumx +package electrs import ( "context" @@ -23,12 +23,12 @@ import ( "github.com/hemilabs/heminetwork/bitcoin" ) -var log = loggo.GetLogger("electrumx") +var log = loggo.GetLogger("electrs") // Prometheus subsystem name. -const promSubsystem = "electrumx" +const promSubsystem = "electrs" -// https://electrumx.readthedocs.io/en/latest/protocol-basics.html +// https://github.com/romanz/electrs type JSONRPCError struct { Code int `json:"code"` @@ -123,7 +123,7 @@ var ( ErrNoTxAtPosition = NewNoTxAtPositionError(errors.New("no tx at position")) ) -// Client implements an electrumx JSON RPC client. +// Client implements an electrs JSON RPC client. type Client struct { connPool *connPool metrics *metrics @@ -135,11 +135,11 @@ var ( ) type ClientOptions struct { - // InitialConnections is the number of initial ElectrumX connections to open + // InitialConnections is the number of initial Electrs connections to open // and keep in the pool. InitialConnections int - // MaxConnections is the maximum number of ElectrumX connections to keep in + // MaxConnections is the maximum number of Electrs connections to keep in // the pool. // // If adding a connection back to the pool would result in the pool having @@ -166,32 +166,32 @@ func newMetrics(namespace string) *metrics { Namespace: namespace, Subsystem: promSubsystem, Name: "connections_open", - Help: "Number of open ElectrumX connections", + Help: "Number of open Electrs connections", }), connsIdle: prometheus.NewGauge(prometheus.GaugeOpts{ Namespace: namespace, Subsystem: promSubsystem, Name: "connections_idle", - Help: "Number of idle ElectrumX connections", + Help: "Number of idle Electrs connections", }), connsOpened: prometheus.NewCounter(prometheus.CounterOpts{ Namespace: namespace, Subsystem: promSubsystem, Name: "connections_opened_total", - Help: "Total number of ElectrumX connections opened", + Help: "Total number of Electrs connections opened", }), connsClosed: prometheus.NewCounter(prometheus.CounterOpts{ Namespace: namespace, Subsystem: promSubsystem, Name: "connections_closed_total", - Help: "Total number of ElectrumX connections closed", + Help: "Total number of Electrs connections closed", }), rpcCallsTotal: prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: namespace, Subsystem: promSubsystem, Name: "rpc_calls_total", - Help: "Total number of ElectrumX RPC calls", + Help: "Total number of Electrs RPC calls", }, []string{"method"}, ), @@ -200,7 +200,7 @@ func newMetrics(namespace string) *metrics { Namespace: namespace, Subsystem: promSubsystem, Name: "rpc_calls_duration_seconds", - Help: "ElectrumX RPC call durations in seconds", + Help: "Electrs RPC call durations in seconds", Buckets: prometheus.DefBuckets, }, []string{"method"}, @@ -219,7 +219,7 @@ func (m *metrics) collectors() []prometheus.Collector { } } -// NewClient returns an initialised electrumx client. +// NewClient returns an initialised electrs client. func NewClient(address string, opts *ClientOptions) (*Client, error) { if opts == nil { opts = new(ClientOptions) @@ -316,11 +316,8 @@ func (c *Client) Balance(ctx context.Context, scriptHash []byte) (*Balance, erro if err != nil { return nil, fmt.Errorf("invalid script hash: %w", err) } - params := struct { - ScriptHash string `json:"scripthash"` - }{ - ScriptHash: hash.String(), - } + + params := []any{hash.String()} var balance Balance if err := c.call(ctx, "blockchain.scripthash.get_balance", ¶ms, &balance); err != nil { return nil, err @@ -329,11 +326,7 @@ func (c *Client) Balance(ctx context.Context, scriptHash []byte) (*Balance, erro } func (c *Client) Broadcast(ctx context.Context, rtx []byte) ([]byte, error) { - params := struct { - RawTx string `json:"raw_tx"` - }{ - RawTx: hex.EncodeToString(rtx), - } + params := []any{hex.EncodeToString(rtx)} var txHashStr string if err := c.call(ctx, "blockchain.transaction.broadcast", ¶ms, &txHashStr); err != nil { return nil, err @@ -348,24 +341,21 @@ func (c *Client) Broadcast(ctx context.Context, rtx []byte) ([]byte, error) { func (c *Client) Height(ctx context.Context) (uint64, error) { // TODO: The way this function is used could be improved. // "blockchain.headers.subscribe" subscribes to receive notifications from - // the ElectrumX server, however this function appears to be used for + // the Electrs server, however this function appears to be used for // polling instead, which could be replaced by handling the requests sent - // from the ElectrumX server. + // from the Electrs server. hn := &HeaderNotification{} if err := c.call(ctx, "blockchain.headers.subscribe", nil, hn); err != nil { return 0, err } + + log.Infof("received height of %d", hn.Height) + return hn.Height, nil } func (c *Client) RawBlockHeader(ctx context.Context, height uint64) (*bitcoin.BlockHeader, error) { - params := struct { - Height uint64 `json:"height"` - CPHeight uint64 `json:"cp_height"` - }{ - Height: height, - CPHeight: 0, - } + params := []any{height} var rbhStr string if err := c.call(ctx, "blockchain.block.header", ¶ms, &rbhStr); err != nil { return nil, fmt.Errorf("get block header: %w", err) @@ -382,13 +372,8 @@ func (c *Client) RawTransaction(ctx context.Context, txHash []byte) ([]byte, err if err != nil { return nil, fmt.Errorf("invalid transaction hash: %w", err) } - params := struct { - TXHash string `json:"tx_hash"` - Verbose bool `json:"verbose"` - }{ - TXHash: hash.String(), - Verbose: false, - } + + params := []any{hash.String(), false} var rtxStr string if err := c.call(ctx, "blockchain.transaction.get", ¶ms, &rtxStr); err != nil { return nil, fmt.Errorf("get transaction: %w", err) @@ -405,13 +390,7 @@ func (c *Client) Transaction(ctx context.Context, txHash []byte) ([]byte, error) if err != nil { return nil, fmt.Errorf("invalid transaction hash: %w", err) } - params := struct { - TXHash string `json:"tx_hash"` - Verbose bool `json:"verbose"` - }{ - TXHash: hash.String(), - Verbose: true, - } + params := []any{hash.String(), true} var txJSON json.RawMessage if err := c.call(ctx, "blockchain.transaction.get", ¶ms, &txJSON); err != nil { return nil, fmt.Errorf("get transaction: %w", err) @@ -420,21 +399,14 @@ func (c *Client) Transaction(ctx context.Context, txHash []byte) ([]byte, error) } func (c *Client) TransactionAtPosition(ctx context.Context, height, index uint64) ([]byte, []string, error) { - params := struct { - Height uint64 `json:"height"` - TXPos uint64 `json:"tx_pos"` - Merkle bool `json:"merkle"` - }{ - Height: height, - TXPos: index, - Merkle: true, - } result := struct { - TXHash string `json:"tx_hash"` + TXHash string `json:"tx_id"` Merkle []string `json:"merkle"` }{} + + params := []any{height, index, true} if err := c.call(ctx, "blockchain.transaction.id_from_pos", ¶ms, &result); err != nil { - if strings.HasPrefix(err.Error(), "no tx at position ") { + if strings.HasPrefix(err.Error(), "invalid tx_pos ") { return nil, nil, NewNoTxAtPositionError(err) } else if strings.HasPrefix(err.Error(), "db error: DBError('block ") && strings.Contains(err.Error(), " not on disk ") { return nil, nil, NewBlockNotOnDiskError(err) @@ -455,11 +427,8 @@ func (c *Client) UTXOs(ctx context.Context, scriptHash []byte) ([]*UTXO, error) if err != nil { return nil, fmt.Errorf("invalid script hash: %w", err) } - params := struct { - ScriptHash string `json:"scripthash"` - }{ - ScriptHash: hash.String(), - } + + params := []any{hash.String()} var eutxos []*exUTXO if err := c.call(ctx, "blockchain.scripthash.listunspent", ¶ms, &eutxos); err != nil { return nil, err diff --git a/service/bfg/bfg.go b/service/bfg/bfg.go index 5e6b1f19..b67e27bd 100644 --- a/service/bfg/bfg.go +++ b/service/bfg/bfg.go @@ -34,7 +34,7 @@ import ( "github.com/hemilabs/heminetwork/database/bfgd" "github.com/hemilabs/heminetwork/database/bfgd/postgres" "github.com/hemilabs/heminetwork/hemi" - "github.com/hemilabs/heminetwork/hemi/electrumx" + "github.com/hemilabs/heminetwork/hemi/electrs" "github.com/hemilabs/heminetwork/hemi/pop" "github.com/hemilabs/heminetwork/service/deucalion" "github.com/hemilabs/heminetwork/service/pprof" @@ -73,17 +73,17 @@ func NewDefaultConfig() *Config { } } -// XXX figure out if this needs to be moved out into the electrumx package. +// XXX figure out if this needs to be moved out into the electrs package. type btcClient interface { Metrics() []prometheus.Collector - Balance(ctx context.Context, scriptHash []byte) (*electrumx.Balance, error) + Balance(ctx context.Context, scriptHash []byte) (*electrs.Balance, error) Broadcast(ctx context.Context, rtx []byte) ([]byte, error) Height(ctx context.Context) (uint64, error) RawBlockHeader(ctx context.Context, height uint64) (*bitcoin.BlockHeader, error) RawTransaction(ctx context.Context, txHash []byte) ([]byte, error) Transaction(ctx context.Context, txHash []byte) ([]byte, error) TransactionAtPosition(ctx context.Context, height, index uint64) ([]byte, []string, error) - UTXOs(ctx context.Context, scriptHash []byte) ([]*electrumx.UTXO, error) + UTXOs(ctx context.Context, scriptHash []byte) ([]*electrs.UTXO, error) Close() error } @@ -228,13 +228,13 @@ func NewServer(cfg *Config) (*Server, error) { } var err error - s.btcClient, err = electrumx.NewClient(cfg.EXBTCAddress, &electrumx.ClientOptions{ + s.btcClient, err = electrs.NewClient(cfg.EXBTCAddress, &electrs.ClientOptions{ InitialConnections: cfg.EXBTCInitialConns, MaxConnections: cfg.EXBTCMaxConns, PromNamespace: promNamespace, }) if err != nil { - return nil, fmt.Errorf("create electrumx client: %w", err) + return nil, fmt.Errorf("create electrs client: %w", err) } // We could use a PGURI verification here. @@ -634,7 +634,7 @@ func (s *Server) processBitcoinBlock(ctx context.Context, height uint64) error { txHash, merkleHashes, err := s.btcClient.TransactionAtPosition(ctx, height, index) if err != nil { - if errors.Is(err, electrumx.ErrNoTxAtPosition) { + if errors.Is(err, electrs.ErrNoTxAtPosition) { // There is no way to tell how many transactions are // in a block, so hopefully we've got them all... return nil