Skip to content

Commit

Permalink
feat(wallet) Wallet Connect session proposal implementation
Browse files Browse the repository at this point in the history
Depends on status-go changes that implements Wallet Connect pair API

Implement Controller to forward requests between status-go and SDK
implementation in QML.

Other changes:

- Source Wallet Connect projectId from env vars
- Mock controller in storybook

Updates #12551
  • Loading branch information
stefandunca committed Nov 8, 2023
1 parent d258b16 commit dda15da
Show file tree
Hide file tree
Showing 21 changed files with 602 additions and 184 deletions.
9 changes: 8 additions & 1 deletion src/app/modules/main/wallet_section/module.nim
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import ./overview/module as overview_module
import ./send/module as send_module

import ./activity/controller as activityc
import ./wallet_connect/controller as wcc

import app/modules/shared_modules/collectibles/controller as collectiblesc
import app/modules/shared_modules/collectible_details/controller as collectible_detailsc

Expand Down Expand Up @@ -81,6 +83,8 @@ type
# instance to be used in temporary, short-lived, workflows (e.g. send popup)
tmpActivityController: activityc.Controller

wcController: wcc.Controller

## Forward declaration
proc onUpdatedKeypairsOperability*(self: Module, updatedKeypairs: seq[KeypairDto])
proc onLocalPairingStatusUpdate*(self: Module, data: LocalPairingStatus)
Expand Down Expand Up @@ -137,7 +141,9 @@ proc newModule*(
result.collectibleDetailsController = collectible_detailsc.newController(int32(backend_collectibles.CollectiblesRequestID.WalletAccount), networkService, events)
result.filter = initFilter(result.controller)

result.view = newView(result, result.activityController, result.tmpActivityController, result.collectiblesController, result.collectibleDetailsController)
result.wcController = wcc.newController(events)

result.view = newView(result, result.activityController, result.tmpActivityController, result.collectiblesController, result.collectibleDetailsController, result.wcController)

method delete*(self: Module) =
self.accountsModule.delete
Expand All @@ -152,6 +158,7 @@ method delete*(self: Module) =
self.tmpActivityController.delete
self.collectiblesController.delete
self.collectibleDetailsController.delete
self.wcController.delete

if not self.addAccountModule.isNil:
self.addAccountModule.delete
Expand Down
11 changes: 10 additions & 1 deletion src/app/modules/main/wallet_section/view.nim
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import app/modules/shared_modules/collectibles/controller as collectiblesc
import app/modules/shared_modules/collectible_details/controller as collectible_detailsc
import ./io_interface
import ../../shared_models/currency_amount
import ./wallet_connect/controller as wcc

QtObject:
type
Expand All @@ -21,20 +22,23 @@ QtObject:
collectibleDetailsController: collectible_detailsc.Controller
isNonArchivalNode: bool
keypairOperabilityForObservedAccount: string
wcController: wcc.Controller

proc setup(self: View) =
self.QObject.setup

proc delete*(self: View) =
self.QObject.delete

proc newView*(delegate: io_interface.AccessInterface, activityController: activityc.Controller, tmpActivityController: activityc.Controller, collectiblesController: collectiblesc.Controller, collectibleDetailsController: collectible_detailsc.Controller): View =
proc newView*(delegate: io_interface.AccessInterface, activityController: activityc.Controller, tmpActivityController: activityc.Controller, collectiblesController: collectiblesc.Controller, collectibleDetailsController: collectible_detailsc.Controller, wcController: wcc.Controller): View =
new(result, delete)
result.delegate = delegate
result.activityController = activityController
result.tmpActivityController = tmpActivityController
result.collectiblesController = collectiblesController
result.collectibleDetailsController = collectibleDetailsController
result.wcController = wcController

result.setup()

proc load*(self: View) =
Expand Down Expand Up @@ -203,3 +207,8 @@ QtObject:
proc destroyKeypairImportPopup*(self: View) {.signal.}
proc emitDestroyKeypairImportPopup*(self: View) =
self.destroyKeypairImportPopup()

proc getWalletConnectController(self: View): QVariant {.slot.} =
return newQVariant(self.wcController)
QtProperty[QVariant] walletConnectController:
read = getWalletConnectController
52 changes: 52 additions & 0 deletions src/app/modules/main/wallet_section/wallet_connect/controller.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import NimQml, logging, json

import backend/wallet_connect as backend

import app/core/eventemitter
import app/core/signals/types

import constants

QtObject:
type
Controller* = ref object of QObject
events: EventEmitter

proc setup(self: Controller) =
self.QObject.setup

proc delete*(self: Controller) =
self.QObject.delete

proc newController*(events: EventEmitter): Controller =
new(result, delete)

result.events = events

result.setup()

# Register for wallet events
result.events.on(SignalType.Wallet.event, proc(e: Args) =
# TODO #12434: async processing
discard
)

# supportedNamespaces is a Namespace as defined in status-go: services/wallet/walletconnect/walletconnect.go
proc proposeUserPair*(self: Controller, sessionProposalJson: string, supportedNamespacesJson: string) {.signal.}

proc pairSessionProposal(self: Controller, sessionProposalJson: string) {.slot.} =
let ok = backend.pair(sessionProposalJson, proc (res: JsonNode) =
let sessionProposalJson = if res.hasKey("sessionProposal"): $res["sessionProposal"] else: ""
let supportedNamespacesJson = if res.hasKey("supportedNamespaces"): $res["supportedNamespaces"] else: ""

self.proposeUserPair(sessionProposalJson, supportedNamespacesJson)
)

if not ok:
error "Failed to pair session"

proc getProjectId*(self: Controller): string {.slot.} =
return constants.WALLET_CONNECT_PROJECT_ID

QtProperty[string] projectId:
read = getProjectId
26 changes: 26 additions & 0 deletions src/backend/wallet_connect.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import options
import json
import core, response_type

from gen import rpc
import backend

# Declared in services/wallet/walletconnect/walletconnect.go
#const eventWCTODO*: string = "wallet-wc-todo"

# Declared in services/wallet/walletconnect/walletconnect.go
const ErrorChainsNotSupported*: string = "chains not supported"

rpc(wCPairSessionProposal, "wallet"):
sessionProposalJson: string

# TODO #12434: async answer
proc pair*(sessionProposalJson: string, callback: proc(response: JsonNode): void): bool =
try:
let response = wCPairSessionProposal(sessionProposalJson)
if response.error == nil and response.result != nil:
callback(response.result)
return response.error == nil
except Exception as e:
echo "@dd wCPairSessionProposal response: ", e.msg
return false
3 changes: 2 additions & 1 deletion src/constants.nim
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,5 @@ let
ALCHEMY_ARBITRUM_GOERLI_TOKEN_RESOLVED* = desktopConfig.alchemyArbitrumGoerliToken
ALCHEMY_OPTIMISM_MAINNET_TOKEN_RESOLVED* = desktopConfig.alchemyOptimismMainnetToken
ALCHEMY_OPTIMISM_GOERLI_TOKEN_RESOLVED* = desktopConfig.alchemyOptimismGoerliToken
OPENSEA_API_KEY_RESOLVED* = desktopConfig.openseaApiKey
OPENSEA_API_KEY_RESOLVED* = desktopConfig.openseaApiKey
WALLET_CONNECT_PROJECT_ID* = BUILD_WALLET_CONNECT_PROJECT_ID
4 changes: 4 additions & 0 deletions src/env_cli_vars.nim
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const BASE_NAME_ALCHEMY_ARBITRUM_MAINNET_TOKEN = "ALCHEMY_ARBITRUM_MAINNET_TOKEN
const BASE_NAME_ALCHEMY_ARBITRUM_GOERLI_TOKEN = "ALCHEMY_ARBITRUM_GOERLI_TOKEN"
const BASE_NAME_ALCHEMY_OPTIMISM_MAINNET_TOKEN = "ALCHEMY_OPTIMISM_MAINNET_TOKEN"
const BASE_NAME_ALCHEMY_OPTIMISM_GOERLI_TOKEN = "ALCHEMY_OPTIMISM_GOERLI_TOKEN"
const BASE_NAME_WALLET_CONNECT_PROJECT_ID = "WALLET_CONNECT_PROJECT_ID"


################################################################################
Expand All @@ -36,6 +37,9 @@ const BUILD_ALCHEMY_ARBITRUM_MAINNET_TOKEN = getEnv(BUILD_TIME_PREFIX & BASE_NAM
const BUILD_ALCHEMY_ARBITRUM_GOERLI_TOKEN = getEnv(BUILD_TIME_PREFIX & BASE_NAME_ALCHEMY_ARBITRUM_GOERLI_TOKEN)
const BUILD_ALCHEMY_OPTIMISM_MAINNET_TOKEN = getEnv(BUILD_TIME_PREFIX & BASE_NAME_ALCHEMY_OPTIMISM_MAINNET_TOKEN)
const BUILD_ALCHEMY_OPTIMISM_GOERLI_TOKEN = getEnv(BUILD_TIME_PREFIX & BASE_NAME_ALCHEMY_OPTIMISM_GOERLI_TOKEN)
const
WALLET_CONNECT_STATUS_PROJECT_ID = "87815d72a81d739d2a7ce15c2cfdefb3"
BUILD_WALLET_CONNECT_PROJECT_ID = getEnv(BUILD_TIME_PREFIX & BASE_NAME_WALLET_CONNECT_PROJECT_ID, WALLET_CONNECT_STATUS_PROJECT_ID)

################################################################################
# Run time evaluated variables
Expand Down
15 changes: 12 additions & 3 deletions storybook/pages/WalletConnectPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import SortFilterProxyModel 0.2

import utils 1.0

import nim 1.0

Item {
id: root

Expand All @@ -29,11 +31,17 @@ Item {
WalletConnect {
id: walletConnect

SplitView.preferredWidth: 400
SplitView.fillWidth: true

projectId: SystemUtils.getEnvVar("WALLET_CONNECT_PROJECT_ID")
backgroundColor: Theme.palette.statusAppLayout.backgroundColor

controller: WalletConnectController {
pairSessionProposal: function(sessionProposalJson) {
proposeUserPair(sessionProposalJson, `{"eip155":{"methods":["eth_sendTransaction","personal_sign"],"chains":["eip155:5"],"events":["accountsChanged","chainChanged"],"accounts":["eip155:5:0x53780d79E83876dAA21beB8AFa87fd64CC29990b","eip155:5:0xBd54A96c0Ae19a220C8E1234f54c940DFAB34639","eip155:5:0x5D7905390b77A937Ae8c444aA8BF7Fa9a6A7DBA0"]}}`)
}
projectId: SystemUtils.getEnvVar("STATUS_BUILD_WALLET_CONNECT_PROJECT_ID")
}

clip: true
}

Expand All @@ -45,7 +53,8 @@ Item {

Text { text: "projectId" }
Text {
text: walletConnect.projectId.substring(0, 3) + "..." + walletConnect.projectId.substring(walletConnect.projectId.length - 3)
readonly property string projectId: walletConnect.controller.projectId
text: projectId.substring(0, 3) + "..." + projectId.substring(projectId.length - 3)
font.bold: true
}
}
Expand Down
15 changes: 15 additions & 0 deletions storybook/stubs/nim/WalletConnectController.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

// Stub for Controller QObject defined in src/app/modules/main/wallet_section/wallet_connect/controller.nim
Item {
id: root

signal proposeUserPair(string sessionProposalJson, string supportedNamespacesJson)

// function pairSessionProposal(/*string*/ sessionProposalJson)
required property var pairSessionProposal

required property string projectId
}
1 change: 1 addition & 0 deletions storybook/stubs/nim/qmldir
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
WalletConnectController 1.0 WalletConnectController.qml
55 changes: 50 additions & 5 deletions test/go/test-wallet_connect/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func loginToAccount(hashedPassword, userFolder, nodeConfigJson string) error {
}
accountsJson := statusgo.OpenAccounts(absUserFolder)
accounts := make([]multiaccounts.Account, 0)
err = getApiResponse(accountsJson, &accounts)
err = getCAPIResponse(accountsJson, &accounts)
if err != nil {
return err
}
Expand All @@ -33,7 +33,7 @@ func loginToAccount(hashedPassword, userFolder, nodeConfigJson string) error {
keystorePath := filepath.Join(filepath.Join(absUserFolder, "keystore/"), account.KeyUID)
initKeystoreJson := statusgo.InitKeystore(keystorePath)
apiResponse := statusgo.APIResponse{}
err = getApiResponse(initKeystoreJson, &apiResponse)
err = getCAPIResponse(initKeystoreJson, &apiResponse)
if err != nil {
return err
}
Expand All @@ -44,7 +44,7 @@ func loginToAccount(hashedPassword, userFolder, nodeConfigJson string) error {
return err
}
loginJson := statusgo.LoginWithConfig(string(accountJson), hashedPassword, nodeConfigJson)
err = getApiResponse(loginJson, &apiResponse)
err = getCAPIResponse(loginJson, &apiResponse)
if err != nil {
return err
}
Expand All @@ -64,7 +64,7 @@ type jsonrpcRequest struct {
Params json.RawMessage `json:"params,omitempty"`
}

func callPrivateMethod(method string, params interface{}) string {
func callPrivateMethod(method string, params []interface{}) string {
var paramsJson json.RawMessage
var err error
if params != nil {
Expand Down Expand Up @@ -131,7 +131,7 @@ func processConfigArgs() (config *Config, nodeConfigJson string, userFolder stri
return
}

func getApiResponse[T any](responseJson string, res T) error {
func getCAPIResponse[T any](responseJson string, res T) error {
apiResponse := statusgo.APIResponse{}
err := json.Unmarshal([]byte(responseJson), &apiResponse)
if err == nil {
Expand All @@ -154,3 +154,48 @@ func getApiResponse[T any](responseJson string, res T) error {

return nil
}

type jsonrpcSuccessfulResponse struct {
jsonrpcMessage
Result json.RawMessage `json:"result"`
}

type jsonrpcErrorResponse struct {
jsonrpcMessage
Error jsonError `json:"error"`
}

// jsonError represents Error message for JSON-RPC responses.
type jsonError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}

func getRPCAPIResponse[T any](responseJson string, res T) error {
errApiResponse := jsonrpcErrorResponse{}
err := json.Unmarshal([]byte(responseJson), &errApiResponse)
if err == nil && errApiResponse.Error.Code != 0 {
return fmt.Errorf("API error: %#v", errApiResponse.Error)
}

apiResponse := jsonrpcSuccessfulResponse{}
err = json.Unmarshal([]byte(responseJson), &apiResponse)
if err != nil {
return fmt.Errorf("failed to unmarshal jsonrpcSuccessfulResponse: %w", err)
}

typeOfT := reflect.TypeOf(res)
kindOfT := typeOfT.Kind()

// Check for valid types: pointer, slice, map
if kindOfT != reflect.Ptr && kindOfT != reflect.Slice && kindOfT != reflect.Map {
return fmt.Errorf("type T must be a pointer, slice, or map")
}

if err := json.Unmarshal(apiResponse.Result, &res); err != nil {
return fmt.Errorf("failed to unmarshal data: %w", err)
}

return nil
}
Loading

0 comments on commit dda15da

Please sign in to comment.