-
Notifications
You must be signed in to change notification settings - Fork 114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
thiagodeev/rpcv08-websocket #651
base: v0.8.0
Are you sure you want to change the base?
Conversation
61eb5fb
to
9199306
Compare
…ith Websocket support - Added gorilla/websocket v1.5.3 as a direct dependency in go.mod. - Introduced NewWebsocketProvider function in provider.go to create a Websocket RPC Provider instance, enhancing the existing HTTP provider functionality.
- Removed the dependency on github.com/ethereum/go-ethereum and replaced it with github.com/NethermindEth/starknet.go/client in go.mod and related files. - Introduced new WebSocket provider functionality in provider.go
…od to work on starknet
- Updated the handler to correctly process subscription IDs for Starknet, accommodating the new structure returned by the Starknet API. - Modified the subscriptionResult struct to include a Starknet-specific subscription ID field. - Adjusted the WebSocket provider to support new block header subscriptions, improving the overall subscription mechanism. - Updated example usage to reflect changes in subscription handling and error management. These changes improve compatibility with Starknet's API and enhance the robustness of the WebSocket client.
- Updated the main CI workflow to include testing for the client with mocks. - Modified subscription handling in `subscription.js` to accommodate the new `subscription_id` structure from Starknet. - Refactored `provider_test.go` to include WebSocket provider support and improved test configurations. - Introduced a new test file `websocket_test.go` to validate WebSocket subscriptions for new block headers, ensuring robust error handling and compatibility with the testnet environment. These changes improve the overall robustness of the RPC client and enhance compatibility with Starknet's API.
f846bb8
to
05d7e90
Compare
4bb568c
to
c73bfce
Compare
Note: at the moment it's not possible to omit optional parameters in json-rpc calls using array as structure type with Juno, and the current client implementation only supports sending parameters as arrays. Therefore, I changed the Subscribe function and now we are able to send optional parameters as object too. That way Juno doesn't return an error
…hanced error handling and testing - Added the SubscribeEvents method to the WebSocket provider, allowing for event subscriptions with optional parameters. - Introduced a new test case in websocket_test.go to validate event subscriptions under various conditions, including handling of empty arguments and specific address filtering. - Updated the HexToFeltNoErr utility function to convert hexadecimal strings to felt objects without error handling, suitable for internal use. These changes improve the robustness of the WebSocket client and enhance compatibility with Starknet's event subscription model.
rpc/websocket.go
Outdated
func (provider *WsProvider) SubscribeEvents(ctx context.Context, events chan<- *EmittedEvent, input EventSubscriptionInput) (*client.ClientSubscription, error) { | ||
var sub *client.ClientSubscription | ||
var err error | ||
|
||
var emptyBlockID BlockID | ||
if input.BlockID == emptyBlockID { | ||
// BlockID has a custom MarshalJSON that doesn't allow zero values. | ||
// Create a temporary struct without BlockID field to properly handle the optional parameter. | ||
tempInput := struct { | ||
FromAddress *felt.Felt `json:"from_address,omitempty"` | ||
Keys [][]*felt.Felt `json:"keys,omitempty"` | ||
}{ | ||
FromAddress: input.FromAddress, | ||
Keys: input.Keys, | ||
} | ||
|
||
sub, err = provider.c.Subscribe(ctx, "starknet", "_subscribeEvents", events, tempInput) | ||
} else { | ||
sub, err = provider.c.Subscribe(ctx, "starknet", "_subscribeEvents", events, input) | ||
} | ||
|
||
if err != nil { | ||
return nil, tryUnwrapToRPCErr(err, ErrTooManyKeysInFilter, ErrTooManyBlocksBack, ErrBlockNotFound, ErrCallOnPending) | ||
} | ||
return sub, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion
func (provider *WsProvider) SubscribeEvents(ctx context.Context, events chan<- *EmittedEvent, input EventSubscriptionInput) (*client.ClientSubscription, error) {
if input.BlockID.IsZero() {
input.BlockID = BlockID{Tag: "latest"}
}
sub, err := provider.c.Subscribe(ctx, "starknet", "_subscribeEvents", events, input)
if err != nil {
return nil, tryUnwrapToRPCErr(err, ErrTooManyKeysInFilter, ErrTooManyBlocksBack, ErrBlockNotFound, ErrCallOnPending)
}
return sub, nil
}
Also, add the IsZero()
method
func (b BlockID) IsZero() bool {
return b == BlockID{}
}
Maybe, also just make the default BlockID
a global varialbe (in types_block.go
) and use that instead (probably cleaner)
var DefaultBlockID = BlockID{Tag: "latest"}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I know it defaults to "latest" even if we don't pass any blockID, but a json request with an empty param is different from a request passing values. The json will be different from the one requested by the user, even if the result is the same.
I just wanted to make the final json request exactly the one the user is creating in the code. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that's a good point. Although, given an absent blockId
is the same as the default value, I think overriding it is okay. Plus this approach is a bit neater, and less error prone imo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please take a look at my new approach and let me know your opinion
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although, we seem to repeat the same logic multiple times in different ways, I feel like we should unify it.
Eg in SubscribeEvents we do
var emptyBlockID BlockID
if options.BlockID == emptyBlockID {
options.BlockID = WithBlockTag("latest")
}
but in SubscribeNewHeads we have
if blockID == nil {
blockID = &BlockID{Tag: "latest"}
}
and again in SubscribeTransactionStatus
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The spec was updated. Take a look at my new approach
0cafe3d
to
359fc44
Compare
// We can also use the subscription returned by the WS methods to unsubscribe from the stream when we're done | ||
sub.Unsubscribe() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Imo this should always be executed if we had no error subscribing in the first place, so I would suggest using defer sub.Unsubscribe() between line 40 and 41
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I think we can simplify this example by removing the code below this line, and explicitly exit after we receive eg 5 blocks form the ws endpoint
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But each block takes ~15-20 seconds to build, so they'll need to wait almost 2 minutes for it.
We can indeed simplify it, but IMO the example should be both simple and complete. My intuition for including the last code part was to show the effects of the parameters and how to use them.
WDYT?
type wsConn interface { | ||
callCloser | ||
Subscribe(ctx context.Context, namespace string, methodSuffix string, channel interface{}, args interface{}) (*client.ClientSubscription, error) | ||
SubscribeWithSliceArgs(ctx context.Context, namespace string, methodSuffix string, channel interface{}, args ...interface{}) (*client.ClientSubscription, error) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need SubscribeWithSliceArgs? Can we not just update Subscribe to be
Subscribe(ctx context.Context, namespace string, methodSuffix string, channel interface{}, args ...interface{})
a quick check suggests it works
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The answer is that the client was passing the args as an array by default, and I was facing an issue with Juno related to optional parameters (it is not accepting to omit any parameters when passing as an array, even if they're optional), so I adapted the "Subscribe" function to send "any" instead of "[]any", and created another method, "SubscribeWithSliceArgs", to maintain compatibility with the remaining codebase and tests.
So when we need to have optional parameters, we need to use "Subscribe", but when all of them are needed, instead of creating a struct for it, we can just send them through "SubscribeWithSliceArgs"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the issue might have been related to not specifying the parameter names in the request, eg updating the SubscribeTransactionStatus
method as follows seems to work
func (provider *WsProvider) SubscribeTransactionStatus(ctx context.Context, newStatus chan<- *NewTxnStatusResp, transactionHash QWEQWE) (*client.ClientSubscription, error) {
sub, err := provider.c.Subscribe(ctx, "starknet", "_subscribeTransactionStatus", newStatus, transactionHash)
if err != nil {
return nil, tryUnwrapToRPCErr(err, ErrTooManyBlocksBack, ErrBlockNotFound)
}
return sub, nil
}
type QWEQWE struct {
TransactionHash *felt.Felt `json:"transaction_hash"`
}
This explicitly sets the parameter names in the request, eg like
{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeTransactionStatus","params":{"transaction_hash":"0x7b"}}
instead of something like this
{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeTransactionStatus","params":["0x7b","0x7b"]}
when using SubscribeWithSliceArgs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The optional
json tag should be able to handle the case where the parameter is optional as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you're right. But the current solution for the problem is:
Subscribe
was changed to receiveany
instead of[]any
- A new method called
SubscribeWithSliceArgs
was created to perfectly replace the older version of theSubscribe
method where it was used. This method simply takes an[]any
and passes it toSubscribe
.
It's simple and works well.
But if we remove the SubscribeWithSliceArgs
, we'll need to wrap single values params in a struct only to make it work, we'll also need to create new structs inside the tests that came from ethrpc only to make it work. I don't see any advantages in doing this, only disadvantages.
WDYT?
rpc/websocket_test.go
Outdated
// Subscription with only blockID should return events from all addresses and keys from the specified block onwards. | ||
// Verify by checking for events with different addresses and keys than the test values. | ||
if !differentFromAddressFound { | ||
if resp.FromAddress != fromAddress { | ||
differentFromAddressFound = true | ||
} | ||
} | ||
|
||
if !differentKeyFound { | ||
if !slices.Contains(resp.Keys, key) { | ||
differentKeyFound = true | ||
} | ||
} | ||
|
||
if differentFromAddressFound && differentKeyFound { | ||
return | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are you trying to check for here??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed it a little bit. It's about testing whether events from different addresses and keys are being returned, as this testcase doesn't have any filter
rpc/websocket_test.go
Outdated
if test.expectedError != nil { | ||
require.Error(t, err) | ||
return | ||
} else { | ||
require.NoError(t, err) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we could just do something like
require.EqualError(t,err,test.ExpectedErr)
if test.expectedError != nil { return}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately not with 'EqualError', because it '...asserts that a function returned an error (i.e. not nil
)...' first.
I think your previous suggestion of removing the 'else' is enough
// TODO: Add mock for testing reorg events. | ||
// A simple test was made to make sure the reorg events are received; it'll be added in the PR 651 comments |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note for myself to revisit this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you see my comment?
…move unused error
type WebsocketProvider interface { | ||
SubscribeEvents(ctx context.Context, events chan<- *EmittedEvent, options *EventSubscriptionInput) (*client.ClientSubscription, error) | ||
SubscribeNewHeads(ctx context.Context, headers chan<- *BlockHeader, subBlockID *SubscriptionBlockID) (*client.ClientSubscription, error) | ||
SubscribePendingTransactions(ctx context.Context, pendingTxns chan<- *SubPendingTxns, options *SubPendingTxnsInput) (*client.ClientSubscription, error) | ||
SubscribeTransactionStatus(ctx context.Context, newStatus chan<- *NewTxnStatusResp, transactionHash *felt.Felt) (*client.ClientSubscription, error) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should add some mocks for the WebsocketProvider, although I would suggest doing this in a separate PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would allow us to run mock tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you want me to fix this right after this RP was merged?
#623
This PR aims to implement some RPC v0.8.0 updates to starknet.go. The ones addressed by this PR are:
New methods:
starknet_subscribeNewHeads
starknet_subscribeEvents
starknet_subscribeTransactionStatus
starknet_subscribePendingTransactions
starknet_unsubscribe
Components
NEW_TXN_STATUS
REORG_DATA
SUBSCRIPTION_BLOCK_ID
Errors:
INVALID_SUBSCRIPTION_ID
TOO_MANY_ADDRESSES_IN_FILTER
TOO_MANY_BLOCKS_BACK
In order to implement websocket connection with Starknet in starknet.go I had to make some adaptations to the client code being currently used, and to do that, I had to copy the entire client code.