Skip to content

Commit 2d073dd

Browse files
author
Paweł Nowosielski
committed
starknet_subscribeTransactionStatus websocket method
1 parent eb1f2ca commit 2d073dd

File tree

13 files changed

+525
-122
lines changed

13 files changed

+525
-122
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"revert_error": "This is hand-made transaction used for txStatus endpoint test",
3+
"execution_status": "REJECTED",
4+
"finality_status": "ACCEPTED_ON_L1",
5+
"status": "REVERTED",
6+
"block_hash": "0x111100000000111100000000333300000000444400000000111100000000111",
7+
"block_number": 304740,
8+
"transaction_index": 1,
9+
"transaction_hash": "0x111100000000222200000000333300000000444400000000555500000000fff",
10+
"l2_to_l1_messages": [],
11+
"events": [],
12+
"actual_fee": "0x247aff6e224"
13+
}

docs/docs/faq.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ docker logs -f juno
7474
<details>
7575
<summary>How can I get real-time updates of new blocks?</summary>
7676

77-
The [WebSocket](websocket#subscribe-to-newly-created-blocks) interface provides a `juno_subscribeNewHeads` method that emits an event when new blocks are added to the blockchain.
77+
The [WebSocket](websocket#subscribe-to-newly-created-blocks) interface provides a `starknet_subscribeNewHeads` method that emits an event when new blocks are added to the blockchain.
7878

7979
</details>
8080

docs/docs/websocket.md

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,16 +96,15 @@ Get the most recent accepted block hash and number with the `starknet_blockHashA
9696

9797
## Subscribe to newly created blocks
9898

99-
The WebSocket server provides a `juno_subscribeNewHeads` method that emits an event when new blocks are added to the blockchain:
99+
The WebSocket server provides a `starknet_subscribeNewHeads` method that emits an event when new blocks are added to the blockchain:
100100

101101
<Tabs>
102102
<TabItem value="request" label="Request">
103103

104104
```json
105105
{
106106
"jsonrpc": "2.0",
107-
"method": "juno_subscribeNewHeads",
108-
"params": [],
107+
"method": "starknet_subscribeNewHeads",
109108
"id": 1
110109
}
111110
```
@@ -129,7 +128,7 @@ When a new block is added, you will receive a message like this:
129128
```json
130129
{
131130
"jsonrpc": "2.0",
132-
"method": "juno_subscribeNewHeads",
131+
"method": "starknet_subscriptionNewHeads",
133132
"params": {
134133
"result": {
135134
"block_hash": "0x840660a07a17ae6a55d39fb6d366698ecda11e02280ca3e9ca4b4f1bad741c",
@@ -149,12 +148,65 @@ When a new block is added, you will receive a message like this:
149148
"l1_da_mode": "BLOB",
150149
"starknet_version": "0.13.1.1"
151150
},
152-
"subscription": 16570962336122680234
151+
"subscription_id": 16570962336122680234
152+
}
153+
}
154+
```
155+
156+
## Subscribe to transaction status changes
157+
158+
The WebSocket server provides a `starknet_subscribeTransactionStatus` method that emits an event when a transaction status changes:
159+
160+
<Tabs>
161+
<TabItem value="request" label="Request">
162+
163+
```json
164+
{
165+
"jsonrpc": "2.0",
166+
"method": "starknet_subscribeTransactionStatus",
167+
"params": [
168+
{
169+
"transaction_hash": "0x631333277e88053336d8c302630b4420dc3ff24018a1c464da37d5e36ea19df"
170+
}
171+
],
172+
"id": 1
173+
}
174+
```
175+
176+
</TabItem>
177+
<TabItem value="response" label="Response">
178+
179+
```json
180+
{
181+
"jsonrpc": "2.0",
182+
"result": 16570962336122680234,
183+
"id": 1
184+
}
185+
```
186+
187+
</TabItem>
188+
</Tabs>
189+
190+
When a transaction get a new status, you will receive a message like this:
191+
192+
```json
193+
{
194+
"jsonrpc": "2.0",
195+
"method": "starknet_subscriptionTransactionsStatus",
196+
"params": {
197+
"result": {
198+
"transaction_hash": "0x631333277e88053336d8c302630b4420dc3ff24018a1c464da37d5e36ea19df",
199+
"status": {
200+
"finality_status": "ACCEPTED_ON_L2",
201+
"execution_status": "SUCCEEDED"
202+
}
203+
},
204+
"subscription_id": 16570962336122680234
153205
}
154206
}
155207
```
156208

157-
## Unsubscribe from newly created blocks
209+
## Unsubscribe from previous subscription
158210

159211
Use the `juno_unsubscribe` method with the `result` value from the subscription response or the `subscription` field from any new block event to stop receiving updates for new blocks:
160212

jsonrpc/server.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -422,10 +422,6 @@ func isBatch(reader *bufio.Reader) bool {
422422
return false
423423
}
424424

425-
func isNil(i any) bool {
426-
return i == nil || reflect.ValueOf(i).IsNil()
427-
}
428-
429425
func (s *Server) handleRequest(ctx context.Context, req *Request) (*response, http.Header, error) {
430426
s.log.Tracew("Received request", "req", req)
431427

@@ -471,7 +467,7 @@ func (s *Server) handleRequest(ctx context.Context, req *Request) (*response, ht
471467
header = (tuple[1].Interface()).(http.Header)
472468
}
473469

474-
if errAny := tuple[errorIndex].Interface(); !isNil(errAny) {
470+
if errAny := tuple[errorIndex].Interface(); !utils.IsNil(errAny) {
475471
res.Error = errAny.(*Error)
476472
if res.Error.Code == InternalError {
477473
s.listener.OnRequestFailed(req.Method, res.Error)
@@ -498,7 +494,7 @@ func (s *Server) buildArguments(ctx context.Context, params any, method Method)
498494
addContext = 1
499495
}
500496

501-
if isNil(params) {
497+
if utils.IsNil(params) {
502498
allParamsAreOptional := utils.All(method.Params, func(p Parameter) bool {
503499
return p.Optional
504500
})

rpc/events.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ type EventsArg struct {
1414
ResultPageRequest
1515
}
1616

17+
type SubscriptionID struct {
18+
ID uint64 `json:"subscription_id"`
19+
}
20+
1721
type EventFilter struct {
1822
FromBlock *BlockID `json:"from_block"`
1923
ToBlock *BlockID `json:"to_block"`
@@ -44,10 +48,6 @@ type EventsChunk struct {
4448
ContinuationToken string `json:"continuation_token,omitempty"`
4549
}
4650

47-
type SubscriptionID struct {
48-
ID uint64 `json:"subscription_id"`
49-
}
50-
5151
/****************************************************
5252
Events Handlers
5353
*****************************************************/

rpc/events_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ func TestEvents(t *testing.T) {
215215
})
216216
}
217217

218+
// TODO[pnowosie]: Refactor. fakeConn - this is redefined in subscription test, but also used in NewHeads
218219
type fakeConn struct {
219220
w io.Writer
220221
}

rpc/handlers.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ var (
6666
ErrUnsupportedTxVersion = &jsonrpc.Error{Code: 61, Message: "the transaction version is not supported"}
6767
ErrUnsupportedContractClassVersion = &jsonrpc.Error{Code: 62, Message: "the contract class version is not supported"}
6868
ErrUnexpectedError = &jsonrpc.Error{Code: 63, Message: "An unexpected error occurred"}
69+
ErrTooManyAddressesInFilter = &jsonrpc.Error{Code: 67, Message: "Too many addresses in filter sender_address filter"}
6970
ErrTooManyBlocksBack = &jsonrpc.Error{Code: 68, Message: fmt.Sprintf("Cannot go back more than %v blocks", maxBlocksBack)}
7071
ErrCallOnPending = &jsonrpc.Error{Code: 69, Message: "This method does not support being called on the pending block"}
7172

@@ -93,8 +94,9 @@ type Handler struct {
9394
vm vm.VM
9495
log utils.Logger
9596

96-
version string
97-
newHeads *feed.Feed[*core.Header]
97+
version string
98+
newHeads *feed.Feed[*core.Header]
99+
pendingTxs *feed.Feed[[]core.Transaction]
98100

99101
idgen func() uint64
100102
mu stdsync.Mutex // protects subscriptions.
@@ -135,6 +137,7 @@ func New(bcReader blockchain.Reader, syncReader sync.Reader, virtualMachine vm.V
135137
},
136138
version: version,
137139
newHeads: feed.New[*core.Header](),
140+
pendingTxs: feed.New[[]core.Transaction](),
138141
subscriptions: make(map[uint64]*subscription),
139142

140143
blockTraceCache: lru.NewCache[traceCacheKey, []TracedBlockTransaction](traceCacheSize),
@@ -177,7 +180,8 @@ func (h *Handler) WithGateway(gatewayClient Gateway) *Handler {
177180
func (h *Handler) Run(ctx context.Context) error {
178181
newHeadsSub := h.syncReader.SubscribeNewHeads().Subscription
179182
defer newHeadsSub.Unsubscribe()
180-
feed.Tee[*core.Header](newHeadsSub, h.newHeads)
183+
feed.Tee(newHeadsSub, h.newHeads)
184+
181185
<-ctx.Done()
182186
for _, sub := range h.subscriptions {
183187
sub.wg.Wait()
@@ -347,6 +351,11 @@ func (h *Handler) Methods() ([]jsonrpc.Method, string) { //nolint: funlen
347351
Name: "juno_subscribeNewHeads",
348352
Handler: h.SubscribeNewHeads,
349353
},
354+
{
355+
Name: "starknet_subscribeTransactionStatus",
356+
Params: []jsonrpc.Parameter{{Name: "transaction_hash"}, {Name: "block", Optional: true}},
357+
Handler: h.SubscribeTxnStatus,
358+
},
350359
{
351360
Name: "juno_unsubscribe",
352361
Params: []jsonrpc.Parameter{{Name: "id"}},

rpc/subscriptions.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,85 @@ func (h *Handler) SubscribeEvents(ctx context.Context, fromAddr *felt.Felt, keys
104104
return &SubscriptionID{ID: id}, nil
105105
}
106106

107+
// SubscribeTxnStatus subscribes to status changes of a transaction. It checks for updates each time a new block is added.
108+
// Subsequent updates are sent only when the transaction status changes.
109+
// The optional block_id parameter is ignored, as status changes are not stored and historical data cannot be sent.
110+
func (h *Handler) SubscribeTxnStatus(ctx context.Context, txHash felt.Felt, _ *BlockID) (*SubscriptionID, *jsonrpc.Error) {
111+
var (
112+
lastKnownStatus, lastSendStatus *TransactionStatus
113+
wrapResult = func(s *TransactionStatus) *NewTransactionStatus {
114+
return &NewTransactionStatus{
115+
TransactionHash: &txHash,
116+
Status: s,
117+
}
118+
}
119+
)
120+
121+
w, ok := jsonrpc.ConnFromContext(ctx)
122+
if !ok {
123+
return nil, jsonrpc.Err(jsonrpc.MethodNotFound, nil)
124+
}
125+
126+
id := h.idgen()
127+
subscriptionCtx, subscriptionCtxCancel := context.WithCancel(ctx)
128+
sub := &subscription{
129+
cancel: subscriptionCtxCancel,
130+
conn: w,
131+
}
132+
133+
lastKnownStatus, rpcErr := h.TransactionStatus(subscriptionCtx, txHash)
134+
if rpcErr != nil {
135+
h.log.Errorw("Failed to get Tx status", "txHash", &txHash, "rpcErr", rpcErr)
136+
return nil, rpcErr
137+
}
138+
139+
h.mu.Lock()
140+
h.subscriptions[id] = sub
141+
h.mu.Unlock()
142+
143+
headerSub := h.newHeads.Subscribe()
144+
sub.wg.Go(func() {
145+
defer func() {
146+
h.unsubscribe(sub, id)
147+
headerSub.Unsubscribe()
148+
}()
149+
150+
if err := h.sendTxnStatus(sub.conn, wrapResult(lastKnownStatus), id); err != nil {
151+
h.log.Errorw("Error while sending Txn status", "txHash", txHash, "err", err)
152+
return
153+
}
154+
lastSendStatus = lastKnownStatus
155+
156+
for {
157+
select {
158+
case <-subscriptionCtx.Done():
159+
return
160+
case <-headerSub.Recv():
161+
lastKnownStatus, rpcErr = h.TransactionStatus(subscriptionCtx, txHash)
162+
if rpcErr != nil {
163+
h.log.Errorw("Failed to get Tx status", "txHash", txHash, "rpcErr", rpcErr)
164+
return
165+
}
166+
167+
if *lastKnownStatus != *lastSendStatus {
168+
if err := h.sendTxnStatus(sub.conn, wrapResult(lastKnownStatus), id); err != nil {
169+
h.log.Errorw("Error while sending Txn status", "txHash", txHash, "err", err)
170+
return
171+
}
172+
lastSendStatus = lastKnownStatus
173+
}
174+
175+
// Stop when final status reached and notified
176+
if isFinal(lastSendStatus) {
177+
return
178+
}
179+
}
180+
}
181+
})
182+
183+
return &SubscriptionID{ID: id}, nil
184+
}
185+
107186
func (h *Handler) processEvents(ctx context.Context, w jsonrpc.Conn, id, from, to uint64, fromAddr *felt.Felt, keys [][]felt.Felt) {
108187
filter, err := h.bcReader.EventFilter(fromAddr, keys)
109188
if err != nil {
@@ -182,3 +261,30 @@ func sendEvents(ctx context.Context, w jsonrpc.Conn, events []*blockchain.Filter
182261
}
183262
return nil
184263
}
264+
265+
type NewTransactionStatus struct {
266+
TransactionHash *felt.Felt `json:"transaction_hash"`
267+
Status *TransactionStatus `json:"status"`
268+
}
269+
270+
// sendTxnStatus creates a response and sends it to the client
271+
func (h *Handler) sendTxnStatus(w jsonrpc.Conn, status *NewTransactionStatus, id uint64) error {
272+
resp, err := json.Marshal(SubscriptionResponse{
273+
Version: "2.0",
274+
Method: "starknet_subscriptionTransactionsStatus",
275+
Params: map[string]any{
276+
"subscription_id": id,
277+
"result": status,
278+
},
279+
})
280+
if err != nil {
281+
return err
282+
}
283+
h.log.Debugw("Sending Txn status", "status", string(resp))
284+
_, err = w.Write(resp)
285+
return err
286+
}
287+
288+
func isFinal(status *TransactionStatus) bool {
289+
return status.Finality == TxnStatusRejected || status.Finality == TxnStatusAcceptedOnL1
290+
}

0 commit comments

Comments
 (0)