diff --git a/rpc/types_transaction_receipt.go b/rpc/types_transaction_receipt.go index 7a6e9e33..30dd01eb 100644 --- a/rpc/types_transaction_receipt.go +++ b/rpc/types_transaction_receipt.go @@ -155,6 +155,11 @@ type TxnStatusResp struct { FailureReason string `json:"failure_reason,omitempty"` } +type NewTxnStatusResp struct { + TransactionHash *felt.Felt `json:"transaction_hash"` + Status TxnStatusResp `json:"status"` +} + type TransactionReceiptWithBlockInfo struct { TransactionReceipt BlockHash *felt.Felt `json:"block_hash,omitempty"` diff --git a/rpc/websocket.go b/rpc/websocket.go index 1302374e..8f6303e0 100644 --- a/rpc/websocket.go +++ b/rpc/websocket.go @@ -67,3 +67,27 @@ func (provider *WsProvider) SubscribeEvents(ctx context.Context, events chan<- * } return sub, nil } + +// Transaction Status subscription. +// Creates a WebSocket stream which at first fires an event with the current known transaction status, +// followed by events for every transaction status update +// +// Parameters: +// - ctx: The context.Context object for controlling the function call +// - newStatus: The channel to send the new transaction status to +// - transactionHash: The transaction hash to fetch status updates for +// Returns: +// - clientSubscription: The client subscription object, used to unsubscribe from the stream and to get errors +// - error: An error, if any +func (provider *WsProvider) SubscribeTransactionStatus(ctx context.Context, newStatus chan<- *NewTxnStatusResp, transactionHash *felt.Felt) (*client.ClientSubscription, error) { + sub, err := provider.c.SubscribeWithSliceArgs(ctx, "starknet", "_subscribeTransactionStatus", newStatus, transactionHash, WithBlockTag("latest")) + if err != nil { + return nil, tryUnwrapToRPCErr(err, ErrTooManyBlocksBack, ErrBlockNotFound) + } + // TODO: wait for Juno to implement this. This is the correct implementation by the spec + // sub, err := provider.c.SubscribeWithSliceArgs(ctx, "starknet", "_subscribeTransactionStatus", newStatus, transactionHash) + // if err != nil { + // return nil, tryUnwrapToRPCErr(err) + // } + return sub, nil +} diff --git a/rpc/websocket_test.go b/rpc/websocket_test.go index 041e7941..858b1b6a 100644 --- a/rpc/websocket_test.go +++ b/rpc/websocket_test.go @@ -346,3 +346,53 @@ func TestSubscribeEvents(t *testing.T) { } }) } + +func TestSubscribeTransactionStatus(t *testing.T) { + if testEnv != "testnet" { + t.Skip("Skipping test as it requires a testnet environment") + } + + testConfig := beforeEach(t) + require.NotNil(t, testConfig.wsBase, "wsProvider base is not set") + + provider := testConfig.provider + blockInterface, err := provider.BlockWithTxHashes(context.Background(), WithBlockTag("latest")) + require.NoError(t, err) + block := blockInterface.(*BlockTxHashes) + + txHash := new(felt.Felt) + for _, tx := range block.Transactions { + status, err := provider.GetTransactionStatus(context.Background(), tx) + require.NoError(t, err) + if status.FinalityStatus == TxnStatus_Accepted_On_L2 { + txHash = tx + break + } + } + + t.Run("normal call", func(t *testing.T) { + wsProvider, err := NewWebsocketProvider(testConfig.wsBase) + require.NoError(t, err) + defer wsProvider.Close() + + events := make(chan *NewTxnStatusResp) + sub, err := wsProvider.SubscribeTransactionStatus(context.Background(), events, txHash) + require.NoError(t, err) + require.NotNil(t, sub) + defer sub.Unsubscribe() + + for { + select { + case resp := <-events: + require.IsType(t, &NewTxnStatusResp{}, resp) + require.Equal(t, txHash, resp.TransactionHash) + require.Equal(t, TxnStatus_Accepted_On_L2, resp.Status.FinalityStatus) + return + case err := <-sub.Err(): + require.NoError(t, err) + case <-time.After(4 * time.Second): + t.Fatal("timeout waiting for events") + } + } + }) +}