feat(connector)_: impl `personal_sign` endpoint (#5681)

This commit is contained in:
Mikhail Rogachev 2024-08-13 11:11:24 +02:00 committed by GitHub
parent c08dedb77d
commit be9ba7604b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 386 additions and 16 deletions

View File

@ -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)
}

View File

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

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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)

View File

@ -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)
}