From c533c584daf674fda4972ffabfd643aac28d6a56 Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Thu, 19 Jan 2023 01:52:38 -0800 Subject: [PATCH] chore: tidy market setup scripts (#2657) --- libs/cypress/src/lib/capsule/contants.ts | 1 + libs/cypress/src/lib/capsule/create-market.ts | 117 ++++------------ .../cypress/src/lib/capsule/ethereum-setup.ts | 131 +++--------------- .../src/lib/capsule/ethereum-wallet.ts | 24 ++++ .../src/lib/capsule/get-ethereum-config.ts | 20 +++ libs/cypress/src/lib/capsule/get-markets.ts | 72 ++++++++++ .../src/lib/capsule/get-party-stake.ts | 24 ++++ libs/cypress/src/lib/capsule/get-proposal.ts | 22 +++ .../cypress/src/lib/capsule/get-vega-asset.ts | 39 ++++++ .../cypress/src/lib/capsule/propose-market.ts | 34 ++--- libs/cypress/src/lib/capsule/request.ts | 31 +---- libs/cypress/src/lib/capsule/vote.ts | 12 +- libs/cypress/src/lib/capsule/wallet-client.ts | 56 ++++++++ .../cypress/src/lib/commands/create-market.ts | 13 -- libs/cypress/src/lib/utils.ts | 18 +++ 15 files changed, 335 insertions(+), 279 deletions(-) create mode 100644 libs/cypress/src/lib/capsule/contants.ts create mode 100644 libs/cypress/src/lib/capsule/ethereum-wallet.ts create mode 100644 libs/cypress/src/lib/capsule/get-ethereum-config.ts create mode 100644 libs/cypress/src/lib/capsule/get-markets.ts create mode 100644 libs/cypress/src/lib/capsule/get-party-stake.ts create mode 100644 libs/cypress/src/lib/capsule/get-proposal.ts create mode 100644 libs/cypress/src/lib/capsule/get-vega-asset.ts create mode 100644 libs/cypress/src/lib/capsule/wallet-client.ts diff --git a/libs/cypress/src/lib/capsule/contants.ts b/libs/cypress/src/lib/capsule/contants.ts new file mode 100644 index 000000000..224bcadf4 --- /dev/null +++ b/libs/cypress/src/lib/capsule/contants.ts @@ -0,0 +1 @@ +export const ASSET_ID_FOR_MARKET = 'fUSDC'; diff --git a/libs/cypress/src/lib/capsule/create-market.ts b/libs/cypress/src/lib/capsule/create-market.ts index 16a393b81..31e5d22a1 100644 --- a/libs/cypress/src/lib/capsule/create-market.ts +++ b/libs/cypress/src/lib/capsule/create-market.ts @@ -1,9 +1,8 @@ import * as Schema from '@vegaprotocol/types'; -import { gql } from 'graphql-request'; import { determineId } from '../utils'; -import { requestGQL, setEndpoints } from './request'; +import { setGraphQLEndpoint } from './request'; import { vote } from './vote'; -import { setupEthereumAccount } from './ethereum-setup'; +import { stakeForVegaPublicKey } from './ethereum-setup'; import { faucetAsset } from './faucet-asset'; import { proposeMarket, @@ -11,6 +10,10 @@ import { waitForProposal, } from './propose-market'; import { createLog } from './logging'; +import { getMarkets } from './get-markets'; +import { createWalletClient } from './wallet-client'; +import { createEthereumWallet } from './ethereum-wallet'; +import { ASSET_ID_FOR_MARKET } from './contants'; const log = createLog('create-market'); @@ -23,8 +26,10 @@ export async function createMarket(cfg: { vegaUrl: string; faucetUrl: string; }) { - // set and store request endpoints - setEndpoints(cfg.vegaWalletUrl, cfg.vegaUrl); + // setup wallet client and graphql clients + setGraphQLEndpoint(cfg.vegaUrl); + createWalletClient(cfg.vegaWalletUrl, cfg.token); + createEthereumWallet(cfg.ethWalletMnemonic, cfg.ethereumProviderUrl); const markets = await getMarkets(); @@ -37,101 +42,33 @@ export async function createMarket(cfg: { return markets; } - await setupEthereumAccount( - cfg.vegaPubKey, - cfg.ethWalletMnemonic, - cfg.ethereumProviderUrl - ); + // To participate in governance (in this case proposing and voting in a market) + // you need to have staked (associated) some Vega with a Vega public key + await stakeForVegaPublicKey(cfg.vegaPubKey); - const result = await faucetAsset(cfg.faucetUrl, 'fUSDC', cfg.vegaPubKey); + // Send some of the asset for the market to be proposed to the test pubkey + const result = await faucetAsset( + cfg.faucetUrl, + ASSET_ID_FOR_MARKET, + cfg.vegaPubKey + ); if (!result.success) { throw new Error('faucet failed'); } - // propose and vote on a market - const proposalTxResult = await proposeMarket(cfg.vegaPubKey, cfg.token); + // Propose a new market + const proposalTxResult = await proposeMarket(cfg.vegaPubKey); const proposalId = determineId(proposalTxResult.transaction.signature.value); log(`proposal created (id: ${proposalId})`); const proposal = await waitForProposal(proposalId); - await vote( - proposal.id, - Schema.VoteValue.VALUE_YES, - cfg.vegaPubKey, - cfg.token - ); + + // Vote on new market proposal + await vote(proposal.id, Schema.VoteValue.VALUE_YES, cfg.vegaPubKey); + + // Wait for the market to be enacted and go into opening auction await waitForEnactment(); - // fetch and return created market + // Fetch the newly created market const newMarkets = await getMarkets(); return newMarkets; } - -async function getMarkets() { - const query = gql` - { - marketsConnection { - edges { - node { - id - decimalPlaces - positionDecimalPlaces - state - tradableInstrument { - instrument { - id - name - code - metadata { - tags - } - product { - ... on Future { - settlementAsset { - id - symbol - decimals - } - quoteName - } - } - } - } - } - } - } - } - `; - - const res = await requestGQL<{ - marketsConnection: { - edges: Array<{ - node: { - id: string; - decimalPlaces: number; - positionDecimalPlaces: number; - state: string; - tradableInstrument: { - instrument: { - id: string; - name: string; - code: string; - metadata: { - tags: string[]; - }; - product: { - settlementAssset: { - id: string; - symbol: string; - decimals: number; - }; - quoteName: string; - }; - }; - }; - }; - }>; - }; - }>(query); - - return res.marketsConnection.edges.map((e) => e.node); -} diff --git a/libs/cypress/src/lib/capsule/ethereum-setup.ts b/libs/cypress/src/lib/capsule/ethereum-setup.ts index 13268d41f..472f5bc0f 100644 --- a/libs/cypress/src/lib/capsule/ethereum-setup.ts +++ b/libs/cypress/src/lib/capsule/ethereum-setup.ts @@ -1,28 +1,17 @@ -import { ethers, Wallet } from 'ethers'; import { StakingBridge, Token } from '@vegaprotocol/smart-contracts'; -import { gql } from 'graphql-request'; -import { requestGQL } from './request'; import { createLog } from './logging'; +import { promiseWithTimeout } from '../utils'; +import { getVegaAsset } from './get-vega-asset'; +import { getEthereumConfig } from './get-ethereum-config'; +import { getPartyStake } from './get-party-stake'; +import { wallet } from './ethereum-wallet'; const log = createLog('ethereum-setup'); -export async function setupEthereumAccount( - vegaPublicKey: string, - ethWalletMnemonic: string, - ethereumProviderUrl: string -) { - // create provider/wallet - const provider = new ethers.providers.JsonRpcProvider({ - url: ethereumProviderUrl, - }); - - const privateKey = Wallet.fromMnemonic( - ethWalletMnemonic, - getAccount() - ).privateKey; - - // this wallet (ozone access etc) is already set up with 6 million vega (eth) - const wallet = new Wallet(privateKey, provider); +export async function stakeForVegaPublicKey(vegaPublicKey: string) { + if (!wallet) { + throw new Error('ethereum wallet not initialized'); + } const vegaAsset = await getVegaAsset(); if (!vegaAsset) { @@ -43,13 +32,13 @@ export async function setupEthereumAccount( '100000' + '0'.repeat(18) ), 1000, - 'tokenContract.approve' + 'approve staking tx' ); await promiseWithTimeout( approveTx.wait(1), 10 * 60 * 1000, - 'approveTx.wait(1)' + 'waiting for 1 stake approval confirmations' ); log('sending approve tx: success'); @@ -65,108 +54,27 @@ export async function setupEthereumAccount( 14000, 'stakingContract.stake(amount, vegaPublicKey)' ); - await promiseWithTimeout(stakeTx.wait(3), 10 * 60 * 1000, 'stakeTx.wait(3)'); + await promiseWithTimeout( + stakeTx.wait(3), + 10 * 60 * 1000, + 'waiting for 3 stake tx confirmations' + ); await waitForStake(vegaPublicKey); log(`sending stake tx: success`); } -function timeout(time = 0, id: string) { - return new Promise((resolve, reject) => { - setTimeout(() => reject(new Error(`${id}: timeout triggered`)), time); - }); -} - -async function promiseWithTimeout( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - promise: Promise, - time: number, - id: string -) { - return await Promise.race([promise, timeout(time, id)]); -} - -async function getVegaAsset() { - const query = gql` - { - assetsConnection { - edges { - node { - id - symbol - source { - ... on ERC20 { - contractAddress - } - } - } - } - } - } - `; - - const res = await requestGQL<{ - assetsConnection: { - edges: Array<{ - node: { - id: string; - symbol: string; - source: { - contractAddress: string; - }; - }; - }>; - }; - }>(query); - return res.assetsConnection.edges - .map((e) => e.node) - .find((a) => a.symbol === 'VEGA'); -} - -async function getEthereumConfig() { - const query = gql` - { - networkParameter(key: "blockchains.ethereumConfig") { - value - } - } - `; - - const res = await requestGQL<{ - networkParameter: { - key: string; - value: string; - }; - }>(query); - return JSON.parse(res.networkParameter.value); -} - function waitForStake(vegaPublicKey: string) { - const query = gql` - { - party(id:"${vegaPublicKey}") { - stakingSummary { - currentStakeAvailable - } - } - } - `; return new Promise((resolve, reject) => { let tick = 1; const interval = setInterval(async () => { log(`confirming stake (attempt: ${tick})`); - if (tick >= 10) { + if (tick >= 30) { clearInterval(interval); reject(new Error('stake link never seen')); } try { - const res = await requestGQL<{ - party: { - stakingSummary: { - currentStakeAvailable: string; - }; - }; - }>(query); + const res = await getPartyStake(vegaPublicKey); if ( res.party?.stakingSummary?.currentStakeAvailable !== null && @@ -186,6 +94,3 @@ function waitForStake(vegaPublicKey: string) { }, 1000); }); } - -// derivation path -const getAccount = (number = 0) => `m/44'/60'/0'/0/${number}`; diff --git a/libs/cypress/src/lib/capsule/ethereum-wallet.ts b/libs/cypress/src/lib/capsule/ethereum-wallet.ts new file mode 100644 index 000000000..8b80a155b --- /dev/null +++ b/libs/cypress/src/lib/capsule/ethereum-wallet.ts @@ -0,0 +1,24 @@ +import { ethers, Wallet } from 'ethers'; + +export let wallet: Wallet | undefined; + +export function createEthereumWallet( + ethWalletMnemonic: string, + ethereumProviderUrl: string +) { + // create provider/wallet + const provider = new ethers.providers.JsonRpcProvider({ + url: ethereumProviderUrl, + }); + + const privateKey = Wallet.fromMnemonic( + ethWalletMnemonic, + getAccount() + ).privateKey; + + // this wallet (ozone access etc) is already set up with 6 million vega (eth) + wallet = new Wallet(privateKey, provider); +} + +// derivation path +const getAccount = (number = 0) => `m/44'/60'/0'/0/${number}`; diff --git a/libs/cypress/src/lib/capsule/get-ethereum-config.ts b/libs/cypress/src/lib/capsule/get-ethereum-config.ts new file mode 100644 index 000000000..1de890e4b --- /dev/null +++ b/libs/cypress/src/lib/capsule/get-ethereum-config.ts @@ -0,0 +1,20 @@ +import { gql } from 'graphql-request'; +import { requestGQL } from './request'; + +export async function getEthereumConfig() { + const query = gql` + { + networkParameter(key: "blockchains.ethereumConfig") { + value + } + } + `; + + const res = await requestGQL<{ + networkParameter: { + key: string; + value: string; + }; + }>(query); + return JSON.parse(res.networkParameter.value); +} diff --git a/libs/cypress/src/lib/capsule/get-markets.ts b/libs/cypress/src/lib/capsule/get-markets.ts new file mode 100644 index 000000000..93d4a7c02 --- /dev/null +++ b/libs/cypress/src/lib/capsule/get-markets.ts @@ -0,0 +1,72 @@ +import { gql } from 'graphql-request'; +import { requestGQL } from './request'; + +export async function getMarkets() { + const query = gql` + { + marketsConnection { + edges { + node { + id + decimalPlaces + positionDecimalPlaces + state + tradableInstrument { + instrument { + id + name + code + metadata { + tags + } + product { + ... on Future { + settlementAsset { + id + symbol + decimals + } + quoteName + } + } + } + } + } + } + } + } + `; + + const res = await requestGQL<{ + marketsConnection: { + edges: Array<{ + node: { + id: string; + decimalPlaces: number; + positionDecimalPlaces: number; + state: string; + tradableInstrument: { + instrument: { + id: string; + name: string; + code: string; + metadata: { + tags: string[]; + }; + product: { + settlementAssset: { + id: string; + symbol: string; + decimals: number; + }; + quoteName: string; + }; + }; + }; + }; + }>; + }; + }>(query); + + return res.marketsConnection.edges.map((e) => e.node); +} diff --git a/libs/cypress/src/lib/capsule/get-party-stake.ts b/libs/cypress/src/lib/capsule/get-party-stake.ts new file mode 100644 index 000000000..c4ce8c72c --- /dev/null +++ b/libs/cypress/src/lib/capsule/get-party-stake.ts @@ -0,0 +1,24 @@ +import { gql } from 'graphql-request'; +import { requestGQL } from './request'; + +export async function getPartyStake(partyId: string) { + const query = gql` + { + party(id:"${partyId}") { + stakingSummary { + currentStakeAvailable + } + } + } + `; + + const res = await requestGQL<{ + party: { + stakingSummary: { + currentStakeAvailable: string; + }; + }; + }>(query); + + return res; +} diff --git a/libs/cypress/src/lib/capsule/get-proposal.ts b/libs/cypress/src/lib/capsule/get-proposal.ts new file mode 100644 index 000000000..831b9c252 --- /dev/null +++ b/libs/cypress/src/lib/capsule/get-proposal.ts @@ -0,0 +1,22 @@ +import { gql } from 'graphql-request'; +import { requestGQL } from './request'; + +export async function getProposal(id: string) { + const query = gql` + { + proposal(id: "${id}") { + id + state + } + } + `; + + const res = await requestGQL<{ + proposal: { + id: string; + state: string; + }; + }>(query); + + return res; +} diff --git a/libs/cypress/src/lib/capsule/get-vega-asset.ts b/libs/cypress/src/lib/capsule/get-vega-asset.ts new file mode 100644 index 000000000..8ca67efeb --- /dev/null +++ b/libs/cypress/src/lib/capsule/get-vega-asset.ts @@ -0,0 +1,39 @@ +import { gql } from 'graphql-request'; +import { requestGQL } from './request'; + +export async function getVegaAsset() { + const query = gql` + { + assetsConnection { + edges { + node { + id + symbol + source { + ... on ERC20 { + contractAddress + } + } + } + } + } + } + `; + + const res = await requestGQL<{ + assetsConnection: { + edges: Array<{ + node: { + id: string; + symbol: string; + source: { + contractAddress: string; + }; + }; + }>; + }; + }>(query); + return res.assetsConnection.edges + .map((e) => e.node) + .find((a) => a.symbol === 'VEGA'); +} diff --git a/libs/cypress/src/lib/capsule/propose-market.ts b/libs/cypress/src/lib/capsule/propose-market.ts index dae5e1bf3..cef6a6e1a 100644 --- a/libs/cypress/src/lib/capsule/propose-market.ts +++ b/libs/cypress/src/lib/capsule/propose-market.ts @@ -1,24 +1,20 @@ import * as Schema from '@vegaprotocol/types'; import { addSeconds, millisecondsToSeconds } from 'date-fns'; -import { gql } from 'graphql-request'; -import { request, requestGQL } from './request'; import { createLog } from './logging'; import type { ProposalSubmissionBody } from '@vegaprotocol/wallet'; +import { getProposal } from './get-proposal'; +import { sendVegaTx } from './wallet-client'; +import { ASSET_ID_FOR_MARKET } from './contants'; const log = createLog('propose-market'); const MIN_CLOSE_SEC = 5; const MIN_ENACT_SEC = 3; -export async function proposeMarket(publicKey: string, token: string) { +export async function proposeMarket(publicKey: string) { log('sending proposal tx'); const proposalTx = createNewMarketProposal(); - const result = await request('client.send_transaction', { - token, - publicKey, - sendingMode: 'TYPE_SYNC', - transaction: proposalTx, - }); + const result = await sendVegaTx(publicKey, proposalTx); return result.result; } @@ -44,8 +40,8 @@ function createNewMarketProposal(): ProposalSubmissionBody { name: 'Test market 1', code: 'TEST.24h', future: { - settlementAsset: 'fUSDC', - quoteName: 'fUSDC', + settlementAsset: ASSET_ID_FOR_MARKET, + quoteName: ASSET_ID_FOR_MARKET, dataSourceSpecForSettlementData: { external: { oracle: { @@ -144,14 +140,6 @@ function createNewMarketProposal(): ProposalSubmissionBody { } export function waitForProposal(id: string): Promise<{ id: string }> { - const query = gql` - { - proposal(id: "${id}") { - id - state - } - } - `; return new Promise((resolve, reject) => { let tick = 0; const interval = setInterval(async () => { @@ -161,13 +149,7 @@ export function waitForProposal(id: string): Promise<{ id: string }> { } try { - const res = await requestGQL<{ - proposal: { - id: string; - state: string; - }; - }>(query); - + const res = await getProposal(id); if ( res.proposal !== null && res.proposal.state === Schema.ProposalState.STATE_OPEN diff --git a/libs/cypress/src/lib/capsule/request.ts b/libs/cypress/src/lib/capsule/request.ts index 09814de47..a4ad31449 100644 --- a/libs/cypress/src/lib/capsule/request.ts +++ b/libs/cypress/src/lib/capsule/request.ts @@ -1,39 +1,14 @@ -import { request as gqlRequest } from 'graphql-request'; +import { request } from 'graphql-request'; -let walletEndpoint = ''; let gqlEndpoint = ''; -export function setEndpoints(walletUrl: string, gqlUrl: string) { - walletEndpoint = walletUrl + '/api/v2/requests'; +export function setGraphQLEndpoint(gqlUrl: string) { gqlEndpoint = gqlUrl; } -export function request(method: string, params: object) { - if (!walletEndpoint) { - throw new Error('gqlEndpoint not set'); - } - const body = { - jsonrpc: '2.0', - method, - params, - id: Math.random().toString(), - }; - return fetch(walletEndpoint, { - method: 'post', - body: JSON.stringify(body), - headers: { - 'Content-Type': 'application/json', - Origin: 'market-setup', - Referer: 'market-setup', - }, - }).then((res) => { - return res.json(); - }); -} - export function requestGQL(query: string): Promise { if (!gqlEndpoint) { throw new Error('gqlEndpoint not set'); } - return gqlRequest(gqlEndpoint, query); + return request(gqlEndpoint, query); } diff --git a/libs/cypress/src/lib/capsule/vote.ts b/libs/cypress/src/lib/capsule/vote.ts index a987ce0a0..2de72af29 100644 --- a/libs/cypress/src/lib/capsule/vote.ts +++ b/libs/cypress/src/lib/capsule/vote.ts @@ -1,24 +1,18 @@ import type * as Schema from '@vegaprotocol/types'; -import { request } from './request'; import { createLog } from './logging'; +import { sendVegaTx } from './wallet-client'; const log = createLog('vote'); export async function vote( proposalId: string, voteValue: Schema.VoteValue, - publicKey: string, - token: string + publicKey: string ) { log(`voting ${voteValue} on ${proposalId}`); const voteTx = createVote(proposalId, voteValue); - const voteResult = await request('client.send_transaction', { - token, - publicKey, - sendingMode: 'TYPE_SYNC', - transaction: voteTx, - }); + const voteResult = await sendVegaTx(publicKey, voteTx); return voteResult.result; } diff --git a/libs/cypress/src/lib/capsule/wallet-client.ts b/libs/cypress/src/lib/capsule/wallet-client.ts new file mode 100644 index 000000000..2eac35d55 --- /dev/null +++ b/libs/cypress/src/lib/capsule/wallet-client.ts @@ -0,0 +1,56 @@ +import type { Transaction } from '@vegaprotocol/wallet'; + +let url = ''; +let token = ''; +let requestId = 1; + +// Note: cant use @vegaprotocol/wallet-client due to webpack oddly not +// being able to handle class syntax. Instead heres a super basic 'client' +// which only sends transactions +export function createWalletClient(walletUrl: string, walletToken: string) { + url = walletUrl + '/api/v2/requests'; + token = walletToken; +} + +export async function sendVegaTx(publicKey: string, transaction: Transaction) { + if (!url || !token) { + throw new Error('client not initialized'); + } + + const res = await request('client.send_transaction', { + publicKey, + transaction, + }); + + return res; +} + +export function request( + method: string, + params: { + publicKey: string; + transaction: Transaction; + } +) { + const body = { + jsonrpc: '2.0', + method, + params: { + ...params, + sendingMode: 'TYPE_SYNC', + token, + }, + id: (requestId++).toString(), + }; + return fetch(url, { + method: 'post', + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json', + Origin: 'market-setup', + Referer: 'market-setup', + }, + }).then((res) => { + return res.json(); + }); +} diff --git a/libs/cypress/src/lib/commands/create-market.ts b/libs/cypress/src/lib/commands/create-market.ts index 6ee165c52..49a802fa2 100644 --- a/libs/cypress/src/lib/commands/create-market.ts +++ b/libs/cypress/src/lib/commands/create-market.ts @@ -10,19 +10,6 @@ declare global { } } -// suppress fetch and xhr logs -const originalLog = Cypress.log; -// @ts-ignore fuck with log to help with debug -Cypress.log = function (options, ...rest) { - // @ts-ignore fuck with log to help with debug - const isRequest = ['fetch', 'xhr'].includes(options.displayName); - if (isRequest) { - return; - } - - return originalLog(options, ...rest); -}; - export const addCreateMarket = () => { Cypress.Commands.add('createMarket', () => { const config = { diff --git a/libs/cypress/src/lib/utils.ts b/libs/cypress/src/lib/utils.ts index 1a63339fc..d58064b7e 100644 --- a/libs/cypress/src/lib/utils.ts +++ b/libs/cypress/src/lib/utils.ts @@ -18,3 +18,21 @@ export function removeDecimal(value: string, decimals: number): string { if (!decimals) return value; return new BigNumber(value || 0).times(Math.pow(10, decimals)).toFixed(0); } + +export async function promiseWithTimeout( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + promise: Promise, + time: number, + name: string +) { + const rejectAfterTimeout = (time = 0) => { + return new Promise((resolve, reject) => { + setTimeout( + () => reject(new Error(`${name}: timed out after ${time}ms`)), + time + ); + }); + }; + + return await Promise.race([promise, rejectAfterTimeout(time)]); +}