diff --git a/services/connector/api.go b/services/connector/api.go index 14b536add..7212abe25 100644 --- a/services/connector/api.go +++ b/services/connector/api.go @@ -23,10 +23,15 @@ func NewAPI(s *Service) *API { r := NewCommandRegistry() c := commands.NewClientSideHandler() + // Transactions and signing r.Register("eth_sendTransaction", &commands.SendTransactionCommand{ Db: s.db, ClientHandler: c, }) + r.Register("personal_sign", &commands.PersonalSignCommand{ + Db: s.db, + ClientHandler: c, + }) // Accounts query and dapp permissions // NOTE: Some dApps expect same behavior for both eth_accounts and eth_requestAccounts @@ -126,3 +131,11 @@ func (api *API) SendTransactionAccepted(args commands.SendTransactionAcceptedArg func (api *API) SendTransactionRejected(args commands.RejectedArgs) error { return api.c.SendTransactionRejected(args) } + +func (api *API) PersonalSignAccepted(args commands.PersonalSignAcceptedArgs) error { + return api.c.PersonalSignAccepted(args) +} + +func (api *API) PersonalSignRejected(args commands.RejectedArgs) error { + return api.c.PersonalSignRejected(args) +} diff --git a/services/connector/commands/client_handler.go b/services/connector/commands/client_handler.go index 3233d1c0c..4dda88bd1 100644 --- a/services/connector/commands/client_handler.go +++ b/services/connector/commands/client_handler.go @@ -20,6 +20,7 @@ var ( 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") + ErrPersonalSignRejectedByUser = fmt.Errorf("personal sign was rejected by user") ErrEmptyRequestID = fmt.Errorf("empty requestID") ErrAnotherConnectorOperationIsAwaitingFor = fmt.Errorf("another connector operation is awaiting for user input") ) @@ -29,6 +30,7 @@ type MessageType int const ( RequestAccountsAccepted MessageType = iota SendTransactionAccepted + PersonalSignAccepted Rejected ) @@ -95,6 +97,20 @@ func (c *ClientSideHandler) RequestShareAccountForDApp(dApp signal.ConnectorDApp } } +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) RequestSendTransaction(dApp signal.ConnectorDApp, chainID uint64, txArgs *transactions.SendTxArgs) (types.Hash, error) { if !c.setRequestRunning() { return types.Hash{}, ErrAnotherConnectorOperationIsAwaitingFor @@ -132,20 +148,6 @@ func (c *ClientSideHandler) RequestSendTransaction(dApp signal.ConnectorDApp, ch } } -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 @@ -163,3 +165,53 @@ func (c *ClientSideHandler) SendTransactionRejected(args RejectedArgs) error { c.responseChannel <- Message{Type: Rejected, Data: args} return nil } + +func (c *ClientSideHandler) RequestPersonalSign(dApp signal.ConnectorDApp, challenge, address string) (string, error) { + if !c.setRequestRunning() { + return "", ErrAnotherConnectorOperationIsAwaitingFor + } + defer c.clearRequestRunning() + + requestID := c.generateRequestID(dApp) + signal.SendConnectorPersonalSign(dApp, requestID, challenge, address) + + timeout := time.After(WalletResponseMaxInterval) + + for { + select { + case msg := <-c.responseChannel: + switch msg.Type { + case PersonalSignAccepted: + response := msg.Data.(PersonalSignAcceptedArgs) + if response.RequestID == requestID { + return response.Signature, nil + } + case Rejected: + response := msg.Data.(RejectedArgs) + if response.RequestID == requestID { + return "", ErrPersonalSignRejectedByUser + } + } + case <-timeout: + return "", ErrWalletResponseTimeout + } + } +} + +func (c *ClientSideHandler) PersonalSignAccepted(args PersonalSignAcceptedArgs) error { + if args.RequestID == "" { + return ErrEmptyRequestID + } + + c.responseChannel <- Message{Type: PersonalSignAccepted, Data: args} + return nil +} + +func (c *ClientSideHandler) PersonalSignRejected(args RejectedArgs) error { + if args.RequestID == "" { + return ErrEmptyRequestID + } + + c.responseChannel <- Message{Type: Rejected, Data: args} + return nil +} diff --git a/services/connector/commands/personal_sign.go b/services/connector/commands/personal_sign.go new file mode 100644 index 000000000..16a1b541f --- /dev/null +++ b/services/connector/commands/personal_sign.go @@ -0,0 +1,79 @@ +package commands + +import ( + "database/sql" + "errors" + "fmt" + + persistence "github.com/status-im/status-go/services/connector/database" + "github.com/status-im/status-go/signal" +) + +var ( + ErrInvalidParamsStructure = errors.New("invalid params structure") +) + +type PersonalSignCommand struct { + Db *sql.DB + ClientHandler ClientSideHandlerInterface +} + +type PersonalSignParams struct { + Challenge string `json:"challenge"` + Address string `json:"address"` +} + +func (r *RPCRequest) getPersonalSignParams() (*PersonalSignParams, error) { + if r.Params == nil || len(r.Params) == 0 { + return nil, ErrEmptyRPCParams + } + + paramMap, ok := r.Params[0].(map[string]interface{}) + if !ok { + return nil, ErrInvalidParamsStructure + } + + // Extract the Challenge and Address fields from paramMap + challenge, ok := paramMap["challenge"].(string) + if !ok { + return nil, fmt.Errorf("missing or invalid 'challenge' field") + } + + address, ok := paramMap["address"].(string) + if !ok { + return nil, fmt.Errorf("missing or invalid 'address' field") + } + + // Create and return the PersonalSignParams + return &PersonalSignParams{ + Challenge: challenge, + Address: address, + }, nil +} + +func (c *PersonalSignCommand) Execute(request RPCRequest) (interface{}, error) { + err := request.Validate() + if err != nil { + return "", err + } + + params, err := request.getPersonalSignParams() + 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.ClientHandler.RequestPersonalSign(signal.ConnectorDApp{ + URL: request.URL, + Name: request.Name, + IconURL: request.IconURL, + }, params.Challenge, params.Address) +} diff --git a/services/connector/commands/personal_sign_test.go b/services/connector/commands/personal_sign_test.go new file mode 100644 index 000000000..e983f7fb5 --- /dev/null +++ b/services/connector/commands/personal_sign_test.go @@ -0,0 +1,183 @@ +package commands + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/signal" +) + +func preparePersonalSignRequest(dApp signal.ConnectorDApp, challenge, address string) (RPCRequest, error) { + params := map[string]interface{}{ + "challenge": challenge, + "address": address, + } + + return ConstructRPCRequest("personal_sign", []interface{}{params}, &dApp) +} + +func TestFailToPersonalSignWithMissingDAppFields(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + cmd := &PersonalSignCommand{Db: db} + + // Missing DApp fields + request, err := ConstructRPCRequest("personal_sign", []interface{}{}, nil) + assert.NoError(t, err) + + result, err := cmd.Execute(request) + assert.Equal(t, ErrRequestMissingDAppData, err) + assert.Empty(t, result) +} + +func TestFailToPersonalSignForUnpermittedDApp(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + cmd := &PersonalSignCommand{Db: db} + + request, err := preparePersonalSignRequest(testDAppData, + "0x506c65617365207369676e2074686973206d65737361676520746f20636f6e6669726d20796f7572206964656e746974792e", + "0x4B0897b0513FdBeEc7C469D9aF4fA6C0752aBea7", + ) + assert.NoError(t, err) + + result, err := cmd.Execute(request) + assert.Equal(t, ErrDAppIsNotPermittedByUser, err) + assert.Empty(t, result) +} + +func TestFailToPersonalSignWithoutParams(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + cmd := &PersonalSignCommand{Db: db} + + request, err := ConstructRPCRequest("personal_sign", nil, &testDAppData) + assert.NoError(t, err) + + result, err := cmd.Execute(request) + assert.Equal(t, ErrEmptyRPCParams, err) + assert.Empty(t, result) +} + +func TestFailToPersonalSignWithSignalTimout(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + clientHandler := NewClientSideHandler() + + cmd := &PersonalSignCommand{ + Db: db, + ClientHandler: clientHandler, + } + + err := PersistDAppData(db, testDAppData, types.Address{0x01}, uint64(0x1)) + assert.NoError(t, err) + + request, err := preparePersonalSignRequest(testDAppData, + "0x506c65617365207369676e2074686973206d65737361676520746f20636f6e6669726d20796f7572206964656e746974792e", + "0x4B0897b0513FdBeEc7C469D9aF4fA6C0752aBea7", + ) + assert.NoError(t, err) + + backupWalletResponseMaxInterval := WalletResponseMaxInterval + WalletResponseMaxInterval = 1 * time.Millisecond + + _, err = cmd.Execute(request) + assert.Equal(t, ErrWalletResponseTimeout, err) + WalletResponseMaxInterval = backupWalletResponseMaxInterval +} + +func TestPersonalSignWithSignalAccepted(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + fakedSignature := "0x051" + + clientHandler := NewClientSideHandler() + + cmd := &PersonalSignCommand{ + Db: db, + ClientHandler: clientHandler, + } + + err := PersistDAppData(db, testDAppData, types.Address{0x01}, uint64(0x1)) + assert.NoError(t, err) + + challenge := "0x506c65617365207369676e2074686973206d65737361676520746f20636f6e6669726d20796f7572206964656e746974792e" + address := "0x4B0897b0513FdBeEc7C469D9aF4fA6C0752aBea7" + request, err := preparePersonalSignRequest(testDAppData, challenge, address) + 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.EventConnectorPersonalSign: + var ev signal.ConnectorPersonalSignSignal + err := json.Unmarshal(evt.Event, &ev) + assert.NoError(t, err) + assert.Equal(t, ev.Challenge, challenge) + assert.Equal(t, ev.Address, address) + + err = clientHandler.PersonalSignAccepted(PersonalSignAcceptedArgs{ + Signature: fakedSignature, + RequestID: ev.RequestID, + }) + assert.NoError(t, err) + } + })) + + response, err := cmd.Execute(request) + assert.NoError(t, err) + assert.Equal(t, response, fakedSignature) +} + +func TestPersonalSignWithSignalRejected(t *testing.T) { + db, close := SetupTestDB(t) + defer close() + + clientHandler := NewClientSideHandler() + + cmd := &PersonalSignCommand{ + Db: db, + ClientHandler: clientHandler, + } + + err := PersistDAppData(db, testDAppData, types.Address{0x01}, uint64(0x1)) + assert.NoError(t, err) + + challenge := "0x506c65617365207369676e2074686973206d65737361676520746f20636f6e6669726d20796f7572206964656e746974792e" + address := "0x4B0897b0513FdBeEc7C469D9aF4fA6C0752aBea7" + request, err := preparePersonalSignRequest(testDAppData, challenge, address) + 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.EventConnectorPersonalSign: + var ev signal.ConnectorPersonalSignSignal + err := json.Unmarshal(evt.Event, &ev) + assert.NoError(t, err) + + err = clientHandler.PersonalSignRejected(RejectedArgs{ + RequestID: ev.RequestID, + }) + assert.NoError(t, err) + } + })) + + _, err = cmd.Execute(request) + assert.Equal(t, ErrPersonalSignRejectedByUser, err) +} diff --git a/services/connector/commands/rpc_traits.go b/services/connector/commands/rpc_traits.go index e0d8e6e42..932c03c35 100644 --- a/services/connector/commands/rpc_traits.go +++ b/services/connector/commands/rpc_traits.go @@ -43,18 +43,27 @@ type SendTransactionAcceptedArgs struct { Hash types.Hash `json:"hash"` } +type PersonalSignAcceptedArgs struct { + RequestID string `json:"requestId"` + Signature string `json:"signature"` +} + 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 + + RequestSendTransaction(dApp signal.ConnectorDApp, chainID uint64, txArgs *transactions.SendTxArgs) (types.Hash, error) SendTransactionAccepted(args SendTransactionAcceptedArgs) error SendTransactionRejected(args RejectedArgs) error + + RequestPersonalSign(dApp signal.ConnectorDApp, challenge, address string) (string, error) + PersonalSignAccepted(args PersonalSignAcceptedArgs) error + PersonalSignRejected(args RejectedArgs) error } type NetworkManagerInterface interface { diff --git a/services/connector/connector_flows_test.go b/services/connector/connector_flows_test.go index 8ddc000d0..0f2661a78 100644 --- a/services/connector/connector_flows_test.go +++ b/services/connector/connector_flows_test.go @@ -38,6 +38,7 @@ func TestRequestAccountsSwitchChainAndSendTransactionFlow(t *testing.T) { accountAddress := types.BytesToAddress(types.FromHex("0x6d0aa2a774b74bb1d36f97700315adf962c69fcg")) expectedHash := types.BytesToHash(types.FromHex("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")) + expectedSignature := "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" dAppPermissionRevoked := false dAppPermissionGranted := false @@ -73,6 +74,16 @@ func TestRequestAccountsSwitchChainAndSendTransactionFlow(t *testing.T) { Hash: expectedHash, }) assert.NoError(t, err) + case signal.EventConnectorPersonalSign: + var ev signal.ConnectorPersonalSignSignal + err := json.Unmarshal(evt.Event, &ev) + assert.NoError(t, err) + + err = api.PersonalSignAccepted(commands.PersonalSignAcceptedArgs{ + RequestID: ev.RequestID, + Signature: expectedSignature, + }) + assert.NoError(t, err) } })) @@ -110,6 +121,12 @@ func TestRequestAccountsSwitchChainAndSendTransactionFlow(t *testing.T) { assert.NoError(t, err) assert.Equal(t, expectedHash.Hex(), response) + // Personal sign + request = "{\"method\": \"personal_sign\", \"params\":[{\"challenge\": \"0x506c65617365207369676e2074686973206d65737361676520746f20636f6e6669726d20796f7572206964656e746974792e\",\"address\":\"0x4B0897b0513FdBeEc7C469D9aF4fA6C0752aBea7\"}], \"url\": \"http://testDAppURL123\", \"name\": \"testDAppName\", \"iconUrl\": \"http://testDAppIconUrl\" }" + response, err = api.CallRPC(request) + assert.NoError(t, err) + assert.Equal(t, expectedSignature, response) + // Revoke permissions request = "{\"method\": \"wallet_revokePermissions\", \"params\": [], \"url\": \"http://testDAppURL123\", \"name\": \"testDAppName\", \"iconUrl\": \"http://testDAppIconUrl\" }" _, err = api.CallRPC(request) diff --git a/signal/events_connector.go b/signal/events_connector.go index 9644a61be..27deaae60 100644 --- a/signal/events_connector.go +++ b/signal/events_connector.go @@ -3,6 +3,7 @@ package signal const ( EventConnectorSendRequestAccounts = "connector.sendRequestAccounts" EventConnectorSendTransaction = "connector.sendTransaction" + EventConnectorPersonalSign = "connector.personalSign" EventConnectorDAppPermissionGranted = "connector.dAppPermissionGranted" EventConnectorDAppPermissionRevoked = "connector.dAppPermissionRevoked" ) @@ -27,6 +28,13 @@ type ConnectorSendTransactionSignal struct { TxArgs string `json:"txArgs"` } +type ConnectorPersonalSignSignal struct { + ConnectorDApp + RequestID string `json:"requestId"` + Challenge string `json:"challenge"` + Address string `json:"address"` +} + func SendConnectorSendRequestAccounts(dApp ConnectorDApp, requestID string) { send(EventConnectorSendRequestAccounts, ConnectorSendRequestAccountsSignal{ ConnectorDApp: dApp, @@ -43,6 +51,15 @@ func SendConnectorSendTransaction(dApp ConnectorDApp, chainID uint64, txArgs str }) } +func SendConnectorPersonalSign(dApp ConnectorDApp, requestID, challenge, address string) { + send(EventConnectorPersonalSign, ConnectorPersonalSignSignal{ + ConnectorDApp: dApp, + RequestID: requestID, + Challenge: challenge, + Address: address, + }) +} + func SendConnectorDAppPermissionGranted(dApp ConnectorDApp) { send(EventConnectorDAppPermissionGranted, dApp) }