diff --git a/angular.json b/angular.json index 9855a44..0ed7dfe 100644 --- a/angular.json +++ b/angular.json @@ -125,6 +125,7 @@ }, "defaultProject": "prokeyLink", "cli": { - "defaultCollection": "@angular-eslint/schematics" + "defaultCollection": "@angular-eslint/schematics", + "analytics": false } } diff --git a/lib/prokey-webcore b/lib/prokey-webcore index 33695ae..4555134 160000 --- a/lib/prokey-webcore +++ b/lib/prokey-webcore @@ -1 +1 @@ -Subproject commit 33695aebb9897c328d1bbf89b306f14939b8f353 +Subproject commit 4555134077e9ca829378232edeca55f69df0f3ce diff --git a/package.json b/package.json index c612f5a..816a6c1 100644 --- a/package.json +++ b/package.json @@ -11,22 +11,24 @@ }, "private": true, "dependencies": { - "@angular/animations": "~13.3.0", - "@angular/cdk": "^13.2.5", - "@angular/common": "~13.3.0", - "@angular/compiler": "~13.3.0", - "@angular/core": "~13.3.0", - "@angular/forms": "~13.3.0", - "@angular/localize": "~13.3.0", - "@angular/material": "13.3.1", - "@angular/platform-browser": "~13.3.0", - "@angular/platform-browser-dynamic": "~13.3.0", - "@angular/router": "~13.3.0", + "@angular/animations": "~14.0.1", + "@angular/cdk": "^14.0.1", + "@angular/common": "~14.0.1", + "@angular/compiler": "~14.0.1", + "@angular/core": "~14.0.1", + "@angular/forms": "~14.0.1", + "@angular/localize": "~14.0.1", + "@angular/material": "14.0.1", + "@angular/platform-browser": "~14.0.1", + "@angular/platform-browser-dynamic": "~14.0.1", + "@angular/router": "~14.0.1", "@ethersproject/bignumber": "^5.6.2", "@ethersproject/providers": "^5.6.8", - "@walletconnect/client": "^1.7.7", - "@walletconnect/types": "^1.7.7", - "@walletconnect/utils": "^1.7.7", + "@walletconnect/client": "^1.8.0", + "@walletconnect/sign-client": "2.17.3", + "@walletconnect/types": "^2.17.3", + "@walletconnect/legacy-types": "^2.0.0", + "@walletconnect/utils": "^2.17.3", "big-integer": "^1.6.51", "buffer": "^6.0.3", "compare-versions": "^4.1.3", @@ -37,14 +39,14 @@ "zone.js": "~0.11.4" }, "devDependencies": { - "@angular-devkit/build-angular": "~13.3.0", - "@angular-eslint/builder": "13.1.0", - "@angular-eslint/eslint-plugin": "13.1.0", - "@angular-eslint/eslint-plugin-template": "13.1.0", - "@angular-eslint/schematics": "13.1.0", - "@angular-eslint/template-parser": "13.1.0", - "@angular/cli": "~13.3.0", - "@angular/compiler-cli": "~13.3.0", + "@angular-devkit/build-angular": "~14.0.1", + "@angular-eslint/builder": "14.0.1", + "@angular-eslint/eslint-plugin": "14.0.1", + "@angular-eslint/eslint-plugin-template": "14.0.1", + "@angular-eslint/schematics": "14.0.1", + "@angular-eslint/template-parser": "14.0.1", + "@angular/cli": "~14.0.1", + "@angular/compiler-cli": "~14.0.1", "@types/hdkey": "^2.0.1", "@types/jasmine": "~3.10.0", "@types/node": "^12.20.46", @@ -60,6 +62,6 @@ "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "~1.7.0", "prettier": "^2.6.1", - "typescript": "~4.5.2" + "typescript": "~4.6.2" } } diff --git a/src/app/app.component.html b/src/app/app.component.html index a4e1126..f15799b 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -113,7 +113,7 @@

Please enter your device PIN

Device is connected

-

+

Now go back to {{ dappMeta.peerMeta.url }} tab and leave this tab open.
Requested operation will be handled here.

diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 49104aa..18c40ea 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -8,11 +8,11 @@ import { DEFAULT_CHAIN_ID, DEFAULT_HD_PATH, LATEST_LEGACY_SIGN_DEVICE_VERSION } import CommandType from './models/CommandType'; import Strings from './utils/Strings'; import { FailureType } from 'lib/prokey-webcore/src/models/DeviceEvents'; -import WalletConnect from '@walletconnect/client'; -import { convertHexToUtf8 } from '@walletconnect/utils'; +import WalletConnect from '@walletconnect/client'; // v1 +import { HexStringToUTF8 } from 'lib/prokey-webcore/src/utils/utils'; import LinkMode from './models/LinkMode'; import WalletConnectRequestType from './models/WalletConnectRequestType'; -import { IJsonRpcRequest, ISessionParams, ITxData } from '@walletconnect/types'; +import { IJsonRpcRequest, ISessionParams, ITxData } from '@walletconnect/legacy-types'; import OptionsType from './models/OptionsType'; import WalletConnectUtil from './utils/walletConnectUtil'; import { closeWindow } from './utils/windowUtil'; @@ -39,6 +39,11 @@ import { HashTypedData } from 'lib/prokey-webcore/src/utils/ethereum-hashTypedDa import { HashTypedDataModel, MessageTypes, TypedMessage } from 'lib/prokey-webcore/src/models/EthereumTypedDataModel'; import { TransportType } from 'lib/prokey-webcore/src/transport/ITransport'; import { ActivatedRoute } from '@angular/router'; + +// Import WalletConnect v2 sign client +import SignClient from '@walletconnect/sign-client'; +import IWCv2SessionMeta from './models/IWCv2SessionMeta'; + @Component({ selector: 'app-root', templateUrl: './app.component.html', @@ -50,8 +55,14 @@ export class AppComponent implements OnInit { private _path: Array = DEFAULT_HD_PATH; private _ethereumBasedTransaction: EthereumTx; private _message: Uint8Array; + + // For WalletConnect v1 private _connector: WalletConnect; + // For WalletConnect v2 + private _signClient: SignClient; + public isWCv2: boolean = false; + //UI Visibility Control isLoading: boolean = true; showPassphraseForm = false; @@ -163,9 +174,9 @@ export class AppComponent implements OnInit { */ getMessage(): string { if (this._walletConnectCallRequest && this._walletConnectCallRequest.params) { - return convertHexToUtf8( + return HexStringToUTF8( this._walletConnectCallRequest.params[ - this._walletConnectCallRequest.method == WalletConnectRequestType.Sign ? 1 : 0 + this._walletConnectCallRequest.method == WalletConnectRequestType.Sign ? 1 : 0 ] ); } else { @@ -178,7 +189,18 @@ export class AppComponent implements OnInit { */ async killWalletConnectSession() { try { - await this._connector.killSession(); + if (this.isWCv2 && this._signClient) { + // Disconnect for v2 + const sessions = this._signClient.session.getAll(); + for (const s of sessions) { + await this._signClient.disconnect({ + topic: s.topic, + reason: { code: 1000, message: 'User disconnected' }, + }); + } + } else if (this._connector) { + await this._connector.killSession(); + } } finally { localStorage.removeItem('walletconnect'); reloadPage(); @@ -201,7 +223,7 @@ export class AppComponent implements OnInit { } } - subscribeToWalletConnectEvents() { + subscribeToWalletConnectEventsV1() { this._connector.on('session_request', async (error, payload) => { if (error) { MyConsole.Error(error, 'EVENT', 'session_request'); @@ -241,7 +263,7 @@ export class AppComponent implements OnInit { }); this._connector.on('connect', (error, payload) => { - MyConsole.Info('WalletConnect connected'); + MyConsole.Info('WalletConnect v1 connected'); if (error) { MyConsole.Error(error, 'EVENT', 'connect'); throw error; @@ -256,7 +278,66 @@ export class AppComponent implements OnInit { }); } - async handleWalletConnectRequest(payload) { + // Example handling for WCV2 events + subscribeToWalletConnectEventsV2() { + // SignClient has a different event structure + + this._signClient.on('session_proposal', async (proposal) => { + const { id, params } = proposal; + const { proposer, requiredNamespaces } = params; + + const result = await this.runCommand(CommandType.GetAddress); + this.currentAddress = result.address; + + const chainId = requiredNamespaces.eip155.chains[0].split(':')[1]; + const wcV2Meta: IWCv2SessionMeta = { + chainId: Number(chainId), + name: proposer.metadata.name, + description: proposer.metadata.description, + url: proposer.metadata.url, + icons: proposer.metadata.icons, + }; + + this.dappMeta = { + accounts: [this.currentAddress], chainId: wcV2Meta.chainId, rpcUrl: wcV2Meta.url, + approved: true, networkId: wcV2Meta.chainId + }; + + // Approve the session + const namespaces = { + eip155: { + accounts: [`eip155:${wcV2Meta.chainId}:${this.currentAddress}`], + methods: ['eth_sendTransaction', 'eth_signTransaction', 'eth_sign', 'personal_sign', 'eth_signTypedData'], + events: ['accountsChanged', 'chainChanged'], + }, + }; + await this._signClient.approve({ + id, + namespaces, + }); + + this.ethersProviderUtil.setConnectedAddress(this.currentAddress); + this.isWalletConnectConnected = true; + }); + + this._signClient.on('session_request', async (event) => { + MyConsole.Info(event, 'SESSION_REQUEST'); + const { topic, params, id } = event; + const { request } = params; + this._walletConnectCallRequest = { + id, + method: request.method, + params: request.params, + } as IJsonRpcRequest; + await this.handleWalletConnectRequest(this._walletConnectCallRequest, true, topic, id); + }); + + this._signClient.on('session_delete', () => { + reloadPage(); + }); + } + + async handleWalletConnectRequest(payload, isV2 = false, topic?: string, requestId?: number) { /** * When Transaction or Message sign request received from WalletConnect * It Immediately fires command on device. @@ -294,7 +375,7 @@ export class AppComponent implements OnInit { * return the result to WalletConnect Api as an approval */ if (payload.method !== WalletConnectRequestType.SignTypedData) { - this.handleCommandResult(requestResult); + this.handleCommandResult(requestResult, isV2, topic, requestId); } else { this.showSignTypedDataConfirm = true; } @@ -305,33 +386,61 @@ export class AppComponent implements OnInit { * if not the default chain id is considered the Ethereum mainnet chainId */ approveWalletConnectSession() { - this._connector.approveSession({ - accounts: [this.currentAddress], - chainId: this.dappMeta.chainId || DEFAULT_CHAIN_ID, - }); - this.ethersProviderUtil.setConnectedAddress(this.currentAddress); - this.isWalletConnectConnected = true; + if (this.isWCv2) { + // WCV2 session is approved in session_proposal step + // Nothing to do here if already approved there. + } else { + this._connector.approveSession({ + accounts: [this.currentAddress], + chainId: this.dappMeta.chainId || DEFAULT_CHAIN_ID, + }); + this.ethersProviderUtil.setConnectedAddress(this.currentAddress); + this.isWalletConnectConnected = true; + } } - handleCommandResult(requestResult: any) { - if (!requestResult) { - this._connector.rejectRequest({ - id: this._walletConnectCallRequest.id, - error: { message: Strings.somethingWentWrong }, - }); + handleCommandResult(requestResult: any, isV2 = false, topic?: string, requestId?: number) { + if (isV2 && this._signClient) { + if (!requestResult) { + this._signClient.respond({ + topic, + response: { + id: requestId, + jsonrpc: '2.0', + error: { code: 5000, message: Strings.somethingWentWrong }, + }, + }); + } else { + this._signClient.respond({ + topic, + response: { + id: requestId, + jsonrpc: '2.0', + result: requestResult, + }, + }); + } } else { - this._connector.approveRequest({ - id: this._walletConnectCallRequest.id, - result: requestResult, - }); + if (!requestResult) { + this._connector.rejectRequest({ + id: this._walletConnectCallRequest.id, + error: { message: Strings.somethingWentWrong }, + }); + } else { + this._connector.approveRequest({ + id: this._walletConnectCallRequest.id, + result: requestResult, + }); + } } + this.isLoading = false; this.showDeviceAction = false; } prepareForSignMessage(hexMessage: string) { this.showDeviceAction = true; - this._message = Util.StringToUint8Array(convertHexToUtf8(hexMessage)); + this._message = Util.StringToUint8Array(HexStringToUTF8(hexMessage)); } supportsEIP1559(first: string, second: string): boolean { @@ -342,7 +451,9 @@ export class AppComponent implements OnInit { this.showSignTypedDataConfirm = false; const commandResult = await this.runCommand(CommandType.SignTypedData); const requestResult = commandResult ? commandResult.signature : null; - this.handleCommandResult(requestResult); + // For typed data sign we need to know if we are in v2 or not. + // If currently saved request was v2, handle accordingly. + this.handleCommandResult(requestResult, this.isWCv2); } async prepareForSignTransaction(tx: ITxData) { @@ -387,19 +498,50 @@ export class AppComponent implements OnInit { } } else { this.mode = LinkMode.WalletConnect; - this._connector = new WalletConnect({ [type]: opt }); - if (!this._connector.connected) { - await this._connector.createSession(); - this.subscribeToWalletConnectEvents(); + + // Detect if URI is WalletConnect v2: + // Typically v2 URIs look like: wc:@2?relay-protocol=...&symKey=... + // We'll do a simple check: + const isV2Uri = typeof opt === 'string' && opt.includes('relay-protocol'); + this.isWCv2 = isV2Uri; + + if (isV2Uri) { + // Initialize WCV2 SignClient + this._signClient = await SignClient.init({ + projectId: '39b971c70b13ae0b3a487ebb2f710fb7', // replace with your projectId from WalletConnect Cloud + relayUrl: 'wss://relay.walletconnect.com', // or your preferred relay + metadata: { + name: "Prokey Hardware Wallet", + description: "Sign transactions with Prokey Hardware Wallet", + url: "https://prokey.io", + icons: ["https://prokey.io/favicon.ico"], + } + }); + + if (type === OptionsType.Uri) { + // pair with the URI + await this._signClient.pair({ uri: opt }); + } else if (type === OptionsType.Session) { + // If you have a persisted session, restore it here + // Not directly applicable for v2 as in v1, but you can handle re-connections as needed + } + this.subscribeToWalletConnectEventsV2(); } else { - this.isWalletConnectConnected = true; - this.dappMeta = { ...opt }; - this.currentAddress = this.dappMeta.accounts[0]; - this.ethersProviderUtil.setConnectedAddress(this.currentAddress); - this.subscribeToWalletConnectEvents(); - if (walletConnectCallRequest) { - this._walletConnectCallRequest = JSON.parse(walletConnectCallRequest); - this.handleWalletConnectRequest(this._walletConnectCallRequest); + // Handle v1 logic + this._connector = new WalletConnect({ [type]: opt }); + if (!this._connector.connected) { + await this._connector.createSession(); + this.subscribeToWalletConnectEventsV1(); + } else { + this.isWalletConnectConnected = true; + this.dappMeta = { ...opt }; + this.currentAddress = this.dappMeta.accounts[0]; + this.ethersProviderUtil.setConnectedAddress(this.currentAddress); + this.subscribeToWalletConnectEventsV1(); + if (walletConnectCallRequest) { + this._walletConnectCallRequest = JSON.parse(walletConnectCallRequest); + this.handleWalletConnectRequest(this._walletConnectCallRequest); + } } } } @@ -471,10 +613,14 @@ export class AppComponent implements OnInit { case FailureType.ActionCancelled: this.showSnackbar(Strings.deviceOperation, Strings.actionCancelled, AlertType.Warning); if (this.mode == LinkMode.WalletConnect) { - this._connector.rejectRequest({ - id: this._walletConnectCallRequest.id, - error: { message: Strings.actionCancelled }, - }); + if (this.isWCv2 && this._signClient) { + // Respond with an error if needed + } else { + this._connector.rejectRequest({ + id: this._walletConnectCallRequest.id, + error: { message: Strings.actionCancelled }, + }); + } this.removeWalletConnectLastRequest(); this.showDeviceAction = false; } else { @@ -487,7 +633,7 @@ export class AppComponent implements OnInit { } try { await this._device.RebootDevice(); - } catch (e) {} + } catch (e) { } this.showSnackbar(Strings.rebootDevice, Strings.shouldRebootDevice, AlertType.Info); setTimeout(() => { reloadPage(); diff --git a/src/app/models/IWCv2SessionMeta.ts b/src/app/models/IWCv2SessionMeta.ts new file mode 100644 index 0000000..49fe7ae --- /dev/null +++ b/src/app/models/IWCv2SessionMeta.ts @@ -0,0 +1,9 @@ +interface IWCv2SessionMeta { + chainId: number; + name: string; + description: string; + url: string; + icons: string[]; + } + +export default IWCv2SessionMeta; \ No newline at end of file diff --git a/src/app/models/WalletConnectTx.ts b/src/app/models/WalletConnectTx.ts index 07c4c83..e6471cb 100644 --- a/src/app/models/WalletConnectTx.ts +++ b/src/app/models/WalletConnectTx.ts @@ -1,4 +1,4 @@ -import { ICallTxData } from '@walletconnect/types'; +import { ICallTxData } from '@walletconnect/legacy-types'; interface WalletConnectTx extends ICallTxData { maxPriorityFeePerGas?: string; diff --git a/src/app/utils/walletConnectUtil.ts b/src/app/utils/walletConnectUtil.ts index 38e3407..2e302b8 100644 --- a/src/app/utils/walletConnectUtil.ts +++ b/src/app/utils/walletConnectUtil.ts @@ -1,4 +1,4 @@ -import { ICallTxData, ITxData } from '@walletconnect/types'; +import { ICallTxData, ITxData } from '@walletconnect/legacy-types'; import { EthersProviderUtilService } from '../ethers-provider-util.service'; import SimplifiedTransaction from '../models/SimplifiedTransaction'; import WalletConnectTx from '../models/WalletConnectTx';