Skip to content
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

Open
wants to merge 36 commits into
base: v0.8.0
Choose a base branch
from
Open

Conversation

thiagodeev
Copy link
Collaborator

@thiagodeev thiagodeev commented Dec 24, 2024

#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.

@thiagodeev thiagodeev force-pushed the v0.8.0 branch 2 times, most recently from 61eb5fb to 9199306 Compare January 6, 2025 11:37
…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
- 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.
@thiagodeev thiagodeev force-pushed the thiagodeev/rpcv08-websocket branch from f846bb8 to 05d7e90 Compare January 13, 2025 16:49
@thiagodeev thiagodeev force-pushed the thiagodeev/rpcv08-websocket branch from 4bb568c to c73bfce Compare January 13, 2025 22:37
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.
utils/Felt.go Outdated Show resolved Hide resolved
account/account_test.go Outdated Show resolved Hide resolved
rpc/types_event.go Outdated Show resolved Hide resolved
rpc/websocket.go Outdated Show resolved Hide resolved
rpc/websocket.go Outdated Show resolved Hide resolved
rpc/websocket.go Outdated
Comment on lines 44 to 69
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
}
Copy link
Contributor

@rianhughes rianhughes Jan 21, 2025

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"}

Copy link
Collaborator Author

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?

Copy link
Contributor

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

Copy link
Collaborator Author

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link
Contributor

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

Copy link
Collaborator Author

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

rpc/websocket.go Outdated Show resolved Hide resolved
@thiagodeev thiagodeev force-pushed the thiagodeev/rpcv08-websocket branch from 0cafe3d to 359fc44 Compare January 30, 2025 05:15
@thiagodeev thiagodeev marked this pull request as ready for review January 30, 2025 05:19
Comment on lines +62 to +64
// We can also use the subscription returned by the WS methods to unsubscribe from the stream when we're done
sub.Unsubscribe()

Copy link
Contributor

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

Copy link
Contributor

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

Copy link
Collaborator Author

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?

Comment on lines +15 to +19
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)
}
Copy link
Contributor

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

Copy link
Collaborator Author

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"

Copy link
Contributor

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

Copy link
Contributor

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

Copy link
Collaborator Author

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 receive any instead of []any
  • A new method called SubscribeWithSliceArgs was created to perfectly replace the older version of the Subscribe method where it was used. This method simply takes an []any and passes it to Subscribe.
    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 Show resolved Hide resolved
rpc/websocket_test.go Outdated Show resolved Hide resolved
rpc/websocket_test.go Outdated Show resolved Hide resolved
rpc/websocket_test.go Outdated Show resolved Hide resolved
rpc/websocket_test.go Outdated Show resolved Hide resolved
Comment on lines 252 to 268
// 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
}
Copy link
Contributor

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??

Copy link
Collaborator Author

@thiagodeev thiagodeev Jan 30, 2025

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 Show resolved Hide resolved
rpc/websocket_test.go Outdated Show resolved Hide resolved
rpc/websocket_test.go Outdated Show resolved Hide resolved
Comment on lines 468 to 473
if test.expectedError != nil {
require.Error(t, err)
return
} else {
require.NoError(t, err)
}
Copy link
Contributor

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}

Copy link
Collaborator Author

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

Comment on lines +542 to +543
// 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
Copy link
Contributor

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

Copy link
Collaborator Author

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?

#651 (comment)

Comment on lines +109 to +114
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)
}
Copy link
Contributor

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

Copy link
Contributor

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

Copy link
Collaborator Author

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants