diff --git a/.golangci.yml b/.golangci.yml index 07efe7370..1983b8983 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -11,6 +11,8 @@ run: # Timeout for analysis, e.g. 30s, 5m. # Default: 1m timeout: 3m + build-tags: + - e2e # This file contains only configs which differ from defaults. diff --git a/go.mod b/go.mod index 614cdb290..5dd6d5367 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/getkin/kin-openapi v0.127.0 github.com/go-redis/redis/v8 v8.11.5 github.com/go-testfixtures/testfixtures/v3 v3.9.0 + github.com/go-zeromq/zmq4 v0.17.0 github.com/golang-migrate/migrate/v4 v4.16.2 github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 @@ -32,7 +33,6 @@ require ( github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.19.1 - github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 @@ -82,7 +82,6 @@ require ( github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-zeromq/goczmq/v4 v4.2.2 // indirect - github.com/go-zeromq/zmq4 v0.17.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -134,6 +133,7 @@ require ( github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect diff --git a/internal/node_client/node_client.go b/internal/node_client/node_client.go new file mode 100644 index 000000000..2a69228b6 --- /dev/null +++ b/internal/node_client/node_client.go @@ -0,0 +1,3 @@ +package node_client + +// Todo: Implement node client diff --git a/internal/node_client/test_utils.go b/internal/node_client/test_utils.go new file mode 100644 index 000000000..1fb2e4833 --- /dev/null +++ b/internal/node_client/test_utils.go @@ -0,0 +1,289 @@ +package node_client + +import ( + "fmt" + "testing" + "time" + + ec "github.com/bitcoin-sv/go-sdk/primitives/ec" + sdkTx "github.com/bitcoin-sv/go-sdk/transaction" + "github.com/bitcoin-sv/go-sdk/transaction/template/p2pkh" + "github.com/bitcoinsv/bsvutil" + "github.com/ordishs/go-bitcoin" + "github.com/stretchr/testify/require" +) + +type UnspentOutput struct { + Txid string `json:"txid"` + Vout uint32 `json:"vout"` + Address string `json:"address"` + Account string `json:"account"` + ScriptPubKey string `json:"scriptPubKey"` + Amount float64 `json:"amount"` + Confirmations int `json:"confirmations"` + Spendable bool `json:"spendable"` + Solvable bool `json:"solvable"` + Safe bool `json:"safe"` +} + +type RawTransaction struct { + Hex string `json:"hex"` + BlockHash string `json:"blockhash,omitempty"` +} + +type BlockData struct { + Height uint64 `json:"height"` + Txs []string `json:"txs"` + MerkleRoot string `json:"merkleroot"` +} + +func GetNewWalletAddress(t *testing.T, bitcoind *bitcoin.Bitcoind) (address, privateKey string) { + address, err := bitcoind.GetNewAddress() + require.NoError(t, err) + t.Logf("new address: %s", address) + + privateKey, err = bitcoind.DumpPrivKey(address) + require.NoError(t, err) + t.Logf("new private key: %s", privateKey) + + accountName := "test-account" + err = bitcoind.SetAccount(address, accountName) + require.NoError(t, err) + + t.Logf("account %s created", accountName) + + return +} + +func SendToAddress(t *testing.T, bitcoind *bitcoin.Bitcoind, address string, bsv float64) (txID string) { + t.Helper() + + txID, err := bitcoind.SendToAddress(address, bsv) + require.NoError(t, err) + + t.Logf("sent %f to %s: %s", bsv, address, txID) + + return +} + +func Generate(t *testing.T, bitcoind *bitcoin.Bitcoind, amount uint64) string { + t.Helper() + + // run command instead + blockHash := ExecCommandGenerate(t, bitcoind, amount) + time.Sleep(5 * time.Second) + + t.Logf( + "generated %d block(s): block hash: %s", + amount, + blockHash, + ) + + return blockHash +} + +func ExecCommandGenerate(t *testing.T, bitcoind *bitcoin.Bitcoind, amount uint64) string { + t.Helper() + t.Logf("Amount to generate: %d", amount) + + hashes, err := bitcoind.Generate(float64(amount)) + require.NoError(t, err) + + return hashes[len(hashes)-1] +} + +func GetUtxos(t *testing.T, bitcoind *bitcoin.Bitcoind, address string) []UnspentOutput { + t.Helper() + + data, err := bitcoind.ListUnspent([]string{address}) + require.NoError(t, err) + + result := make([]UnspentOutput, len(data)) + + for index, utxo := range data { + t.Logf("UTXO Txid: %s, Amount: %f, Address: %s\n", utxo.TXID, utxo.Amount, utxo.Address) + result[index] = UnspentOutput{ + Txid: utxo.TXID, + Vout: utxo.Vout, + Address: utxo.Address, + ScriptPubKey: utxo.ScriptPubKey, + Amount: utxo.Amount, + Confirmations: int(utxo.Confirmations), + } + } + + return result +} + +func CreateTxChain(privateKey string, utxo0 UnspentOutput, length int) ([]*sdkTx.Transaction, error) { + batch := make([]*sdkTx.Transaction, length) + const feeSat = 10 + utxoTxID := utxo0.Txid + utxoVout := uint32(utxo0.Vout) + utxoSatoshis := uint64(utxo0.Amount * 1e8) + utxoScript := utxo0.ScriptPubKey + utxoAddress := utxo0.Address + + for i := 0; i < length; i++ { + tx := sdkTx.NewTransaction() + + utxo, err := sdkTx.NewUTXO(utxoTxID, utxoVout, utxoScript, utxoSatoshis) + if err != nil { + return nil, fmt.Errorf("failed creating UTXO: %v", err) + } + + err = tx.AddInputsFromUTXOs(utxo) + if err != nil { + return nil, fmt.Errorf("failed adding input: %v", err) + } + + amountToSend := utxoSatoshis - feeSat + + err = tx.PayToAddress(utxoAddress, amountToSend) + if err != nil { + return nil, fmt.Errorf("failed to pay to address: %v", err) + } + + // Sign the input + wif, err := bsvutil.DecodeWIF(privateKey) + if err != nil { + return nil, err + } + + privateKeyDecoded := wif.PrivKey.Serialize() + pk, _ := ec.PrivateKeyFromBytes(privateKeyDecoded) + + unlockingScriptTemplate, err := p2pkh.Unlock(pk, nil) + if err != nil { + return nil, err + } + + for _, input := range tx.Inputs { + input.UnlockingScriptTemplate = unlockingScriptTemplate + } + + err = tx.Sign() + if err != nil { + return nil, err + } + + batch[i] = tx + + utxoTxID = tx.TxID() + utxoVout = 0 + utxoSatoshis = amountToSend + utxoScript = utxo0.ScriptPubKey + } + + return batch, nil +} + +func FundNewWallet(t *testing.T, bitcoind *bitcoin.Bitcoind) (addr, privKey string) { + t.Helper() + + addr, privKey = GetNewWalletAddress(t, bitcoind) + SendToAddress(t, bitcoind, addr, 0.001) + // mine a block with the transaction from above + Generate(t, bitcoind, 1) + + return +} + +func GetBlockRootByHeight(t *testing.T, bitcoind *bitcoin.Bitcoind, blockHeight int) string { + t.Helper() + block, err := bitcoind.GetBlockByHeight(blockHeight) + require.NoError(t, err) + + return block.MerkleRoot +} + +func GetRawTx(t *testing.T, bitcoind *bitcoin.Bitcoind, txID string) RawTransaction { + t.Helper() + + rawTx, err := bitcoind.GetRawTransaction(txID) + require.NoError(t, err) + + return RawTransaction{ + Hex: rawTx.Hex, + BlockHash: rawTx.BlockHash, + } +} + +func GetBlockDataByBlockHash(t *testing.T, bitcoind *bitcoin.Bitcoind, blockHash string) BlockData { + t.Helper() + + block, err := bitcoind.GetBlock(blockHash) + require.NoError(t, err) + + return BlockData{ + Height: block.Height, + Txs: block.Tx, + MerkleRoot: block.MerkleRoot, + } +} + +func CreateTx(privateKey string, address string, utxo UnspentOutput, fee ...uint64) (*sdkTx.Transaction, error) { + return CreateTxFrom(privateKey, address, []UnspentOutput{utxo}, fee...) +} + +func CreateTxFrom(privateKey string, address string, utxos []UnspentOutput, fee ...uint64) (*sdkTx.Transaction, error) { + tx := sdkTx.NewTransaction() + + // Add an input using the UTXOs + for _, utxo := range utxos { + utxoTxID := utxo.Txid + utxoVout := utxo.Vout + utxoSatoshis := uint64(utxo.Amount * 1e8) // Convert BTC to satoshis + utxoScript := utxo.ScriptPubKey + + u, err := sdkTx.NewUTXO(utxoTxID, utxoVout, utxoScript, utxoSatoshis) + if err != nil { + return nil, fmt.Errorf("failed creating UTXO: %v", err) + } + err = tx.AddInputsFromUTXOs(u) + if err != nil { + return nil, fmt.Errorf("failed adding input: %v", err) + } + } + // Add an output to the address you've previously created + recipientAddress := address + + var feeValue uint64 + if len(fee) > 0 { + feeValue = fee[0] + } else { + feeValue = 20 // Set your default fee value here + } + amountToSend := tx.TotalInputSatoshis() - feeValue + + err := tx.PayToAddress(recipientAddress, amountToSend) + if err != nil { + return nil, fmt.Errorf("failed to pay to address: %v", err) + } + + // Sign the input + wif, err := bsvutil.DecodeWIF(privateKey) + if err != nil { + return nil, fmt.Errorf("failed to decode WIF: %v", err) + } + + // Extract raw private key bytes directly from the WIF structure + privateKeyDecoded := wif.PrivKey.Serialize() + pk, _ := ec.PrivateKeyFromBytes(privateKeyDecoded) + + unlockingScriptTemplate, err := p2pkh.Unlock(pk, nil) + if err != nil { + return nil, err + } + + for _, input := range tx.Inputs { + input.UnlockingScriptTemplate = unlockingScriptTemplate + } + + err = tx.Sign() + if err != nil { + return nil, err + } + + return tx, nil +} diff --git a/test/Dockerfile b/test/Dockerfile index 1dc4c0c30..2594426af 100644 --- a/test/Dockerfile +++ b/test/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22.5-alpine3.20 +FROM golang:1.22.5-alpine3.20 AS build-stage # Set the Current Working Directory inside the container WORKDIR /app @@ -7,18 +7,25 @@ WORKDIR /app COPY go.mod go.sum ./ # Download all dependencies. Dependencies will be cached if the go.mod and the go.sum files are not changed +COPY go.mod go.sum ./ RUN go mod download +RUN go mod verify + +COPY cmd/ cmd/ +COPY internal/ internal/ +COPY pkg/ pkg/ +COPY config/ config/ +COPY test/ test/ + +# Build tests +RUN go test --tags=e2e -v -failfast github.com/bitcoin-sv/arc/test -c -o /e2e.test + +# Deploy the application binary into a lean image +FROM scratch -COPY ./test/e2e_globals.go ./e2e_globals.go -COPY ./test/init_test.go ./init_test.go -COPY ./test/utils.go ./utils.go -COPY ./test/fixtures ./fixtures +WORKDIR /test -# Copy tests to run -COPY ./test/submit_single_test.go ./submit_01_single_test.go -COPY ./test/submit_batch_test.go ./submit_02_batch_test.go -COPY ./test/submit_double_spending_test.go ./submit_03_double_spending_test.go -COPY ./test/submit_beef_test.go ./submit_04_beef_test.go +COPY --from=build-stage /e2e.test /test/e2e.test -# This will compile and run the tests -CMD [ "go", "test", "--tags=e2e", "-v", "-failfast", "./..."] +# Run tests +CMD ["/test/e2e.test"] diff --git a/test/e2e_globals.go b/test/e2e_globals.go index c17aacd3c..1e443851f 100644 --- a/test/e2e_globals.go +++ b/test/e2e_globals.go @@ -6,21 +6,23 @@ import ( "github.com/ordishs/go-bitcoin" ) -const ( - host = "node1" - port = 18332 - user = "bitcoin" - password = "bitcoin" -) - -const ( - feeSat = 10 +var ( + nodeHost = "node1" arcEndpoint = "http://api:9090/" - v1Tx = "v1/tx" - v1Txs = "v1/txs" arcEndpointV1Tx = arcEndpoint + v1Tx arcEndpointV1Txs = arcEndpoint + v1Txs ) +const ( + nodePort = 18332 + nodeUser = "bitcoin" + nodePassword = "bitcoin" +) + +const ( + v1Tx = "v1/tx" + v1Txs = "v1/txs" +) + var bitcoind *bitcoin.Bitcoind diff --git a/test/init_test.go b/test/init_test.go index dcbc6137d..b44a8b794 100644 --- a/test/init_test.go +++ b/test/init_test.go @@ -27,8 +27,18 @@ func TestMain(m *testing.M) { func setupSut() { log.Printf("init tests") + if os.Getenv("NODE_HOST") != "" { + nodeHost = os.Getenv("NODE_HOST") + } + + if os.Getenv("ARC_ENDPOINT") != "" { + arcEndpoint = os.Getenv("ARC_ENDPOINT") + arcEndpointV1Tx = arcEndpoint + v1Tx + arcEndpointV1Txs = arcEndpoint + v1Txs + } + var err error - bitcoind, err = bitcoin.New(host, port, user, password, false) + bitcoind, err = bitcoin.New(nodeHost, nodePort, nodeUser, nodePassword, false) if err != nil { log.Fatalln("Failed to create bitcoind instance:", err) } diff --git a/test/submit_single_test.go b/test/submit_01_single_test.go similarity index 82% rename from test/submit_single_test.go rename to test/submit_01_single_test.go index 9a294ecc8..349e848d5 100644 --- a/test/submit_single_test.go +++ b/test/submit_01_single_test.go @@ -3,11 +3,11 @@ package test import ( + "embed" "encoding/hex" "encoding/json" "fmt" "net/http" - "os" "strconv" "testing" "time" @@ -15,20 +15,25 @@ import ( sdkTx "github.com/bitcoin-sv/go-sdk/transaction" "github.com/libsv/go-bc" "github.com/stretchr/testify/require" + + "github.com/bitcoin-sv/arc/internal/node_client" ) +//go:embed fixtures/malformedTxHexString.txt +var fixtures embed.FS + func TestSubmitSingle(t *testing.T) { - address, privateKey := fundNewWallet(t) + address, privateKey := node_client.FundNewWallet(t, bitcoind) - utxos := getUtxos(t, address) + utxos := node_client.GetUtxos(t, bitcoind, address) require.True(t, len(utxos) > 0, "No UTXOs available for the address") - tx, err := createTx(privateKey, address, utxos[0]) + tx, err := node_client.CreateTx(privateKey, address, utxos[0]) require.NoError(t, err) rawTx, err := tx.EFHex() require.NoError(t, err) - malFormedRawTx, err := os.ReadFile("./fixtures/malformedTxHexString.txt") + malFormedRawTx, err := fixtures.ReadFile("fixtures/malformedTxHexString.txt") require.NoError(t, err) type malformedTransactionRequest struct { @@ -63,14 +68,13 @@ func TestSubmitSingle(t *testing.T) { for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { - // Send POST request response := postRequest[TransactionResponse](t, arcEndpointV1Tx, createPayload(t, tc.body), nil, tc.expectedStatusCode) if tc.expectedStatusCode != http.StatusOK { return } - require.Equal(t, Status_SEEN_ON_NETWORK, response.TxStatus) + require.Equal(t, StatusSeenOnNetwork, response.TxStatus) time.Sleep(1 * time.Second) // give ARC time to perform the status update on DB @@ -78,19 +82,19 @@ func TestSubmitSingle(t *testing.T) { txID := response.Txid response = postRequest[TransactionResponse](t, arcEndpointV1Tx, createPayload(t, TransactionRequest{RawTx: rawTx}), nil, http.StatusOK) require.Equal(t, txID, response.Txid) - require.Equal(t, Status_SEEN_ON_NETWORK, response.TxStatus) + require.Equal(t, StatusSeenOnNetwork, response.TxStatus) // Check transaction status - statusUrl := fmt.Sprintf("%s/%s", arcEndpointV1Tx, txID) + statusURL := fmt.Sprintf("%s/%s", arcEndpointV1Tx, txID) statusResponse := getRequest[TransactionResponse](t, fmt.Sprintf("%s/%s", arcEndpointV1Tx, txID)) - require.Equal(t, Status_SEEN_ON_NETWORK, statusResponse.TxStatus) + require.Equal(t, StatusSeenOnNetwork, statusResponse.TxStatus) t.Logf("Transaction status: %s", statusResponse.TxStatus) - generate(t, 1) + node_client.Generate(t, bitcoind, 1) - statusResponse = getRequest[TransactionResponse](t, statusUrl) - require.Equal(t, Status_MINED, statusResponse.TxStatus) + statusResponse = getRequest[TransactionResponse](t, statusURL) + require.Equal(t, StatusMined, statusResponse.TxStatus) t.Logf("Transaction status: %s", statusResponse.TxStatus) @@ -108,7 +112,7 @@ func TestSubmitSingle(t *testing.T) { require.NoError(t, err) require.NotNil(t, statusResponse.BlockHeight) - blockRoot := getBlockRootByHeight(t, int(*statusResponse.BlockHeight)) + blockRoot := node_client.GetBlockRootByHeight(t, bitcoind, int(*statusResponse.BlockHeight)) require.Equal(t, blockRoot, root) }) } @@ -116,12 +120,11 @@ func TestSubmitSingle(t *testing.T) { func TestSubmitMined(t *testing.T) { t.Run("submit mined tx", func(t *testing.T) { - // submit an unregistered, already mined transaction. ARC should return the status as MINED for the transaction. // given - address, _ := fundNewWallet(t) - utxos := getUtxos(t, address) + address, _ := node_client.FundNewWallet(t, bitcoind) + utxos := node_client.GetUtxos(t, bitcoind, address) rawTx, _ := bitcoind.GetRawTransaction(utxos[0].Txid) tx, _ := sdkTx.NewTransactionFromHex(rawTx.Hex) @@ -130,14 +133,14 @@ func TestSubmitMined(t *testing.T) { callbackReceivedChan := make(chan *TransactionResponse) callbackErrChan := make(chan error) - callbackUrl, token, shutdown := startCallbackSrv(t, callbackReceivedChan, callbackErrChan, nil) + callbackURL, token, shutdown := startCallbackSrv(t, callbackReceivedChan, callbackErrChan, nil) defer shutdown() // when _ = postRequest[TransactionResponse](t, arcEndpointV1Tx, createPayload(t, TransactionRequest{RawTx: exRawTx}), map[string]string{ - "X-WaitFor": Status_MINED, - "X-CallbackUrl": callbackUrl, + "X-WaitFor": StatusMined, + "X-CallbackUrl": callbackURL, "X-CallbackToken": token, }, http.StatusOK) @@ -147,7 +150,7 @@ func TestSubmitMined(t *testing.T) { select { case status := <-callbackReceivedChan: require.Equal(t, rawTx.TxID, status.Txid) - require.Equal(t, Status_MINED, status.TxStatus) + require.Equal(t, StatusMined, status.TxStatus) case err := <-callbackErrChan: t.Fatalf("callback error: %v", err) case <-callbackTimeout: @@ -158,12 +161,12 @@ func TestSubmitMined(t *testing.T) { func TestSubmitQueued(t *testing.T) { t.Run("queued", func(t *testing.T) { - address, privateKey := fundNewWallet(t) + address, privateKey := node_client.FundNewWallet(t, bitcoind) - utxos := getUtxos(t, address) + utxos := node_client.GetUtxos(t, bitcoind, address) require.True(t, len(utxos) > 0, "No UTXOs available for the address") - tx, err := createTx(privateKey, address, utxos[0]) + tx, err := node_client.CreateTx(privateKey, address, utxos[0]) require.NoError(t, err) rawTx, err := tx.EFHex() @@ -171,20 +174,20 @@ func TestSubmitQueued(t *testing.T) { resp := postRequest[TransactionResponse](t, arcEndpointV1Tx, createPayload(t, TransactionRequest{RawTx: rawTx}), map[string]string{ - "X-WaitFor": Status_QUEUED, + "X-WaitFor": StatusQueued, "X-MaxTimeout": strconv.Itoa(1), }, http.StatusOK) - require.Equal(t, Status_QUEUED, resp.TxStatus) + require.Equal(t, StatusQueued, resp.TxStatus) - statusUrl := fmt.Sprintf("%s/%s", arcEndpointV1Tx, tx.TxID()) + statusURL := fmt.Sprintf("%s/%s", arcEndpointV1Tx, tx.TxID()) checkSeenLoop: for { select { case <-time.NewTicker(1 * time.Second).C: - statusResponse := getRequest[TransactionResponse](t, statusUrl) - if statusResponse.TxStatus == Status_SEEN_ON_NETWORK { + statusResponse := getRequest[TransactionResponse](t, statusURL) + if statusResponse.TxStatus == StatusSeenOnNetwork { break checkSeenLoop } case <-time.NewTimer(10 * time.Second).C: @@ -192,14 +195,14 @@ func TestSubmitQueued(t *testing.T) { } } - generate(t, 1) + node_client.Generate(t, bitcoind, 1) checkMinedLoop: for { select { case <-time.NewTicker(1 * time.Second).C: - statusResponse := getRequest[TransactionResponse](t, statusUrl) - if statusResponse.TxStatus == Status_MINED { + statusResponse := getRequest[TransactionResponse](t, statusURL) + if statusResponse.TxStatus == StatusMined { break checkMinedLoop } @@ -211,7 +214,6 @@ func TestSubmitQueued(t *testing.T) { } func TestCallback(t *testing.T) { - tt := []struct { name string numberOfTxs int @@ -263,11 +265,11 @@ func TestCallback(t *testing.T) { for range tc.numberOfCallbackServers { callbackReceivedChan, callbackErrChan, calbackResponseFn := prepareCallback(t, callbacksNumber) - callbackUrl, token, shutdown := startCallbackSrv(t, callbackReceivedChan, callbackErrChan, calbackResponseFn) + callbackURL, token, shutdown := startCallbackSrv(t, callbackReceivedChan, callbackErrChan, calbackResponseFn) defer shutdown() callbackServers = append(callbackServers, &callbackServer{ - url: callbackUrl, + url: callbackURL, token: token, responseChan: callbackReceivedChan, errChan: callbackErrChan, @@ -275,18 +277,18 @@ func TestCallback(t *testing.T) { } // create transactions - address, privateKey := getNewWalletAddress(t) + address, privateKey := node_client.GetNewWalletAddress(t, bitcoind) for i := range tc.numberOfTxs { - sendToAddress(t, address, float64(10+i)) + node_client.SendToAddress(t, bitcoind, address, float64(10+i)) } - generate(t, 1) + node_client.Generate(t, bitcoind, 1) - utxos := getUtxos(t, address) + utxos := node_client.GetUtxos(t, bitcoind, address) require.True(t, len(utxos) >= tc.numberOfTxs, "Insufficient UTXOs available for the address") txs := make([]*sdkTx.Transaction, 0, tc.numberOfTxs) for i := range tc.numberOfTxs { - tx, err := createTx(privateKey, address, utxos[i]) + tx, err := node_client.CreateTx(privateKey, address, utxos[i]) require.NoError(t, err) txs = append(txs, tx) @@ -305,11 +307,10 @@ func TestCallback(t *testing.T) { testTxSubmission(t, callbackSrv.url, callbackSrv.token, false, tx) } } - } // mine transactions - generate(t, 1) + node_client.Generate(t, bitcoind, 1) // then @@ -341,7 +342,7 @@ func TestCallback(t *testing.T) { delete(expectedTxsCallbacks, callback.Txid) // remove after receiving expected callbacks } - require.Equal(t, Status_MINED, callback.TxStatus) + require.Equal(t, StatusMined, callback.TxStatus) case err := <-srv.errChan: t.Fatalf("callback server %d received - failed to parse %d callback %v", i, j, err) @@ -357,7 +358,6 @@ func TestCallback(t *testing.T) { } func TestBatchCallback(t *testing.T) { - tt := []struct { name string numberOfTxs int @@ -409,11 +409,11 @@ func TestBatchCallback(t *testing.T) { for range tc.numberOfCallbackServers { callbackReceivedChan, callbackErrChan, calbackResponseFn := prepareBatchCallback(t, callbacksNumber) - callbackUrl, token, shutdown := startBatchCallbackSrv(t, callbackReceivedChan, callbackErrChan, calbackResponseFn) + callbackURL, token, shutdown := startBatchCallbackSrv(t, callbackReceivedChan, callbackErrChan, calbackResponseFn) defer shutdown() callbackServers = append(callbackServers, &callbackServer{ - url: callbackUrl, + url: callbackURL, token: token, responseChan: callbackReceivedChan, errChan: callbackErrChan, @@ -421,18 +421,18 @@ func TestBatchCallback(t *testing.T) { } // create transactions - address, privateKey := getNewWalletAddress(t) + address, privateKey := node_client.GetNewWalletAddress(t, bitcoind) for i := range tc.numberOfTxs { - sendToAddress(t, address, float64(10+i)) + node_client.SendToAddress(t, bitcoind, address, float64(10+i)) } - generate(t, 1) + node_client.Generate(t, bitcoind, 1) - utxos := getUtxos(t, address) + utxos := node_client.GetUtxos(t, bitcoind, address) require.True(t, len(utxos) >= tc.numberOfTxs, "Insufficient UTXOs available for the address") txs := make([]*sdkTx.Transaction, 0, tc.numberOfTxs) for i := range tc.numberOfTxs { - tx, err := createTx(privateKey, address, utxos[i]) + tx, err := node_client.CreateTx(privateKey, address, utxos[i]) require.NoError(t, err) txs = append(txs, tx) @@ -450,11 +450,10 @@ func TestBatchCallback(t *testing.T) { testTxSubmission(t, callbackSrv.url, callbackSrv.token, true, tx) } } - } // mine transactions - generate(t, 1) + node_client.Generate(t, bitcoind, 1) // then @@ -489,7 +488,7 @@ func TestBatchCallback(t *testing.T) { delete(expectedTxsCallbacks, callback.Txid) // remove after receiving expected callbacks } - require.Equal(t, Status_MINED, callback.TxStatus) + require.Equal(t, StatusMined, callback.TxStatus) } case err := <-srv.errChan: @@ -506,7 +505,6 @@ func TestBatchCallback(t *testing.T) { } func TestSkipValidation(t *testing.T) { - tt := []struct { name string skipFeeValidation bool @@ -539,14 +537,14 @@ func TestSkipValidation(t *testing.T) { for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { - address, privateKey := fundNewWallet(t) + address, privateKey := node_client.FundNewWallet(t, bitcoind) - utxos := getUtxos(t, address) + utxos := node_client.GetUtxos(t, bitcoind, address) require.True(t, len(utxos) > 0, "No UTXOs available for the address") fee := uint64(0) - lowFeeTx, err := createTx(privateKey, address, utxos[0], fee) + lowFeeTx, err := node_client.CreateTx(privateKey, address, utxos[0], fee) require.NoError(t, err) lawFeeRawTx, err := lowFeeTx.EFHex() @@ -554,13 +552,13 @@ func TestSkipValidation(t *testing.T) { resp := postRequest[TransactionResponse](t, arcEndpointV1Tx, createPayload(t, TransactionRequest{RawTx: lawFeeRawTx}), map[string]string{ - "X-WaitFor": Status_SEEN_ON_NETWORK, + "X-WaitFor": StatusSeenOnNetwork, "X-SkipFeeValidation": strconv.FormatBool(tc.skipFeeValidation), "X-SkipTxValidation": strconv.FormatBool(tc.skipTxValidation), }, tc.expectedStatusCode) if tc.expectedStatusCode == http.StatusOK { - require.Equal(t, Status_SEEN_ON_NETWORK, resp.TxStatus) + require.Equal(t, StatusSeenOnNetwork, resp.TxStatus) } }) } @@ -589,7 +587,7 @@ func TestPostCumulativeFeesValidation(t *testing.T) { skipFeeValidation: true, }, expectedStatusCode: 200, - expectedTxStatus: Status_SEEN_ON_NETWORK, + expectedTxStatus: StatusSeenOnNetwork, }, { name: "post zero fee tx with cumulative fees validation and with skiping cumulative fee validation - cumulative fee validation is ommited", @@ -598,7 +596,7 @@ func TestPostCumulativeFeesValidation(t *testing.T) { }, lastTxFee: 17, expectedStatusCode: 200, - expectedTxStatus: Status_SEEN_ON_NETWORK, + expectedTxStatus: StatusSeenOnNetwork, }, { name: "post txs chain with too low fee with cumulative fees validation", @@ -616,7 +614,7 @@ func TestPostCumulativeFeesValidation(t *testing.T) { }, lastTxFee: 90, expectedStatusCode: 200, - expectedTxStatus: Status_SEEN_ON_NETWORK, + expectedTxStatus: StatusSeenOnNetwork, }, { name: "post txs chain with cumulative fees validation - chain too long - ignore it", @@ -626,26 +624,26 @@ func TestPostCumulativeFeesValidation(t *testing.T) { lastTxFee: 260, chainLong: 25, expectedStatusCode: 200, - expectedTxStatus: Status_REJECTED, + expectedTxStatus: StatusRejected, }, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { // when - address, privateKey := getNewWalletAddress(t) + address, privateKey := node_client.GetNewWalletAddress(t, bitcoind) // create mined ancestors const minedAncestorsCount = 2 var minedAncestors sdkTx.Transactions - sendToAddress(t, address, 0.0001) - sendToAddress(t, address, 0.00011) - utxos := getUtxos(t, address) + node_client.SendToAddress(t, bitcoind, address, 0.0001) + node_client.SendToAddress(t, bitcoind, address, 0.00011) + utxos := node_client.GetUtxos(t, bitcoind, address) require.GreaterOrEqual(t, len(utxos), minedAncestorsCount, "No UTXOs available for the address") for i := range minedAncestorsCount { - minedTx, err := createTx(privateKey, address, utxos[i], 10) + minedTx, err := node_client.CreateTx(privateKey, address, utxos[i], 10) require.NoError(t, err) minedAncestors = append(minedAncestors, minedTx) @@ -670,7 +668,7 @@ func TestPostCumulativeFeesValidation(t *testing.T) { for i := 0; i < zeroChainCount; i++ { output := parentTx.Outputs[0] - utxo := NodeUnspentUtxo{ + utxo := node_client.UnspentOutput{ Txid: parentTx.TxID(), Vout: 0, Address: address, @@ -678,7 +676,7 @@ func TestPostCumulativeFeesValidation(t *testing.T) { Amount: float64(float64(output.Satoshis) / 1e8), } - tx, err := createTx(privateKey, address, utxo, zeroFee) + tx, err := node_client.CreateTx(privateKey, address, utxo, zeroFee) require.NoError(t, err) chain[i] = tx @@ -695,11 +693,11 @@ func TestPostCumulativeFeesValidation(t *testing.T) { body := TransactionRequest{RawTx: rawTx} resp := postRequest[TransactionResponse](t, arcEndpointV1Tx, createPayload(t, body), - map[string]string{"X-WaitFor": Status_SEEN_ON_NETWORK}, 200) + map[string]string{"X-WaitFor": StatusSeenOnNetwork}, 200) - require.Equal(t, Status_SEEN_ON_NETWORK, resp.TxStatus) + require.Equal(t, StatusSeenOnNetwork, resp.TxStatus) } - generate(t, 1) // mine posted transactions + node_client.Generate(t, bitcoind, 1) // mine posted transactions for _, chain := range zeroFeeChains { for _, tx := range chain { @@ -709,22 +707,22 @@ func TestPostCumulativeFeesValidation(t *testing.T) { body := TransactionRequest{RawTx: rawTx} resp := postRequest[TransactionResponse](t, arcEndpointV1Tx, createPayload(t, body), map[string]string{ - "X-WaitFor": Status_SEEN_ON_NETWORK, + "X-WaitFor": StatusSeenOnNetwork, "X-SkipFeeValidation": strconv.FormatBool(true), }, 200) - require.Equal(t, Status_SEEN_ON_NETWORK, resp.TxStatus) + require.Equal(t, StatusSeenOnNetwork, resp.TxStatus) } } // then // create last transaction - var nodeUtxos []NodeUnspentUtxo + var nodeUtxos []node_client.UnspentOutput for _, chain := range zeroFeeChains { // get otput from the lastes tx in the chain parentTx := chain[len(chain)-1] output := parentTx.Outputs[0] - utxo := NodeUnspentUtxo{ + utxo := node_client.UnspentOutput{ Txid: parentTx.TxID(), Vout: 0, Address: address, @@ -735,7 +733,7 @@ func TestPostCumulativeFeesValidation(t *testing.T) { nodeUtxos = append(nodeUtxos, utxo) } - lastTx, err := createTxFrom(privateKey, address, nodeUtxos, tc.lastTxFee) + lastTx, err := node_client.CreateTxFrom(privateKey, address, nodeUtxos, tc.lastTxFee) require.NoError(t, err) rawTx, err := lastTx.EFHex() @@ -743,7 +741,7 @@ func TestPostCumulativeFeesValidation(t *testing.T) { response := postRequest[TransactionResponse](t, arcEndpointV1Tx, createPayload(t, TransactionRequest{RawTx: rawTx}), map[string]string{ - "X-WaitFor": Status_SEEN_ON_NETWORK, + "X-WaitFor": StatusSeenOnNetwork, "X-CumulativeFeeValidation": strconv.FormatBool(tc.options.performCumulativeFeesValidation), "X-SkipFeeValidation": strconv.FormatBool(tc.options.skipFeeValidation), }, tc.expectedStatusCode) @@ -790,14 +788,14 @@ func TestScriptValidation(t *testing.T) { } for _, tc := range tt { - address, privateKey := fundNewWallet(t) + address, privateKey := node_client.FundNewWallet(t, bitcoind) - utxos := getUtxos(t, address) + utxos := node_client.GetUtxos(t, bitcoind, address) require.True(t, len(utxos) > 0, "No UTXOs available for the address") fee := uint64(10) - lowFeeTx, err := createTx(privateKey, address, utxos[0], fee) + lowFeeTx, err := node_client.CreateTx(privateKey, address, utxos[0], fee) require.NoError(t, err) sc, err := generateNewUnlockingScriptFromRandomKey() diff --git a/test/submit_02_batch_test.go b/test/submit_02_batch_test.go new file mode 100644 index 000000000..e65b49b15 --- /dev/null +++ b/test/submit_02_batch_test.go @@ -0,0 +1,63 @@ +//go:build e2e + +package test + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/bitcoin-sv/arc/internal/node_client" +) + +func TestBatchChainedTxs(t *testing.T) { + t.Run("submit batch of chained transactions", func(t *testing.T) { + address, privateKey := node_client.FundNewWallet(t, bitcoind) + + utxos := node_client.GetUtxos(t, bitcoind, address) + require.True(t, len(utxos) > 0, "No UTXOs available for the address") + + txs, err := node_client.CreateTxChain(privateKey, utxos[0], 20) + require.NoError(t, err) + + request := make([]TransactionRequest, len(txs)) + for i, tx := range txs { + rawTx, err := tx.EFHex() + require.NoError(t, err) + request[i] = TransactionRequest{ + RawTx: rawTx, + } + } + + // Send POST request + t.Logf("submitting batch of %d chained txs", len(txs)) + resp := postRequest[TransactionResponseBatch](t, arcEndpointV1Txs, createPayload(t, request), nil, http.StatusOK) + hasFailed := false + for i, txResponse := range resp { + if !assert.Equal(t, StatusSeenOnNetwork, txResponse.TxStatus, fmt.Sprintf("index: %d", i)) { + hasFailed = true + } + } + if hasFailed { + t.FailNow() + } + + time.Sleep(1 * time.Second) + + // repeat request to ensure response remains the same + t.Logf("re-submitting batch of %d chained txs", len(txs)) + resp = postRequest[TransactionResponseBatch](t, arcEndpointV1Txs, createPayload(t, request), nil, http.StatusOK) + for i, txResponse := range resp { + if !assert.Equal(t, StatusSeenOnNetwork, txResponse.TxStatus, fmt.Sprintf("index: %d", i)) { + hasFailed = true + } + } + if hasFailed { + t.FailNow() + } + }) +} diff --git a/test/submit_double_spending_test.go b/test/submit_03_double_spending_test.go similarity index 53% rename from test/submit_double_spending_test.go rename to test/submit_03_double_spending_test.go index 7f755b23b..6ded35c70 100644 --- a/test/submit_double_spending_test.go +++ b/test/submit_03_double_spending_test.go @@ -11,24 +11,26 @@ import ( sdkTx "github.com/bitcoin-sv/go-sdk/transaction" "github.com/stretchr/testify/require" + + "github.com/bitcoin-sv/arc/internal/node_client" ) func TestDoubleSpend(t *testing.T) { t.Run("submit tx with a double spend tx before and after tx got mined - ext format", func(t *testing.T) { - address, privateKey := fundNewWallet(t) + address, privateKey := node_client.FundNewWallet(t, bitcoind) - utxos := getUtxos(t, address) + utxos := node_client.GetUtxos(t, bitcoind, address) require.True(t, len(utxos) > 0, "No UTXOs available for the address") - tx1, err := createTx(privateKey, address, utxos[0]) + tx1, err := node_client.CreateTx(privateKey, address, utxos[0]) require.NoError(t, err) // submit first transaction rawTx, err := tx1.EFHex() require.NoError(t, err) - resp := postRequest[TransactionResponse](t, arcEndpointV1Tx, createPayload(t, TransactionRequest{RawTx: rawTx}), map[string]string{"X-WaitFor": Status_SEEN_ON_NETWORK}, http.StatusOK) - require.Equal(t, Status_SEEN_ON_NETWORK, resp.TxStatus) + resp := postRequest[TransactionResponse](t, arcEndpointV1Tx, createPayload(t, TransactionRequest{RawTx: rawTx}), map[string]string{"X-WaitFor": StatusSeenOnNetwork}, http.StatusOK) + require.Equal(t, StatusSeenOnNetwork, resp.TxStatus) // send double spending transaction when first tx is in mempool tx2 := createTxToNewAddress(t, privateKey, utxos[0]) @@ -36,50 +38,50 @@ func TestDoubleSpend(t *testing.T) { require.NoError(t, err) // submit second transaction - resp = postRequest[TransactionResponse](t, arcEndpointV1Tx, createPayload(t, TransactionRequest{RawTx: rawTx}), map[string]string{"X-WaitFor": Status_DOUBLE_SPEND_ATTEMPTED}, http.StatusOK) - require.Equal(t, Status_DOUBLE_SPEND_ATTEMPTED, resp.TxStatus) + resp = postRequest[TransactionResponse](t, arcEndpointV1Tx, createPayload(t, TransactionRequest{RawTx: rawTx}), map[string]string{"X-WaitFor": StatusDoubleSpendAttempted}, http.StatusOK) + require.Equal(t, StatusDoubleSpendAttempted, resp.TxStatus) require.Equal(t, []string{tx1.TxID()}, *resp.CompetingTxs) // give arc time to update the status of all competing transactions time.Sleep(5 * time.Second) - statusUrl := fmt.Sprintf("%s/%s", arcEndpointV1Tx, tx1.TxID()) - statusResp := getRequest[TransactionResponse](t, statusUrl) + statusURL := fmt.Sprintf("%s/%s", arcEndpointV1Tx, tx1.TxID()) + statusResp := getRequest[TransactionResponse](t, statusURL) // verify that the first tx was also set to DOUBLE_SPEND_ATTEMPTED - require.Equal(t, Status_DOUBLE_SPEND_ATTEMPTED, statusResp.TxStatus) + require.Equal(t, StatusDoubleSpendAttempted, statusResp.TxStatus) require.Equal(t, []string{tx2.TxID()}, *statusResp.CompetingTxs) // mine the first tx - generate(t, 1) + node_client.Generate(t, bitcoind, 1) // verify that one of the competing transactions was mined, and the other was rejected - tx1_statusUrl := fmt.Sprintf("%s/%s", arcEndpointV1Tx, tx1.TxID()) - tx1_statusResp := getRequest[TransactionResponse](t, tx1_statusUrl) + tx1StatusURL := fmt.Sprintf("%s/%s", arcEndpointV1Tx, tx1.TxID()) + tx1StatusResp := getRequest[TransactionResponse](t, tx1StatusURL) - tx2_statusUrl := fmt.Sprintf("%s/%s", arcEndpointV1Tx, tx2.TxID()) - tx2_statusResp := getRequest[TransactionResponse](t, tx2_statusUrl) + tx2StatusURL := fmt.Sprintf("%s/%s", arcEndpointV1Tx, tx2.TxID()) + tx2StatusResp := getRequest[TransactionResponse](t, tx2StatusURL) - require.Contains(t, []string{tx1_statusResp.TxStatus, tx2_statusResp.TxStatus}, Status_MINED) - require.Contains(t, []string{tx1_statusResp.TxStatus, tx2_statusResp.TxStatus}, Status_REJECTED) + require.Contains(t, []string{tx1StatusResp.TxStatus, tx2StatusResp.TxStatus}, StatusMined) + require.Contains(t, []string{tx1StatusResp.TxStatus, tx2StatusResp.TxStatus}, StatusRejected) - require.Contains(t, []string{*tx1_statusResp.ExtraInfo, *tx2_statusResp.ExtraInfo}, "previously double spend attempted") - require.Contains(t, []string{*tx1_statusResp.ExtraInfo, *tx2_statusResp.ExtraInfo}, "double spend attempted") + require.Contains(t, []string{*tx1StatusResp.ExtraInfo, *tx2StatusResp.ExtraInfo}, "previously double spend attempted") + require.Contains(t, []string{*tx1StatusResp.ExtraInfo, *tx2StatusResp.ExtraInfo}, "double spend attempted") // send double spending transaction when previous tx was mined txMined := createTxToNewAddress(t, privateKey, utxos[0]) rawTx, err = txMined.EFHex() require.NoError(t, err) - resp = postRequest[TransactionResponse](t, arcEndpointV1Tx, createPayload(t, TransactionRequest{RawTx: rawTx}), map[string]string{"X-WaitFor": Status_SEEN_IN_ORPHAN_MEMPOOL}, http.StatusOK) - require.Equal(t, Status_SEEN_IN_ORPHAN_MEMPOOL, resp.TxStatus) + resp = postRequest[TransactionResponse](t, arcEndpointV1Tx, createPayload(t, TransactionRequest{RawTx: rawTx}), map[string]string{"X-WaitFor": StatusSeenInOrphanMempool}, http.StatusOK) + require.Equal(t, StatusSeenInOrphanMempool, resp.TxStatus) }) } -func createTxToNewAddress(t *testing.T, privateKey string, utxo NodeUnspentUtxo) *sdkTx.Transaction { +func createTxToNewAddress(t *testing.T, privateKey string, utxo node_client.UnspentOutput) *sdkTx.Transaction { address, err := bitcoind.GetNewAddress() require.NoError(t, err) - tx1, err := createTx(privateKey, address, utxo) + tx1, err := node_client.CreateTx(privateKey, address, utxo) require.NoError(t, err) return tx1 diff --git a/test/submit_beef_test.go b/test/submit_04_beef_test.go similarity index 89% rename from test/submit_beef_test.go rename to test/submit_04_beef_test.go index bd3f3327b..72fd73d5c 100644 --- a/test/submit_beef_test.go +++ b/test/submit_04_beef_test.go @@ -13,45 +13,46 @@ import ( "github.com/libsv/go-bc" "github.com/libsv/go-p2p/chaincfg/chainhash" "github.com/stretchr/testify/require" + + "github.com/bitcoin-sv/arc/internal/node_client" ) func TestBeef(t *testing.T) { - t.Run("valid beef with unmined parents - response for the tip, callback for each", func(t *testing.T) { // given - address, privateKey := getNewWalletAddress(t) - dstAddress, _ := getNewWalletAddress(t) + address, privateKey := node_client.GetNewWalletAddress(t, bitcoind) + dstAddress, _ := node_client.GetNewWalletAddress(t, bitcoind) - txID := sendToAddress(t, address, 0.002) - hash := generate(t, 1) + txID := node_client.SendToAddress(t, bitcoind, address, 0.002) + hash := node_client.Generate(t, bitcoind, 1) beef, middleTx, tx, expectedCallbacks := prepareBeef(t, txID, hash, address, dstAddress, privateKey) callbackReceivedChan := make(chan *TransactionResponse, expectedCallbacks) // do not block callback server responses callbackErrChan := make(chan error, expectedCallbacks) - callbackUrl, token, shutdown := startCallbackSrv(t, callbackReceivedChan, callbackErrChan, nil) + callbackURL, token, shutdown := startCallbackSrv(t, callbackReceivedChan, callbackErrChan, nil) defer shutdown() waitForStatusTimeoutSeconds := 30 // when resp := postRequest[TransactionResponse](t, arcEndpointV1Tx, createPayload(t, TransactionRequest{RawTx: beef}), map[string]string{ - "X-WaitFor": Status_SEEN_ON_NETWORK, - "X-CallbackUrl": callbackUrl, + "X-WaitFor": StatusSeenOnNetwork, + "X-CallbackUrl": callbackURL, "X-CallbackToken": token, "X-MaxTimeout": strconv.Itoa(waitForStatusTimeoutSeconds), }, http.StatusOK) // then - require.Equal(t, Status_SEEN_ON_NETWORK, resp.TxStatus) + require.Equal(t, StatusSeenOnNetwork, resp.TxStatus) - generate(t, 1) + node_client.Generate(t, bitcoind, 1) - statusUrl := fmt.Sprintf("%s/%s", arcEndpointV1Tx, tx.TxID()) - statusResp := getRequest[TransactionResponse](t, statusUrl) - require.Equal(t, Status_MINED, statusResp.TxStatus) + statusURL := fmt.Sprintf("%s/%s", arcEndpointV1Tx, tx.TxID()) + statusResp := getRequest[TransactionResponse](t, statusURL) + require.Equal(t, StatusMined, statusResp.TxStatus) // verify callbacks for both unmined txs in BEEF lastTxCallbackReceived := false @@ -61,10 +62,10 @@ func TestBeef(t *testing.T) { select { case status := <-callbackReceivedChan: if status.Txid == middleTx.TxID() { - require.Equal(t, Status_MINED, status.TxStatus) + require.Equal(t, StatusMined, status.TxStatus) middleTxCallbackReceived = true } else if status.Txid == tx.TxID() { - require.Equal(t, Status_MINED, status.TxStatus) + require.Equal(t, StatusMined, status.TxStatus) lastTxCallbackReceived = true } else { t.Fatalf("received unknown status for txid: %s", status.Txid) @@ -79,7 +80,6 @@ func TestBeef(t *testing.T) { require.Equal(t, true, lastTxCallbackReceived) require.Equal(t, true, middleTxCallbackReceived) }) - } func TestBeef_Fail(t *testing.T) { @@ -108,7 +108,6 @@ func TestBeef_Fail(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - resp := postRequest[ErrorFee](t, arcEndpointV1Tx, createPayload(t, TransactionRequest{RawTx: tc.beefStr}), nil, tc.expectedErrCode) require.Equal(t, tc.expectedErrMsgDetail, resp.Detail) require.Equal(t, tc.expectedErrTxID, resp.Txid) @@ -119,11 +118,11 @@ func TestBeef_Fail(t *testing.T) { func prepareBeef(t *testing.T, inputTxID, blockHash, fromAddress, toAddress, privateKey string) (string, *sdkTx.Transaction, *sdkTx.Transaction, int) { expectedCallbacks := 0 - rawTx := getRawTx(t, inputTxID) + rawTx := node_client.GetRawTx(t, bitcoind, inputTxID) t.Logf("rawTx: %+v", rawTx) require.Equal(t, blockHash, rawTx.BlockHash, "block hash mismatch") - blockData := getBlockDataByBlockHash(t, blockHash) + blockData := node_client.GetBlockDataByBlockHash(t, bitcoind, blockHash) t.Logf("blockdata: %+v", blockData) merkleHashes, txIndex := prepareMerkleHashesAndTxIndex(t, blockData.Txs, inputTxID) @@ -136,26 +135,26 @@ func prepareBeef(t *testing.T, inputTxID, blockHash, fromAddress, toAddress, pri t.Logf("merkleroot from bump: %s", merkleRootFromBump) require.Equal(t, blockData.MerkleRoot, merkleRootFromBump, "merkle roots mismatch") - utxos := getUtxos(t, fromAddress) + utxos := node_client.GetUtxos(t, bitcoind, fromAddress) require.True(t, len(utxos) > 0, "No UTXOs available for the address") - middleAddress, middlePrivKey := getNewWalletAddress(t) - middleTx, err := createTx(privateKey, middleAddress, utxos[0]) + middleAddress, middlePrivKey := node_client.GetNewWalletAddress(t, bitcoind) + middleTx, err := node_client.CreateTx(privateKey, middleAddress, utxos[0]) require.NoError(t, err, "could not create middle tx for beef") t.Logf("middle tx created, hex: %s, txid: %s", middleTx.String(), middleTx.TxID()) - expectedCallbacks += 1 + expectedCallbacks++ - middleUtxo := NodeUnspentUtxo{ + middleUtxo := node_client.UnspentOutput{ Txid: middleTx.TxID(), Vout: 0, ScriptPubKey: middleTx.Outputs[0].LockingScriptHex(), Amount: float64(middleTx.Outputs[0].Satoshis) / 1e8, // satoshis to BSV } - tx, err := createTx(middlePrivKey, toAddress, middleUtxo) + tx, err := node_client.CreateTx(middlePrivKey, toAddress, middleUtxo) require.NoError(t, err, "could not create tx") t.Logf("tx created, hex: %s, txid: %s", tx.String(), tx.TxID()) - expectedCallbacks += 1 + expectedCallbacks++ beef := buildBeefString(t, rawTx.Hex, bump, middleTx, tx) t.Logf("beef created, hex: %s", beef) diff --git a/test/submit_batch_test.go b/test/submit_batch_test.go deleted file mode 100644 index d2188dbc3..000000000 --- a/test/submit_batch_test.go +++ /dev/null @@ -1,130 +0,0 @@ -//go:build e2e - -package test - -import ( - "fmt" - "net/http" - "testing" - "time" - - ec "github.com/bitcoin-sv/go-sdk/primitives/ec" - "github.com/stretchr/testify/require" - - sdkTx "github.com/bitcoin-sv/go-sdk/transaction" - "github.com/bitcoin-sv/go-sdk/transaction/template/p2pkh" - "github.com/bitcoinsv/bsvutil" - "github.com/stretchr/testify/assert" -) - -func TestBatchChainedTxs(t *testing.T) { - - t.Run("submit batch of chained transactions", func(t *testing.T) { - address, privateKey := fundNewWallet(t) - - utxos := getUtxos(t, address) - require.True(t, len(utxos) > 0, "No UTXOs available for the address") - - txs, err := createTxChain(privateKey, utxos[0], 20) - require.NoError(t, err) - - request := make([]TransactionRequest, len(txs)) - for i, tx := range txs { - rawTx, err := tx.EFHex() - require.NoError(t, err) - request[i] = TransactionRequest{ - RawTx: rawTx, - } - } - - // Send POST request - t.Logf("submitting batch of %d chained txs", len(txs)) - resp := postRequest[TransactionResponseBatch](t, arcEndpointV1Txs, createPayload(t, request), nil, http.StatusOK) - hasFailed := false - for i, txResponse := range resp { - if !assert.Equal(t, Status_SEEN_ON_NETWORK, txResponse.TxStatus, fmt.Sprintf("index: %d", i)) { - hasFailed = true - } - } - if hasFailed { - t.FailNow() - } - - time.Sleep(1 * time.Second) - - // repeat request to ensure response remains the same - t.Logf("re-submitting batch of %d chained txs", len(txs)) - resp = postRequest[TransactionResponseBatch](t, arcEndpointV1Txs, createPayload(t, request), nil, http.StatusOK) - for i, txResponse := range resp { - if !assert.Equal(t, Status_SEEN_ON_NETWORK, txResponse.TxStatus, fmt.Sprintf("index: %d", i)) { - hasFailed = true - } - } - if hasFailed { - t.FailNow() - } - }) -} - -func createTxChain(privateKey string, utxo0 NodeUnspentUtxo, length int) ([]*sdkTx.Transaction, error) { - batch := make([]*sdkTx.Transaction, length) - - utxoTxID := utxo0.Txid - utxoVout := uint32(utxo0.Vout) - utxoSatoshis := uint64(utxo0.Amount * 1e8) - utxoScript := utxo0.ScriptPubKey - utxoAddress := utxo0.Address - - for i := 0; i < length; i++ { - tx := sdkTx.NewTransaction() - - utxo, err := sdkTx.NewUTXO(utxoTxID, utxoVout, utxoScript, utxoSatoshis) - if err != nil { - return nil, fmt.Errorf("failed creating UTXO: %v", err) - } - - err = tx.AddInputsFromUTXOs(utxo) - if err != nil { - return nil, fmt.Errorf("failed adding input: %v", err) - } - - amountToSend := utxoSatoshis - feeSat - - err = tx.PayToAddress(utxoAddress, amountToSend) - if err != nil { - return nil, fmt.Errorf("failed to pay to address: %v", err) - } - - // Sign the input - wif, err := bsvutil.DecodeWIF(privateKey) - if err != nil { - return nil, err - } - - privateKeyDecoded := wif.PrivKey.Serialize() - pk, _ := ec.PrivateKeyFromBytes(privateKeyDecoded) - - unlockingScriptTemplate, err := p2pkh.Unlock(pk, nil) - if err != nil { - return nil, err - } - - for _, input := range tx.Inputs { - input.UnlockingScriptTemplate = unlockingScriptTemplate - } - - err = tx.Sign() - if err != nil { - return nil, err - } - - batch[i] = tx - - utxoTxID = tx.TxID() - utxoVout = 0 - utxoSatoshis = amountToSend - utxoScript = utxo0.ScriptPubKey - } - - return batch, nil -} diff --git a/test/utils.go b/test/utils.go index d225b5004..15f2d877d 100644 --- a/test/utils.go +++ b/test/utils.go @@ -20,23 +20,16 @@ import ( "github.com/bitcoin-sv/go-sdk/script" sdkTx "github.com/bitcoin-sv/go-sdk/transaction" "github.com/bitcoin-sv/go-sdk/transaction/template/p2pkh" - "github.com/bitcoinsv/bsvutil" "github.com/stretchr/testify/require" ) const ( - Status_QUEUED = "QUEUED" - Status_RECEIVED = "RECEIVED" - Status_STORED = "STORED" - Status_ANNOUNCED_TO_NETWORK = "ANNOUNCED_TO_NETWORK" - Status_REQUESTED_BY_NETWORK = "REQUESTED_BY_NETWORK" - Status_SENT_TO_NETWORK = "SENT_TO_NETWORK" - Status_ACCEPTED_BY_NETWORK = "ACCEPTED_BY_NETWORK" - Status_SEEN_IN_ORPHAN_MEMPOOL = "SEEN_IN_ORPHAN_MEMPOOL" - Status_SEEN_ON_NETWORK = "SEEN_ON_NETWORK" - Status_DOUBLE_SPEND_ATTEMPTED = "DOUBLE_SPEND_ATTEMPTED" - Status_REJECTED = "REJECTED" - Status_MINED = "MINED" + StatusQueued = "QUEUED" + StatusSeenInOrphanMempool = "SEEN_IN_ORPHAN_MEMPOOL" + StatusSeenOnNetwork = "SEEN_ON_NETWORK" + StatusDoubleSpendAttempted = "DOUBLE_SPEND_ATTEMPTED" + StatusRejected = "REJECTED" + StatusMined = "MINED" ) type TransactionResponseBatch []TransactionResponse @@ -68,30 +61,6 @@ type ErrorFee struct { Txid string `json:"txid"` } -type NodeUnspentUtxo struct { - Txid string `json:"txid"` - Vout uint32 `json:"vout"` - Address string `json:"address"` - Account string `json:"account"` - ScriptPubKey string `json:"scriptPubKey"` - Amount float64 `json:"amount"` - Confirmations int `json:"confirmations"` - Spendable bool `json:"spendable"` - Solvable bool `json:"solvable"` - Safe bool `json:"safe"` -} - -type RawTransaction struct { - Hex string `json:"hex"` - BlockHash string `json:"blockhash,omitempty"` -} - -type BlockData struct { - Height uint64 `json:"height"` - Txs []string `json:"txs"` - MerkleRoot string `json:"merkleroot"` -} - func createPayload[T any](t *testing.T, body T) io.Reader { payLoad, err := json.Marshal(body) require.NoError(t, err) @@ -141,188 +110,6 @@ func postRequest[T any](t *testing.T, url string, reader io.Reader, headers map[ return response } -// PtrTo returns a pointer to the given value. -func PtrTo[T any](v T) *T { - return &v -} - -func getNewWalletAddress(t *testing.T) (address, privateKey string) { - address, err := bitcoind.GetNewAddress() - require.NoError(t, err) - t.Logf("new address: %s", address) - - privateKey, err = bitcoind.DumpPrivKey(address) - require.NoError(t, err) - t.Logf("new private key: %s", privateKey) - - accountName := "test-account" - err = bitcoind.SetAccount(address, accountName) - require.NoError(t, err) - - t.Logf("account %s created", accountName) - - return -} - -func sendToAddress(t *testing.T, address string, bsv float64) (txID string) { - t.Helper() - - txID, err := bitcoind.SendToAddress(address, bsv) - require.NoError(t, err) - - t.Logf("sent %f to %s: %s", bsv, address, txID) - - return -} - -func generate(t *testing.T, amount uint64) string { - t.Helper() - - // run command instead - blockHash := execCommandGenerate(t, amount) - time.Sleep(5 * time.Second) - - t.Logf( - "generated %d block(s): block hash: %s", - amount, - blockHash, - ) - - return blockHash -} - -func execCommandGenerate(t *testing.T, amount uint64) string { - t.Helper() - t.Logf("Amount to generate: %d", amount) - - hashes, err := bitcoind.Generate(float64(amount)) - require.NoError(t, err) - - return hashes[len(hashes)-1] -} - -func getUtxos(t *testing.T, address string) []NodeUnspentUtxo { - t.Helper() - - data, err := bitcoind.ListUnspent([]string{address}) - require.NoError(t, err) - - result := make([]NodeUnspentUtxo, len(data)) - - for index, utxo := range data { - t.Logf("UTXO Txid: %s, Amount: %f, Address: %s\n", utxo.TXID, utxo.Amount, utxo.Address) - result[index] = NodeUnspentUtxo{ - Txid: utxo.TXID, - Vout: utxo.Vout, - Address: utxo.Address, - ScriptPubKey: utxo.ScriptPubKey, - Amount: utxo.Amount, - Confirmations: int(utxo.Confirmations), - } - } - - return result -} - -func getBlockRootByHeight(t *testing.T, blockHeight int) string { - t.Helper() - block, err := bitcoind.GetBlockByHeight(blockHeight) - require.NoError(t, err) - - return block.MerkleRoot -} - -func getRawTx(t *testing.T, txID string) RawTransaction { - t.Helper() - - rawTx, err := bitcoind.GetRawTransaction(txID) - require.NoError(t, err) - - return RawTransaction{ - Hex: rawTx.Hex, - BlockHash: rawTx.BlockHash, - } -} - -func getBlockDataByBlockHash(t *testing.T, blockHash string) BlockData { - t.Helper() - - block, err := bitcoind.GetBlock(blockHash) - require.NoError(t, err) - - return BlockData{ - Height: block.Height, - Txs: block.Tx, - MerkleRoot: block.MerkleRoot, - } -} - -func createTx(privateKey string, address string, utxo NodeUnspentUtxo, fee ...uint64) (*sdkTx.Transaction, error) { - return createTxFrom(privateKey, address, []NodeUnspentUtxo{utxo}, fee...) -} - -func createTxFrom(privateKey string, address string, utxos []NodeUnspentUtxo, fee ...uint64) (*sdkTx.Transaction, error) { - tx := sdkTx.NewTransaction() - - // Add an input using the UTXOs - for _, utxo := range utxos { - utxoTxID := utxo.Txid - utxoVout := utxo.Vout - utxoSatoshis := uint64(utxo.Amount * 1e8) // Convert BTC to satoshis - utxoScript := utxo.ScriptPubKey - - u, err := sdkTx.NewUTXO(utxoTxID, utxoVout, utxoScript, utxoSatoshis) - if err != nil { - return nil, fmt.Errorf("failed creating UTXO: %v", err) - } - err = tx.AddInputsFromUTXOs(u) - if err != nil { - return nil, fmt.Errorf("failed adding input: %v", err) - } - } - // Add an output to the address you've previously created - recipientAddress := address - - var feeValue uint64 - if len(fee) > 0 { - feeValue = fee[0] - } else { - feeValue = 20 // Set your default fee value here - } - amountToSend := tx.TotalInputSatoshis() - feeValue - - err := tx.PayToAddress(recipientAddress, amountToSend) - if err != nil { - return nil, fmt.Errorf("failed to pay to address: %v", err) - } - - // Sign the input - wif, err := bsvutil.DecodeWIF(privateKey) - if err != nil { - return nil, fmt.Errorf("failed to decode WIF: %v", err) - } - - // Extract raw private key bytes directly from the WIF structure - privateKeyDecoded := wif.PrivKey.Serialize() - pk, _ := ec.PrivateKeyFromBytes(privateKeyDecoded) - - unlockingScriptTemplate, err := p2pkh.Unlock(pk, nil) - if err != nil { - return nil, err - } - - for _, input := range tx.Inputs { - input.UnlockingScriptTemplate = unlockingScriptTemplate - } - - err = tx.Sign() - if err != nil { - return nil, err - } - - return tx, nil -} - func generateRandomString(length int) string { const letterBytes = "abcdefghijklmnopqrstuvwxyz0123456789" @@ -337,7 +124,7 @@ type callbackResponseFn func(w http.ResponseWriter, rc chan *TransactionResponse type callbackBatchResponseFn func(w http.ResponseWriter, rc chan *CallbackBatchResponse, ec chan error, status *CallbackBatchResponse) // use buffered channels for multiple callbacks -func startCallbackSrv(t *testing.T, receivedChan chan *TransactionResponse, errChan chan error, alternativeResponseFn callbackResponseFn) (callbackUrl, token string, shutdownFn func()) { +func startCallbackSrv(t *testing.T, receivedChan chan *TransactionResponse, errChan chan error, alternativeResponseFn callbackResponseFn) (callbackURL, token string, shutdownFn func()) { t.Helper() callback := generateRandomString(16) @@ -347,7 +134,7 @@ func startCallbackSrv(t *testing.T, receivedChan chan *TransactionResponse, errC hostname, err := os.Hostname() require.NoError(t, err) - callbackUrl = fmt.Sprintf("http://%s:9000/%s", hostname, callback) + callbackURL = fmt.Sprintf("http://%s:9000/%s", hostname, callback) http.HandleFunc(fmt.Sprintf("/%s", callback), func(w http.ResponseWriter, req *http.Request) { // check auth @@ -381,7 +168,7 @@ func startCallbackSrv(t *testing.T, receivedChan chan *TransactionResponse, errC srv := &http.Server{Addr: ":9000"} shutdownFn = func() { - t.Logf("shutting down callback listener %s", callbackUrl) + t.Logf("shutting down callback listener %s", callbackURL) close(receivedChan) close(errChan) @@ -392,7 +179,7 @@ func startCallbackSrv(t *testing.T, receivedChan chan *TransactionResponse, errC } go func(server *http.Server) { - t.Logf("starting callback server %s", callbackUrl) + t.Logf("starting callback server %s", callbackURL) err := server.ListenAndServe() if err != nil { return @@ -403,7 +190,7 @@ func startCallbackSrv(t *testing.T, receivedChan chan *TransactionResponse, errC } // use buffered channels for multiple callbacks -func startBatchCallbackSrv(t *testing.T, receivedChan chan *CallbackBatchResponse, errChan chan error, alternativeResponseFn callbackBatchResponseFn) (callbackUrl, token string, shutdownFn func()) { +func startBatchCallbackSrv(t *testing.T, receivedChan chan *CallbackBatchResponse, errChan chan error, alternativeResponseFn callbackBatchResponseFn) (callbackURL, token string, shutdownFn func()) { t.Helper() callback := generateRandomString(16) @@ -413,7 +200,7 @@ func startBatchCallbackSrv(t *testing.T, receivedChan chan *CallbackBatchRespons hostname, err := os.Hostname() require.NoError(t, err) - callbackUrl = fmt.Sprintf("http://%s:9000/%s/batch", hostname, callback) + callbackURL = fmt.Sprintf("http://%s:9000/%s/batch", hostname, callback) http.HandleFunc(fmt.Sprintf("/%s/batch", callback), func(w http.ResponseWriter, req *http.Request) { // check auth @@ -447,7 +234,7 @@ func startBatchCallbackSrv(t *testing.T, receivedChan chan *CallbackBatchRespons srv := &http.Server{Addr: ":9000"} shutdownFn = func() { - t.Logf("shutting down callback listener %s", callbackUrl) + t.Logf("shutting down callback listener %s", callbackURL) close(receivedChan) close(errChan) @@ -458,7 +245,7 @@ func startBatchCallbackSrv(t *testing.T, receivedChan chan *CallbackBatchRespons } go func(server *http.Server) { - t.Logf("starting callback server %s", callbackUrl) + t.Logf("starting callback server %s", callbackURL) err := server.ListenAndServe() if err != nil { return @@ -509,18 +296,7 @@ func respondToCallback(w http.ResponseWriter, success bool) error { return nil } -func fundNewWallet(t *testing.T) (addr, privKey string) { - t.Helper() - - addr, privKey = getNewWalletAddress(t) - sendToAddress(t, addr, 0.001) - // mine a block with the transaction from above - generate(t, 1) - - return -} - -func testTxSubmission(t *testing.T, callbackUrl string, token string, callbackBatch bool, tx *sdkTx.Transaction) { +func testTxSubmission(t *testing.T, callbackURL string, token string, callbackBatch bool, tx *sdkTx.Transaction) { t.Helper() rawTx, err := tx.EFHex() @@ -528,13 +304,13 @@ func testTxSubmission(t *testing.T, callbackUrl string, token string, callbackBa response := postRequest[TransactionResponse](t, arcEndpointV1Tx, createPayload(t, TransactionRequest{RawTx: rawTx}), map[string]string{ - "X-WaitFor": Status_SEEN_ON_NETWORK, - "X-CallbackUrl": callbackUrl, + "X-WaitFor": StatusSeenOnNetwork, + "X-CallbackUrl": callbackURL, "X-CallbackToken": token, "X-CallbackBatch": strconv.FormatBool(callbackBatch), "X-MaxTimeout": "7", }, http.StatusOK) - require.Equal(t, Status_SEEN_ON_NETWORK, response.TxStatus) + require.Equal(t, StatusSeenOnNetwork, response.TxStatus) } func prepareCallback(t *testing.T, callbackNumbers int) (chan *TransactionResponse, chan error, callbackResponseFn) { @@ -546,7 +322,7 @@ func prepareCallback(t *testing.T, callbackNumbers int) (chan *TransactionRespon responseVisitMap := make(map[string]int) mu := &sync.Mutex{} - calbackResponseFn := func(w http.ResponseWriter, rc chan *TransactionResponse, ec chan error, status *TransactionResponse) { + calbackResponseFn := func(w http.ResponseWriter, rc chan *TransactionResponse, _ chan error, status *TransactionResponse) { mu.Lock() callbackNumber := responseVisitMap[status.Txid] callbackNumber++ @@ -556,7 +332,6 @@ func prepareCallback(t *testing.T, callbackNumbers int) (chan *TransactionRespon respondWithSuccess := false if callbackNumber < callbackNumbers { respondWithSuccess = false - } else { respondWithSuccess = true } @@ -580,7 +355,7 @@ func prepareBatchCallback(t *testing.T, callbackNumbers int) (chan *CallbackBatc responseVisitMap := make(map[string]int) mu := &sync.Mutex{} - calbackResponseFn := func(w http.ResponseWriter, rc chan *CallbackBatchResponse, ec chan error, status *CallbackBatchResponse) { + calbackResponseFn := func(w http.ResponseWriter, rc chan *CallbackBatchResponse, _ chan error, status *CallbackBatchResponse) { mu.Lock() callbackNumber := responseVisitMap[status.Callbacks[0].Txid] callbackNumber++ @@ -590,7 +365,6 @@ func prepareBatchCallback(t *testing.T, callbackNumbers int) (chan *CallbackBatc respondWithSuccess := false if callbackNumber < callbackNumbers { respondWithSuccess = false - } else { respondWithSuccess = true }