From aac3799b14c76c7a155614ec9d069623bbc84bb6 Mon Sep 17 00:00:00 2001 From: botond <105208209+notbot00@users.noreply.github.com> Date: Fri, 30 Dec 2022 19:05:27 +0100 Subject: [PATCH] chore: add wallet client (#2462) * chore: migrate to wallet client * fix: bump client with new esm release version * fix: add new no client error * fix: bump wallet client to 0.1.2 and remove empty params from method calls * fix: bump wallet client * fix: format * fix: reset client on url change * fix: trading-deal-ticket tests after wallet client added * fix: amend and cancel order tests, global connect tests * chore: fix typescript error * fix: connect wallet before mobile view test for console-lite Co-authored-by: Matthew Russell --- .../src/integration/market-selector.test.ts | 1 + .../src/integration/trading-deal-ticket.cy.ts | 86 ++++--- .../src/support/encode-transaction.ts | 11 - .../src/support/order-validation.ts | 9 +- .../src/connectors/json-rpc-connector.ts | 223 +++++------------- libs/wallet/src/context.ts | 2 +- libs/wallet/src/provider.tsx | 20 +- package.json | 1 + yarn.lock | 13 +- 9 files changed, 140 insertions(+), 226 deletions(-) delete mode 100644 apps/trading-e2e/src/support/encode-transaction.ts diff --git a/apps/console-lite-e2e/src/integration/market-selector.test.ts b/apps/console-lite-e2e/src/integration/market-selector.test.ts index 4cd2fcb8d..d4340f8db 100644 --- a/apps/console-lite-e2e/src/integration/market-selector.test.ts +++ b/apps/console-lite-e2e/src/integration/market-selector.test.ts @@ -67,6 +67,7 @@ describe('market selector', { tags: '@smoke' }, () => { it('mobile view', () => { cy.viewport('iphone-xr'); cy.visit(`/trading/${marketId}`); + cy.connectVegaWallet(); cy.get('[role="dialog"]').should('not.exist'); cy.getByTestId('arrow-button').click(); cy.get('[role="dialog"]').should('be.visible'); diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts index e0ffeb9b0..c5f89ee30 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts @@ -30,11 +30,20 @@ const displayTomorrow = () => { describe('time in force default values', () => { before(() => { + cy.window().then((win) => { + win.localStorage.setItem( + 'vega_wallet_config', + JSON.stringify({ + token: Cypress.env('VEGA_WALLET_API_TOKEN'), + connector: 'jsonRpc', + url: 'http://localhost:1789', + }) + ); + }); cy.mockTradingPage(); cy.mockSubscription(); cy.visit('/#/markets/market-0'); cy.wait('@Market'); - cy.connectVegaWallet(); }); it('must have market order set up to IOC by default', function () { @@ -59,20 +68,15 @@ describe('time in force default values', () => { describe('must submit order', { tags: '@smoke' }, () => { // 7002-SORD-039 before(() => { + setVegaConfig(); cy.mockTradingPage(); cy.mockSubscription(); cy.visit('/#/markets/market-0'); cy.wait('@Market'); - cy.connectVegaWallet(); - cy.window().then(function (window) { - cy.wrap(window.localStorage.getItem('vega_wallet_config')).as('cfg'); - }); }); beforeEach(() => { - cy.window().then(function (window) { - window.localStorage.setItem('vega_wallet_config', this.cfg); - }); + setVegaConfig(); }); it('successfully places market buy order', () => { @@ -158,6 +162,7 @@ describe( { tags: '@regression' }, () => { before(() => { + setVegaConfig(); cy.mockTradingPage( Schema.MarketState.STATE_SUSPENDED, Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION, @@ -166,16 +171,10 @@ describe( cy.mockSubscription(); cy.visit('/#/markets/market-0'); cy.wait('@Market'); - cy.connectVegaWallet(); - cy.window().then(function (window) { - cy.wrap(window.localStorage.getItem('vega_wallet_config')).as('cfg'); - }); }); beforeEach(() => { - cy.window().then(function (window) { - window.localStorage.setItem('vega_wallet_config', this.cfg); - }); + setVegaConfig(); }); it('successfully places limit buy order', () => { @@ -232,6 +231,7 @@ describe( { tags: '@regression' }, () => { before(() => { + setVegaConfig(); cy.mockTradingPage( Schema.MarketState.STATE_SUSPENDED, Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION, @@ -240,16 +240,10 @@ describe( cy.mockSubscription(); cy.visit('/#/markets/market-0'); cy.wait('@Market'); - cy.connectVegaWallet(); - cy.window().then(function (window) { - cy.wrap(window.localStorage.getItem('vega_wallet_config')).as('cfg'); - }); }); beforeEach(() => { - cy.window().then(function (window) { - window.localStorage.setItem('vega_wallet_config', this.cfg); - }); + setVegaConfig(); }); it('successfully places limit buy order', () => { @@ -306,6 +300,7 @@ describe( { tags: '@regression' }, () => { before(() => { + setVegaConfig(); cy.mockTradingPage( Schema.MarketState.STATE_SUSPENDED, Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION, @@ -314,16 +309,10 @@ describe( cy.mockSubscription(); cy.visit('/#/markets/market-0'); cy.wait('@Market'); - cy.connectVegaWallet(); - cy.window().then(function (window) { - cy.wrap(window.localStorage.getItem('vega_wallet_config')).as('cfg'); - }); }); beforeEach(() => { - cy.window().then(function (window) { - window.localStorage.setItem('vega_wallet_config', this.cfg); - }); + setVegaConfig(); }); it('successfully places limit buy order', () => { @@ -421,10 +410,10 @@ describe('deal ticket validation', { tags: '@smoke' }, () => { describe('deal ticket size validation', { tags: '@smoke' }, function () { beforeEach(() => { + setVegaConfig(); cy.mockTradingPage(); cy.visit('/#/markets/market-0'); cy.wait('@Market'); - cy.connectVegaWallet(); }); it('must warn if order size input has too many digits after the decimal place', function () { @@ -455,14 +444,18 @@ describe('deal ticket size validation', { tags: '@smoke' }, function () { describe('limit order validations', { tags: '@smoke' }, () => { before(() => { + setVegaConfig(); cy.mockTradingPage(); cy.mockSubscription(); cy.visit('/#/markets/market-0'); - cy.connectVegaWallet(); cy.wait('@Market'); cy.getByTestId(toggleLimit).click(); }); + beforeEach(() => { + setVegaConfig(); + }); + it('must see the price unit', function () { //7002-SORD-018 cy.getByTestId(orderPriceField) @@ -544,11 +537,17 @@ describe('limit order validations', { tags: '@smoke' }, () => { describe('market order validations', { tags: '@smoke' }, () => { before(() => { + setVegaConfig(); cy.mockTradingPage(); cy.visit('/#/markets/market-0'); cy.wait('@Market'); cy.getByTestId(toggleMarket).click(); }); + + beforeEach(() => { + setVegaConfig(); + }); + it('must not see the price unit', function () { //7002-SORD-019 cy.getByTestId(orderPriceField).should('not.exist'); @@ -587,6 +586,7 @@ describe('market order validations', { tags: '@smoke' }, () => { describe('suspended market validation', { tags: '@regression' }, () => { before(() => { + setVegaConfig(); cy.mockTradingPage( Schema.MarketState.STATE_SUSPENDED, Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION, @@ -595,7 +595,10 @@ describe('suspended market validation', { tags: '@regression' }, () => { cy.mockSubscription(); cy.visit('/#/markets/market-0'); cy.wait('@Market'); - cy.connectVegaWallet(); + }); + + beforeEach(() => { + setVegaConfig(); }); it('should show warning for market order', function () { @@ -608,6 +611,7 @@ describe('suspended market validation', { tags: '@regression' }, () => { 'This market is in auction until it reaches sufficient liquidity. Only limit orders are permitted when market is in auction' ); }); + it('should show info for allowed TIF', function () { cy.getByTestId(toggleLimit).click(); cy.getByTestId(orderPriceField).clear().type('0.1'); @@ -635,6 +639,7 @@ describe('suspended market validation', { tags: '@regression' }, () => { describe('account validation', { tags: '@regression' }, () => { describe('zero balance error', () => { beforeEach(() => { + setVegaConfig(); cy.mockTradingPage(); cy.mockGQL((req) => { aliasGQLQuery( @@ -663,7 +668,6 @@ describe('account validation', { tags: '@regression' }, () => { }); cy.mockSubscription(); cy.visit('/#/markets/market-0'); - cy.connectVegaWallet(); cy.wait('@Market'); }); @@ -682,6 +686,7 @@ describe('account validation', { tags: '@regression' }, () => { describe('not enough balance warning', () => { beforeEach(() => { + setVegaConfig(); cy.mockTradingPage(); cy.mockGQL((req) => { aliasGQLQuery( @@ -699,9 +704,9 @@ describe('account validation', { tags: '@regression' }, () => { }); cy.mockSubscription(); cy.visit('/#/markets/market-0'); - cy.connectVegaWallet(); cy.wait('@Market'); }); + it('should display info and button for deposit', () => { //7002-SORD-003 // warning should show immediately @@ -740,3 +745,16 @@ const createOrder = (order: OrderSubmission): void => { } cy.getByTestId(placeOrderBtn).click(); }; + +const setVegaConfig = () => { + cy.window().then((win) => { + win.localStorage.setItem( + 'vega_wallet_config', + JSON.stringify({ + token: Cypress.env('VEGA_WALLET_API_TOKEN'), + connector: 'jsonRpc', + url: 'http://localhost:1789', + }) + ); + }); +}; diff --git a/apps/trading-e2e/src/support/encode-transaction.ts b/apps/trading-e2e/src/support/encode-transaction.ts deleted file mode 100644 index 06455de0c..000000000 --- a/apps/trading-e2e/src/support/encode-transaction.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ethers } from 'ethers'; -import type { Transaction } from '@vegaprotocol/wallet'; - -/** - * Base64 encode a transaction object - */ -export const encodeTransaction = (tx: Transaction): string => { - return ethers.utils.base64.encode( - ethers.utils.toUtf8Bytes(JSON.stringify(tx)) - ); -}; diff --git a/apps/trading-e2e/src/support/order-validation.ts b/apps/trading-e2e/src/support/order-validation.ts index 312423fc0..60ae94786 100644 --- a/apps/trading-e2e/src/support/order-validation.ts +++ b/apps/trading-e2e/src/support/order-validation.ts @@ -7,7 +7,6 @@ import type { OrderSubmissionBody, Transaction, } from '@vegaprotocol/wallet'; -import { encodeTransaction } from './encode-transaction'; export const testOrderSubmission = ( order: OrderSubmission, @@ -18,9 +17,6 @@ export const testOrderSubmission = ( ...expected, }; - expectedOrder.expiresAt = expectedOrder.expiresAt || undefined; - expectedOrder.price = expectedOrder.price || undefined; - const transaction: OrderSubmissionBody = { orderSubmission: expectedOrder, }; @@ -36,9 +32,6 @@ export const testOrderAmendment = ( ...expected, }; - expectedOrder.expiresAt = expectedOrder.expiresAt || undefined; - expectedOrder.price = expectedOrder.price || undefined; - const transaction: OrderAmendmentBody = { orderAmendment: expectedOrder, }; @@ -70,7 +63,7 @@ const vegaWalletTransaction = (transaction: Transaction) => { ?.token, publicKey: Cypress.env('VEGA_PUBLIC_KEY2'), sendingMode: 'TYPE_SYNC', - encodedTransaction: encodeTransaction(transaction), + transaction, }); cy.getByTestId(dialogTitle).should( 'have.text', diff --git a/libs/wallet/src/connectors/json-rpc-connector.ts b/libs/wallet/src/connectors/json-rpc-connector.ts index 330b55972..a33390682 100644 --- a/libs/wallet/src/connectors/json-rpc-connector.ts +++ b/libs/wallet/src/connectors/json-rpc-connector.ts @@ -1,74 +1,11 @@ import { t } from '@vegaprotocol/react-helpers'; -import { z } from 'zod'; +import { WalletClient } from '@vegaprotocol/wallet-client'; import { clearConfig, getConfig, setConfig } from '../storage'; -import { encodeTransaction } from '../utils'; import type { Transaction, VegaConnector } from './vega-connector'; import { WalletError } from './vega-connector'; const VERSION = 'v2'; -enum Methods { - ConnectWallet = 'client.connect_wallet', - DisconnectWallet = 'client.disconnect_wallet', - ListKeys = 'client.list_keys', - SendTransaction = 'client.send_transaction', - GetChainId = 'client.get_chain_id', -} - -const BaseSchema = z.object({ - id: z.string(), - jsonrpc: z.literal('2.0'), -}); - -const ConnectWalletSchema = BaseSchema.extend({ - result: z.object({ - token: z.string(), - }), -}); - -const ListKeysSchema = BaseSchema.extend({ - result: z.object({ - keys: z.array( - z.object({ - publicKey: z.string(), - name: z.string(), - }) - ), - }), -}); - -const GetChainIdSchema = BaseSchema.extend({ - result: z.object({ - chainID: z.string(), - }), -}); - -const SendTransactionSchema = BaseSchema.extend({ - result: z.object({ - receivedAt: z.string(), - sentAt: z.string(), - transactionHash: z.string(), - transaction: z.object({ - signature: z.object({ - value: z.string(), - }), - }), - }), -}); - -type JsonRpcError = { - message: string; - code: number; - data?: string; -}; - -type Response = - | z.infer - | z.infer - | z.infer - | z.infer - | { error: JsonRpcError }; - export const ClientErrors = { NO_SERVICE: new WalletError(t('No service'), 100), NO_TOKEN: new WalletError(t('No token'), 101), @@ -86,51 +23,69 @@ export const ClientErrors = { ), } as const; +class NoClientError extends Error { + constructor() { + super( + t('No client found. The connector needs to be initialized with a url.') + ); + } +} + export class JsonRpcConnector implements VegaConnector { version = VERSION; - url: string | null = null; + private _url: string | null = null; token: string | null = null; reqId = 0; + client?: WalletClient; constructor() { const cfg = getConfig(); - if (cfg) { - this.token = cfg.token; + + this.token = cfg?.token ?? null; + + if (cfg && cfg.url) { this.url = cfg.url; + this.client = new WalletClient({ + address: cfg.url, + token: cfg.token ?? undefined, + }); } } + set url(url: string) { + this._url = url; + this.client = new WalletClient({ + address: url, + token: this.token ?? undefined, + }); + } + async getChainId() { - const result = await this.request(Methods.GetChainId); - if ('error' in result) { - throw this.wrapError(result.error); + if (!this.client) { + throw new NoClientError(); } - const parseResult = GetChainIdSchema.safeParse(result); - if (parseResult.success) { - return parseResult.data.result; - } else { + try { + const { result } = await this.client.GetChainId(); + return result; + } catch (err) { throw ClientErrors.INVALID_RESPONSE; } } async connectWallet() { - const result = await this.request(Methods.ConnectWallet, { - hostname: window.location.host, - }); - if ('error' in result) { - throw this.wrapError(result.error); + if (!this.client) { + throw new NoClientError(); } - const parseResult = ConnectWalletSchema.safeParse(result); - if (parseResult.success) { - // store token and other config for eager connect and subsequent requests + try { + const { result } = await this.client.ConnectWallet(); setConfig({ - token: parseResult.data.result.token, + token: result.token, connector: 'jsonRpc', url: this.url, }); - return parseResult.data.result; - } else { + return result; + } catch (err) { throw ClientErrors.INVALID_RESPONSE; } } @@ -138,80 +93,57 @@ export class JsonRpcConnector implements VegaConnector { // connect actually calling list_keys here, not to be confused with connect_wallet // which retrieves the session token async connect() { - const cfg = getConfig(); - if (!cfg?.token) { - throw ClientErrors.NO_TOKEN; + if (!this.client) { + throw new NoClientError(); } - const result = await this.request(Methods.ListKeys, { - token: cfg.token, - }); - if ('error' in result) { - throw this.wrapError(result.error); - } - const parseResult = ListKeysSchema.safeParse(result); - if (parseResult.success) { - return parseResult.data.result.keys; - } else { + + try { + const { result } = await this.client.ListKeys(); + return result.keys; + } catch (err) { throw ClientErrors.INVALID_RESPONSE; } } async disconnect() { - const cfg = getConfig(); - - if (cfg?.token) { - await this.request(Methods.DisconnectWallet, { - token: cfg.token, - }); + if (!this.client) { + throw new NoClientError(); } + await this.client.DisconnectWallet(); clearConfig(); } async sendTx(pubKey: string, transaction: Transaction) { - const cfg = getConfig(); - if (!cfg?.token) { - throw ClientErrors.NO_TOKEN; + if (!this.client) { + throw new NoClientError(); } - const result = await this.request(Methods.SendTransaction, { - token: cfg.token, - publicKey: pubKey, - sendingMode: 'TYPE_SYNC', - encodedTransaction: encodeTransaction(transaction), - }); + try { + const { result } = await this.client.SendTransaction({ + publicKey: pubKey, + sendingMode: 'TYPE_SYNC', + transaction, + }); - if ('error' in result) { - // In the case of sending a tx, error code 3001 indicates that the - // user rejected the tx. Returning null will allow the dialog to close immediately - if (result.error.code === 3001) { - return null; - } else { - throw this.wrapError(result.error); - } - } - - const parsedResult = SendTransactionSchema.safeParse(result); - - if (parsedResult.success) { return { - transactionHash: parsedResult.data.result.transactionHash, - sentAt: parsedResult.data.result.sentAt, - receivedAt: parsedResult.data.result.receivedAt, - signature: parsedResult.data.result.transaction.signature.value, + transactionHash: result.transactionHash, + sentAt: result.sentAt, + receivedAt: result.receivedAt, + signature: result.transaction.signature.value, }; - } else { + } catch (err) { throw ClientErrors.INVALID_RESPONSE; } } async checkCompat() { try { - const result = await fetch(`${this.url}/api/${this.version}/methods`); + const result = await fetch(`${this._url}/api/${this.version}/methods`); if (!result.ok) { const err = ClientErrors.INVALID_WALLET; err.data = t( - `The wallet running at ${this.url} is not supported. Required version is ${this.version}` + `The wallet running at ${this._url} is not supported. Required version is ${this.version}` ); throw err; } @@ -224,29 +156,4 @@ export class JsonRpcConnector implements VegaConnector { throw ClientErrors.NO_SERVICE; } } - - private async request(method: Methods, params?: object): Promise { - try { - const result = await fetch(`${this.url}/api/${this.version}/requests`, { - method: 'post', - body: JSON.stringify({ - jsonrpc: '2.0', - method, - params, - id: `${this.reqId++}`, - }), - headers: { - 'Content-Type': 'application/json', - }, - }); - const json = await result.json(); - return json; - } catch (err) { - throw ClientErrors.NO_SERVICE; - } - } - - private wrapError(error: JsonRpcError) { - return new WalletError(error.message, error.code, error.data); - } } diff --git a/libs/wallet/src/context.ts b/libs/wallet/src/context.ts index ebf4366c1..fec5f99c5 100644 --- a/libs/wallet/src/context.ts +++ b/libs/wallet/src/context.ts @@ -17,7 +17,7 @@ export interface VegaWalletContextShape { connect: (connector: VegaConnector) => Promise; /** Disconnects from the connector and clears public key state */ - disconnect: () => Promise; + disconnect: () => Promise; /** Sets the current selected public key */ selectPubKey: (pubKey: string) => void; diff --git a/libs/wallet/src/provider.tsx b/libs/wallet/src/provider.tsx index 517212562..9749fdb45 100644 --- a/libs/wallet/src/provider.tsx +++ b/libs/wallet/src/provider.tsx @@ -59,23 +59,19 @@ export const VegaWalletProvider = ({ children }: VegaWalletProviderProps) => { }, []); const disconnect = useCallback(async () => { + // always clear state after attempted disconnection.. this + // is because long lived token sessions (used in tests) + // cannot be cleared. Clearing state will force user to reconnect + // again as expected + setPubKeys(null); + setPubKey(null); + LocalStorage.removeItem(WALLET_KEY); try { await connector.current?.disconnect(); - setPubKeys(null); - setPubKey(null); connector.current = null; - LocalStorage.removeItem(WALLET_KEY); - return true; } catch (err) { console.error(err); - if (err instanceof WalletError && err.code === 100) { - setPubKeys(null); - setPubKey(null); - connector.current = null; - LocalStorage.removeItem(WALLET_KEY); - return true; - } - return false; + connector.current = null; } }, []); diff --git a/package.json b/package.json index de4466f4f..09a6ae4d9 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@sentry/nextjs": "^6.19.3", "@sentry/react": "^6.19.2", "@sentry/tracing": "^6.19.2", + "@vegaprotocol/wallet-client": "0.1.4", "@walletconnect/ethereum-provider": "^1.7.5", "@web3-react/core": "8.0.20-beta.0", "@web3-react/metamask": "8.0.16-beta.0", diff --git a/yarn.lock b/yarn.lock index a8a36da10..ecc122e1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7559,6 +7559,15 @@ "@typescript-eslint/types" "5.40.0" eslint-visitor-keys "^3.3.0" +"@vegaprotocol/wallet-client@0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@vegaprotocol/wallet-client/-/wallet-client-0.1.4.tgz#202fa1a84dbef57199810383f2887a7ee0afd64c" + integrity sha512-uGEbusoi3lwyl7Nn9ovBg9YHrfcH/Rl33KUcLfdMeTX8FZO+n7BVm3ejd00e5RsM/PJlqJ1oW4qkiq7kervxng== + dependencies: + express "4.18.2" + nanoid "3.3.4" + node-fetch "2.6.7" + "@walletconnect/browser-utils@^1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@walletconnect/browser-utils/-/browser-utils-1.8.0.tgz#33c10e777aa6be86c713095b5206d63d32df0951" @@ -12798,7 +12807,7 @@ expect@^29.0.0: jest-message-util "^29.1.2" jest-util "^29.1.2" -express@^4.17.1, express@^4.17.3: +express@4.18.2, express@^4.17.1, express@^4.17.3: version "4.18.2" resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== @@ -17333,7 +17342,7 @@ nan@^2.12.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== -nanoid@^3.3.1, nanoid@^3.3.4: +nanoid@3.3.4, nanoid@^3.3.1, nanoid@^3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==