diff --git a/src/app/modules/main/wallet_section/module.nim b/src/app/modules/main/wallet_section/module.nim index cab75374d3b..8de6c42cf42 100644 --- a/src/app/modules/main/wallet_section/module.nim +++ b/src/app/modules/main/wallet_section/module.nim @@ -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 @@ -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) @@ -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 @@ -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 diff --git a/src/app/modules/main/wallet_section/view.nim b/src/app/modules/main/wallet_section/view.nim index 427a96f011a..73145854e12 100644 --- a/src/app/modules/main/wallet_section/view.nim +++ b/src/app/modules/main/wallet_section/view.nim @@ -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 @@ -21,6 +22,7 @@ QtObject: collectibleDetailsController: collectible_detailsc.Controller isNonArchivalNode: bool keypairOperabilityForObservedAccount: string + wcController: wcc.Controller proc setup(self: View) = self.QObject.setup @@ -28,13 +30,15 @@ QtObject: 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) = @@ -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 diff --git a/src/app/modules/main/wallet_section/wallet_connect/controller.nim b/src/app/modules/main/wallet_section/wallet_connect/controller.nim new file mode 100644 index 00000000000..ad45720e2f7 --- /dev/null +++ b/src/app/modules/main/wallet_section/wallet_connect/controller.nim @@ -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 \ No newline at end of file diff --git a/src/backend/wallet_connect.nim b/src/backend/wallet_connect.nim index e69de29bb2d..cbba1790c69 100644 --- a/src/backend/wallet_connect.nim +++ b/src/backend/wallet_connect.nim @@ -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 diff --git a/src/constants.nim b/src/constants.nim index 12da4c63f85..36c472010bf 100644 --- a/src/constants.nim +++ b/src/constants.nim @@ -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 \ No newline at end of file + OPENSEA_API_KEY_RESOLVED* = desktopConfig.openseaApiKey + WALLET_CONNECT_PROJECT_ID* = BUILD_WALLET_CONNECT_PROJECT_ID \ No newline at end of file diff --git a/src/env_cli_vars.nim b/src/env_cli_vars.nim index 1e755942ab9..0ca8878037b 100644 --- a/src/env_cli_vars.nim +++ b/src/env_cli_vars.nim @@ -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" ################################################################################ @@ -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 diff --git a/storybook/pages/WalletConnectPage.qml b/storybook/pages/WalletConnectPage.qml index 2183922ade4..68f831b13ce 100644 --- a/storybook/pages/WalletConnectPage.qml +++ b/storybook/pages/WalletConnectPage.qml @@ -19,6 +19,8 @@ import SortFilterProxyModel 0.2 import utils 1.0 +import nim 1.0 + Item { id: root @@ -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 } @@ -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 } } diff --git a/storybook/stubs/nim/WalletConnectController.qml b/storybook/stubs/nim/WalletConnectController.qml new file mode 100644 index 00000000000..bf795e58623 --- /dev/null +++ b/storybook/stubs/nim/WalletConnectController.qml @@ -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 +} \ No newline at end of file diff --git a/storybook/stubs/nim/qmldir b/storybook/stubs/nim/qmldir new file mode 100644 index 00000000000..2981d63a0f7 --- /dev/null +++ b/storybook/stubs/nim/qmldir @@ -0,0 +1 @@ +WalletConnectController 1.0 WalletConnectController.qml \ No newline at end of file diff --git a/test/go/test-wallet_connect/helpers.go b/test/go/test-wallet_connect/helpers.go index 4bac1b3a5b5..b61d823097c 100644 --- a/test/go/test-wallet_connect/helpers.go +++ b/test/go/test-wallet_connect/helpers.go @@ -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 } @@ -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 } @@ -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 } @@ -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 { @@ -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 { @@ -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 +} diff --git a/test/go/test-wallet_connect/index.html b/test/go/test-wallet_connect/index.html index e0cff76b112..47f42c024be 100644 --- a/test/go/test-wallet_connect/index.html +++ b/test/go/test-wallet_connect/index.html @@ -13,7 +13,8 @@ />
=0;i--){for(var c=t.words[i],u=h-1;u>=0;u--){var l=c>>u&1;n!==r[0]&&(n=this.sqr(n)),0!==l||0!==o?(o<<=1,o|=l,(4==++a||0===i&&0===u)&&(n=this.mul(n,r[o]),a=0,o=0)):a=0}h=26}return n},A.prototype.convertTo=function(e){var t=e.umod(this.m);return t===e?t.clone():t},A.prototype.convertFrom=function(e){var t=e.clone();return t.red=null,t},s.mont=function(e){return new O(e)},n(O,A),O.prototype.convertTo=function(e){return this.imod(e.ushln(this.shift))},O.prototype.convertFrom=function(e){var t=this.imod(e.mul(this.rinv));return t.red=null,t},O.prototype.imul=function(e,t){if(e.isZero()||t.isZero())return e.words[0]=0,e.length=1,e;var r=e.imul(t),i=r.maskn(this.shift).mul(this.minv).imaskn(this.shift).mul(this.m),n=r.isub(i).iushrn(this.shift),s=n;return n.cmp(this.m)>=0?s=n.isub(this.m):n.cmpn(0)<0&&(s=n.iadd(this.m)),s._forceRed(this)},O.prototype.mul=function(e,t){if(e.isZero()||t.isZero())return new s(0)._forceRed(this);var r=e.mul(t),i=r.maskn(this.shift).mul(this.minv).imaskn(this.shift).mul(this.m),n=r.isub(i).iushrn(this.shift),o=n;return n.cmp(this.m)>=0?o=n.isub(this.m):n.cmpn(0)<0&&(o=n.iadd(this.m)),o._forceRed(this)},O.prototype.invm=function(e){return this.imod(e._invmp(this.m).mul(this.r2))._forceRed(this)}}(e=r.nmd(e),this)},4020:e=>{var t="%[a-f0-9]{2}",r=new RegExp("("+t+")|([^%]+?)","gi"),i=new RegExp("("+t+")+","gi");function n(e,t){try{return[decodeURIComponent(e.join(""))]}catch(e){}if(1===e.length)return e;t=t||1;var r=e.slice(0,t),i=e.slice(t);return Array.prototype.concat.call([],n(r),n(i))}function s(e){try{return decodeURIComponent(e)}catch(s){for(var t=e.match(r)||[],i=1;i>>0)>>0},t.sum64_4_lo=function(e,t,r,i,n,s,o,a){return t+i+s+a>>>0},t.sum64_5_hi=function(e,t,r,i,n,s,o,a,h,c){var u=0,l=t;return u+=(l=l+i>>>0)>>0)>>0)-1?l:0,e.charCodeAt(d+1)){case 100:case 102:if(u>=h)break;if(null==r[u])break;l>>16|L<<16)|0)>>>20|I<<12,A=(A^=N=N+(C=(C^=E=E+A|0)>>>16|C<<16)|0)>>>20|A<<12,O=(O^=R=R+(U=(U^=S=S+O|0)>>>16|U<<16)|0)>>>20|O<<12,x=(x^=T=T+(k=(k^=M=M+x|0)>>>16|k<<16)|0)>>>20|x<<12,O=(O^=R=R+(U=(U^=S=S+O|0)>>>24|U<<8)|0)>>>25|O<<7,x=(x^=T=T+(k=(k^=M=M+x|0)>>>24|k<<8)|0)>>>25|x<<7,A=(A^=N=N+(C=(C^=E=E+A|0)>>>24|C<<8)|0)>>>25|A<<7,I=(I^=P=P+(L=(L^=_=_+I|0)>>>24|L<<8)|0)>>>25|I<<7,A=(A^=R=R+(k=(k^=_=_+A|0)>>>16|k<<16)|0)>>>20|A<<12,O=(O^=T=T+(L=(L^=E=E+O|0)>>>16|L<<16)|0)>>>20|O<<12,x=(x^=P=P+(C=(C^=S=S+x|0)>>>16|C<<16)|0)>>>20|x<<12,I=(I^=N=N+(U=(U^=M=M+I|0)>>>16|U<<16)|0)>>>20|I<<12,x=(x^=P=P+(C=(C^=S=S+x|0)>>>24|C<<8)|0)>>>25|x<<7,I=(I^=N=N+(U=(U^=M=M+I|0)>>>24|U<<8)|0)>>>25|I<<7,O=(O^=T=T+(L=(L^=E=E+O|0)>>>24|L<<8)|0)>>>25|O<<7,A=(A^=R=R+(k=(k^=_=_+A|0)>>>24|k<<8)|0)>>>25|A<<7;i.writeUint32LE(_+n|0,e,0),i.writeUint32LE(E+o|0,e,4),i.writeUint32LE(S+a|0,e,8),i.writeUint32LE(M+h|0,e,12),i.writeUint32LE(I+c|0,e,16),i.writeUint32LE(A+u|0,e,20),i.writeUint32LE(O+l|0,e,24),i.writeUint32LE(x+f|0,e,28),i.writeUint32LE(P+d|0,e,32),i.writeUint32LE(N+p|0,e,36),i.writeUint32LE(R+g|0,e,40),i.writeUint32LE(T+y|0,e,44),i.writeUint32LE(L+m|0,e,48),i.writeUint32LE(C+v|0,e,52),i.writeUint32LE(U+w|0,e,56),i.writeUint32LE(k+b|0,e,60)}function a(e,t,r,i,s){if(void 0===s&&(s=0),32!==e.length)throw new Error("ChaCha: key size must be 32 bytes");if(i.length>4)*S[n],r=t[n]>>8,t[n]&=255;for(n=0;n<32;n++)t[n]-=r*S[n];for(i=0;i<32;i++)t[i+1]+=t[i]>>8,e[i]=255&t[i]}function I(e){const t=new Float64Array(64);for(let r=0;r<64;r++)t[r]=e[r];for(let t=0;t<64;t++)e[t]=0;M(e,t)}t.Xx=function(e,t){const r=new Float64Array(64),s=[n(),n(),n(),n()],o=(0,i.hash)(e.subarray(0,32));o[0]&=248,o[31]&=127,o[31]|=64;const a=new Uint8Array(64);a.set(o.subarray(32),32);const h=new i.SHA512;h.update(a.subarray(32)),h.update(t);const c=h.digest();h.clean(),I(c),E(s,c),_(a,s),h.reset(),h.update(a.subarray(0,32)),h.update(e.subarray(32)),h.update(t);const u=h.digest();I(u);for(let e=0;e<32;e++)r[e]=c[e];for(let e=0;e<32;e++)for(let t=0;t<32;t++)r[e+t]+=u[e]*o[t];return M(a.subarray(32),r),a}},9984:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.isSerializableHash=function(e){return void 0!==e.saveState&&void 0!==e.restoreState&&void 0!==e.cleanSavedState}},512:(e,t,r)=>{var i=r(5629),n=r(7309),s=function(){function e(e,t,r,n){void 0===r&&(r=new Uint8Array(0)),this._counter=new Uint8Array(1),this._hash=e,this._info=n;var s=i.hmac(this._hash,r,t);this._hmac=new i.HMAC(e,s),this._buffer=new Uint8Array(this._hmac.digestLength),this._bufpos=this._buffer.length}return e.prototype._fillBuffer=function(){this._counter[0]++;var e=this._counter[0];if(0===e)throw new Error("hkdf: cannot expand more");this._hmac.reset(),e>1&&this._hmac.update(this._buffer),this._info&&this._hmac.update(this._info),this._hmac.update(this._counter),this._hmac.finish(this._buffer),this._bufpos=0},e.prototype.expand=function(e){for(var t=new Uint8Array(e),r=0;r=18?(s-=18,o+=1,this.words[o]|=n>>>26):s+=8;else for(i=(e.length-t)%2==0?t+1:t;i