Add subscribe event or transaction by websocket
This commit is contained in:
@@ -23,6 +23,7 @@ Powred by [BlockVision](https://blockvision.org/) & [SuiVision](https://suivisio
|
|||||||
+ Customized request method `SuiCall`.
|
+ Customized request method `SuiCall`.
|
||||||
+ Unsigned methods can be executed without loading your keystore file.
|
+ Unsigned methods can be executed without loading your keystore file.
|
||||||
+ Provide the method `SignAndExecuteTransactionBlock` to send signed transaction.
|
+ Provide the method `SignAndExecuteTransactionBlock` to send signed transaction.
|
||||||
|
+ Support subscriptions to events or transactions via websockets.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
|||||||
9
common/wsconn/subscription.go
Normal file
9
common/wsconn/subscription.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package wsconn
|
||||||
|
|
||||||
|
type SubscriptionResp struct {
|
||||||
|
Jsonrpc string `json:"jsonrpc"`
|
||||||
|
Result int64 `json:"result"`
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
91
common/wsconn/wsconn.go
Normal file
91
common/wsconn/wsconn.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package wsconn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/block-vision/sui-go-sdk/models"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WsConn struct {
|
||||||
|
Conn *websocket.Conn
|
||||||
|
wsUrl string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CallOp struct {
|
||||||
|
Method string
|
||||||
|
Params []interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWsConn(wsUrl string) *WsConn {
|
||||||
|
dialer := websocket.Dialer{}
|
||||||
|
conn, _, err := dialer.Dial(wsUrl, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error connecting to Websocket Server:", err, wsUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &WsConn{
|
||||||
|
Conn: conn,
|
||||||
|
wsUrl: wsUrl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WsConn) Call(ctx context.Context, op CallOp, receiveMsgCh chan []byte) error {
|
||||||
|
jsonRPCCall := models.JsonRPCRequest{
|
||||||
|
JsonRPC: "2.0",
|
||||||
|
ID: time.Now().UnixMilli(),
|
||||||
|
Method: op.Method,
|
||||||
|
Params: op.Params,
|
||||||
|
}
|
||||||
|
|
||||||
|
callBytes, err := json.Marshal(jsonRPCCall)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.Conn.WriteMessage(websocket.TextMessage, callBytes)
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, messageData, err := w.Conn.ReadMessage()
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var rsp SubscriptionResp
|
||||||
|
if gjson.ParseBytes(messageData).Get("error").Exists() {
|
||||||
|
return fmt.Errorf(gjson.ParseBytes(messageData).Get("error").String())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(gjson.ParseBytes(messageData).String()), &rsp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("establish successfully, subscriptionID: %d, Waiting to accept data...\n", rsp.Result)
|
||||||
|
|
||||||
|
go func(conn *websocket.Conn) {
|
||||||
|
for {
|
||||||
|
messageType, messageData, err := conn.ReadMessage()
|
||||||
|
if nil != err {
|
||||||
|
log.Println(err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch messageType {
|
||||||
|
case websocket.TextMessage:
|
||||||
|
receiveMsgCh <- messageData
|
||||||
|
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(w.Conn)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -5,4 +5,9 @@ const (
|
|||||||
BvMainnetEndpoint = "https://sui-mainnet-endpoint.blockvision.org"
|
BvMainnetEndpoint = "https://sui-mainnet-endpoint.blockvision.org"
|
||||||
SuiTestnetEndpoint = "https://fullnode.testnet.sui.io"
|
SuiTestnetEndpoint = "https://fullnode.testnet.sui.io"
|
||||||
SuiMainnetEndpoint = "https://fullnode.mainnet.sui.io"
|
SuiMainnetEndpoint = "https://fullnode.mainnet.sui.io"
|
||||||
|
|
||||||
|
WssBvTestnetEndpoint = "wss://sui-testnet-endpoint.blockvision.org/websocket"
|
||||||
|
WssBvMainnetEndpoint = "wss://sui-mainnet-endpoint.blockvision.org/websocket"
|
||||||
|
WssSuiTestnetEndpoint = "wss://fullnode.testnet.sui.io"
|
||||||
|
WssSuiMainnetEndpoint = "wss://fullnode.mainnet.sui.io"
|
||||||
)
|
)
|
||||||
|
|||||||
63
examples/subscribe/main.go
Normal file
63
examples/subscribe/main.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/block-vision/sui-go-sdk/constant"
|
||||||
|
"github.com/block-vision/sui-go-sdk/models"
|
||||||
|
"github.com/block-vision/sui-go-sdk/sui"
|
||||||
|
"github.com/block-vision/sui-go-sdk/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
go SubscribeEvent()
|
||||||
|
go SubscribeTransaction()
|
||||||
|
select {}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SubscribeEvent() {
|
||||||
|
var ctx = context.Background()
|
||||||
|
var cli = sui.NewSuiWebsocketClient(constant.WssBvTestnetEndpoint)
|
||||||
|
|
||||||
|
receiveMsgCh := make(chan models.SuiEventResponse, 10)
|
||||||
|
err := cli.SubscribeEvent(ctx, models.SuiXSubscribeEventsRequest{
|
||||||
|
SuiEventFilter: map[string]interface{}{
|
||||||
|
"All": []string{},
|
||||||
|
},
|
||||||
|
}, receiveMsgCh)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg := <-receiveMsgCh:
|
||||||
|
utils.PrettyPrint(msg)
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SubscribeTransaction() {
|
||||||
|
var ctx = context.Background()
|
||||||
|
var cli = sui.NewSuiWebsocketClient(constant.WssBvTestnetEndpoint)
|
||||||
|
|
||||||
|
receiveMsgCh := make(chan models.SuiEffects, 10)
|
||||||
|
err := cli.SubscribeTransaction(ctx, models.SuiXSubscribeTransactionsRequest{
|
||||||
|
TransactionFilter: models.TransactionFilterByFromAddress{
|
||||||
|
FromAddress: "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
},
|
||||||
|
}, receiveMsgCh)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg := <-receiveMsgCh:
|
||||||
|
utils.PrettyPrint(msg)
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
go.mod
1
go.mod
@@ -4,6 +4,7 @@ go 1.20
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-playground/validator/v10 v10.12.0
|
github.com/go-playground/validator/v10 v10.12.0
|
||||||
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/tidwall/gjson v1.14.4
|
github.com/tidwall/gjson v1.14.4
|
||||||
github.com/tyler-smith/go-bip39 v1.1.0
|
github.com/tyler-smith/go-bip39 v1.1.0
|
||||||
golang.org/x/crypto v0.8.0
|
golang.org/x/crypto v0.8.0
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -8,6 +8,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
|
github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
|
||||||
github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
|
github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
|
||||||
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4=
|
github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4=
|
||||||
github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ=
|
github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
|||||||
@@ -216,6 +216,44 @@ type SuiTransactionBlockResponseQuery struct {
|
|||||||
|
|
||||||
type TransactionFilter map[string]interface{}
|
type TransactionFilter map[string]interface{}
|
||||||
|
|
||||||
|
// TransactionFilterByFromAddress is a filter for from address
|
||||||
|
type TransactionFilterByFromAddress struct {
|
||||||
|
FromAddress string `json:"FromAddress"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionFilterByToAddress is a filter for to address
|
||||||
|
type TransactionFilterByToAddress struct {
|
||||||
|
ToAddress string `json:"ToAddress"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionFilterByInputObject is a filter for input objects
|
||||||
|
type TransactionFilterByInputObject struct {
|
||||||
|
// InputObject is the id of the object
|
||||||
|
InputObject string `json:"InputObject"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionFilterByChangedObjectFilter is a filter for changed objects
|
||||||
|
type TransactionFilterByChangedObjectFilter struct {
|
||||||
|
// ChangedObject is a filter for changed objects
|
||||||
|
ChangedObject string `json:"ChangedObject"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionFilterByMoveFunction is a filter for move functions
|
||||||
|
type TransactionFilterByMoveFunction struct {
|
||||||
|
MoveFunction MoveFunction `json:"MoveFunction"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MoveFunction struct {
|
||||||
|
Package string `json:"package"`
|
||||||
|
Module *string `json:"module"`
|
||||||
|
Function *string `json:"function"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SuiXSubscribeTransactionsRequest struct {
|
||||||
|
// the transaction query criteria.
|
||||||
|
TransactionFilter interface{} `json:"filter"`
|
||||||
|
}
|
||||||
|
|
||||||
type SuiXQueryTransactionBlocksRequest struct {
|
type SuiXQueryTransactionBlocksRequest struct {
|
||||||
SuiTransactionBlockResponseQuery SuiTransactionBlockResponseQuery
|
SuiTransactionBlockResponseQuery SuiTransactionBlockResponseQuery
|
||||||
// optional paging cursor
|
// optional paging cursor
|
||||||
@@ -246,3 +284,8 @@ type SuiDevInspectTransactionBlockRequest struct {
|
|||||||
// The epoch to perform the call. Will be set from the system state object if not provided
|
// The epoch to perform the call. Will be set from the system state object if not provided
|
||||||
Epoch string `json:"epoch"`
|
Epoch string `json:"epoch"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SuiXSubscribeEventsRequest struct {
|
||||||
|
// the event query criteria.
|
||||||
|
SuiEventFilter interface{} `json:"suiEventFilter"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
type Request struct {
|
|
||||||
Name string
|
|
||||||
HttpMethod string
|
|
||||||
HttpPath string
|
|
||||||
}
|
|
||||||
89
sui/subscribe_api.go
Normal file
89
sui/subscribe_api.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package sui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/block-vision/sui-go-sdk/common/wsconn"
|
||||||
|
"github.com/block-vision/sui-go-sdk/models"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ISubscribeAPI interface {
|
||||||
|
SubscribeEvent(ctx context.Context, req models.SuiXSubscribeEventsRequest, msgCh chan models.SuiEventResponse) error
|
||||||
|
SubscribeTransaction(ctx context.Context, req models.SuiXSubscribeTransactionsRequest, msgCh chan models.SuiEffects) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type suiSubscribeImpl struct {
|
||||||
|
conn *wsconn.WsConn
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeEvent implements the method `suix_subscribeEvent`, subscribe to a stream of Sui event.
|
||||||
|
func (s *suiSubscribeImpl) SubscribeEvent(ctx context.Context, req models.SuiXSubscribeEventsRequest, msgCh chan models.SuiEventResponse) error {
|
||||||
|
rsp := make(chan []byte, 10)
|
||||||
|
err := s.conn.Call(ctx, wsconn.CallOp{
|
||||||
|
Method: "suix_subscribeEvent",
|
||||||
|
Params: []interface{}{
|
||||||
|
req.SuiEventFilter,
|
||||||
|
},
|
||||||
|
}, rsp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case messageData := <-rsp:
|
||||||
|
var result models.SuiEventResponse
|
||||||
|
if gjson.ParseBytes(messageData).Get("error").Exists() {
|
||||||
|
log.Fatal(gjson.ParseBytes(messageData).Get("error").String())
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(gjson.ParseBytes(messageData).Get("params.result").String()), &result)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msgCh <- result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeTransaction implements the method `suix_subscribeTransaction`, subscribe to a stream of Sui transaction effects.
|
||||||
|
func (s *suiSubscribeImpl) SubscribeTransaction(ctx context.Context, req models.SuiXSubscribeTransactionsRequest, msgCh chan models.SuiEffects) error {
|
||||||
|
rsp := make(chan []byte, 10)
|
||||||
|
err := s.conn.Call(ctx, wsconn.CallOp{
|
||||||
|
Method: "suix_subscribeTransaction",
|
||||||
|
Params: []interface{}{
|
||||||
|
req.TransactionFilter,
|
||||||
|
},
|
||||||
|
}, rsp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case messageData := <-rsp:
|
||||||
|
var result models.SuiEffects
|
||||||
|
if gjson.ParseBytes(messageData).Get("error").Exists() {
|
||||||
|
log.Fatal(gjson.ParseBytes(messageData).Get("error").String())
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(gjson.ParseBytes(messageData).Get("params.result").String()), &result)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msgCh <- result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
28
sui/websocket.go
Normal file
28
sui/websocket.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// Copyright (c) BlockVision, Inc. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/block-vision/sui-go-sdk/common/wsconn"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ISuiWebsocketAPI defines the subscription API related interface, and then implement it by the WebsocketClient.
|
||||||
|
type ISuiWebsocketAPI interface {
|
||||||
|
ISubscribeAPI
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebsocketClient implements SuiWebsocketAPI related interfaces.
|
||||||
|
type WebsocketClient struct {
|
||||||
|
ISubscribeAPI
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSuiWebsocketClient instantiates the WebsocketClient to call the methods of each module.
|
||||||
|
func NewSuiWebsocketClient(rpcUrl string) ISuiWebsocketAPI {
|
||||||
|
conn := wsconn.NewWsConn(rpcUrl)
|
||||||
|
return &WebsocketClient{
|
||||||
|
ISubscribeAPI: &suiSubscribeImpl{
|
||||||
|
conn: conn,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user