Skip to content

Commit

Permalink
feat: add mined update (#745)
Browse files Browse the repository at this point in the history
  • Loading branch information
shotasilagadzetaal authored Jan 15, 2025
1 parent 7303948 commit c9b088d
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 21 deletions.
9 changes: 4 additions & 5 deletions internal/api/handler/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -31,7 +31,6 @@ import (
)

const (
maxTimeout = 30
timeoutSecondsDefault = 5
mapExpiryTimeDefault = 24 * time.Hour
)
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}

Expand Down
4 changes: 2 additions & 2 deletions internal/metamorph/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions internal/metamorph/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
19 changes: 8 additions & 11 deletions internal/metamorph/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
const (
checkStatusIntervalDefault = 5 * time.Second
minedDoubleSpendMsg = "previously double spend attempted"
MaxTimeout = 30
)

var (
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
1 change: 0 additions & 1 deletion internal/metamorph/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
74 changes: 72 additions & 2 deletions test/submit_01_single_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
})
}

Expand Down

0 comments on commit c9b088d

Please sign in to comment.