chore: tidy market setup scripts (#2657)

This commit is contained in:
Matthew Russell 2023-01-19 01:52:38 -08:00 committed by GitHub
parent c1ebda9274
commit c533c584da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 335 additions and 279 deletions

View File

@ -0,0 +1 @@
export const ASSET_ID_FOR_MARKET = 'fUSDC';

View File

@ -1,9 +1,8 @@
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { gql } from 'graphql-request';
import { determineId } from '../utils'; import { determineId } from '../utils';
import { requestGQL, setEndpoints } from './request'; import { setGraphQLEndpoint } from './request';
import { vote } from './vote'; import { vote } from './vote';
import { setupEthereumAccount } from './ethereum-setup'; import { stakeForVegaPublicKey } from './ethereum-setup';
import { faucetAsset } from './faucet-asset'; import { faucetAsset } from './faucet-asset';
import { import {
proposeMarket, proposeMarket,
@ -11,6 +10,10 @@ import {
waitForProposal, waitForProposal,
} from './propose-market'; } from './propose-market';
import { createLog } from './logging'; 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'); const log = createLog('create-market');
@ -23,8 +26,10 @@ export async function createMarket(cfg: {
vegaUrl: string; vegaUrl: string;
faucetUrl: string; faucetUrl: string;
}) { }) {
// set and store request endpoints // setup wallet client and graphql clients
setEndpoints(cfg.vegaWalletUrl, cfg.vegaUrl); setGraphQLEndpoint(cfg.vegaUrl);
createWalletClient(cfg.vegaWalletUrl, cfg.token);
createEthereumWallet(cfg.ethWalletMnemonic, cfg.ethereumProviderUrl);
const markets = await getMarkets(); const markets = await getMarkets();
@ -37,101 +42,33 @@ export async function createMarket(cfg: {
return markets; return markets;
} }
await setupEthereumAccount( // To participate in governance (in this case proposing and voting in a market)
cfg.vegaPubKey, // you need to have staked (associated) some Vega with a Vega public key
cfg.ethWalletMnemonic, await stakeForVegaPublicKey(cfg.vegaPubKey);
cfg.ethereumProviderUrl
);
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) { if (!result.success) {
throw new Error('faucet failed'); throw new Error('faucet failed');
} }
// propose and vote on a market // Propose a new market
const proposalTxResult = await proposeMarket(cfg.vegaPubKey, cfg.token); const proposalTxResult = await proposeMarket(cfg.vegaPubKey);
const proposalId = determineId(proposalTxResult.transaction.signature.value); const proposalId = determineId(proposalTxResult.transaction.signature.value);
log(`proposal created (id: ${proposalId})`); log(`proposal created (id: ${proposalId})`);
const proposal = await waitForProposal(proposalId); const proposal = await waitForProposal(proposalId);
await vote(
proposal.id, // Vote on new market proposal
Schema.VoteValue.VALUE_YES, await vote(proposal.id, Schema.VoteValue.VALUE_YES, cfg.vegaPubKey);
cfg.vegaPubKey,
cfg.token // Wait for the market to be enacted and go into opening auction
);
await waitForEnactment(); await waitForEnactment();
// fetch and return created market // Fetch the newly created market
const newMarkets = await getMarkets(); const newMarkets = await getMarkets();
return newMarkets; 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);
}

View File

@ -1,28 +1,17 @@
import { ethers, Wallet } from 'ethers';
import { StakingBridge, Token } from '@vegaprotocol/smart-contracts'; import { StakingBridge, Token } from '@vegaprotocol/smart-contracts';
import { gql } from 'graphql-request';
import { requestGQL } from './request';
import { createLog } from './logging'; 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'); const log = createLog('ethereum-setup');
export async function setupEthereumAccount( export async function stakeForVegaPublicKey(vegaPublicKey: string) {
vegaPublicKey: string, if (!wallet) {
ethWalletMnemonic: string, throw new Error('ethereum wallet not initialized');
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);
const vegaAsset = await getVegaAsset(); const vegaAsset = await getVegaAsset();
if (!vegaAsset) { if (!vegaAsset) {
@ -43,13 +32,13 @@ export async function setupEthereumAccount(
'100000' + '0'.repeat(18) '100000' + '0'.repeat(18)
), ),
1000, 1000,
'tokenContract.approve' 'approve staking tx'
); );
await promiseWithTimeout( await promiseWithTimeout(
approveTx.wait(1), approveTx.wait(1),
10 * 60 * 1000, 10 * 60 * 1000,
'approveTx.wait(1)' 'waiting for 1 stake approval confirmations'
); );
log('sending approve tx: success'); log('sending approve tx: success');
@ -65,108 +54,27 @@ export async function setupEthereumAccount(
14000, 14000,
'stakingContract.stake(amount, vegaPublicKey)' '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); await waitForStake(vegaPublicKey);
log(`sending stake tx: success`); 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<any>,
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) { function waitForStake(vegaPublicKey: string) {
const query = gql`
{
party(id:"${vegaPublicKey}") {
stakingSummary {
currentStakeAvailable
}
}
}
`;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let tick = 1; let tick = 1;
const interval = setInterval(async () => { const interval = setInterval(async () => {
log(`confirming stake (attempt: ${tick})`); log(`confirming stake (attempt: ${tick})`);
if (tick >= 10) { if (tick >= 30) {
clearInterval(interval); clearInterval(interval);
reject(new Error('stake link never seen')); reject(new Error('stake link never seen'));
} }
try { try {
const res = await requestGQL<{ const res = await getPartyStake(vegaPublicKey);
party: {
stakingSummary: {
currentStakeAvailable: string;
};
};
}>(query);
if ( if (
res.party?.stakingSummary?.currentStakeAvailable !== null && res.party?.stakingSummary?.currentStakeAvailable !== null &&
@ -186,6 +94,3 @@ function waitForStake(vegaPublicKey: string) {
}, 1000); }, 1000);
}); });
} }
// derivation path
const getAccount = (number = 0) => `m/44'/60'/0'/0/${number}`;

View File

@ -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}`;

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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');
}

View File

@ -1,24 +1,20 @@
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { addSeconds, millisecondsToSeconds } from 'date-fns'; import { addSeconds, millisecondsToSeconds } from 'date-fns';
import { gql } from 'graphql-request';
import { request, requestGQL } from './request';
import { createLog } from './logging'; import { createLog } from './logging';
import type { ProposalSubmissionBody } from '@vegaprotocol/wallet'; 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 log = createLog('propose-market');
const MIN_CLOSE_SEC = 5; const MIN_CLOSE_SEC = 5;
const MIN_ENACT_SEC = 3; const MIN_ENACT_SEC = 3;
export async function proposeMarket(publicKey: string, token: string) { export async function proposeMarket(publicKey: string) {
log('sending proposal tx'); log('sending proposal tx');
const proposalTx = createNewMarketProposal(); const proposalTx = createNewMarketProposal();
const result = await request('client.send_transaction', { const result = await sendVegaTx(publicKey, proposalTx);
token,
publicKey,
sendingMode: 'TYPE_SYNC',
transaction: proposalTx,
});
return result.result; return result.result;
} }
@ -44,8 +40,8 @@ function createNewMarketProposal(): ProposalSubmissionBody {
name: 'Test market 1', name: 'Test market 1',
code: 'TEST.24h', code: 'TEST.24h',
future: { future: {
settlementAsset: 'fUSDC', settlementAsset: ASSET_ID_FOR_MARKET,
quoteName: 'fUSDC', quoteName: ASSET_ID_FOR_MARKET,
dataSourceSpecForSettlementData: { dataSourceSpecForSettlementData: {
external: { external: {
oracle: { oracle: {
@ -144,14 +140,6 @@ function createNewMarketProposal(): ProposalSubmissionBody {
} }
export function waitForProposal(id: string): Promise<{ id: string }> { export function waitForProposal(id: string): Promise<{ id: string }> {
const query = gql`
{
proposal(id: "${id}") {
id
state
}
}
`;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let tick = 0; let tick = 0;
const interval = setInterval(async () => { const interval = setInterval(async () => {
@ -161,13 +149,7 @@ export function waitForProposal(id: string): Promise<{ id: string }> {
} }
try { try {
const res = await requestGQL<{ const res = await getProposal(id);
proposal: {
id: string;
state: string;
};
}>(query);
if ( if (
res.proposal !== null && res.proposal !== null &&
res.proposal.state === Schema.ProposalState.STATE_OPEN res.proposal.state === Schema.ProposalState.STATE_OPEN

View File

@ -1,39 +1,14 @@
import { request as gqlRequest } from 'graphql-request'; import { request } from 'graphql-request';
let walletEndpoint = '';
let gqlEndpoint = ''; let gqlEndpoint = '';
export function setEndpoints(walletUrl: string, gqlUrl: string) { export function setGraphQLEndpoint(gqlUrl: string) {
walletEndpoint = walletUrl + '/api/v2/requests';
gqlEndpoint = gqlUrl; 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<T>(query: string): Promise<T> { export function requestGQL<T>(query: string): Promise<T> {
if (!gqlEndpoint) { if (!gqlEndpoint) {
throw new Error('gqlEndpoint not set'); throw new Error('gqlEndpoint not set');
} }
return gqlRequest(gqlEndpoint, query); return request(gqlEndpoint, query);
} }

View File

@ -1,24 +1,18 @@
import type * as Schema from '@vegaprotocol/types'; import type * as Schema from '@vegaprotocol/types';
import { request } from './request';
import { createLog } from './logging'; import { createLog } from './logging';
import { sendVegaTx } from './wallet-client';
const log = createLog('vote'); const log = createLog('vote');
export async function vote( export async function vote(
proposalId: string, proposalId: string,
voteValue: Schema.VoteValue, voteValue: Schema.VoteValue,
publicKey: string, publicKey: string
token: string
) { ) {
log(`voting ${voteValue} on ${proposalId}`); log(`voting ${voteValue} on ${proposalId}`);
const voteTx = createVote(proposalId, voteValue); const voteTx = createVote(proposalId, voteValue);
const voteResult = await request('client.send_transaction', { const voteResult = await sendVegaTx(publicKey, voteTx);
token,
publicKey,
sendingMode: 'TYPE_SYNC',
transaction: voteTx,
});
return voteResult.result; return voteResult.result;
} }

View File

@ -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();
});
}

View File

@ -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 = () => { export const addCreateMarket = () => {
Cypress.Commands.add('createMarket', () => { Cypress.Commands.add('createMarket', () => {
const config = { const config = {

View File

@ -18,3 +18,21 @@ export function removeDecimal(value: string, decimals: number): string {
if (!decimals) return value; if (!decimals) return value;
return new BigNumber(value || 0).times(Math.pow(10, decimals)).toFixed(0); 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<any>,
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)]);
}