diff --git a/node/geth_node.go b/node/geth_node.go index 78aac7067..ccdc57a07 100644 --- a/node/geth_node.go +++ b/node/geth_node.go @@ -117,6 +117,8 @@ func newGethNodeConfig(config *params.NodeConfig) (*node.Config, error) { nc.WSModules = config.FormatAPIModules() nc.WSHost = config.WSHost nc.WSPort = config.WSPort + // FIXME: this is a temporary solution to allow all origins + nc.WSOrigins = []string{"*"} } if config.ClusterConfig.Enabled { diff --git a/node/status_node_services.go b/node/status_node_services.go index 1cb67a0bf..f3cf57a24 100644 --- a/node/status_node_services.go +++ b/node/status_node_services.go @@ -429,7 +429,7 @@ func wakuRateLimiter(wakuCfg *params.WakuConfig, clusterCfg *params.ClusterConfi func (b *StatusNode) connectorService() *connector.Service { if b.connectorSrvc == nil { - b.connectorSrvc = connector.NewService(b.rpcClient, b.connectorSrvc) + b.connectorSrvc = connector.NewService(b.walletDB, b.rpcClient, b.rpcClient.NetworkManager) } return b.connectorSrvc } diff --git a/services/connector/api.go b/services/connector/api.go index 5c43b1feb..563dfb895 100644 --- a/services/connector/api.go +++ b/services/connector/api.go @@ -1,42 +1,75 @@ package connector import ( - "encoding/json" - "fmt" + "github.com/status-im/status-go/services/connector/commands" + persistence "github.com/status-im/status-go/services/connector/database" ) type API struct { s *Service r *CommandRegistry + c *commands.ClientSideHandler } func NewAPI(s *Service) *API { r := NewCommandRegistry() + c := commands.NewClientSideHandler() + + r.Register("eth_sendTransaction", &commands.SendTransactionCommand{ + Db: s.db, + ClientHandler: c, + }) + + // Accounts query and dapp permissions + r.Register("eth_accounts", &commands.AccountsCommand{Db: s.db}) + r.Register("eth_requestAccounts", &commands.RequestAccountsCommand{ + ClientHandler: c, + AccountsCommand: commands.AccountsCommand{Db: s.db}, + }) + + // Active chain per dapp management + r.Register("eth_chainId", &commands.ChainIDCommand{Db: s.db}) + r.Register("wallet_switchEthereumChain", &commands.SwitchEthereumChainCommand{ + Db: s.db, + NetworkManager: s.nm, + }) return &API{ s: s, r: r, + c: c, } } -type RPCRequest struct { - JSONRPC string `json:"jsonrpc"` - ID int `json:"id"` - Method string `json:"method"` - Params []interface{} `json:"params"` -} - func (api *API) CallRPC(inputJSON string) (string, error) { - var request RPCRequest - - err := json.Unmarshal([]byte(inputJSON), &request) + request, err := commands.RPCRequestFromJSON(inputJSON) if err != nil { - return "", fmt.Errorf("error unmarshalling JSON: %v", err) + return "", err } if command, exists := api.r.GetCommand(request.Method); exists { - return command.Execute(inputJSON) + return command.Execute(request) } - return api.s.rpcClient.CallRaw(inputJSON), nil + return api.s.rpc.CallRaw(inputJSON), nil +} + +func (api *API) RecallDAppPermission(origin string) error { + return persistence.DeleteDApp(api.s.db, origin) +} + +func (api *API) RequestAccountsAccepted(args commands.RequestAccountsAcceptedArgs) error { + return api.c.RequestAccountsAccepted(args) +} + +func (api *API) RequestAccountsRejected(args commands.RejectedArgs) error { + return api.c.RequestAccountsRejected(args) +} + +func (api *API) SendTransactionAccepted(args commands.SendTransactionAcceptedArgs) error { + return api.c.SendTransactionAccepted(args) +} + +func (api *API) SendTransactionRejected(args commands.RejectedArgs) error { + return api.c.SendTransactionRejected(args) } diff --git a/services/connector/api_test.go b/services/connector/api_test.go index 6229fa5cf..26cae73f7 100644 --- a/services/connector/api_test.go +++ b/services/connector/api_test.go @@ -9,15 +9,16 @@ import ( gethrpc "github.com/ethereum/go-ethereum/rpc" - "github.com/status-im/status-go/appdatabase" "github.com/status-im/status-go/params" statusRPC "github.com/status-im/status-go/rpc" + "github.com/status-im/status-go/services/connector/commands" "github.com/status-im/status-go/t/helpers" "github.com/status-im/status-go/transactions/fake" + "github.com/status-im/status-go/walletdatabase" ) func createDB(t *testing.T) (*sql.DB, func()) { - db, cleanup, err := helpers.SetupTestSQLDB(appdatabase.DbInitializer{}, "provider-tests-") + db, cleanup, err := helpers.SetupTestSQLDB(walletdatabase.DbInitializer{}, "provider-tests-") require.NoError(t, err) return db, func() { require.NoError(t, cleanup()) } } @@ -38,7 +39,7 @@ func setupTestAPI(t *testing.T) (*API, func()) { rpcClient, err := statusRPC.NewClient(client, 1, upstreamConfig, nil, db) require.NoError(t, err) - service := NewService(rpcClient, nil) + service := NewService(db, rpcClient, nil) return NewAPI(service), cancel } @@ -48,45 +49,36 @@ func TestCallRPC(t *testing.T) { defer cancel() tests := []struct { - request string - expectError bool - expectedContains string - notContains bool + request string + expectError error }{ { - request: "{\"method\": \"eth_blockNumber\", \"params\": []}", - expectError: false, - expectedContains: "does not exist/is not available", - notContains: true, + request: "{\"method\": \"eth_chainId\", \"params\": []}", + expectError: commands.ErrRequestMissingDAppData, }, { - request: "{\"method\": \"eth_blockNumbers\", \"params\": []}", - expectError: false, - expectedContains: "does not exist/is not available", - notContains: false, + request: "{\"method\": \"eth_accounts\", \"params\": []}", + expectError: commands.ErrRequestMissingDAppData, }, { - request: "", - expectError: true, - expectedContains: "does not exist/is not available", - notContains: true, + request: "{\"method\": \"eth_requestAccounts\", \"params\": []}", + expectError: commands.ErrRequestMissingDAppData, + }, + { + request: "{\"method\": \"eth_sendTransaction\", \"params\": []}", + expectError: commands.ErrRequestMissingDAppData, + }, + { + request: "{\"method\": \"wallet_switchEthereumChain\", \"params\": []}", + expectError: commands.ErrRequestMissingDAppData, }, } for _, tt := range tests { t.Run(tt.request, func(t *testing.T) { - response, err := api.CallRPC(tt.request) - if tt.expectError { - require.Error(t, err) - } else { - require.NoError(t, err) - require.NotEmpty(t, response) - if tt.notContains { - require.NotContains(t, response, tt.expectedContains) - } else { - require.Contains(t, response, tt.expectedContains) - } - } + _, err := api.CallRPC(tt.request) + require.Error(t, err) + require.Equal(t, tt.expectError, err) }) } } diff --git a/services/connector/command_registry.go b/services/connector/command_registry.go index 2499d5f57..2d907e4fc 100644 --- a/services/connector/command_registry.go +++ b/services/connector/command_registry.go @@ -1,20 +1,22 @@ package connector +import "github.com/status-im/status-go/services/connector/commands" + type CommandRegistry struct { - commands map[string]RPCCommand + commands map[string]commands.RPCCommand } func NewCommandRegistry() *CommandRegistry { return &CommandRegistry{ - commands: make(map[string]RPCCommand), + commands: make(map[string]commands.RPCCommand), } } -func (r *CommandRegistry) Register(method string, command RPCCommand) { +func (r *CommandRegistry) Register(method string, command commands.RPCCommand) { r.commands[method] = command } -func (r *CommandRegistry) GetCommand(method string) (RPCCommand, bool) { +func (r *CommandRegistry) GetCommand(method string) (commands.RPCCommand, bool) { command, exists := r.commands[method] return command, exists } diff --git a/services/connector/commands/accounts.go b/services/connector/commands/accounts.go new file mode 100644 index 000000000..e03af82c2 --- /dev/null +++ b/services/connector/commands/accounts.go @@ -0,0 +1,48 @@ +package commands + +import ( + "database/sql" + "encoding/json" + "fmt" + + "github.com/status-im/status-go/eth-node/types" + persistence "github.com/status-im/status-go/services/connector/database" +) + +type AccountsCommand struct { + Db *sql.DB +} + +type AccountsResponse struct { + Accounts []types.Address `json:"accounts"` +} + +func (c *AccountsCommand) dAppToAccountsResponse(dApp *persistence.DApp) (string, error) { + response := AccountsResponse{ + Accounts: []types.Address{dApp.SharedAccount}, + } + responseJSON, err := json.Marshal(response) + if err != nil { + return "", fmt.Errorf("failed to marshal response: %v", err) + } + + return string(responseJSON), nil +} + +func (c *AccountsCommand) Execute(request RPCRequest) (string, error) { + err := request.Validate() + if err != nil { + return "", err + } + + dApp, err := persistence.SelectDAppByUrl(c.Db, request.URL) + if err != nil { + return "", err + } + + if dApp == nil { + return "", ErrDAppIsNotPermittedByUser + } + + return c.dAppToAccountsResponse(dApp) +} diff --git a/services/connector/commands/accounts_test.go b/services/connector/commands/accounts_test.go new file mode 100644 index 000000000..e44aa0b26 --- /dev/null +++ b/services/connector/commands/accounts_test.go @@ -0,0 +1,64 @@ +package commands + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/status-im/status-go/eth-node/types" +) + +func TestFailToGetAccountWithMissingDAppFields(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + cmd := &AccountsCommand{Db: db} + + // Missing DApp fields + request, err := ConstructRPCRequest("eth_accounts", []interface{}{}, nil) + assert.NoError(t, err) + + result, err := cmd.Execute(request) + assert.Equal(t, ErrRequestMissingDAppData, err) + assert.Empty(t, result) +} + +func TestFailToGetAccountForUnpermittedDApp(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + cmd := &AccountsCommand{Db: db} + + request, err := ConstructRPCRequest("eth_accounts", []interface{}{}, &testDAppData) + assert.NoError(t, err) + + result, err := cmd.Execute(request) + assert.Equal(t, ErrDAppIsNotPermittedByUser, err) + assert.Empty(t, result) +} + +func TestGetAccountForPermittedDApp(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + cmd := &AccountsCommand{Db: db} + + sharedAccount := types.HexToAddress("0x6d0aa2a774b74bb1d36f97700315adf962c69fcg") + + err := PersistDAppData(db, testDAppData, sharedAccount, 0x123) + assert.NoError(t, err) + + request, err := ConstructRPCRequest("eth_accounts", []interface{}{}, &testDAppData) + assert.NoError(t, err) + + response, err := cmd.Execute(request) + assert.NoError(t, err) + + result := &AccountsResponse{} + err = json.Unmarshal([]byte(response), result) + + assert.NoError(t, err) + assert.Len(t, result.Accounts, 1) + assert.Equal(t, sharedAccount, result.Accounts[0]) +} diff --git a/services/connector/commands/chain_id.go b/services/connector/commands/chain_id.go new file mode 100644 index 000000000..ed5bdbec6 --- /dev/null +++ b/services/connector/commands/chain_id.go @@ -0,0 +1,30 @@ +package commands + +import ( + "database/sql" + + persistence "github.com/status-im/status-go/services/connector/database" + walletCommon "github.com/status-im/status-go/services/wallet/common" +) + +type ChainIDCommand struct { + Db *sql.DB +} + +func (c *ChainIDCommand) Execute(request RPCRequest) (string, error) { + err := request.Validate() + if err != nil { + return "", err + } + + dApp, err := persistence.SelectDAppByUrl(c.Db, request.URL) + if err != nil { + return "", err + } + + if dApp == nil { + return "", ErrDAppIsNotPermittedByUser + } + + return walletCommon.ChainID(dApp.ChainID).String(), nil +} diff --git a/services/connector/commands/chain_id_test.go b/services/connector/commands/chain_id_test.go new file mode 100644 index 000000000..77061f39f --- /dev/null +++ b/services/connector/commands/chain_id_test.go @@ -0,0 +1,59 @@ +package commands + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/status-im/status-go/eth-node/types" + walletCommon "github.com/status-im/status-go/services/wallet/common" +) + +func TestFailToGetChainIdWithMissingDAppFields(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + cmd := &ChainIDCommand{Db: db} + + // Missing DApp fields + request, err := ConstructRPCRequest("eth_chainId", []interface{}{}, nil) + assert.NoError(t, err) + + result, err := cmd.Execute(request) + assert.Equal(t, ErrRequestMissingDAppData, err) + assert.Empty(t, result) +} + +func TestFailToGetChainIdForUnpermittedDApp(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + cmd := &ChainIDCommand{Db: db} + + request, err := ConstructRPCRequest("eth_chainId", []interface{}{}, &testDAppData) + assert.NoError(t, err) + + result, err := cmd.Execute(request) + assert.Equal(t, ErrDAppIsNotPermittedByUser, err) + assert.Empty(t, result) +} + +func TestGetChainIdForPermittedDApp(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + cmd := &ChainIDCommand{Db: db} + + sharedAccount := types.HexToAddress("0x6d0aa2a774b74bb1d36f97700315adf962c69fcg") + chainID := uint64(0x123) + + err := PersistDAppData(db, testDAppData, sharedAccount, chainID) + assert.NoError(t, err) + + request, err := ConstructRPCRequest("eth_chainId", []interface{}{}, &testDAppData) + assert.NoError(t, err) + + response, err := cmd.Execute(request) + assert.NoError(t, err) + assert.Equal(t, walletCommon.ChainID(chainID).String(), response) +} diff --git a/services/connector/commands/client_handler.go b/services/connector/commands/client_handler.go new file mode 100644 index 000000000..3233d1c0c --- /dev/null +++ b/services/connector/commands/client_handler.go @@ -0,0 +1,165 @@ +package commands + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "sync/atomic" + "time" + + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/signal" + "github.com/status-im/status-go/transactions" +) + +var ( + WalletResponseMaxInterval = 20 * time.Minute + + ErrWalletResponseTimeout = fmt.Errorf("timeout waiting for wallet response") + ErrEmptyAccountsShared = fmt.Errorf("empty accounts were shared by wallet") + ErrRequestAccountsRejectedByUser = fmt.Errorf("request accounts was rejected by user") + ErrSendTransactionRejectedByUser = fmt.Errorf("send transaction was rejected by user") + ErrEmptyRequestID = fmt.Errorf("empty requestID") + ErrAnotherConnectorOperationIsAwaitingFor = fmt.Errorf("another connector operation is awaiting for user input") +) + +type MessageType int + +const ( + RequestAccountsAccepted MessageType = iota + SendTransactionAccepted + Rejected +) + +type Message struct { + Type MessageType + Data interface{} +} + +type ClientSideHandler struct { + responseChannel chan Message + isRequestRunning int32 +} + +func NewClientSideHandler() *ClientSideHandler { + return &ClientSideHandler{ + responseChannel: make(chan Message, 1), // Buffer of 1 to avoid blocking + isRequestRunning: 0, + } +} + +func (c *ClientSideHandler) generateRequestID(dApp signal.ConnectorDApp) string { + rawID := fmt.Sprintf("%d%s", time.Now().UnixMilli(), dApp.URL) + hash := sha256.Sum256([]byte(rawID)) + return hex.EncodeToString(hash[:]) +} + +func (c *ClientSideHandler) setRequestRunning() bool { + return atomic.CompareAndSwapInt32(&c.isRequestRunning, 0, 1) +} + +func (c *ClientSideHandler) clearRequestRunning() { + atomic.StoreInt32(&c.isRequestRunning, 0) +} + +func (c *ClientSideHandler) RequestShareAccountForDApp(dApp signal.ConnectorDApp) (types.Address, uint64, error) { + if !c.setRequestRunning() { + return types.Address{}, 0, ErrAnotherConnectorOperationIsAwaitingFor + } + defer c.clearRequestRunning() + + requestID := c.generateRequestID(dApp) + signal.SendConnectorSendRequestAccounts(dApp, requestID) + + timeout := time.After(WalletResponseMaxInterval) + + for { + select { + case msg := <-c.responseChannel: + switch msg.Type { + case RequestAccountsAccepted: + response := msg.Data.(RequestAccountsAcceptedArgs) + if response.RequestID == requestID { + return response.Account, response.ChainID, nil + } + case Rejected: + response := msg.Data.(RejectedArgs) + if response.RequestID == requestID { + return types.Address{}, 0, ErrRequestAccountsRejectedByUser + } + } + case <-timeout: + return types.Address{}, 0, ErrWalletResponseTimeout + } + } +} + +func (c *ClientSideHandler) RequestSendTransaction(dApp signal.ConnectorDApp, chainID uint64, txArgs *transactions.SendTxArgs) (types.Hash, error) { + if !c.setRequestRunning() { + return types.Hash{}, ErrAnotherConnectorOperationIsAwaitingFor + } + defer c.clearRequestRunning() + + txArgsJson, err := json.Marshal(txArgs) + if err != nil { + return types.Hash{}, fmt.Errorf("failed to marshal txArgs: %v", err) + } + + requestID := c.generateRequestID(dApp) + signal.SendConnectorSendTransaction(dApp, chainID, string(txArgsJson), requestID) + + timeout := time.After(WalletResponseMaxInterval) + + for { + select { + case msg := <-c.responseChannel: + switch msg.Type { + case SendTransactionAccepted: + response := msg.Data.(SendTransactionAcceptedArgs) + if response.RequestID == requestID { + return response.Hash, nil + } + case Rejected: + response := msg.Data.(RejectedArgs) + if response.RequestID == requestID { + return types.Hash{}, ErrSendTransactionRejectedByUser + } + } + case <-timeout: + return types.Hash{}, ErrWalletResponseTimeout + } + } +} + +func (c *ClientSideHandler) RequestAccountsAccepted(args RequestAccountsAcceptedArgs) error { + c.responseChannel <- Message{Type: RequestAccountsAccepted, Data: args} + return nil +} + +func (c *ClientSideHandler) RequestAccountsRejected(args RejectedArgs) error { + if args.RequestID == "" { + return ErrEmptyRequestID + } + + c.responseChannel <- Message{Type: Rejected, Data: args} + return nil +} + +func (c *ClientSideHandler) SendTransactionAccepted(args SendTransactionAcceptedArgs) error { + if args.RequestID == "" { + return ErrEmptyRequestID + } + + c.responseChannel <- Message{Type: SendTransactionAccepted, Data: args} + return nil +} + +func (c *ClientSideHandler) SendTransactionRejected(args RejectedArgs) error { + if args.RequestID == "" { + return ErrEmptyRequestID + } + + c.responseChannel <- Message{Type: Rejected, Data: args} + return nil +} diff --git a/services/connector/commands/client_handler_test.go b/services/connector/commands/client_handler_test.go new file mode 100644 index 000000000..c28a344b0 --- /dev/null +++ b/services/connector/commands/client_handler_test.go @@ -0,0 +1,28 @@ +package commands + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestClientHandlerTimeout(t *testing.T) { + clientHandler := NewClientSideHandler() + + backupWalletResponseMaxInterval := WalletResponseMaxInterval + WalletResponseMaxInterval = 1 * time.Millisecond + + _, _, err := clientHandler.RequestShareAccountForDApp(testDAppData) + assert.Equal(t, ErrWalletResponseTimeout, err) + WalletResponseMaxInterval = backupWalletResponseMaxInterval +} + +func TestRequestRejectedWhileWaiting(t *testing.T) { + clientHandler := NewClientSideHandler() + + clientHandler.setRequestRunning() + + _, _, err := clientHandler.RequestShareAccountForDApp(testDAppData) + assert.Equal(t, ErrAnotherConnectorOperationIsAwaitingFor, err) +} diff --git a/services/connector/commands/request_accounts.go b/services/connector/commands/request_accounts.go new file mode 100644 index 000000000..78752d843 --- /dev/null +++ b/services/connector/commands/request_accounts.go @@ -0,0 +1,65 @@ +package commands + +import ( + "errors" + + "github.com/status-im/status-go/multiaccounts/accounts" + persistence "github.com/status-im/status-go/services/connector/database" + "github.com/status-im/status-go/signal" +) + +// errors +var ( + ErrAccountsRequestDeniedByUser = errors.New("accounts request denied by user") + ErrNoAccountsAvailable = errors.New("no accounts available") +) + +type RequestAccountsCommand struct { + ClientHandler ClientSideHandlerInterface + AccountsCommand +} + +type RawAccountsResponse struct { + JSONRPC string `json:"jsonrpc"` + ID int `json:"id"` + Result []accounts.Account `json:"result"` +} + +func (c *RequestAccountsCommand) Execute(request RPCRequest) (string, error) { + err := request.Validate() + if err != nil { + return "", err + } + + dApp, err := persistence.SelectDAppByUrl(c.Db, request.URL) + if err != nil { + return "", err + } + + // FIXME: this may have a security issue in case some malicious software tries to fake the origin + if dApp == nil { + account, chainID, err := c.ClientHandler.RequestShareAccountForDApp(signal.ConnectorDApp{ + URL: request.URL, + Name: request.Name, + IconURL: request.IconURL, + }) + if err != nil { + return "", err + } + + dApp = &persistence.DApp{ + URL: request.URL, + Name: request.Name, + IconURL: request.IconURL, + SharedAccount: account, + ChainID: chainID, + } + + err = persistence.UpsertDApp(c.Db, dApp) + if err != nil { + return "", err + } + } + + return c.dAppToAccountsResponse(dApp) +} diff --git a/services/connector/commands/request_accounts_test.go b/services/connector/commands/request_accounts_test.go new file mode 100644 index 000000000..2e497e8d7 --- /dev/null +++ b/services/connector/commands/request_accounts_test.go @@ -0,0 +1,153 @@ +package commands + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/status-im/status-go/eth-node/types" + persistence "github.com/status-im/status-go/services/connector/database" + walletCommon "github.com/status-im/status-go/services/wallet/common" + "github.com/status-im/status-go/signal" +) + +func TestFailToRequestAccountsWithMissingDAppFields(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + cmd := &RequestAccountsCommand{AccountsCommand: AccountsCommand{Db: db}} + + // Missing DApp fields + request, err := ConstructRPCRequest("eth_requestAccounts", []interface{}{}, nil) + assert.NoError(t, err) + + result, err := cmd.Execute(request) + assert.Equal(t, ErrRequestMissingDAppData, err) + assert.Empty(t, result) +} + +func TestRequestAccountsWithSignalTimeout(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + clientHandler := NewClientSideHandler() + + cmd := &RequestAccountsCommand{ + ClientHandler: clientHandler, + AccountsCommand: AccountsCommand{Db: db}, + } + + request, err := prepareSendTransactionRequest(testDAppData, types.Address{0x01}) + assert.NoError(t, err) + + backupWalletResponseMaxInterval := WalletResponseMaxInterval + WalletResponseMaxInterval = 1 * time.Millisecond + + _, err = cmd.Execute(request) + assert.Equal(t, ErrWalletResponseTimeout, err) + WalletResponseMaxInterval = backupWalletResponseMaxInterval +} + +func TestRequestAccountsAcceptedAndRequestAgain(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + clientHandler := NewClientSideHandler() + + cmd := &RequestAccountsCommand{ + ClientHandler: clientHandler, + AccountsCommand: AccountsCommand{Db: db}, + } + + request, err := ConstructRPCRequest("eth_requestAccounts", []interface{}{}, &testDAppData) + assert.NoError(t, err) + + accountAddress := types.Address{0x03} + + signal.SetMobileSignalHandler(signal.MobileSignalHandler(func(s []byte) { + var evt EventType + err := json.Unmarshal(s, &evt) + assert.NoError(t, err) + + switch evt.Type { + case signal.EventConnectorSendRequestAccounts: + var ev signal.ConnectorSendRequestAccountsSignal + err := json.Unmarshal(evt.Event, &ev) + assert.NoError(t, err) + + err = clientHandler.RequestAccountsAccepted(RequestAccountsAcceptedArgs{ + RequestID: ev.RequestID, + Account: accountAddress, + ChainID: walletCommon.EthereumMainnet, + }) + assert.NoError(t, err) + } + })) + + response, err := cmd.Execute(request) + assert.NoError(t, err) + + result := &AccountsResponse{} + err = json.Unmarshal([]byte(response), result) + + assert.NoError(t, err) + assert.Len(t, result.Accounts, 1) + assert.Equal(t, accountAddress, result.Accounts[0]) + + // Check dApp in the database + dApp, err := persistence.SelectDAppByUrl(db, request.URL) + assert.NoError(t, err) + assert.Equal(t, request.Name, dApp.Name) + assert.Equal(t, request.IconURL, dApp.IconURL) + assert.Equal(t, accountAddress, dApp.SharedAccount) + assert.Equal(t, walletCommon.EthereumMainnet, dApp.ChainID) + + // This should not invoke UI side + response, err = cmd.Execute(request) + assert.NoError(t, err) + + err = json.Unmarshal([]byte(response), result) + + assert.NoError(t, err) + assert.Len(t, result.Accounts, 1) + assert.Equal(t, accountAddress, result.Accounts[0]) +} + +func TestRequestAccountsRejected(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + clientHandler := NewClientSideHandler() + + cmd := &RequestAccountsCommand{ + ClientHandler: clientHandler, + AccountsCommand: AccountsCommand{Db: db}, + } + + request, err := ConstructRPCRequest("eth_requestAccounts", []interface{}{}, &testDAppData) + assert.NoError(t, err) + + signal.SetMobileSignalHandler(signal.MobileSignalHandler(func(s []byte) { + var evt EventType + err := json.Unmarshal(s, &evt) + assert.NoError(t, err) + + switch evt.Type { + case signal.EventConnectorSendRequestAccounts: + var ev signal.ConnectorSendRequestAccountsSignal + err := json.Unmarshal(evt.Event, &ev) + assert.NoError(t, err) + + err = clientHandler.RequestAccountsRejected(RejectedArgs{ + RequestID: ev.RequestID, + }) + assert.NoError(t, err) + } + })) + + _, err = cmd.Execute(request) + assert.Equal(t, ErrRequestAccountsRejectedByUser, err) + +} diff --git a/services/connector/commands/rpc_traits.go b/services/connector/commands/rpc_traits.go new file mode 100644 index 000000000..873728ad4 --- /dev/null +++ b/services/connector/commands/rpc_traits.go @@ -0,0 +1,83 @@ +package commands + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/params" + "github.com/status-im/status-go/signal" + "github.com/status-im/status-go/transactions" +) + +// errors +var ( + ErrRequestMissingDAppData = errors.New("request missing dApp data") + ErrDAppIsNotPermittedByUser = errors.New("dApp is not permitted by user") + ErrEmptyRPCParams = errors.New("empty rpc params") +) + +type RPCRequest struct { + JSONRPC string `json:"jsonrpc"` + ID int `json:"id"` + Method string `json:"method"` + Params []interface{} `json:"params"` + URL string `json:"url"` + Name string `json:"name"` + IconURL string `json:"iconUrl"` +} + +type RPCCommand interface { + Execute(request RPCRequest) (string, error) +} + +type RequestAccountsAcceptedArgs struct { + RequestID string `json:"requestId"` + Account types.Address `json:"account"` + ChainID uint64 `json:"chainId"` +} + +type SendTransactionAcceptedArgs struct { + RequestID string `json:"requestId"` + Hash types.Hash `json:"hash"` +} + +type RejectedArgs struct { + RequestID string `json:"requestId"` +} + +type ClientSideHandlerInterface interface { + RequestShareAccountForDApp(dApp signal.ConnectorDApp) (types.Address, uint64, error) + RequestSendTransaction(dApp signal.ConnectorDApp, chainID uint64, txArgs *transactions.SendTxArgs) (types.Hash, error) + + RequestAccountsAccepted(args RequestAccountsAcceptedArgs) error + RequestAccountsRejected(args RejectedArgs) error + SendTransactionAccepted(args SendTransactionAcceptedArgs) error + SendTransactionRejected(args RejectedArgs) error +} + +type NetworkManagerInterface interface { + GetActiveNetworks() ([]*params.Network, error) +} + +type RPCClientInterface interface { + CallRaw(body string) string +} + +func RPCRequestFromJSON(inputJSON string) (RPCRequest, error) { + var request RPCRequest + + err := json.Unmarshal([]byte(inputJSON), &request) + if err != nil { + return RPCRequest{}, fmt.Errorf("error unmarshalling JSON: %v", err) + } + return request, nil +} + +func (r *RPCRequest) Validate() error { + if r.URL == "" || r.Name == "" { + return ErrRequestMissingDAppData + } + return nil +} diff --git a/services/connector/commands/send_transaction.go b/services/connector/commands/send_transaction.go new file mode 100644 index 000000000..37429cac9 --- /dev/null +++ b/services/connector/commands/send_transaction.go @@ -0,0 +1,81 @@ +package commands + +import ( + "database/sql" + "encoding/json" + "errors" + "fmt" + + persistence "github.com/status-im/status-go/services/connector/database" + "github.com/status-im/status-go/signal" + "github.com/status-im/status-go/transactions" +) + +var ( + ErrParamsFromAddressIsNotShared = errors.New("from parameter address is not dApp's shared account") + ErrNoTransactionParamsFound = errors.New("no transaction in params found") +) + +type SendTransactionCommand struct { + Db *sql.DB + ClientHandler ClientSideHandlerInterface +} + +func (r *RPCRequest) getSendTransactionParams() (*transactions.SendTxArgs, error) { + if r.Params == nil || len(r.Params) == 0 { + return nil, ErrEmptyRPCParams + } + + paramMap, ok := r.Params[0].(map[string]interface{}) + if !ok { + return nil, ErrNoTransactionParamsFound + } + + paramBytes, err := json.Marshal(paramMap) + if err != nil { + return nil, fmt.Errorf("error marshalling first transaction param: %v", err) + } + + var sendTxArgs transactions.SendTxArgs + err = json.Unmarshal(paramBytes, &sendTxArgs) + if err != nil { + return nil, fmt.Errorf("error unmarshalling first transaction param to SendTxArgs: %v", err) + } + + return &sendTxArgs, nil +} + +func (c *SendTransactionCommand) Execute(request RPCRequest) (string, error) { + err := request.Validate() + if err != nil { + return "", err + } + + dApp, err := persistence.SelectDAppByUrl(c.Db, request.URL) + if err != nil { + return "", err + } + + if dApp == nil { + return "", ErrDAppIsNotPermittedByUser + } + + params, err := request.getSendTransactionParams() + if err != nil { + return "", err + } + + if params.From != dApp.SharedAccount { + return "", ErrParamsFromAddressIsNotShared + } + + hash, err := c.ClientHandler.RequestSendTransaction(signal.ConnectorDApp{ + URL: request.URL, + Name: request.Name, + IconURL: request.IconURL, + }, dApp.ChainID, params) + if err != nil { + return "", err + } + return hash.String(), nil +} diff --git a/services/connector/commands/send_transaction_test.go b/services/connector/commands/send_transaction_test.go new file mode 100644 index 000000000..ad7201c95 --- /dev/null +++ b/services/connector/commands/send_transaction_test.go @@ -0,0 +1,175 @@ +package commands + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + hexutil "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/signal" + "github.com/status-im/status-go/transactions" +) + +func prepareSendTransactionRequest(dApp signal.ConnectorDApp, from types.Address) (RPCRequest, error) { + sendArgs := transactions.SendTxArgs{ + From: from, + To: &types.Address{0x02}, + Value: &hexutil.Big{}, + Data: types.HexBytes("0x0"), + } + + sendArgsJSON, err := json.Marshal(sendArgs) + if err != nil { + return RPCRequest{}, err + } + + var sendArgsMap map[string]interface{} + err = json.Unmarshal(sendArgsJSON, &sendArgsMap) + if err != nil { + return RPCRequest{}, err + } + + params := []interface{}{sendArgsMap} + + return ConstructRPCRequest("eth_sendTransaction", params, &dApp) +} + +func TestFailToSendTransactionWithoutPermittedDApp(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + cmd := &SendTransactionCommand{Db: db} + + // Don't save dApp in the database + request, err := prepareSendTransactionRequest(testDAppData, types.Address{0x1}) + assert.NoError(t, err) + + _, err = cmd.Execute(request) + assert.Equal(t, ErrDAppIsNotPermittedByUser, err) +} + +func TestFailToSendTransactionWithWrongAddress(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + cmd := &SendTransactionCommand{Db: db} + + err := PersistDAppData(db, testDAppData, types.Address{0x01}, uint64(0x1)) + assert.NoError(t, err) + + request, err := prepareSendTransactionRequest(testDAppData, types.Address{0x02}) + assert.NoError(t, err) + + _, err = cmd.Execute(request) + assert.Equal(t, ErrParamsFromAddressIsNotShared, err) +} + +func TestSendTransactionWithSignalTimout(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + clientHandler := NewClientSideHandler() + + cmd := &SendTransactionCommand{ + Db: db, + ClientHandler: clientHandler, + } + + err := PersistDAppData(db, testDAppData, types.Address{0x01}, uint64(0x1)) + assert.NoError(t, err) + + request, err := prepareSendTransactionRequest(testDAppData, types.Address{0x01}) + assert.NoError(t, err) + + backupWalletResponseMaxInterval := WalletResponseMaxInterval + WalletResponseMaxInterval = 1 * time.Millisecond + + _, err = cmd.Execute(request) + assert.Equal(t, ErrWalletResponseTimeout, err) + WalletResponseMaxInterval = backupWalletResponseMaxInterval +} + +func TestSendTransactionWithSignalAccepted(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + fakedTransactionHash := types.Hash{0x051} + + clientHandler := NewClientSideHandler() + + cmd := &SendTransactionCommand{ + Db: db, + ClientHandler: clientHandler, + } + + err := PersistDAppData(db, testDAppData, types.Address{0x01}, uint64(0x1)) + assert.NoError(t, err) + + request, err := prepareSendTransactionRequest(testDAppData, types.Address{0x01}) + assert.NoError(t, err) + + signal.SetMobileSignalHandler(signal.MobileSignalHandler(func(s []byte) { + var evt EventType + err := json.Unmarshal(s, &evt) + assert.NoError(t, err) + + switch evt.Type { + case signal.EventConnectorSendTransaction: + var ev signal.ConnectorSendTransactionSignal + err := json.Unmarshal(evt.Event, &ev) + assert.NoError(t, err) + + err = clientHandler.SendTransactionAccepted(SendTransactionAcceptedArgs{ + Hash: fakedTransactionHash, + RequestID: ev.RequestID, + }) + assert.NoError(t, err) + } + })) + + response, err := cmd.Execute(request) + assert.NoError(t, err) + assert.Equal(t, response, fakedTransactionHash.String()) +} + +func TestSendTransactionWithSignalRejected(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + clientHandler := NewClientSideHandler() + + cmd := &SendTransactionCommand{ + Db: db, + ClientHandler: clientHandler, + } + + err := PersistDAppData(db, testDAppData, types.Address{0x01}, uint64(0x1)) + assert.NoError(t, err) + + request, err := prepareSendTransactionRequest(testDAppData, types.Address{0x01}) + assert.NoError(t, err) + + signal.SetMobileSignalHandler(signal.MobileSignalHandler(func(s []byte) { + var evt EventType + err := json.Unmarshal(s, &evt) + assert.NoError(t, err) + + switch evt.Type { + case signal.EventConnectorSendTransaction: + var ev signal.ConnectorSendTransactionSignal + err := json.Unmarshal(evt.Event, &ev) + assert.NoError(t, err) + + err = clientHandler.SendTransactionRejected(RejectedArgs{ + RequestID: ev.RequestID, + }) + assert.NoError(t, err) + } + })) + + _, err = cmd.Execute(request) + assert.Equal(t, ErrSendTransactionRejectedByUser, err) +} diff --git a/services/connector/commands/switch_ethereum_chain.go b/services/connector/commands/switch_ethereum_chain.go new file mode 100644 index 000000000..ce0650650 --- /dev/null +++ b/services/connector/commands/switch_ethereum_chain.go @@ -0,0 +1,94 @@ +package commands + +import ( + "database/sql" + "errors" + "slices" + + persistence "github.com/status-im/status-go/services/connector/database" + walletCommon "github.com/status-im/status-go/services/wallet/common" +) + +// errors +var ( + ErrNoActiveNetworks = errors.New("no active networks") + ErrUnsupportedNetwork = errors.New("unsupported network") + ErrNoChainIDParamsFound = errors.New("no chain id in params found") +) + +type SwitchEthereumChainCommand struct { + NetworkManager NetworkManagerInterface + Db *sql.DB +} + +func (r *RPCRequest) getChainID() (uint64, error) { + if r.Params == nil || len(r.Params) == 0 { + return 0, ErrEmptyRPCParams + } + + switch v := r.Params[0].(type) { + case float64: + return uint64(v), nil + case int: + return uint64(v), nil + case int64: + return uint64(v), nil + case uint64: + return v, nil + default: + return 0, ErrNoChainIDParamsFound + } +} + +func (c *SwitchEthereumChainCommand) getSupportedChainIDs() ([]uint64, error) { + activeNetworks, err := c.NetworkManager.GetActiveNetworks() + if err != nil { + return nil, err + } + + if len(activeNetworks) < 1 { + return nil, ErrNoActiveNetworks + } + + chainIDs := make([]uint64, len(activeNetworks)) + for i, network := range activeNetworks { + chainIDs[i] = network.ChainID + } + + return chainIDs, nil +} + +func (c *SwitchEthereumChainCommand) Execute(request RPCRequest) (string, error) { + err := request.Validate() + if err != nil { + return "", err + } + + requestedChainID, err := request.getChainID() + if err != nil { + return "", err + } + + chainIDs, err := c.getSupportedChainIDs() + if err != nil { + return "", err + } + + if !slices.Contains(chainIDs, requestedChainID) { + return "", ErrUnsupportedNetwork + } + + dApp, err := persistence.SelectDAppByUrl(c.Db, request.URL) + if err != nil { + return "", err + } + + dApp.ChainID = requestedChainID + + err = persistence.UpsertDApp(c.Db, dApp) + if err != nil { + return "", err + } + + return walletCommon.ChainID(dApp.ChainID).String(), nil +} diff --git a/services/connector/commands/switch_ethereum_chain_test.go b/services/connector/commands/switch_ethereum_chain_test.go new file mode 100644 index 000000000..0a3e2478f --- /dev/null +++ b/services/connector/commands/switch_ethereum_chain_test.go @@ -0,0 +1,98 @@ +package commands + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/params" + walletCommon "github.com/status-im/status-go/services/wallet/common" +) + +func TestFailToSwitchEthereumChainWithMissingDAppFields(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + cmd := &SwitchEthereumChainCommand{Db: db} + + // Missing DApp fields + request, err := ConstructRPCRequest("wallet_switchEthereumChain", []interface{}{}, nil) + assert.NoError(t, err) + + result, err := cmd.Execute(request) + assert.Equal(t, ErrRequestMissingDAppData, err) + assert.Empty(t, result) +} + +func TestFailToSwitchEthereumChainWithNoChainId(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + cmd := &SwitchEthereumChainCommand{Db: db} + + request, err := ConstructRPCRequest("wallet_switchEthereumChain", []interface{}{}, &testDAppData) + assert.NoError(t, err) + + _, err = cmd.Execute(request) + assert.Equal(t, ErrEmptyRPCParams, err) +} + +func TestFailToSwitchEthereumChainWithUnsupportedChainId(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + nm := NetworkManagerMock{} + nm.SetNetworks([]*params.Network{ + { + ChainID: walletCommon.EthereumMainnet, + }, + }) + + cmd := &SwitchEthereumChainCommand{ + Db: db, + NetworkManager: &nm, + } + + params := make([]interface{}, 1) + params[0] = walletCommon.BinanceTestChainID // some unrecoginzed chain id + + request, err := ConstructRPCRequest("wallet_switchEthereumChain", params, &testDAppData) + assert.NoError(t, err) + + _, err = cmd.Execute(request) + assert.Equal(t, ErrUnsupportedNetwork, err) +} + +func TestSwitchEthereumChain(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + nm := NetworkManagerMock{} + nm.SetNetworks([]*params.Network{ + { + ChainID: walletCommon.EthereumMainnet, + }, + { + ChainID: walletCommon.EthereumGoerli, + }, + }) + + cmd := &SwitchEthereumChainCommand{ + Db: db, + NetworkManager: &nm, + } + + params := make([]interface{}, 1) + params[0] = walletCommon.EthereumMainnet + + request, err := ConstructRPCRequest("wallet_switchEthereumChain", params, &testDAppData) + assert.NoError(t, err) + + err = PersistDAppData(db, testDAppData, types.HexToAddress("0x6d0aa2a774b74bb1d36f97700315adf962c69fcg"), walletCommon.EthereumMainnet) + assert.NoError(t, err) + + response, err := cmd.Execute(request) + assert.NoError(t, err) + assert.Equal(t, walletCommon.ChainID(walletCommon.EthereumMainnet).String(), response) +} diff --git a/services/connector/commands/test_helpers.go b/services/connector/commands/test_helpers.go new file mode 100644 index 000000000..01e59cdcb --- /dev/null +++ b/services/connector/commands/test_helpers.go @@ -0,0 +1,88 @@ +package commands + +import ( + "database/sql" + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/params" + persistence "github.com/status-im/status-go/services/connector/database" + "github.com/status-im/status-go/signal" + "github.com/status-im/status-go/t/helpers" + "github.com/status-im/status-go/walletdatabase" +) + +var testDAppData = signal.ConnectorDApp{ + URL: "http://testDAppURL", + Name: "testDAppName", + IconURL: "http://testDAppIconUrl", +} + +type RPCClientMock struct { + response string +} + +type NetworkManagerMock struct { + networks []*params.Network +} + +type EventType struct { + Type string `json:"type"` + Event json.RawMessage `json:"event"` +} + +func (c *RPCClientMock) CallRaw(request string) string { + return c.response +} + +func (c *RPCClientMock) SetResponse(response string) { + c.response = response +} + +func (nm *NetworkManagerMock) GetActiveNetworks() ([]*params.Network, error) { + return nm.networks, nil +} + +func (nm *NetworkManagerMock) SetNetworks(networks []*params.Network) { + nm.networks = networks +} + +func SetupTestDB(t *testing.T) (db *sql.DB, close func()) { + db, err := helpers.SetupTestMemorySQLDB(walletdatabase.DbInitializer{}) + require.NoError(t, err) + return db, func() { + require.NoError(t, db.Close()) + } +} + +func PersistDAppData(db *sql.DB, dApp signal.ConnectorDApp, sharedAccount types.Address, chainID uint64) error { + dAppDb := persistence.DApp{ + URL: dApp.URL, + Name: dApp.Name, + IconURL: dApp.IconURL, + SharedAccount: sharedAccount, + ChainID: chainID, + } + + return persistence.UpsertDApp(db, &dAppDb) +} + +func ConstructRPCRequest(method string, params []interface{}, dApp *signal.ConnectorDApp) (RPCRequest, error) { + request := RPCRequest{ + JSONRPC: "2.0", + ID: 1, + Method: method, + Params: params, + } + + if dApp != nil { + request.URL = dApp.URL + request.Name = dApp.Name + request.IconURL = dApp.IconURL + } + + return request, nil +} diff --git a/services/connector/connector_flows_test.go b/services/connector/connector_flows_test.go new file mode 100644 index 000000000..079cc6c2c --- /dev/null +++ b/services/connector/connector_flows_test.go @@ -0,0 +1,112 @@ +package connector + +import ( + "encoding/json" + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/params" + "github.com/status-im/status-go/services/connector/commands" + "github.com/status-im/status-go/signal" +) + +func TestRequestAccountsSwitchChainAndSendTransactionFlow(t *testing.T) { + db, close := createDB(t) + defer close() + + nm := commands.NetworkManagerMock{} + nm.SetNetworks([]*params.Network{ + { + ChainID: 0x1, + Layer: 1, + }, + { + ChainID: 0x5, + Layer: 1, + }, + }) + rpc := commands.RPCClientMock{} + + service := NewService(db, &rpc, &nm) + + api := NewAPI(service) + + // Try to request accounts without permission + request := "{\"method\":\"eth_accounts\",\"params\":[],\"url\":\"http://testDAppURL123\",\"name\":\"testDAppName\",\"iconUrl\":\"http://testDAppIconUrl\"}" + response, err := api.CallRPC(request) + assert.Empty(t, response) + assert.Error(t, err) + assert.Equal(t, commands.ErrDAppIsNotPermittedByUser, err) + + accountAddress := types.BytesToAddress(types.FromHex("0x6d0aa2a774b74bb1d36f97700315adf962c69fcg")) + expectedHash := types.BytesToHash(types.FromHex("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")) + + signal.SetMobileSignalHandler(signal.MobileSignalHandler(func(s []byte) { + var evt commands.EventType + err := json.Unmarshal(s, &evt) + assert.NoError(t, err) + + switch evt.Type { + case signal.EventConnectorSendRequestAccounts: + var ev signal.ConnectorSendRequestAccountsSignal + err := json.Unmarshal(evt.Event, &ev) + assert.NoError(t, err) + + err = api.RequestAccountsAccepted(commands.RequestAccountsAcceptedArgs{ + RequestID: ev.RequestID, + Account: accountAddress, + ChainID: 0x1, + }) + assert.NoError(t, err) + case signal.EventConnectorSendTransaction: + var ev signal.ConnectorSendTransactionSignal + err := json.Unmarshal(evt.Event, &ev) + assert.NoError(t, err) + + err = api.SendTransactionAccepted(commands.SendTransactionAcceptedArgs{ + RequestID: ev.RequestID, + Hash: expectedHash, + }) + assert.NoError(t, err) + } + })) + + // Request accounts, now for real + request = "{\"method\": \"eth_requestAccounts\", \"params\": [], \"url\": \"http://testDAppURL123\", \"name\": \"testDAppName\", \"iconUrl\": \"http://testDAppIconUrl\" }" + expectedResponse := strings.ToLower(fmt.Sprintf(`{"accounts":["%s"]}`, accountAddress.Hex())) + response, err = api.CallRPC(request) + assert.NoError(t, err) + assert.Equal(t, expectedResponse, response) + + // Request to switch ethereum chain + expectedChainId := 0x5 + request = fmt.Sprintf("{\"method\": \"wallet_switchEthereumChain\", \"params\": [%d], \"url\": \"http://testDAppURL123\", \"name\": \"testDAppName\", \"iconUrl\": \"http://testDAppIconUrl\" }", expectedChainId) + expectedResponse = fmt.Sprintf(`%d`, expectedChainId) + response, err = api.CallRPC(request) + assert.NoError(t, err) + assert.Equal(t, expectedResponse, response) + + // Check if the chain was switched + request = "{\"method\": \"eth_chainId\", \"params\": [], \"url\": \"http://testDAppURL123\", \"name\": \"testDAppName\", \"iconUrl\": \"http://testDAppIconUrl\" }" + response, err = api.CallRPC(request) + assert.NoError(t, err) + assert.Equal(t, expectedResponse, response) + + // Check the account after switching chain + request = "{\"method\": \"eth_accounts\", \"params\": [], \"url\": \"http://testDAppURL123\", \"name\": \"testDAppName\", \"iconUrl\": \"http://testDAppIconUrl\" }" + expectedResponse = strings.ToLower(fmt.Sprintf(`{"accounts":["%s"]}`, accountAddress.Hex())) + response, err = api.CallRPC(request) + assert.NoError(t, err) + assert.Equal(t, expectedResponse, response) + + // Send transaction + request = fmt.Sprintf("{\"method\": \"eth_sendTransaction\", \"params\":[{\"from\":\"%s\",\"to\":\"0x0200000000000000000000000000000000000000\",\"value\":\"0x12345\",\"data\":\"0x307830\"}], \"url\": \"http://testDAppURL123\", \"name\": \"testDAppName\", \"iconUrl\": \"http://testDAppIconUrl\" }", accountAddress.Hex()) + expectedResponse = expectedHash.Hex() + response, err = api.CallRPC(request) + assert.NoError(t, err) + assert.Equal(t, expectedResponse, response) +} diff --git a/services/connector/database/persistence.go b/services/connector/database/persistence.go new file mode 100644 index 000000000..201a633b9 --- /dev/null +++ b/services/connector/database/persistence.go @@ -0,0 +1,40 @@ +package persistence + +import ( + "database/sql" + + "github.com/status-im/status-go/eth-node/types" +) + +const upsertDAppQuery = "INSERT INTO connector_dapps (url, name, icon_url, shared_account, chain_id) VALUES (?, ?, ?, ?, ?) ON CONFLICT(url) DO UPDATE SET name = excluded.name, icon_url = excluded.icon_url, shared_account = excluded.shared_account, chain_id = excluded.chain_id" +const selectDAppByUrlQuery = "SELECT name, icon_url, shared_account, chain_id FROM connector_dapps WHERE url = ?" +const deleteDAppQuery = "DELETE FROM connector_dapps WHERE url = ?" + +type DApp struct { + URL string `json:"url"` + Name string `json:"name"` + IconURL string `json:"iconUrl"` + SharedAccount types.Address `json:"sharedAccount"` + ChainID uint64 `json:"chainId"` +} + +func UpsertDApp(db *sql.DB, dApp *DApp) error { + _, err := db.Exec(upsertDAppQuery, dApp.URL, dApp.Name, dApp.IconURL, dApp.SharedAccount, dApp.ChainID) + return err +} + +func SelectDAppByUrl(db *sql.DB, url string) (*DApp, error) { + dApp := &DApp{ + URL: url, + } + err := db.QueryRow(selectDAppByUrlQuery, url).Scan(&dApp.Name, &dApp.IconURL, &dApp.SharedAccount, &dApp.ChainID) + if err == sql.ErrNoRows { + return nil, nil + } + return dApp, err +} + +func DeleteDApp(db *sql.DB, url string) error { + _, err := db.Exec(deleteDAppQuery, url) + return err +} diff --git a/services/connector/database/persistence_test.go b/services/connector/database/persistence_test.go new file mode 100644 index 000000000..f6a525394 --- /dev/null +++ b/services/connector/database/persistence_test.go @@ -0,0 +1,82 @@ +package persistence + +import ( + "testing" + + "database/sql" + + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/t/helpers" + "github.com/status-im/status-go/walletdatabase" + + "github.com/stretchr/testify/require" +) + +var testDApp = DApp{ + Name: "Test DApp", + URL: "https://test-dapp-url.com", + IconURL: "https://test-dapp-icon-url.com", + SharedAccount: types.HexToAddress("0x1234567890"), + ChainID: 0x1, +} + +func setupTestDB(t *testing.T) (db *sql.DB, close func()) { + db, err := helpers.SetupTestMemorySQLDB(walletdatabase.DbInitializer{}) + require.NoError(t, err) + return db, func() { + require.NoError(t, db.Close()) + } +} + +func TestInsertAndSelectDApp(t *testing.T) { + db, close := setupTestDB(t) + defer close() + + err := UpsertDApp(db, &testDApp) + require.NoError(t, err) + + dAppBack, err := SelectDAppByUrl(db, testDApp.URL) + require.NoError(t, err) + require.Equal(t, &testDApp, dAppBack) +} + +func TestInsertAndUpdateDApp(t *testing.T) { + db, close := setupTestDB(t) + defer close() + + err := UpsertDApp(db, &testDApp) + require.NoError(t, err) + + updatedDApp := DApp{ + Name: "Updated Test DApp", + URL: testDApp.URL, + IconURL: "https://updated-test-dapp-icon-url.com", + } + + err = UpsertDApp(db, &updatedDApp) + require.NoError(t, err) + + dAppBack, err := SelectDAppByUrl(db, testDApp.URL) + require.NoError(t, err) + require.Equal(t, &updatedDApp, dAppBack) + require.NotEqual(t, &testDApp, dAppBack) +} + +func TestInsertAndRemoveDApp(t *testing.T) { + db, close := setupTestDB(t) + defer close() + + err := UpsertDApp(db, &testDApp) + require.NoError(t, err) + + dAppBack, err := SelectDAppByUrl(db, testDApp.URL) + require.NoError(t, err) + require.Equal(t, &testDApp, dAppBack) + + err = DeleteDApp(db, testDApp.URL) + require.NoError(t, err) + + dAppBack, err = SelectDAppByUrl(db, testDApp.URL) + require.NoError(t, err) + require.Empty(t, dAppBack) +} diff --git a/services/connector/rpc_command.go b/services/connector/rpc_command.go deleted file mode 100644 index dfd216d66..000000000 --- a/services/connector/rpc_command.go +++ /dev/null @@ -1,5 +0,0 @@ -package connector - -type RPCCommand interface { - Execute(inputJSON string) (string, error) -} diff --git a/services/connector/service.go b/services/connector/service.go index 3fdd6103a..aede99c5e 100644 --- a/services/connector/service.go +++ b/services/connector/service.go @@ -1,22 +1,26 @@ package connector import ( + "database/sql" + "github.com/ethereum/go-ethereum/p2p" gethrpc "github.com/ethereum/go-ethereum/rpc" - "github.com/status-im/status-go/rpc" + "github.com/status-im/status-go/services/connector/commands" ) -func NewService(rpcClient *rpc.Client, connectorSrvc *Service) *Service { +func NewService(db *sql.DB, rpc commands.RPCClientInterface, nm commands.NetworkManagerInterface) *Service { return &Service{ - rpcClient: rpcClient, - connectorSrvc: connectorSrvc, + db: db, + rpc: rpc, + nm: nm, } } type Service struct { - rpcClient *rpc.Client - connectorSrvc *Service + db *sql.DB + rpc commands.RPCClientInterface + nm commands.NetworkManagerInterface } func (s *Service) Start() error { diff --git a/services/connector/service_test.go b/services/connector/service_test.go index 494feb8f1..859f47a2d 100644 --- a/services/connector/service_test.go +++ b/services/connector/service_test.go @@ -9,13 +9,14 @@ import ( gethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/status-im/status-go/params" - "github.com/status-im/status-go/rpc" statusRPC "github.com/status-im/status-go/rpc" + "github.com/status-im/status-go/services/connector/commands" "github.com/status-im/status-go/transactions/fake" ) func TestNewService(t *testing.T) { - db, _ := createDB(t) + db, close := createDB(t) + defer close() txServiceMockCtrl := gomock.NewController(t) server, _ := fake.NewTestServer(txServiceMockCtrl) @@ -30,22 +31,26 @@ func TestNewService(t *testing.T) { rpcClient, err := statusRPC.NewClient(client, 1, upstreamConfig, nil, db) require.NoError(t, err) - mockConnectorService := &Service{} - service := NewService(rpcClient, mockConnectorService) + service := NewService(db, rpcClient, rpcClient.NetworkManager) assert.NotNil(t, service) - assert.Equal(t, rpcClient, service.rpcClient) - assert.Equal(t, mockConnectorService, service.connectorSrvc) + assert.Equal(t, rpcClient.NetworkManager, service.nm) } func TestService_Start(t *testing.T) { - service := NewService(&rpc.Client{}, &Service{}) + db, close := createDB(t) + defer close() + + service := NewService(db, &commands.RPCClientMock{}, &commands.NetworkManagerMock{}) err := service.Start() assert.NoError(t, err) } func TestService_Stop(t *testing.T) { - service := NewService(&rpc.Client{}, &Service{}) + db, close := createDB(t) + defer close() + + service := NewService(db, &commands.RPCClientMock{}, &commands.NetworkManagerMock{}) err := service.Stop() assert.NoError(t, err) } @@ -63,7 +68,10 @@ func TestService_APIs(t *testing.T) { } func TestService_Protocols(t *testing.T) { - service := NewService(&rpc.Client{}, &Service{}) + db, close := createDB(t) + defer close() + + service := NewService(db, &commands.RPCClientMock{}, &commands.NetworkManagerMock{}) protocols := service.Protocols() assert.Nil(t, protocols) } diff --git a/signal/events_connector.go b/signal/events_connector.go new file mode 100644 index 000000000..a3ea1315e --- /dev/null +++ b/signal/events_connector.go @@ -0,0 +1,42 @@ +package signal + +const ( + EventConnectorSendRequestAccounts = "connector.sendRequestAccounts" + EventConnectorSendTransaction = "connector.sendTransaction" +) + +type ConnectorDApp struct { + URL string `json:"url"` + Name string `json:"name"` + IconURL string `json:"iconUrl"` +} + +// ConnectorSendRequestAccountsSignal is triggered when a request for accounts is sent. +type ConnectorSendRequestAccountsSignal struct { + ConnectorDApp + RequestID string `json:"requestId"` +} + +// ConnectorSendTransactionSignal is triggered when a transaction is requested to be sent. +type ConnectorSendTransactionSignal struct { + ConnectorDApp + RequestID string `json:"requestId"` + ChainID uint64 `json:"chainId"` + TxArgs string `json:"txArgs"` +} + +func SendConnectorSendRequestAccounts(dApp ConnectorDApp, requestID string) { + send(EventConnectorSendRequestAccounts, ConnectorSendRequestAccountsSignal{ + ConnectorDApp: dApp, + RequestID: requestID, + }) +} + +func SendConnectorSendTransaction(dApp ConnectorDApp, chainID uint64, txArgs string, requestID string) { + send(EventConnectorSendTransaction, ConnectorSendTransactionSignal{ + ConnectorDApp: dApp, + RequestID: requestID, + ChainID: chainID, + TxArgs: txArgs, + }) +} diff --git a/walletdatabase/migrations/bindata.go b/walletdatabase/migrations/bindata.go index 188848509..af8220d75 100644 --- a/walletdatabase/migrations/bindata.go +++ b/walletdatabase/migrations/bindata.go @@ -31,6 +31,7 @@ // 1716313614_add_rpc_limits_table.up.sql (203B) // 1716912885_add_wallet_connect_dapps.up.sql (750B) // 1721136888_recreate_indices_balance_history_remove_dups.up.sql (861B) +// 1721306883_add_connector_dapps.up.sql (360B) // doc.go (94B) package migrations @@ -41,7 +42,6 @@ import ( "crypto/sha256" "fmt" "io" - "io/ioutil" "os" "path/filepath" "strings" @@ -51,7 +51,7 @@ import ( func bindataRead(data []byte, name string) ([]byte, error) { gz, err := gzip.NewReader(bytes.NewBuffer(data)) if err != nil { - return nil, fmt.Errorf("read %q: %v", name, err) + return nil, fmt.Errorf("read %q: %w", name, err) } var buf bytes.Buffer @@ -59,7 +59,7 @@ func bindataRead(data []byte, name string) ([]byte, error) { clErr := gz.Close() if err != nil { - return nil, fmt.Errorf("read %q: %v", name, err) + return nil, fmt.Errorf("read %q: %w", name, err) } if clErr != nil { return nil, err @@ -720,6 +720,26 @@ func _1721136888_recreate_indices_balance_history_remove_dupsUpSql() (*asset, er return a, nil } +var __1721306883_add_connector_dappsUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x6c\x8e\xc1\x4e\x32\x31\x14\x85\xf7\x7d\x8a\xb3\xfc\xff\xc4\x79\x02\x57\x83\x54\x6c\xc4\xc1\x0c\x25\xc0\x6a\x52\xda\xab\x73\x43\x6d\x27\x6d\x07\x5e\xdf\x0c\x48\x62\xd4\xdd\xcd\x39\x5f\xce\xfd\xaa\x0a\x36\x86\x40\xb6\xc4\xd4\x39\x33\x0c\x19\xc5\x1c\x3c\xe1\x48\x34\xdd\xc9\xd8\x23\xe2\xdb\x0d\x22\x07\x57\x5f\xa0\x88\x21\xc5\x13\x3b\x82\x81\xe7\x70\x9c\x92\xd2\x13\x27\x70\x70\x7c\x62\x37\x1a\x8f\x4c\x39\x73\x0c\x59\x54\x15\x72\x1f\x47\xef\x70\x20\x18\xcf\xef\x81\x1c\xce\x5c\x7a\x9c\x8d\xf7\x54\xba\xaf\xfd\xef\x0a\x42\x3c\xb4\xb2\xd6\x12\xba\x9e\x2d\x25\xd4\x23\x9a\x95\x86\xdc\xa9\xb5\x5e\xff\x92\xfe\x27\x00\x60\x4c\x1e\x5a\xee\x34\x5e\x5b\xf5\x52\xb7\x7b\x3c\xcb\xfd\xdd\xa5\x09\xe6\x83\xae\xd5\x34\xd2\x6c\x96\xcb\x6b\x9e\x7b\x93\xc8\x75\xc6\xda\x38\x86\xf2\x17\x61\x7b\xc3\xa1\x63\x87\x4d\xb3\x56\x8b\x46\xce\x31\x53\x0b\xd5\xfc\xc4\xd8\xc6\xd0\xdd\xfe\x8b\xff\xd8\x2a\xfd\xb4\xda\x68\xb4\xab\xad\x9a\xdf\x8b\xcf\x00\x00\x00\xff\xff\x97\x7a\xcf\x29\x68\x01\x00\x00") + +func _1721306883_add_connector_dappsUpSqlBytes() ([]byte, error) { + return bindataRead( + __1721306883_add_connector_dappsUpSql, + "1721306883_add_connector_dapps.up.sql", + ) +} + +func _1721306883_add_connector_dappsUpSql() (*asset, error) { + bytes, err := _1721306883_add_connector_dappsUpSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "1721306883_add_connector_dapps.up.sql", size: 360, mode: os.FileMode(0644), modTime: time.Unix(1700000000, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x6d, 0xb4, 0x56, 0x49, 0x98, 0x3d, 0x7e, 0x87, 0x93, 0xd2, 0x10, 0xd8, 0x29, 0x1a, 0x3d, 0xca, 0xa1, 0x31, 0x54, 0x7c, 0x10, 0xa5, 0x6b, 0xf4, 0xbe, 0x61, 0xc4, 0x9c, 0x42, 0x12, 0x9d, 0x5a}} + return a, nil +} + var _docGo = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x2c\xcb\x41\x0e\x02\x31\x08\x05\xd0\x7d\x4f\xf1\x2f\x00\xe8\xca\xc4\xc4\xc3\xa0\x43\x08\x19\x5b\xc6\x96\xfb\xc7\x4d\xdf\xfe\x5d\xfa\x39\xd5\x0d\xeb\xf7\x6d\x4d\xc4\xf3\xe9\x36\x6c\x6a\x19\x3c\xe9\x1d\xe3\xd0\x52\x50\xcf\xa3\xa2\xdb\xeb\xfe\xb8\x6d\xa0\xeb\x74\xf4\xf0\xa9\x15\x39\x16\x28\xc1\x2c\x7b\xb0\x27\x58\xda\x3f\x00\x00\xff\xff\x57\xd4\xd5\x90\x5e\x00\x00\x00") func docGoBytes() ([]byte, error) { @@ -831,80 +851,55 @@ func AssetNames() []string { // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ - "1691753758_initial.up.sql": _1691753758_initialUpSql, - - "1692701329_add_collectibles_and_collections_data_cache.up.sql": _1692701329_add_collectibles_and_collections_data_cacheUpSql, - - "1692701339_add_scope_to_pending.up.sql": _1692701339_add_scope_to_pendingUpSql, - - "1694540071_add_collectibles_ownership_update_timestamp.up.sql": _1694540071_add_collectibles_ownership_update_timestampUpSql, - - "1694692748_add_raw_balance_to_token_balances.up.sql": _1694692748_add_raw_balance_to_token_balancesUpSql, - + "1691753758_initial.up.sql": _1691753758_initialUpSql, + "1692701329_add_collectibles_and_collections_data_cache.up.sql": _1692701329_add_collectibles_and_collections_data_cacheUpSql, + "1692701339_add_scope_to_pending.up.sql": _1692701339_add_scope_to_pendingUpSql, + "1694540071_add_collectibles_ownership_update_timestamp.up.sql": _1694540071_add_collectibles_ownership_update_timestampUpSql, + "1694692748_add_raw_balance_to_token_balances.up.sql": _1694692748_add_raw_balance_to_token_balancesUpSql, "1695133989_add_community_id_to_collectibles_and_collections_data_cache.up.sql": _1695133989_add_community_id_to_collectibles_and_collections_data_cacheUpSql, - - "1695932536_balance_history_v2.up.sql": _1695932536_balance_history_v2UpSql, - - "1696853635_input_data.up.sql": _1696853635_input_dataUpSql, - - "1698117918_add_community_id_to_tokens.up.sql": _1698117918_add_community_id_to_tokensUpSql, - - "1698257443_add_community_metadata_to_wallet_db.up.sql": _1698257443_add_community_metadata_to_wallet_dbUpSql, - - "1699987075_add_timestamp_and_state_to_community_data_cache.up.sql": _1699987075_add_timestamp_and_state_to_community_data_cacheUpSql, - - "1700414564_add_wallet_connect_pairings_table.up.sql": _1700414564_add_wallet_connect_pairings_tableUpSql, - - "1701101493_add_token_blocks_range.up.sql": _1701101493_add_token_blocks_rangeUpSql, - - "1702467441_wallet_connect_sessions_instead_of_pairings.up.sql": _1702467441_wallet_connect_sessions_instead_of_pairingsUpSql, - - "1702577524_add_community_collections_and_collectibles_images_cache.up.sql": _1702577524_add_community_collections_and_collectibles_images_cacheUpSql, - - "1702867707_add_balance_to_collectibles_ownership_cache.up.sql": _1702867707_add_balance_to_collectibles_ownership_cacheUpSql, - - "1703686612_add_color_to_saved_addresses.up.sql": _1703686612_add_color_to_saved_addressesUpSql, - + "1695932536_balance_history_v2.up.sql": _1695932536_balance_history_v2UpSql, + "1696853635_input_data.up.sql": _1696853635_input_dataUpSql, + "1698117918_add_community_id_to_tokens.up.sql": _1698117918_add_community_id_to_tokensUpSql, + "1698257443_add_community_metadata_to_wallet_db.up.sql": _1698257443_add_community_metadata_to_wallet_dbUpSql, + "1699987075_add_timestamp_and_state_to_community_data_cache.up.sql": _1699987075_add_timestamp_and_state_to_community_data_cacheUpSql, + "1700414564_add_wallet_connect_pairings_table.up.sql": _1700414564_add_wallet_connect_pairings_tableUpSql, + "1701101493_add_token_blocks_range.up.sql": _1701101493_add_token_blocks_rangeUpSql, + "1702467441_wallet_connect_sessions_instead_of_pairings.up.sql": _1702467441_wallet_connect_sessions_instead_of_pairingsUpSql, + "1702577524_add_community_collections_and_collectibles_images_cache.up.sql": _1702577524_add_community_collections_and_collectibles_images_cacheUpSql, + "1702867707_add_balance_to_collectibles_ownership_cache.up.sql": _1702867707_add_balance_to_collectibles_ownership_cacheUpSql, + "1703686612_add_color_to_saved_addresses.up.sql": _1703686612_add_color_to_saved_addressesUpSql, "1704701942_remove_favourite_and_change_primary_key_for_saved_addresses.up.sql": _1704701942_remove_favourite_and_change_primary_key_for_saved_addressesUpSql, - - "1704913491_add_type_and_tx_timestamp_to_collectibles_ownership_cache.up.sql": _1704913491_add_type_and_tx_timestamp_to_collectibles_ownership_cacheUpSql, - - "1705664490_add_balance_check_fields_blocks_ranges_sequential.up.sql": _1705664490_add_balance_check_fields_blocks_ranges_sequentialUpSql, - - "1706531789_remove_gasfee-only-eth-transfers.up.sql": _1706531789_remove_gasfeeOnlyEthTransfersUpSql, - - "1707160323_add_contract_type_table.up.sql": _1707160323_add_contract_type_tableUpSql, - - "1708089811_add_nullable_fiesl_blocks_ranges.up.sql": _1708089811_add_nullable_fiesl_blocks_rangesUpSql, - - "1710189541_add_nonce_to_pending_transactions.up.sql": _1710189541_add_nonce_to_pending_transactionsUpSql, - - "1712567001_add_soulbound_collectible_cache.up.sql": _1712567001_add_soulbound_collectible_cacheUpSql, - - "1714670633_add_id_to_multi_transaction_table.up.sql": _1714670633_add_id_to_multi_transaction_tableUpSql, - - "1715637927_add_collection_socials.up.sql": _1715637927_add_collection_socialsUpSql, - - "1715839555_rename_chain_prefixes.up.sql": _1715839555_rename_chain_prefixesUpSql, - - "1716313614_add_rpc_limits_table.up.sql": _1716313614_add_rpc_limits_tableUpSql, - - "1716912885_add_wallet_connect_dapps.up.sql": _1716912885_add_wallet_connect_dappsUpSql, - - "1721136888_recreate_indices_balance_history_remove_dups.up.sql": _1721136888_recreate_indices_balance_history_remove_dupsUpSql, - - "doc.go": docGo, + "1704913491_add_type_and_tx_timestamp_to_collectibles_ownership_cache.up.sql": _1704913491_add_type_and_tx_timestamp_to_collectibles_ownership_cacheUpSql, + "1705664490_add_balance_check_fields_blocks_ranges_sequential.up.sql": _1705664490_add_balance_check_fields_blocks_ranges_sequentialUpSql, + "1706531789_remove_gasfee-only-eth-transfers.up.sql": _1706531789_remove_gasfeeOnlyEthTransfersUpSql, + "1707160323_add_contract_type_table.up.sql": _1707160323_add_contract_type_tableUpSql, + "1708089811_add_nullable_fiesl_blocks_ranges.up.sql": _1708089811_add_nullable_fiesl_blocks_rangesUpSql, + "1710189541_add_nonce_to_pending_transactions.up.sql": _1710189541_add_nonce_to_pending_transactionsUpSql, + "1712567001_add_soulbound_collectible_cache.up.sql": _1712567001_add_soulbound_collectible_cacheUpSql, + "1714670633_add_id_to_multi_transaction_table.up.sql": _1714670633_add_id_to_multi_transaction_tableUpSql, + "1715637927_add_collection_socials.up.sql": _1715637927_add_collection_socialsUpSql, + "1715839555_rename_chain_prefixes.up.sql": _1715839555_rename_chain_prefixesUpSql, + "1716313614_add_rpc_limits_table.up.sql": _1716313614_add_rpc_limits_tableUpSql, + "1716912885_add_wallet_connect_dapps.up.sql": _1716912885_add_wallet_connect_dappsUpSql, + "1721136888_recreate_indices_balance_history_remove_dups.up.sql": _1721136888_recreate_indices_balance_history_remove_dupsUpSql, + "1721306883_add_connector_dapps.up.sql": _1721306883_add_connector_dappsUpSql, + "doc.go": docGo, } +// AssetDebug is true if the assets were built with the debug flag enabled. +const AssetDebug = false + // AssetDir returns the file names below a certain // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: -// data/ -// foo.txt -// img/ -// a.png -// b.png +// +// data/ +// foo.txt +// img/ +// a.png +// b.png +// // then AssetDir("data") would return []string{"foo.txt", "img"}, // AssetDir("data/img") would return []string{"a.png", "b.png"}, // AssetDir("foo.txt") and AssetDir("notexist") would return an error, and @@ -937,38 +932,39 @@ type bintree struct { } var _bintree = &bintree{nil, map[string]*bintree{ - "1691753758_initial.up.sql": &bintree{_1691753758_initialUpSql, map[string]*bintree{}}, - "1692701329_add_collectibles_and_collections_data_cache.up.sql": &bintree{_1692701329_add_collectibles_and_collections_data_cacheUpSql, map[string]*bintree{}}, - "1692701339_add_scope_to_pending.up.sql": &bintree{_1692701339_add_scope_to_pendingUpSql, map[string]*bintree{}}, - "1694540071_add_collectibles_ownership_update_timestamp.up.sql": &bintree{_1694540071_add_collectibles_ownership_update_timestampUpSql, map[string]*bintree{}}, - "1694692748_add_raw_balance_to_token_balances.up.sql": &bintree{_1694692748_add_raw_balance_to_token_balancesUpSql, map[string]*bintree{}}, - "1695133989_add_community_id_to_collectibles_and_collections_data_cache.up.sql": &bintree{_1695133989_add_community_id_to_collectibles_and_collections_data_cacheUpSql, map[string]*bintree{}}, - "1695932536_balance_history_v2.up.sql": &bintree{_1695932536_balance_history_v2UpSql, map[string]*bintree{}}, - "1696853635_input_data.up.sql": &bintree{_1696853635_input_dataUpSql, map[string]*bintree{}}, - "1698117918_add_community_id_to_tokens.up.sql": &bintree{_1698117918_add_community_id_to_tokensUpSql, map[string]*bintree{}}, - "1698257443_add_community_metadata_to_wallet_db.up.sql": &bintree{_1698257443_add_community_metadata_to_wallet_dbUpSql, map[string]*bintree{}}, - "1699987075_add_timestamp_and_state_to_community_data_cache.up.sql": &bintree{_1699987075_add_timestamp_and_state_to_community_data_cacheUpSql, map[string]*bintree{}}, - "1700414564_add_wallet_connect_pairings_table.up.sql": &bintree{_1700414564_add_wallet_connect_pairings_tableUpSql, map[string]*bintree{}}, - "1701101493_add_token_blocks_range.up.sql": &bintree{_1701101493_add_token_blocks_rangeUpSql, map[string]*bintree{}}, - "1702467441_wallet_connect_sessions_instead_of_pairings.up.sql": &bintree{_1702467441_wallet_connect_sessions_instead_of_pairingsUpSql, map[string]*bintree{}}, - "1702577524_add_community_collections_and_collectibles_images_cache.up.sql": &bintree{_1702577524_add_community_collections_and_collectibles_images_cacheUpSql, map[string]*bintree{}}, - "1702867707_add_balance_to_collectibles_ownership_cache.up.sql": &bintree{_1702867707_add_balance_to_collectibles_ownership_cacheUpSql, map[string]*bintree{}}, - "1703686612_add_color_to_saved_addresses.up.sql": &bintree{_1703686612_add_color_to_saved_addressesUpSql, map[string]*bintree{}}, - "1704701942_remove_favourite_and_change_primary_key_for_saved_addresses.up.sql": &bintree{_1704701942_remove_favourite_and_change_primary_key_for_saved_addressesUpSql, map[string]*bintree{}}, - "1704913491_add_type_and_tx_timestamp_to_collectibles_ownership_cache.up.sql": &bintree{_1704913491_add_type_and_tx_timestamp_to_collectibles_ownership_cacheUpSql, map[string]*bintree{}}, - "1705664490_add_balance_check_fields_blocks_ranges_sequential.up.sql": &bintree{_1705664490_add_balance_check_fields_blocks_ranges_sequentialUpSql, map[string]*bintree{}}, - "1706531789_remove_gasfee-only-eth-transfers.up.sql": &bintree{_1706531789_remove_gasfeeOnlyEthTransfersUpSql, map[string]*bintree{}}, - "1707160323_add_contract_type_table.up.sql": &bintree{_1707160323_add_contract_type_tableUpSql, map[string]*bintree{}}, - "1708089811_add_nullable_fiesl_blocks_ranges.up.sql": &bintree{_1708089811_add_nullable_fiesl_blocks_rangesUpSql, map[string]*bintree{}}, - "1710189541_add_nonce_to_pending_transactions.up.sql": &bintree{_1710189541_add_nonce_to_pending_transactionsUpSql, map[string]*bintree{}}, - "1712567001_add_soulbound_collectible_cache.up.sql": &bintree{_1712567001_add_soulbound_collectible_cacheUpSql, map[string]*bintree{}}, - "1714670633_add_id_to_multi_transaction_table.up.sql": &bintree{_1714670633_add_id_to_multi_transaction_tableUpSql, map[string]*bintree{}}, - "1715637927_add_collection_socials.up.sql": &bintree{_1715637927_add_collection_socialsUpSql, map[string]*bintree{}}, - "1715839555_rename_chain_prefixes.up.sql": &bintree{_1715839555_rename_chain_prefixesUpSql, map[string]*bintree{}}, - "1716313614_add_rpc_limits_table.up.sql": &bintree{_1716313614_add_rpc_limits_tableUpSql, map[string]*bintree{}}, - "1716912885_add_wallet_connect_dapps.up.sql": &bintree{_1716912885_add_wallet_connect_dappsUpSql, map[string]*bintree{}}, - "1721136888_recreate_indices_balance_history_remove_dups.up.sql": &bintree{_1721136888_recreate_indices_balance_history_remove_dupsUpSql, map[string]*bintree{}}, - "doc.go": &bintree{docGo, map[string]*bintree{}}, + "1691753758_initial.up.sql": {_1691753758_initialUpSql, map[string]*bintree{}}, + "1692701329_add_collectibles_and_collections_data_cache.up.sql": {_1692701329_add_collectibles_and_collections_data_cacheUpSql, map[string]*bintree{}}, + "1692701339_add_scope_to_pending.up.sql": {_1692701339_add_scope_to_pendingUpSql, map[string]*bintree{}}, + "1694540071_add_collectibles_ownership_update_timestamp.up.sql": {_1694540071_add_collectibles_ownership_update_timestampUpSql, map[string]*bintree{}}, + "1694692748_add_raw_balance_to_token_balances.up.sql": {_1694692748_add_raw_balance_to_token_balancesUpSql, map[string]*bintree{}}, + "1695133989_add_community_id_to_collectibles_and_collections_data_cache.up.sql": {_1695133989_add_community_id_to_collectibles_and_collections_data_cacheUpSql, map[string]*bintree{}}, + "1695932536_balance_history_v2.up.sql": {_1695932536_balance_history_v2UpSql, map[string]*bintree{}}, + "1696853635_input_data.up.sql": {_1696853635_input_dataUpSql, map[string]*bintree{}}, + "1698117918_add_community_id_to_tokens.up.sql": {_1698117918_add_community_id_to_tokensUpSql, map[string]*bintree{}}, + "1698257443_add_community_metadata_to_wallet_db.up.sql": {_1698257443_add_community_metadata_to_wallet_dbUpSql, map[string]*bintree{}}, + "1699987075_add_timestamp_and_state_to_community_data_cache.up.sql": {_1699987075_add_timestamp_and_state_to_community_data_cacheUpSql, map[string]*bintree{}}, + "1700414564_add_wallet_connect_pairings_table.up.sql": {_1700414564_add_wallet_connect_pairings_tableUpSql, map[string]*bintree{}}, + "1701101493_add_token_blocks_range.up.sql": {_1701101493_add_token_blocks_rangeUpSql, map[string]*bintree{}}, + "1702467441_wallet_connect_sessions_instead_of_pairings.up.sql": {_1702467441_wallet_connect_sessions_instead_of_pairingsUpSql, map[string]*bintree{}}, + "1702577524_add_community_collections_and_collectibles_images_cache.up.sql": {_1702577524_add_community_collections_and_collectibles_images_cacheUpSql, map[string]*bintree{}}, + "1702867707_add_balance_to_collectibles_ownership_cache.up.sql": {_1702867707_add_balance_to_collectibles_ownership_cacheUpSql, map[string]*bintree{}}, + "1703686612_add_color_to_saved_addresses.up.sql": {_1703686612_add_color_to_saved_addressesUpSql, map[string]*bintree{}}, + "1704701942_remove_favourite_and_change_primary_key_for_saved_addresses.up.sql": {_1704701942_remove_favourite_and_change_primary_key_for_saved_addressesUpSql, map[string]*bintree{}}, + "1704913491_add_type_and_tx_timestamp_to_collectibles_ownership_cache.up.sql": {_1704913491_add_type_and_tx_timestamp_to_collectibles_ownership_cacheUpSql, map[string]*bintree{}}, + "1705664490_add_balance_check_fields_blocks_ranges_sequential.up.sql": {_1705664490_add_balance_check_fields_blocks_ranges_sequentialUpSql, map[string]*bintree{}}, + "1706531789_remove_gasfee-only-eth-transfers.up.sql": {_1706531789_remove_gasfeeOnlyEthTransfersUpSql, map[string]*bintree{}}, + "1707160323_add_contract_type_table.up.sql": {_1707160323_add_contract_type_tableUpSql, map[string]*bintree{}}, + "1708089811_add_nullable_fiesl_blocks_ranges.up.sql": {_1708089811_add_nullable_fiesl_blocks_rangesUpSql, map[string]*bintree{}}, + "1710189541_add_nonce_to_pending_transactions.up.sql": {_1710189541_add_nonce_to_pending_transactionsUpSql, map[string]*bintree{}}, + "1712567001_add_soulbound_collectible_cache.up.sql": {_1712567001_add_soulbound_collectible_cacheUpSql, map[string]*bintree{}}, + "1714670633_add_id_to_multi_transaction_table.up.sql": {_1714670633_add_id_to_multi_transaction_tableUpSql, map[string]*bintree{}}, + "1715637927_add_collection_socials.up.sql": {_1715637927_add_collection_socialsUpSql, map[string]*bintree{}}, + "1715839555_rename_chain_prefixes.up.sql": {_1715839555_rename_chain_prefixesUpSql, map[string]*bintree{}}, + "1716313614_add_rpc_limits_table.up.sql": {_1716313614_add_rpc_limits_tableUpSql, map[string]*bintree{}}, + "1716912885_add_wallet_connect_dapps.up.sql": {_1716912885_add_wallet_connect_dappsUpSql, map[string]*bintree{}}, + "1721136888_recreate_indices_balance_history_remove_dups.up.sql": {_1721136888_recreate_indices_balance_history_remove_dupsUpSql, map[string]*bintree{}}, + "1721306883_add_connector_dapps.up.sql": {_1721306883_add_connector_dappsUpSql, map[string]*bintree{}}, + "doc.go": {docGo, map[string]*bintree{}}, }} // RestoreAsset restores an asset under the given directory. @@ -985,7 +981,7 @@ func RestoreAsset(dir, name string) error { if err != nil { return err } - err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) + err = os.WriteFile(_filePath(dir, name), data, info.Mode()) if err != nil { return err } diff --git a/walletdatabase/migrations/sql/1721306883_add_connector_dapps.up.sql b/walletdatabase/migrations/sql/1721306883_add_connector_dapps.up.sql new file mode 100644 index 000000000..3c4413f87 --- /dev/null +++ b/walletdatabase/migrations/sql/1721306883_add_connector_dapps.up.sql @@ -0,0 +1,10 @@ +-- connector_dapps table keeps track of connected dApps to provide a link to their individual sessions +-- should be aligned with wallet_connect_dapps table + +CREATE TABLE IF NOT EXISTS connector_dapps ( + url TEXT PRIMARY KEY, + name TEXT NOT NULL, + shared_account TEXT NOT NULL, + chain_id UNSIGNED BIGINT NOT NULL, + icon_url TEXT +) WITHOUT ROWID;