From c9b088deb1fe00e58d03fad00f0bd03deb62a34a Mon Sep 17 00:00:00 2001 From: shotasilagadzetaal <139438093+shotasilagadzetaal@users.noreply.github.com> Date: Wed, 15 Jan 2025 19:10:40 +0400 Subject: [PATCH] feat: add mined update (#745) --- internal/api/handler/default.go | 9 ++-- internal/metamorph/client.go | 4 +- internal/metamorph/processor.go | 6 +++ internal/metamorph/server.go | 19 ++++---- internal/metamorph/server_test.go | 1 - test/submit_01_single_test.go | 74 ++++++++++++++++++++++++++++++- 6 files changed, 92 insertions(+), 21 deletions(-) diff --git a/internal/api/handler/default.go b/internal/api/handler/default.go index 0b31e84e2..1e4836d04 100644 --- a/internal/api/handler/default.go +++ b/internal/api/handler/default.go @@ -12,6 +12,7 @@ import ( "time" "github.com/bitcoin-sv/arc/internal/api/handler/internal/merkle_verifier" + "github.com/bitcoin-sv/arc/internal/metamorph" sdkTx "github.com/bitcoin-sv/go-sdk/transaction" "github.com/labstack/echo/v4" @@ -20,7 +21,6 @@ import ( "github.com/bitcoin-sv/arc/internal/beef" "github.com/bitcoin-sv/arc/internal/blocktx" - "github.com/bitcoin-sv/arc/internal/metamorph" "github.com/bitcoin-sv/arc/internal/metamorph/metamorph_api" "github.com/bitcoin-sv/arc/internal/tracing" "github.com/bitcoin-sv/arc/internal/validator" @@ -31,7 +31,6 @@ import ( ) const ( - maxTimeout = 30 timeoutSecondsDefault = 5 mapExpiryTimeDefault = 24 * time.Hour ) @@ -41,7 +40,7 @@ var ( ErrCallbackURLNotAcceptable = errors.New("callback URL not acceptable") ErrStatusNotSupported = errors.New("status not supported") ErrDecodingBeef = errors.New("error while decoding BEEF") - ErrMaxTimeoutExceeded = fmt.Errorf("max timeout can not be higher than %d", maxTimeout) + ErrMaxTimeoutExceeded = fmt.Errorf("max timeout can not be higher than %d", metamorph.MaxTimeout) ) type ArcDefaultHandler struct { @@ -301,7 +300,7 @@ func (m ArcDefaultHandler) postTransaction(ctx echo.Context, params api.POSTTran // POSTTransaction ... func (m ArcDefaultHandler) POSTTransaction(ctx echo.Context, params api.POSTTransactionParams) (err error) { timeout := m.defaultTimeout - if params.XMaxTimeout != nil && *params.XMaxTimeout < maxTimeout { + if params.XMaxTimeout != nil && *params.XMaxTimeout < metamorph.MaxTimeout { timeout = time.Second * time.Duration(*params.XMaxTimeout) } @@ -484,7 +483,7 @@ func (m ArcDefaultHandler) postTransactions(ctx echo.Context, params api.POSTTra // POSTTransactions ... func (m ArcDefaultHandler) POSTTransactions(ctx echo.Context, params api.POSTTransactionsParams) (err error) { timeout := m.defaultTimeout - if params.XMaxTimeout != nil && *params.XMaxTimeout < maxTimeout { + if params.XMaxTimeout != nil && *params.XMaxTimeout < metamorph.MaxTimeout { timeout = time.Second * time.Duration(*params.XMaxTimeout) } diff --git a/internal/metamorph/client.go b/internal/metamorph/client.go index 06af8b9e1..efdb06ba4 100644 --- a/internal/metamorph/client.go +++ b/internal/metamorph/client.go @@ -283,7 +283,7 @@ func (m *Metamorph) SubmitTransaction(ctx context.Context, tx *sdkTx.Transaction deadline, _ := ctx.Deadline() // increase time to make sure that expiration happens from inside the metramorph function - newDeadline := deadline.Add(time.Second * 2) + newDeadline := deadline.Add(time.Second * MaxTimeout) // Create a new context with the updated deadline newCtx, newCancel := context.WithDeadline(context.Background(), newDeadline) @@ -345,7 +345,7 @@ func (m *Metamorph) SubmitTransactions(ctx context.Context, txs sdkTx.Transactio deadline, _ := ctx.Deadline() // decrease time to get initial deadline - newDeadline := deadline.Add(time.Second * 5) + newDeadline := deadline.Add(time.Second * MaxTimeout) // increase time to make sure that expiration happens from inside the metramorph function newCtx, newCancel := context.WithDeadline(context.Background(), newDeadline) diff --git a/internal/metamorph/processor.go b/internal/metamorph/processor.go index dea8596b5..827daea91 100644 --- a/internal/metamorph/processor.go +++ b/internal/metamorph/processor.go @@ -315,6 +315,12 @@ func (p *Processor) updateMined(ctx context.Context, txsBlocks []*blocktx_api.Tr } for _, data := range updatedData { + // if we have a pending request with given transaction hash, provide mined status + p.responseProcessor.UpdateStatus(data.Hash, StatusAndError{ + Hash: data.Hash, + Status: metamorph_api.Status_MINED, + }) + if len(data.Callbacks) > 0 { requests := toSendRequest(data) for _, request := range requests { diff --git a/internal/metamorph/server.go b/internal/metamorph/server.go index d175e6cc0..332959993 100644 --- a/internal/metamorph/server.go +++ b/internal/metamorph/server.go @@ -30,6 +30,7 @@ import ( const ( checkStatusIntervalDefault = 5 * time.Second minedDoubleSpendMsg = "previously double spend attempted" + MaxTimeout = 30 ) var ( @@ -144,8 +145,8 @@ func (s *Server) PutTransaction(ctx context.Context, req *metamorph_api.Transact // decrease time to get initial deadline newDeadline := deadline - if time.Now().Add(2 * time.Second).Before(deadline) { - newDeadline = deadline.Add(-(time.Second * 2)) + if time.Now().Add(MaxTimeout * time.Second).Before(deadline) { + newDeadline = deadline.Add(-(time.Second * MaxTimeout)) } // Create a new context with the updated deadline @@ -173,8 +174,8 @@ func (s *Server) PutTransactions(ctx context.Context, req *metamorph_api.Transac // decrease time to get initial deadline newDeadline := deadline - if time.Now().Add(2 * time.Second).Before(deadline) { - newDeadline = deadline.Add(-(time.Second * 2)) + if time.Now().Add(MaxTimeout * time.Second).Before(deadline) { + newDeadline = deadline.Add(-(time.Second * MaxTimeout)) } // Create a new context with the updated deadline @@ -298,17 +299,13 @@ func (s *Server) processTransaction(ctx context.Context, waitForStatus metamorph returnedStatus.TimedOut = true return returnedStatus case <-checkStatusTicker.C: - - // Check in intervals whether the tx was seen on network & updated on DB by another metamorph instance - if waitForStatus != metamorph_api.Status_SEEN_ON_NETWORK { - continue - } - + // it's possible the transaction status was received and updated in db by another metamorph + // check if that's the case and we have a new tx status to return var tx *metamorph_api.TransactionStatus tx, err = s.GetTransactionStatus(ctx, &metamorph_api.TransactionStatusRequest{ Txid: txID, }) - if err == nil && tx.Status == waitForStatus { + if err == nil && tx.Status >= waitForStatus { return tx } case res := <-responseChannel: diff --git a/internal/metamorph/server_test.go b/internal/metamorph/server_test.go index 919b237f6..dedd442f4 100644 --- a/internal/metamorph/server_test.go +++ b/internal/metamorph/server_test.go @@ -169,7 +169,6 @@ func TestPutTransaction(t *testing.T) { timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() actualStatus, err := sut.PutTransaction(timeoutCtx, txRequest) - fmt.Println("shota ", err) // then assert.NoError(t, err) diff --git a/test/submit_01_single_test.go b/test/submit_01_single_test.go index c5db2bdab..357774607 100644 --- a/test/submit_01_single_test.go +++ b/test/submit_01_single_test.go @@ -194,15 +194,16 @@ func TestSubmitMined(t *testing.T) { }() // when - _ = postRequest[TransactionResponse](t, arcEndpointV1Tx, createPayload(t, TransactionRequest{RawTx: exRawTx}), + transactionResponse := postRequest[TransactionResponse](t, arcEndpointV1Tx, createPayload(t, TransactionRequest{RawTx: exRawTx}), map[string]string{ "X-WaitFor": StatusMined, "X-CallbackUrl": callbackURL, "X-CallbackToken": token, + "X-MaxTimeout": "10", }, http.StatusOK) // wait for callback - callbackTimeout := time.After(10 * time.Second) + callbackTimeout := time.After(15 * time.Second) select { case status := <-callbackReceivedChan: @@ -214,6 +215,75 @@ func TestSubmitMined(t *testing.T) { case <-callbackTimeout: t.Fatal("callback exceeded timeout") } + + require.Equal(t, rawTx.TxID, transactionResponse.Txid) + require.Equal(t, StatusMined, transactionResponse.TxStatus) + require.Equal(t, merklePathStr, *transactionResponse.MerklePath) + }) +} + +func TestReturnMinedStatus(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, _ := node_client.FundNewWallet(t, bitcoind) + utxos := node_client.GetUtxos(t, bitcoind, address) + + rawTx, _ := bitcoind.GetRawTransaction(utxos[0].Txid) + tx, _ := sdkTx.NewTransactionFromHex(rawTx.Hex) + exRawTx := tx.String() + + callbackReceivedChan := make(chan *TransactionResponse) + callbackErrChan := make(chan error) + + lis, err := net.Listen("tcp", ":9000") + require.NoError(t, err) + mux := http.NewServeMux() + defer func() { + t.Log("closing listener") + err = lis.Close() + require.NoError(t, err) + }() + + callbackURL, token := registerHandlerForCallback(t, callbackReceivedChan, callbackErrChan, nil, mux) + defer func() { + t.Log("closing channels") + + close(callbackReceivedChan) + close(callbackErrChan) + }() + + go func() { + t.Logf("starting callback server") + err = http.Serve(lis, mux) + if err != nil { + t.Log("callback server stopped") + } + }() + + transactionResponse := postRequest[TransactionResponse](t, arcEndpointV1Tx, createPayload(t, TransactionRequest{RawTx: exRawTx}), + map[string]string{ + "X-WaitFor": StatusMined, + "X-CallbackUrl": callbackURL, + "X-CallbackToken": token, + "X-MaxTimeout": "10", + }, http.StatusOK) + + // wait for callback + callbackTimeout := time.After(15 * time.Second) + + select { + case status := <-callbackReceivedChan: + require.Equal(t, rawTx.TxID, status.Txid) + require.Equal(t, StatusMined, status.TxStatus) + case err := <-callbackErrChan: + t.Fatalf("callback error: %v", err) + case <-callbackTimeout: + t.Fatal("callback exceeded timeout") + } + + require.Equal(t, StatusMined, transactionResponse.TxStatus) }) }