chore(#315): Convert token to use wallet lib

* delete token version of vega wallet serivce

* update use-user-vote to use new wallet service

* remove typo

* add further types for transaction submissions, add assets to withdraw page query

* update api client package to get generated types, adjust render logic of withdrawals page

* fix withdrawals list rendering

* update determine id function to not use nodejs buffer

* update service api client so it accepts new tx types

* remove stray logs and formatting

* make filtering erc20 assets the responsibility of the withdraw/deposit lib and not the app

* remove sha3 dep and use js-sha3 and ethers to determine ids

* use hook for fetching withdrawals form lib, add type policy to ensure withdrawal state is updated correctly

* fix: markets page feature
This commit is contained in:
Matthew Russell 2022-05-17 09:04:41 -04:00 committed by GitHub
parent b10432fdf3
commit d8bf887245
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 265 additions and 1106 deletions

View File

@ -147,6 +147,7 @@ const IconLine = ({ inverted }: { inverted: boolean }) => (
const NavDrawer = ({ inverted }: { inverted: boolean }) => { const NavDrawer = ({ inverted }: { inverted: boolean }) => {
const { appState, appDispatch } = useAppState(); const { appState, appDispatch } = useAppState();
return ( return (
<> <>
<button <button

View File

@ -25,7 +25,6 @@ export const TransactionComplete = ({
<p> <p>
<EtherscanLink tx={hash} /> <EtherscanLink tx={hash} />
</p> </p>
LINK
{footer && <p data-testid="transaction-complete-footer">{footer}</p>} {footer && <p data-testid="transaction-complete-footer">{footer}</p>}
</Callout> </Callout>
); );

View File

@ -1,143 +0,0 @@
import React from 'react';
import * as Sentry from '@sentry/react';
import { gql, useApolloClient } from '@apollo/client';
import { sigToId } from '../lib/sig-to-id';
import { vegaWalletService } from '../lib/vega-wallet/vega-wallet-service';
import type { WithdrawSubmissionInput } from '../lib/vega-wallet/vega-wallet-service';
import type {
WithdrawalPoll,
WithdrawalPollVariables,
} from './__generated__/WithdrawalPoll';
export enum Status {
Idle,
Submitted,
Pending,
Success,
Failure,
}
type Submit = (
amount: string,
asset: string,
receiverAddress: string
) => Promise<void>;
const WITHDRAWAL_QUERY = gql`
query WithdrawalPoll($partyId: ID!) {
party(id: $partyId) {
id
withdrawals {
id
amount
status
asset {
id
symbol
decimals
}
createdTimestamp
withdrawnTimestamp
txHash
details {
... on Erc20WithdrawalDetails {
receiverAddress
}
}
}
}
}
`;
export function useCreateWithdrawal(pubKey: string): [Status, Submit] {
const mountedRef = React.useRef(true);
const client = useApolloClient();
const [status, setStatus] = React.useState(Status.Idle);
const [id, setId] = React.useState('');
const safeSetStatus = (status: Status) => {
if (mountedRef.current) {
setStatus(status);
}
};
const submit = React.useCallback(
async (amount: string, asset: string, receiverAddress: string) => {
const command: WithdrawSubmissionInput = {
pubKey,
withdrawSubmission: {
amount,
asset,
ext: {
erc20: {
receiverAddress,
},
},
},
};
safeSetStatus(Status.Submitted);
try {
const [err, res] = await vegaWalletService.commandSync(command);
if (err || !res) {
safeSetStatus(Status.Failure);
} else {
const id = sigToId(res.signature.value);
setId(id);
// Now await subscription
}
safeSetStatus(Status.Pending);
} catch (err) {
safeSetStatus(Status.Failure);
Sentry.captureException(err);
}
},
[pubKey]
);
React.useEffect(() => {
let interval: ReturnType<typeof setInterval>;
if (status === Status.Pending) {
interval = setInterval(async () => {
try {
const { data } = await client.query<
WithdrawalPoll,
WithdrawalPollVariables
>({
fetchPolicy: 'network-only',
query: WITHDRAWAL_QUERY,
variables: { partyId: pubKey },
});
// find matching withdrawals
const withdrawal = data?.party?.withdrawals?.find((e) => {
return e.id === id;
});
if (withdrawal) {
safeSetStatus(Status.Success);
clearInterval(interval);
}
} catch (err) {
clearInterval(interval);
}
}, 1000);
}
return () => {
clearInterval(interval);
};
}, [client, id, pubKey, status]);
React.useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, []);
return [status, submit];
}

View File

@ -1,66 +0,0 @@
import React from 'react';
import { gql, useApolloClient } from '@apollo/client';
import type {
Erc20ApprovalPoll,
Erc20ApprovalPoll_erc20WithdrawalApproval,
Erc20ApprovalPollVariables,
} from './__generated__/Erc20ApprovalPoll';
const ERC20_APPROVAL_QUERY = gql`
query Erc20ApprovalPoll($withdrawalId: ID!) {
erc20WithdrawalApproval(withdrawalId: $withdrawalId) {
assetSource
amount
nonce
signatures
targetAddress
expiry
}
}
`;
export const usePollERC20Approval = (withdrawalId: string) => {
const mountedRef = React.useRef(true);
const client = useApolloClient();
const [erc20Approval, setErc20Approval] =
React.useState<Erc20ApprovalPoll_erc20WithdrawalApproval | null>(null);
const safeSetErc20Approval = (
approval: Erc20ApprovalPoll_erc20WithdrawalApproval
) => {
if (mountedRef.current) {
setErc20Approval(approval);
}
};
React.useEffect(() => {
const interval = setInterval(async () => {
try {
const res = await client.query<
Erc20ApprovalPoll,
Erc20ApprovalPollVariables
>({
query: ERC20_APPROVAL_QUERY,
variables: { withdrawalId },
});
if (res.data.erc20WithdrawalApproval) {
safeSetErc20Approval(res.data.erc20WithdrawalApproval);
clearInterval(interval);
}
} catch (err) {
// No op. If the erc20 withdrawal is not created yet it will error
// but we will just want to poll until it is. There is no bus event for
// erc20 approvals yet..
}
}, 1000);
return () => {
clearInterval(interval);
mountedRef.current = false;
};
}, [withdrawalId, client]);
return erc20Approval;
};

View File

@ -76,6 +76,7 @@
"Loading": "Loading...", "Loading": "Loading...",
"Something went wrong": "Something went wrong", "Something went wrong": "Something went wrong",
"Try again": "Try again", "Try again": "Try again",
"Incomplete": "Incomplete",
"Complete": "Complete", "Complete": "Complete",
"View on Etherscan (opens in a new tab)": "View on Etherscan (opens in a new tab)", "View on Etherscan (opens in a new tab)": "View on Etherscan (opens in a new tab)",
"Transaction in progress": "Transaction in progress", "Transaction in progress": "Transaction in progress",
@ -437,7 +438,7 @@
"withdrawalsText": "These withdrawals need to be completed with an Ethereum transaction.", "withdrawalsText": "These withdrawals need to be completed with an Ethereum transaction.",
"withdrawalsNone": "You don't have any pending withdrawals.", "withdrawalsNone": "You don't have any pending withdrawals.",
"withdrawalsCompleteButton": "Finish withdrawal", "withdrawalsCompleteButton": "Finish withdrawal",
"withdrawalsPreparingButton": "Preparing withdrawal", "withdrawalTransaction": "Transaction ({{foreignChain}})",
"signature": "Signature", "signature": "Signature",
"created": "Created", "created": "Created",
"toEthereum": "To (Ethereum)", "toEthereum": "To (Ethereum)",

View File

@ -136,6 +136,13 @@ export function createClient() {
}, },
}, },
}, },
Withdrawal: {
fields: {
pendingOnForeignChain: {
read: (isPending = false) => isPending,
},
},
},
}, },
}); });

View File

@ -1,9 +0,0 @@
import { sigToId } from './sig-to-id';
test('creating an id from a signature', () => {
const sig =
'3594b7f4e1c078aaa65b3efac57e1436da64d60c2961bf432dc62daa36f8b0b2602ed9c990ee46dd8ab5181e19c19b06587f080d22fc4543c1869970e678f20f';
const expected =
'9ebd2c424a9426bf5f38112878eb2f99bfdf5ec17b5782b944e6be8177084327';
expect(sigToId(sig)).toEqual(expected);
});

View File

@ -1,18 +0,0 @@
import { ethers } from 'ethers';
import { SHA3 } from 'sha3';
export const sigToId = (sig: string) => {
// Prepend 0x
if (sig.slice(0, 2) !== '0x') {
sig = '0x' + sig;
}
// Create the ID
const hash = new SHA3(256);
const bytes = ethers.utils.arrayify(sig);
hash.update(Buffer.from(bytes));
const id = ethers.utils.hexlify(hash.digest());
// Remove 0x as core doesn't keep them in the API
return id.substring(2);
};

View File

@ -1,303 +0,0 @@
import type { VoteValue } from '../../__generated__/globalTypes';
import type { VegaKey } from '../../contexts/app-state/app-state-context';
import type { VOTE_VALUE_MAP } from '../../routes/governance/components/vote-details';
import { LocalStorage } from '@vegaprotocol/react-helpers';
import type { GenericErrorResponse } from './vega-wallet-types';
export const MINIMUM_WALLET_VERSION =
process.env['NX_SUPPORTED_WALLET_VERSION'];
export const DEFAULT_WALLET_URL = 'http://localhost:1789';
export const HOSTED_WALLET_URL = 'https://wallet.testnet.vega.xyz';
const TOKEN_STORAGE_KEY = 'vega_wallet_token';
const WALLET_URL_KEY = 'vega_wallet_url';
const KEY_STORAGE_KEY = 'vega_wallet_key';
const Endpoints = {
STATUS: 'status',
TOKEN: 'auth/token',
KEYS: 'keys',
COMMAND: 'command/sync',
VERSION: 'version',
};
export const Errors = {
NO_TOKEN: 'No token',
SERVICE_UNAVAILABLE: 'Wallet service unavailable',
SESSION_EXPIRED: 'Session expired',
INVALID_CREDENTIALS: 'Invalid credentials',
COMMAND_FAILED: 'Command failed',
INVALID_URL: 'Invalid wallet URL',
};
export interface DelegateSubmissionInput {
pubKey: string;
delegateSubmission: {
nodeId: string;
amount: string;
};
}
export interface UndelegateSubmissionInput {
pubKey: string;
undelegateSubmission: {
nodeId: string;
amount: string;
method: 'METHOD_NOW' | 'METHOD_AT_END_OF_EPOCH';
};
}
export interface VoteSubmissionInput {
pubKey: string;
voteSubmission: {
value: typeof VOTE_VALUE_MAP[VoteValue];
proposalId: string;
};
}
export interface WithdrawSubmissionInput {
pubKey: string;
withdrawSubmission: {
amount: string;
asset: string;
ext: {
erc20: {
receiverAddress: string;
};
};
};
}
export type CommandSyncInput =
| DelegateSubmissionInput
| UndelegateSubmissionInput
| VoteSubmissionInput
| WithdrawSubmissionInput;
export interface CommandSyncResponse {
inputData: string;
pubKey: string;
signature: {
algo: string;
value: string;
version: number;
};
version: number;
}
export interface IVegaWalletService {
url: string;
token: string;
getToken(params: {
wallet: string;
passphrase: string;
}): Promise<[string | undefined, string | undefined]>;
revokeToken(): Promise<[string | undefined, boolean]>;
getKeys(): Promise<[string | undefined, VegaKey[] | undefined]>;
}
export class VegaWalletService implements IVegaWalletService {
version: number;
url: string;
token: string;
key: string;
constructor() {
this.version = 1;
this.url = LocalStorage.getItem(WALLET_URL_KEY) || DEFAULT_WALLET_URL;
this.token = LocalStorage.getItem(TOKEN_STORAGE_KEY) || '';
this.key = LocalStorage.getItem(KEY_STORAGE_KEY) || '';
}
async getToken(params: {
wallet: string;
passphrase: string;
url: string;
}): Promise<[string | undefined, string | undefined]> {
const urlValid = this.validateUrl(params.url);
if (urlValid) {
this.setWalletUrl(params.url);
} else {
return [Errors.INVALID_URL, undefined];
}
try {
const res = await fetch(`${this.getUrl()}/${Endpoints.TOKEN}`, {
method: 'post',
body: JSON.stringify(params),
});
const json = await res.json();
if ('token' in json) {
this.setToken(json.token);
return [undefined, json.token];
} else {
return [Errors.INVALID_CREDENTIALS, undefined];
}
} catch (err) {
return this.handleServiceUnavailable();
}
}
async revokeToken(): Promise<[string | undefined, boolean]> {
if (!this.token) {
return [Errors.NO_TOKEN, false];
}
try {
const res = await fetch(`${this.getUrl()}/${Endpoints.TOKEN}`, {
method: 'delete',
headers: { authorization: `Bearer ${this.token}` },
});
const json = await res.json();
if (json.success) {
this.clearKey();
this.clearToken();
this.clearWalletUrl();
return [undefined, true];
} else {
return [undefined, false];
}
} catch (err) {
return this.handleServiceUnavailable(false);
}
}
async getKeys(): Promise<[string | undefined, VegaKey[] | undefined]> {
if (!this.token) {
return [Errors.NO_TOKEN, undefined];
}
try {
const res = await fetch(`${this.getUrl()}/${Endpoints.KEYS}`, {
headers: { authorization: `Bearer ${this.token}` },
});
const err = this.verifyResponse(res);
if (err) {
return [err, undefined];
}
const json = await res.json();
return [undefined, json.keys];
} catch (err) {
return this.handleServiceUnavailable();
}
}
async commandSync(
body: CommandSyncInput
): Promise<[string | undefined, CommandSyncResponse | undefined]> {
if (!this.token) {
return [Errors.NO_TOKEN, undefined];
}
try {
const res = await fetch(`${this.getUrl()}/${Endpoints.COMMAND}`, {
method: 'post',
body: JSON.stringify({
...body,
propagate: true,
}),
headers: { authorization: `Bearer ${this.token}` },
});
const err = this.verifyResponse(res);
if (err) {
return [err, undefined];
}
const json = await res.json();
if ('errors' in json) {
return [Errors.COMMAND_FAILED, undefined];
} else {
return [undefined, json];
}
} catch (err) {
return this.handleServiceUnavailable();
}
}
setKey(key: string) {
this.key = key;
LocalStorage.setItem(KEY_STORAGE_KEY, key);
}
private clearKey() {
this.key = '';
LocalStorage.removeItem(KEY_STORAGE_KEY);
}
private setToken(token: string) {
this.token = token;
LocalStorage.setItem(TOKEN_STORAGE_KEY, token);
}
private clearToken() {
this.token = '';
LocalStorage.removeItem(TOKEN_STORAGE_KEY);
}
private setWalletUrl(url: string) {
this.url = url;
LocalStorage.setItem(WALLET_URL_KEY, url);
}
private clearWalletUrl() {
this.url = DEFAULT_WALLET_URL;
LocalStorage.removeItem(WALLET_URL_KEY);
}
private getUrl() {
return `${this.url}/api/v${this.version}`;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private handleServiceUnavailable(returnVal?: boolean): [string, any] {
this.clearWalletUrl();
return [Errors.SERVICE_UNAVAILABLE, returnVal];
}
private validateUrl(url: string) {
try {
new URL(url);
} catch (err) {
return false;
}
return true;
}
/**
* Parses the response object to either return an error string or null if
* everything looks good. Clears token 403 response returned
*/
private verifyResponse(res: Response): string | null {
if (res.status === 403) {
this.clearToken();
return Errors.SESSION_EXPIRED;
} else if (!res.ok) {
return Errors.COMMAND_FAILED;
}
return null;
}
}
export function hasErrorProperty(obj: unknown): obj is GenericErrorResponse {
if (
(obj as GenericErrorResponse).error !== undefined &&
typeof (obj as GenericErrorResponse).error === 'string'
) {
return true;
}
return false;
}
export const vegaWalletService = new VegaWalletService();

View File

@ -1,3 +0,0 @@
export interface GenericErrorResponse {
error: string;
}

View File

@ -2,8 +2,6 @@ import { captureException, captureMessage } from '@sentry/minimal';
import * as React from 'react'; import * as React from 'react';
import { VoteValue } from '../../../../__generated__/globalTypes'; import { VoteValue } from '../../../../__generated__/globalTypes';
import type { VoteSubmissionInput } from '../../../../lib/vega-wallet/vega-wallet-service';
import { vegaWalletService } from '../../../../lib/vega-wallet/vega-wallet-service';
import { VOTE_VALUE_MAP } from './vote-types'; import { VOTE_VALUE_MAP } from './vote-types';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
@ -44,7 +42,7 @@ export function useUserVote(
yesVotes: Votes | null, yesVotes: Votes | null,
noVotes: Votes | null noVotes: Votes | null
) { ) {
const { keypair } = useVegaWallet(); const { keypair, sendTx } = useVegaWallet();
const yes = React.useMemo(() => yesVotes || [], [yesVotes]); const yes = React.useMemo(() => yesVotes || [], [yesVotes]);
const no = React.useMemo(() => noVotes || [], [noVotes]); const no = React.useMemo(() => noVotes || [], [noVotes]);
@ -98,19 +96,15 @@ export function useUserVote(
setVoteState(VoteState.Pending); setVoteState(VoteState.Pending);
try { try {
const variables: VoteSubmissionInput = { const variables = {
pubKey: keypair.pub, pubKey: keypair.pub,
propagate: true,
voteSubmission: { voteSubmission: {
value: VOTE_VALUE_MAP[value], value: VOTE_VALUE_MAP[value],
proposalId, proposalId,
}, },
}; };
const [err] = await vegaWalletService.commandSync(variables); await sendTx(variables);
if (err) {
setVoteState(VoteState.Failed);
captureException(err);
}
// Now await vote via poll in parent component // Now await vote via poll in parent component
} catch (err) { } catch (err) {

View File

@ -58,10 +58,12 @@ export const AssociatePage = ({
() => totalVestedBalance.plus(totalLockedBalance).isEqualTo(0), () => totalVestedBalance.plus(totalLockedBalance).isEqualTo(0),
[totalLockedBalance, totalVestedBalance] [totalLockedBalance, totalVestedBalance]
); );
const zeroVega = React.useMemo( const zeroVega = React.useMemo(
() => walletBalance.isEqualTo(0), () => walletBalance.isEqualTo(0),
[walletBalance] [walletBalance]
); );
React.useEffect(() => { React.useEffect(() => {
if (zeroVega && !zeroVesting) { if (zeroVega && !zeroVesting) {
setSelectedStakingMethod(StakingMethod.Contract); setSelectedStakingMethod(StakingMethod.Contract);

View File

@ -5,8 +5,8 @@ import { useTranslation } from 'react-i18next';
import { useAppState } from '../../contexts/app-state/app-state-context'; import { useAppState } from '../../contexts/app-state/app-state-context';
import { BigNumber } from '../../lib/bignumber'; import { BigNumber } from '../../lib/bignumber';
import { removeDecimal } from '../../lib/decimals'; import { removeDecimal } from '../../lib/decimals';
import { vegaWalletService } from '../../lib/vega-wallet/vega-wallet-service'; import { useVegaWallet } from '@vegaprotocol/wallet';
import type { UndelegateSubmissionInput } from '../../lib/vega-wallet/vega-wallet-service'; import type { UndelegateSubmissionBody } from '@vegaprotocol/vegawallet-service-api-client';
interface PendingStakeProps { interface PendingStakeProps {
pendingAmount: BigNumber; pendingAmount: BigNumber;
@ -27,28 +27,26 @@ export const PendingStake = ({
pubkey, pubkey,
}: PendingStakeProps) => { }: PendingStakeProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { sendTx } = useVegaWallet();
const { appState } = useAppState(); const { appState } = useAppState();
const [formState, setFormState] = React.useState(FormState.Default); const [formState, setFormState] = React.useState(FormState.Default);
const removeStakeNow = async () => { const removeStakeNow = async () => {
setFormState(FormState.Pending); setFormState(FormState.Pending);
const undelegateInput: UndelegateSubmissionInput = {
pubKey: pubkey,
undelegateSubmission: {
nodeId,
amount: removeDecimal(new BigNumber(pendingAmount), appState.decimals),
method: 'METHOD_NOW',
},
};
try { try {
const command = undelegateInput; const command: UndelegateSubmissionBody = {
const [err] = await vegaWalletService.commandSync(command); pubKey: pubkey,
propagate: true,
if (err) { undelegateSubmission: {
setFormState(FormState.Failure); nodeId,
Sentry.captureException(err); amount: removeDecimal(
} new BigNumber(pendingAmount),
appState.decimals
),
method: 'METHOD_NOW',
},
};
await sendTx(command);
} catch (err) { } catch (err) {
setFormState(FormState.Failure); setFormState(FormState.Failure);
Sentry.captureException(err); Sentry.captureException(err);

View File

@ -12,11 +12,6 @@ import { useNetworkParam } from '../../hooks/use-network-param';
import { useSearchParams } from '../../hooks/use-search-params'; import { useSearchParams } from '../../hooks/use-search-params';
import { BigNumber } from '../../lib/bignumber'; import { BigNumber } from '../../lib/bignumber';
import { addDecimal, removeDecimal } from '../../lib/decimals'; import { addDecimal, removeDecimal } from '../../lib/decimals';
import type {
DelegateSubmissionInput,
UndelegateSubmissionInput,
} from '../../lib/vega-wallet/vega-wallet-service';
import { vegaWalletService } from '../../lib/vega-wallet/vega-wallet-service';
import type { import type {
PartyDelegations, PartyDelegations,
PartyDelegationsVariables, PartyDelegationsVariables,
@ -25,6 +20,11 @@ import { StakeFailure } from './stake-failure';
import { StakePending } from './stake-pending'; import { StakePending } from './stake-pending';
import { StakeSuccess } from './stake-success'; import { StakeSuccess } from './stake-success';
import { Button, FormGroup } from '@vegaprotocol/ui-toolkit'; import { Button, FormGroup } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type {
DelegateSubmissionBody,
UndelegateSubmissionBody,
} from '@vegaprotocol/vegawallet-service-api-client';
export const PARTY_DELEGATIONS_QUERY = gql` export const PARTY_DELEGATIONS_QUERY = gql`
query PartyDelegations($partyId: ID!) { query PartyDelegations($partyId: ID!) {
@ -81,6 +81,7 @@ export const StakingForm = ({
const navigate = useNavigate(); const navigate = useNavigate();
const client = useApolloClient(); const client = useApolloClient();
const { appState } = useAppState(); const { appState } = useAppState();
const { sendTx } = useVegaWallet();
const [formState, setFormState] = React.useState(FormState.Default); const [formState, setFormState] = React.useState(FormState.Default);
const { t } = useTranslation(); const { t } = useTranslation();
const [action, setAction] = React.useState<StakeAction>(params.action); const [action, setAction] = React.useState<StakeAction>(params.action);
@ -114,15 +115,17 @@ export const StakingForm = ({
async function onSubmit() { async function onSubmit() {
setFormState(FormState.Pending); setFormState(FormState.Pending);
const delegateInput: DelegateSubmissionInput = { const delegateInput: DelegateSubmissionBody = {
pubKey: pubkey, pubKey: pubkey,
propagate: true,
delegateSubmission: { delegateSubmission: {
nodeId, nodeId,
amount: removeDecimal(new BigNumber(amount), appState.decimals), amount: removeDecimal(new BigNumber(amount), appState.decimals),
}, },
}; };
const undelegateInput: UndelegateSubmissionInput = { const undelegateInput: UndelegateSubmissionBody = {
pubKey: pubkey, pubKey: pubkey,
propagate: true,
undelegateSubmission: { undelegateSubmission: {
nodeId, nodeId,
amount: removeDecimal(new BigNumber(amount), appState.decimals), amount: removeDecimal(new BigNumber(amount), appState.decimals),
@ -134,12 +137,7 @@ export const StakingForm = ({
}; };
try { try {
const command = action === Actions.Add ? delegateInput : undelegateInput; const command = action === Actions.Add ? delegateInput : undelegateInput;
const [err] = await vegaWalletService.commandSync(command); await sendTx(command);
if (err) {
setFormState(FormState.Failure);
Sentry.captureException(err);
}
// await success via poll // await success via poll
} catch (err) { } catch (err) {

View File

@ -140,11 +140,53 @@ export interface WithdrawPage_party {
withdrawals: WithdrawPage_party_withdrawals[] | null; withdrawals: WithdrawPage_party_withdrawals[] | null;
} }
export interface WithdrawPage_assets_source_BuiltinAsset {
__typename: "BuiltinAsset";
}
export interface WithdrawPage_assets_source_ERC20 {
__typename: "ERC20";
/**
* The address of the erc20 contract
*/
contractAddress: string;
}
export type WithdrawPage_assets_source = WithdrawPage_assets_source_BuiltinAsset | WithdrawPage_assets_source_ERC20;
export interface WithdrawPage_assets {
__typename: "Asset";
/**
* The id of the asset
*/
id: string;
/**
* The symbol of the asset (e.g: GBP)
*/
symbol: string;
/**
* The full name of the asset (e.g: Great British Pound)
*/
name: string;
/**
* The precision of the asset
*/
decimals: number;
/**
* The origin source of the asset (e.g: an erc20 asset)
*/
source: WithdrawPage_assets_source;
}
export interface WithdrawPage { export interface WithdrawPage {
/** /**
* An entity that is trading on the VEGA network * An entity that is trading on the VEGA network
*/ */
party: WithdrawPage_party | null; party: WithdrawPage_party | null;
/**
* The list of all assets in use in the vega network
*/
assets: WithdrawPage_assets[] | null;
} }
export interface WithdrawPageVariables { export interface WithdrawPageVariables {

View File

@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { AccountType } from '../../__generated__/globalTypes'; import { AccountType } from '../../__generated__/globalTypes';
import { EthWalletContainer } from '../../components/eth-wallet-container';
import { Heading } from '../../components/heading'; import { Heading } from '../../components/heading';
import { SplashLoader } from '../../components/splash-loader'; import { SplashLoader } from '../../components/splash-loader';
import { VegaWalletContainer } from '../../components/vega-wallet-container'; import { VegaWalletContainer } from '../../components/vega-wallet-container';
@ -15,7 +14,7 @@ import type {
WithdrawPage, WithdrawPage,
WithdrawPageVariables, WithdrawPageVariables,
} from './__generated__/WithdrawPage'; } from './__generated__/WithdrawPage';
import { WithdrawForm } from './withdraw-form'; import { WithdrawManager } from '@vegaprotocol/withdraws';
const Withdraw = () => { const Withdraw = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -24,9 +23,11 @@ const Withdraw = () => {
<> <>
<Heading title={t('withdrawPageHeading')} /> <Heading title={t('withdrawPageHeading')} />
<p>{t('withdrawPageText')}</p> <p>{t('withdrawPageText')}</p>
<VegaWalletContainer> <div className="mb-24">
{(currVegaKey) => <WithdrawContainer currVegaKey={currVegaKey} />} <VegaWalletContainer>
</VegaWalletContainer> {(currVegaKey) => <WithdrawContainer currVegaKey={currVegaKey} />}
</VegaWalletContainer>
</div>
<Callout title={t('withdrawPageInfoCalloutTitle')}> <Callout title={t('withdrawPageInfoCalloutTitle')}>
<p className="mb-0">{t('withdrawPageInfoCalloutText')}</p> <p className="mb-0">{t('withdrawPageInfoCalloutText')}</p>
</Callout> </Callout>
@ -74,6 +75,17 @@ const WITHDRAW_PAGE_QUERY = gql`
} }
} }
} }
assets {
id
symbol
name
decimals
source {
... on ERC20 {
contractAddress
}
}
}
} }
`; `;
@ -123,27 +135,21 @@ export const WithdrawContainer = ({ currVegaKey }: WithdrawContainerProps) => {
return ( return (
<> <>
{hasPendingWithdrawals && ( {hasPendingWithdrawals && (
<Callout <div className="mb-24">
title={t('pendingWithdrawalsCalloutTitle')} <Callout
intent={Intent.Prompt} title={t('pendingWithdrawalsCalloutTitle')}
> intent={Intent.Prompt}
<p>{t('pendingWithdrawalsCalloutText')}</p> >
<p className="mb-0"> <p>{t('pendingWithdrawalsCalloutText')}</p>
<Link to={Routes.WITHDRAWALS}> <p className="mb-0">
{t('pendingWithdrawalsCalloutButton')} <Link to={Routes.WITHDRAWALS}>
</Link> {t('pendingWithdrawalsCalloutButton')}
</p> </Link>
</Callout> </p>
</Callout>
</div>
)} )}
<EthWalletContainer> <WithdrawManager assets={data.assets || []} accounts={accounts} />
{(connectedAddress) => (
<WithdrawForm
accounts={accounts}
currVegaKey={currVegaKey}
connectedAddress={connectedAddress}
/>
)}
</EthWalletContainer>
</> </>
); );
}; };

View File

@ -1,153 +0,0 @@
import { Callout, FormGroup, Intent, Select } from '@vegaprotocol/ui-toolkit';
import { ethers } from 'ethers';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router';
import { Loader } from '../../components/loader';
import { StatefulButton } from '../../components/stateful-button';
import { AmountInput } from '../../components/token-input';
import type { VegaKeyExtended } from '@vegaprotocol/wallet';
import {
Status as WithdrawStatus,
useCreateWithdrawal,
} from '../../hooks/use-create-withdrawal';
import { BigNumber } from '../../lib/bignumber';
import { removeDecimal } from '../../lib/decimals';
import { Routes } from '../router-config';
import type { WithdrawPage_party_accounts } from './__generated__/WithdrawPage';
import { EthAddressInput } from './eth-address-input';
interface WithdrawFormProps {
accounts: WithdrawPage_party_accounts[];
currVegaKey: VegaKeyExtended;
connectedAddress: string;
}
export const WithdrawForm = ({
accounts,
currVegaKey,
connectedAddress,
}: WithdrawFormProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const [amountStr, setAmount] = React.useState('');
const [account, setAccount] = React.useState(accounts[0]);
const [status, submit] = useCreateWithdrawal(currVegaKey.pub);
const [destinationAddress, setDestinationAddress] =
React.useState(connectedAddress);
const amount = React.useMemo(
() => new BigNumber(amountStr || 0),
[amountStr]
);
const maximum = React.useMemo(() => {
if (account) {
return new BigNumber(account.balanceFormatted);
}
return new BigNumber(0);
}, [account]);
const valid = React.useMemo(() => {
if (
!destinationAddress ||
amount.isLessThanOrEqualTo(0) ||
amount.isGreaterThan(maximum)
) {
return false;
}
return true;
}, [destinationAddress, amount, maximum]);
const addressValid = React.useMemo(() => {
return ethers.utils.isAddress(destinationAddress);
}, [destinationAddress]);
// Navigate to complete withdrawals page once withdrawal
// creation is complete
React.useEffect(() => {
if (status === WithdrawStatus.Success) {
navigate(Routes.WITHDRAWALS);
}
}, [status, navigate]);
return (
<form
className="my-40"
onSubmit={(e) => {
e.preventDefault();
if (!valid || !addressValid) return;
submit(
removeDecimal(amount, account.asset.decimals),
account.asset.id,
destinationAddress
);
}}
>
<FormGroup label={t('withdrawFormAssetLabel')} labelFor="asset">
{accounts.length ? (
<Select
name="asset"
id="asset"
onChange={(e) => {
const account = accounts.find(
(a) => a.asset.id === e.currentTarget.value
);
if (!account) throw new Error('No account');
setAccount(account);
}}
>
{accounts.map((a) => (
<option value={a.asset.id}>
{a.asset.symbol} ({a.balanceFormatted})
</option>
))}
</Select>
) : (
<p className="text-white-60">{t('withdrawFormNoAsset')}</p>
)}
</FormGroup>
<div className="mb-24">
<Callout
title={t('withdrawPreparedWarningHeading')}
intent={Intent.Warning}
>
<p>{t('withdrawPreparedWarningText1')}</p>
<p className="mb-0">{t('withdrawPreparedWarningText2')}</p>
</Callout>
</div>
<EthAddressInput
onChange={setDestinationAddress}
address={destinationAddress}
connectedAddress={connectedAddress}
isValid={addressValid}
/>
<FormGroup label={t('withdrawFormAmountLabel')} labelFor="amount">
<AmountInput
amount={amountStr}
setAmount={setAmount}
maximum={maximum}
currency={'VEGA'}
/>
</FormGroup>
<StatefulButton
type="submit"
disabled={!addressValid || !valid || status === WithdrawStatus.Pending}
>
{status === WithdrawStatus.Pending ? (
<>
<Loader />
<span>{t('withdrawFormSubmitButtonPending')}</span>
</>
) : (
t('withdrawFormSubmitButtonIdle', {
amount: amountStr,
symbol: account?.asset.symbol,
})
)}
</StatefulButton>
</form>
);
};

View File

@ -1,105 +0,0 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { WithdrawalStatus } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: WithdrawalsPage
// ====================================================
export interface WithdrawalsPage_party_withdrawals_asset {
__typename: "Asset";
/**
* The id of the asset
*/
id: string;
/**
* The symbol of the asset (e.g: GBP)
*/
symbol: string;
/**
* The precision of the asset
*/
decimals: number;
}
export interface WithdrawalsPage_party_withdrawals_party {
__typename: "Party";
/**
* Party identifier
*/
id: string;
}
export interface WithdrawalsPage_party_withdrawals_details {
__typename: "Erc20WithdrawalDetails";
/**
* The ethereum address of the receiver of the asset funds
*/
receiverAddress: string;
}
export interface WithdrawalsPage_party_withdrawals {
__typename: "Withdrawal";
/**
* The Vega internal id of the withdrawal
*/
id: string;
/**
* The amount to be withdrawn
*/
amount: string;
/**
* The asset to be withdrawn
*/
asset: WithdrawalsPage_party_withdrawals_asset;
/**
* The PartyID initiating the withdrawal
*/
party: WithdrawalsPage_party_withdrawals_party;
/**
* The current status of the withdrawal
*/
status: WithdrawalStatus;
/**
* RFC3339Nano time at which the withdrawal was created
*/
createdTimestamp: string;
/**
* RFC3339Nano time at which the withdrawal was finalized
*/
withdrawnTimestamp: string | null;
/**
* Hash of the transaction on the foreign chain
*/
txHash: string | null;
/**
* Foreign chain specific details about the withdrawal
*/
details: WithdrawalsPage_party_withdrawals_details | null;
}
export interface WithdrawalsPage_party {
__typename: "Party";
/**
* Party identifier
*/
id: string;
/**
* The list of all withdrawals initiated by the party
*/
withdrawals: WithdrawalsPage_party_withdrawals[] | null;
}
export interface WithdrawalsPage {
/**
* An entity that is trading on the VEGA network
*/
party: WithdrawalsPage_party | null;
}
export interface WithdrawalsPageVariables {
partyId: string;
}

View File

@ -1,6 +1,4 @@
import { gql, useQuery } from '@apollo/client'; import { Splash } from '@vegaprotocol/ui-toolkit';
import { Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit';
import { useWeb3React } from '@web3-react/core';
import { format } from 'date-fns'; import { format } from 'date-fns';
import orderBy from 'lodash/orderBy'; import orderBy from 'lodash/orderBy';
import React from 'react'; import React from 'react';
@ -10,23 +8,16 @@ import { EtherscanLink } from '@vegaprotocol/ui-toolkit';
import { Heading } from '../../components/heading'; import { Heading } from '../../components/heading';
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit'; import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
import { SplashLoader } from '../../components/splash-loader'; import { SplashLoader } from '../../components/splash-loader';
import { TransactionButton } from '../../components/transaction-button';
import { VegaWalletContainer } from '../../components/vega-wallet-container'; import { VegaWalletContainer } from '../../components/vega-wallet-container';
import type { VegaKeyExtended } from '@vegaprotocol/wallet'; import type { VegaKeyExtended } from '@vegaprotocol/wallet';
import { useContracts } from '../../contexts/contracts/contracts-context';
import { TxState } from '../../hooks/transaction-reducer';
import { usePollERC20Approval } from '../../hooks/use-erc-poll20-approval';
import { useRefreshBalances } from '../../hooks/use-refresh-balances';
import { useTransaction } from '../../hooks/use-transaction';
import { BigNumber } from '../../lib/bignumber'; import { BigNumber } from '../../lib/bignumber';
import { DATE_FORMAT_DETAILED } from '../../lib/date-formats'; import { DATE_FORMAT_DETAILED } from '../../lib/date-formats';
import { addDecimal } from '../../lib/decimals'; import { addDecimal } from '../../lib/decimals';
import { truncateMiddle } from '../../lib/truncate-middle'; import { truncateMiddle } from '../../lib/truncate-middle';
import type { import type { Withdrawals_party_withdrawals } from '@vegaprotocol/withdraws';
WithdrawalsPage, import { useCompleteWithdraw, useWithdrawals } from '@vegaprotocol/withdraws';
WithdrawalsPage_party_withdrawals, import { TransactionDialog } from '@vegaprotocol/web3';
WithdrawalsPageVariables, import { WithdrawalStatus } from '../../__generated__/globalTypes';
} from './__generated__/WithdrawalsPage';
const Withdrawals = () => { const Withdrawals = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -34,13 +25,6 @@ const Withdrawals = () => {
return ( return (
<> <>
<Heading title={t('withdrawalsTitle')} /> <Heading title={t('withdrawalsTitle')} />
<p>{t('withdrawalsText')}</p>
<Callout
title={t('withdrawalsPreparedWarningHeading')}
intent={Intent.Warning}
>
<p>{t('withdrawalsPreparedWarningText')}</p>
</Callout>
<VegaWalletContainer> <VegaWalletContainer>
{(currVegaKey) => ( {(currVegaKey) => (
<WithdrawPendingContainer currVegaKey={currVegaKey} /> <WithdrawPendingContainer currVegaKey={currVegaKey} />
@ -54,51 +38,12 @@ interface WithdrawPendingContainerProps {
currVegaKey: VegaKeyExtended; currVegaKey: VegaKeyExtended;
} }
const WITHDRAWALS_PAGE_QUERY = gql`
query WithdrawalsPage($partyId: ID!) {
party(id: $partyId) {
id
withdrawals {
id
amount
asset {
id
symbol
decimals
}
party {
id
}
status
createdTimestamp
withdrawnTimestamp
txHash
details {
... on Erc20WithdrawalDetails {
receiverAddress
}
}
}
}
}
`;
const WithdrawPendingContainer = ({ const WithdrawPendingContainer = ({
currVegaKey, currVegaKey,
}: WithdrawPendingContainerProps) => { }: WithdrawPendingContainerProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { account } = useWeb3React(); const { transaction, submit } = useCompleteWithdraw();
const refreshBalances = useRefreshBalances(account || ''); const { data, loading, error } = useWithdrawals();
const { data, loading, error, refetch } = useQuery<
WithdrawalsPage,
WithdrawalsPageVariables
>(WITHDRAWALS_PAGE_QUERY, {
variables: { partyId: currVegaKey.pub },
// This must be network-only because you are navigated to this page automatically after the withdrawal is created,
// if you have already visited this page the query result is cached with 0 withdrawals, so we need to refetch every
// time to ensure the withdrawal is shown immediately
fetchPolicy: 'network-only',
});
const withdrawals = React.useMemo(() => { const withdrawals = React.useMemo(() => {
if (!data?.party?.withdrawals?.length) return []; if (!data?.party?.withdrawals?.length) return [];
@ -132,61 +77,60 @@ const WithdrawPendingContainer = ({
} }
return ( return (
<ul role="list"> <>
{withdrawals.map((w) => ( <h2>{t('withdrawalsPreparedWarningHeading')}</h2>
<li key={w.id} className="mb-28"> <p>{t('withdrawalsText')}</p>
<Withdrawal <p>{t('withdrawalsPreparedWarningText')}</p>
withdrawal={w} <ul role="list">
refetchWithdrawals={refetch} {withdrawals.map((w) => (
refetchBalances={refreshBalances} <li key={w.id}>
/> <Withdrawal withdrawal={w} complete={submit} />
</li> </li>
))} ))}
</ul> </ul>
<TransactionDialog name="withdraw" {...transaction} />
</>
); );
}; };
interface WithdrawalProps { interface WithdrawalProps {
withdrawal: WithdrawalsPage_party_withdrawals; withdrawal: Withdrawals_party_withdrawals;
refetchWithdrawals: () => void; complete: (withdrawalId: string) => void;
refetchBalances: () => void;
} }
export const Withdrawal = ({ export const Withdrawal = ({ withdrawal, complete }: WithdrawalProps) => {
withdrawal,
refetchWithdrawals,
refetchBalances,
}: WithdrawalProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const erc20Approval = usePollERC20Approval(withdrawal.id);
const { erc20Bridge } = useContracts(); const renderStatus = ({
const { state, perform, reset } = useTransaction(() => { id,
if (!erc20Approval) { status,
throw new Error('Withdraw needs approval object'); txHash,
} pendingOnForeignChain,
if (!withdrawal.details?.receiverAddress) { }: Withdrawals_party_withdrawals) => {
throw new Error('Missing receiver address'); if (pendingOnForeignChain) {
return t('Pending');
} }
return erc20Bridge.withdraw({ if (status === WithdrawalStatus.Finalized) {
assetSource: erc20Approval.assetSource, if (txHash) {
amount: erc20Approval.amount, return t('Complete');
nonce: erc20Approval.nonce, } else {
signatures: erc20Approval.signatures, return (
// TODO: switch when targetAddress is populated and deployed to mainnet data.erc20WithdrawalApproval.targetAddress, <>
targetAddress: withdrawal.details.receiverAddress, {t('Incomplete')}{' '}
}); <button
}); className="text-white underline"
onClick={() => complete(id)}
React.useEffect(() => { >
// Once complete we need to refetch the withdrawals so that pending withdrawal {t('withdrawalsCompleteButton')}
// is updated to have a txHash indicating it is complete. Updating your account balance </button>
// is already handled by the query in the VegaWallet that polls </>
if (state.txState === TxState.Complete) { );
refetchWithdrawals(); }
refetchBalances();
} }
}, [state, refetchWithdrawals, refetchBalances]);
return status;
};
return ( return (
<div> <div>
@ -201,10 +145,6 @@ export const Withdrawal = ({
{withdrawal.asset.symbol} {withdrawal.asset.symbol}
</span> </span>
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow>
{t('from')}
<span>{truncateMiddle(withdrawal.party.id)}</span>
</KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow>
{t('toEthereum')} {t('toEthereum')}
<span> <span>
@ -226,27 +166,23 @@ export const Withdrawal = ({
</span> </span>
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow>
{t('Signature')} {t('withdrawalTransaction', { foreignChain: 'Ethereum' })}
<span title={erc20Approval?.signatures}> <span>
{!erc20Approval?.signatures {withdrawal.txHash ? (
? t('Loading') <EtherscanLink
: truncateMiddle(erc20Approval.signatures)} tx={withdrawal.txHash}
text={truncateMiddle(withdrawal.txHash)}
/>
) : (
'-'
)}
</span> </span>
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow>
{t('status')}
{renderStatus(withdrawal)}
</KeyValueTableRow>
</KeyValueTable> </KeyValueTable>
<TransactionButton
text={
!erc20Approval
? t('withdrawalsPreparingButton')
: t('withdrawalsCompleteButton')
}
transactionState={state}
forceTxState={withdrawal.txHash ? TxState.Complete : undefined}
forceTxHash={withdrawal.txHash}
disabled={!erc20Approval}
start={perform}
reset={reset}
/>
</div> </div>
); );
}; };

View File

@ -8,10 +8,12 @@ Feature: Markets page
Scenario: Select active market Scenario: Select active market
Given I am on the markets page Given I am on the markets page
And I can view markets
When I click on "Active" mocked market When I click on "Active" mocked market
Then trading page for "active" market is displayed Then trading page for "active" market is displayed
Scenario: Select suspended market Scenario: Select suspended market
Given I am on the markets page Given I am on the markets page
And I can view markets
When I click on "Suspended" mocked market When I click on "Suspended" mocked market
Then trading page for "suspended" market is displayed Then trading page for "suspended" market is displayed

View File

@ -10,6 +10,9 @@ export default class MarketPage extends BasePage {
marketStateColId = 'data'; marketStateColId = 'data';
validateMarketsAreDisplayed() { validateMarketsAreDisplayed() {
// We need this to ensure that ag-grid is fully rendered before asserting
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000);
cy.get('.ag-root-wrapper').should('be.visible'); cy.get('.ag-root-wrapper').should('be.visible');
} }

View File

@ -15,21 +15,19 @@ const mockMarkets = () => {
}); });
}; };
beforeEach(() => {
mockMarkets();
});
Then('I navigate to markets page', () => { Then('I navigate to markets page', () => {
mockMarkets();
marketsPage.navigateToMarkets(); marketsPage.navigateToMarkets();
cy.wait('@Markets');
}); });
Given('I am on the markets page', () => { Given('I am on the markets page', () => {
mockMarkets();
cy.visit('/markets'); cy.visit('/markets');
cy.wait('@Markets'); cy.wait('@Markets');
}); });
Then('I can view markets', () => { Then('I can view markets', () => {
cy.wait('@Markets');
marketsPage.validateMarketsAreDisplayed(); marketsPage.validateMarketsAreDisplayed();
}); });

View File

@ -1,18 +0,0 @@
import type { AssetFields } from './__generated__/AssetFields';
export interface ERC20Asset extends AssetFields {
source: {
__typename: 'ERC20';
contractAddress: string;
};
}
type UnknownAsset = Pick<AssetFields, '__typename' | 'source'>;
// Type guard to ensure an asset is an ERC20 token
export const isERC20Asset = (asset: UnknownAsset): asset is ERC20Asset => {
if (asset.source.__typename === 'ERC20') {
return true;
}
return false;
};

View File

@ -6,7 +6,6 @@ import { DepositManager } from '@vegaprotocol/deposits';
import { t } from '@vegaprotocol/react-helpers'; import { t } from '@vegaprotocol/react-helpers';
import { Splash } from '@vegaprotocol/ui-toolkit'; import { Splash } from '@vegaprotocol/ui-toolkit';
import { ASSET_FRAGMENT } from '../../../lib/query-fragments'; import { ASSET_FRAGMENT } from '../../../lib/query-fragments';
import { isERC20Asset } from '../../../lib/assets';
const DEPOSIT_PAGE_QUERY = gql` const DEPOSIT_PAGE_QUERY = gql`
${ASSET_FRAGMENT} ${ASSET_FRAGMENT}
@ -45,7 +44,7 @@ export const DepositContainer = ({
<DepositManager <DepositManager
bridgeAddress={ethereumConfig.collateral_bridge_contract.address} bridgeAddress={ethereumConfig.collateral_bridge_contract.address}
requiredConfirmations={ethereumConfig.confirmations} requiredConfirmations={ethereumConfig.confirmations}
assets={data.assets.filter(isERC20Asset)} assets={data.assets}
initialAssetId={assetId} initialAssetId={assetId}
/> />
); );

View File

@ -6,7 +6,6 @@ import { WithdrawManager } from '@vegaprotocol/withdraws';
import { ASSET_FRAGMENT } from '../../../lib/query-fragments'; import { ASSET_FRAGMENT } from '../../../lib/query-fragments';
import Link from 'next/link'; import Link from 'next/link';
import { PageQueryContainer } from '../../../components/page-query-container'; import { PageQueryContainer } from '../../../components/page-query-container';
import { isERC20Asset } from '../../../lib/assets';
import type { import type {
WithdrawPageQuery, WithdrawPageQuery,
WithdrawPageQueryVariables, WithdrawPageQueryVariables,
@ -85,7 +84,7 @@ export const WithdrawPageContainer = ({
</p> </p>
) : null} ) : null}
<WithdrawManager <WithdrawManager
assets={data.assets.filter(isERC20Asset)} assets={data.assets}
accounts={data.party?.accounts || []} accounts={data.party?.accounts || []}
initialAssetId={assetId} initialAssetId={assetId}
/> />

View File

@ -82,8 +82,8 @@ export const DepositForm = ({
}); });
const onDeposit = async (fields: FormFields) => { const onDeposit = async (fields: FormFields) => {
if (!selectedAsset) { if (!selectedAsset || selectedAsset.source.__typename !== 'ERC20') {
throw new Error('Asset not selected'); throw new Error('Invalid asset');
} }
submitDeposit({ submitDeposit({
@ -153,11 +153,13 @@ export const DepositForm = ({
<FormGroup label={t('Asset')} labelFor="asset" className="relative"> <FormGroup label={t('Asset')} labelFor="asset" className="relative">
<Select {...register('asset', { validate: { required } })} id="asset"> <Select {...register('asset', { validate: { required } })} id="asset">
<option value="">{t('Please select')}</option> <option value="">{t('Please select')}</option>
{assets.map((a) => ( {assets
<option key={a.id} value={a.id}> .filter((a) => a.source.__typename === 'ERC20')
{a.name} .map((a) => (
</option> <option key={a.id} value={a.id}>
))} {a.name}
</option>
))}
</Select> </Select>
{errors.asset?.message && ( {errors.asset?.message && (
<InputError intent="danger" className="mt-4" forInput="asset"> <InputError intent="danger" className="mt-4" forInput="asset">

View File

@ -10,16 +10,23 @@ import { useSubmitFaucet } from './use-submit-faucet';
import { EthTxStatus, TransactionDialog } from '@vegaprotocol/web3'; import { EthTxStatus, TransactionDialog } from '@vegaprotocol/web3';
import { useTokenContract, useBridgeContract } from '@vegaprotocol/web3'; import { useTokenContract, useBridgeContract } from '@vegaprotocol/web3';
interface ERC20AssetSource {
__typename: 'ERC20';
contractAddress: string;
}
interface BuiltinAssetSource {
__typename: 'BuiltinAsset';
}
type AssetSource = ERC20AssetSource | BuiltinAssetSource;
export interface Asset { export interface Asset {
__typename: 'Asset'; __typename: 'Asset';
id: string; id: string;
symbol: string; symbol: string;
name: string; name: string;
decimals: number; decimals: number;
source: { source: AssetSource;
__typename: 'ERC20';
contractAddress: string;
};
} }
interface DepositManagerProps { interface DepositManagerProps {
@ -44,7 +51,9 @@ export const DepositManager = ({
}, [assets, assetId]); }, [assets, assetId]);
const tokenContract = useTokenContract( const tokenContract = useTokenContract(
asset?.source.contractAddress, asset?.source.__typename === 'ERC20'
? asset.source.contractAddress
: undefined,
process.env['NX_VEGA_ENV'] !== 'MAINNET' process.env['NX_VEGA_ENV'] !== 'MAINNET'
); );
const bridgeContract = useBridgeContract(); const bridgeContract = useBridgeContract();

View File

@ -14,7 +14,7 @@ export const useGetDepositLimits = (
asset?: Asset asset?: Asset
): Limits | null => { ): Limits | null => {
const getLimits = useCallback(async () => { const getLimits = useCallback(async () => {
if (!contract || !asset) { if (!contract || !asset || asset.source.__typename !== 'ERC20') {
return; return;
} }

View File

@ -1,23 +1,10 @@
import { ethers } from 'ethers'; import { ethers } from 'ethers';
import { SHA3 } from 'sha3'; import { sha3_256 } from 'js-sha3';
import { remove0x } from './remove-0x';
/** /**
* This function creates an ID in the same way that core does on the backend. This way we * This function creates an ID in the same way that core does on the backend. This way we
* Can match up the newly created order with incoming orders via a subscription * Can match up the newly created order with incoming orders via a subscription
*/ */
export const determineId = (sig: string) => { export const determineId = (sig: string) => {
// Prepend 0x return sha3_256(ethers.utils.arrayify('0x' + sig));
if (sig.slice(0, 2) !== '0x') {
sig = '0x' + sig;
}
// Create the ID
const hash = new SHA3(256);
const bytes = ethers.utils.arrayify(sig);
hash.update(Buffer.from(bytes));
const id = ethers.utils.hexlify(hash.digest());
// Remove 0x as core doesn't keep them in the API
return remove0x(id);
}; };

View File

@ -1,9 +1,8 @@
import type { import type {
VegaKey, VegaKey,
TransactionResponse, TransactionResponse,
OrderSubmissionBody,
WithdrawSubmissionBody,
} from '@vegaprotocol/vegawallet-service-api-client'; } from '@vegaprotocol/vegawallet-service-api-client';
import type { TransactionSubmission } from '../types';
export { RestConnector } from './rest-connector'; export { RestConnector } from './rest-connector';
type ErrorResponse = type ErrorResponse =
@ -26,6 +25,6 @@ export interface VegaConnector {
/** Send a TX to the network. Only support order submission for now */ /** Send a TX to the network. Only support order submission for now */
sendTx: ( sendTx: (
body: OrderSubmissionBody | WithdrawSubmissionBody body: TransactionSubmission
) => Promise<TransactionResponse | ErrorResponse>; ) => Promise<TransactionResponse | ErrorResponse>;
} }

View File

@ -1,8 +1,4 @@
import type { import type { Configuration } from '@vegaprotocol/vegawallet-service-api-client';
Configuration,
OrderSubmissionBody,
WithdrawSubmissionBody,
} from '@vegaprotocol/vegawallet-service-api-client';
import { import {
createConfiguration, createConfiguration,
DefaultApi, DefaultApi,
@ -10,6 +6,7 @@ import {
import { LocalStorage } from '@vegaprotocol/react-helpers'; import { LocalStorage } from '@vegaprotocol/react-helpers';
import { WALLET_CONFIG } from '../storage-keys'; import { WALLET_CONFIG } from '../storage-keys';
import type { VegaConnector } from '.'; import type { VegaConnector } from '.';
import type { TransactionSubmission } from '../types';
// Perhaps there should be a default ConnectorConfig that others can extend off. Do all connectors // Perhaps there should be a default ConnectorConfig that others can extend off. Do all connectors
// need to use local storage, I don't think so... // need to use local storage, I don't think so...
@ -88,7 +85,7 @@ export class RestConnector implements VegaConnector {
} }
} }
async sendTx(body: OrderSubmissionBody | WithdrawSubmissionBody) { async sendTx(body: TransactionSubmission) {
try { try {
return await this.service.commandSyncPost(body); return await this.service.commandSyncPost(body);
} catch (err) { } catch (err) {

View File

@ -5,10 +5,7 @@ import type { VegaKeyExtended, VegaWalletContextShape } from '.';
import type { VegaConnector } from './connectors'; import type { VegaConnector } from './connectors';
import { VegaWalletContext } from './context'; import { VegaWalletContext } from './context';
import { WALLET_KEY } from './storage-keys'; import { WALLET_KEY } from './storage-keys';
import type { import type { TransactionSubmission } from './types';
OrderSubmissionBody,
WithdrawSubmissionBody,
} from '@vegaprotocol/vegawallet-service-api-client';
interface VegaWalletProviderProps { interface VegaWalletProviderProps {
children: ReactNode; children: ReactNode;
@ -72,16 +69,13 @@ export const VegaWalletProvider = ({ children }: VegaWalletProviderProps) => {
} }
}, []); }, []);
const sendTx = useCallback( const sendTx = useCallback((body: TransactionSubmission) => {
(body: OrderSubmissionBody | WithdrawSubmissionBody) => { if (!connector.current) {
if (!connector.current) { return null;
return null; }
}
return connector.current.sendTx(body); return connector.current.sendTx(body);
}, }, []);
[]
);
// Current selected keypair derived from publicKey state // Current selected keypair derived from publicKey state
const keypair = useMemo(() => { const keypair = useMemo(() => {

View File

@ -1,5 +1,8 @@
import type { import type {
DelegateSubmissionBody,
OrderSubmissionBody, OrderSubmissionBody,
UndelegateSubmissionBody,
VoteSubmissionBody,
WithdrawSubmissionBody, WithdrawSubmissionBody,
} from '@vegaprotocol/vegawallet-service-api-client'; } from '@vegaprotocol/vegawallet-service-api-client';
@ -25,4 +28,7 @@ export enum OrderTimeInForce {
// Will make Transaction a union type as other transactions are added // Will make Transaction a union type as other transactions are added
export type TransactionSubmission = export type TransactionSubmission =
| OrderSubmissionBody | OrderSubmissionBody
| WithdrawSubmissionBody; | WithdrawSubmissionBody
| VoteSubmissionBody
| DelegateSubmissionBody
| UndelegateSubmissionBody;

View File

@ -4,3 +4,4 @@ export * from './lib/withdrawals-table';
export * from './lib/use-complete-withdraw'; export * from './lib/use-complete-withdraw';
export * from './lib/use-withdraw'; export * from './lib/use-withdraw';
export * from './lib/use-withdrawals'; export * from './lib/use-withdrawals';
export * from './lib/__generated__/Withdrawals';

View File

@ -11,6 +11,7 @@ export const generateAsset = (override?: PartialDeep<Asset>) => {
name: 'asset-name', name: 'asset-name',
decimals: 5, decimals: 5,
source: { source: {
__typename: 'ERC20',
contractAddress: 'contract-address', contractAddress: 'contract-address',
}, },
}; };

View File

@ -1,13 +1,22 @@
import type { AccountType } from '@vegaprotocol/types'; import type { AccountType } from '@vegaprotocol/types';
interface ERC20AssetSource {
__typename: 'ERC20';
contractAddress: string;
}
interface BuiltinAssetSource {
__typename: 'BuiltinAsset';
}
type AssetSource = ERC20AssetSource | BuiltinAssetSource;
export interface Asset { export interface Asset {
id: string; id: string;
symbol: string; symbol: string;
name: string; name: string;
decimals: number; decimals: number;
source: { source: AssetSource;
contractAddress: string;
};
} }
export interface Account { export interface Account {

View File

@ -90,11 +90,13 @@ export const WithdrawForm = ({
id="asset" id="asset"
> >
<option value="">{t('Please select')}</option> <option value="">{t('Please select')}</option>
{assets.map((a) => ( {assets
<option key={a.id} value={a.id}> .filter((a) => a.source.__typename === 'ERC20')
{a.name} .map((a) => (
</option> <option key={a.id} value={a.id}>
))} {a.name}
</option>
))}
</Select> </Select>
)} )}
/> />

View File

@ -29,7 +29,7 @@
"@sentry/react": "^6.19.2", "@sentry/react": "^6.19.2",
"@sentry/tracing": "^6.19.2", "@sentry/tracing": "^6.19.2",
"@vegaprotocol/smart-contracts-sdk": "^1.6.0", "@vegaprotocol/smart-contracts-sdk": "^1.6.0",
"@vegaprotocol/vegawallet-service-api-client": "^0.4.9", "@vegaprotocol/vegawallet-service-api-client": "^0.4.11",
"@walletconnect/ethereum-provider": "^1.7.5", "@walletconnect/ethereum-provider": "^1.7.5",
"@web3-react/core": "8.0.20-beta.0", "@web3-react/core": "8.0.20-beta.0",
"@web3-react/metamask": "8.0.16-beta.0", "@web3-react/metamask": "8.0.16-beta.0",
@ -52,6 +52,7 @@
"i18next": "^20.3.5", "i18next": "^20.3.5",
"i18next-browser-languagedetector": "^6.1.2", "i18next-browser-languagedetector": "^6.1.2",
"immer": "^9.0.12", "immer": "^9.0.12",
"js-sha3": "^0.8.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"next": "^12.0.7", "next": "^12.0.7",
"pennant": "0.4.8", "pennant": "0.4.8",
@ -69,7 +70,6 @@
"react-window-infinite-loader": "^1.0.7", "react-window-infinite-loader": "^1.0.7",
"recharts": "^2.1.2", "recharts": "^2.1.2",
"regenerator-runtime": "0.13.7", "regenerator-runtime": "0.13.7",
"sha3": "^2.1.4",
"tailwindcss": "^3.0.23", "tailwindcss": "^3.0.23",
"tslib": "^2.0.0", "tslib": "^2.0.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",

View File

@ -6205,10 +6205,10 @@
ethers "^5.5.2" ethers "^5.5.2"
lodash "^4.17.21" lodash "^4.17.21"
"@vegaprotocol/vegawallet-service-api-client@^0.4.9": "@vegaprotocol/vegawallet-service-api-client@^0.4.11":
version "0.4.9" version "0.4.11"
resolved "https://registry.yarnpkg.com/@vegaprotocol/vegawallet-service-api-client/-/vegawallet-service-api-client-0.4.9.tgz#9c2566407e9c86248f2c170b1f495afdcb347d87" resolved "https://registry.yarnpkg.com/@vegaprotocol/vegawallet-service-api-client/-/vegawallet-service-api-client-0.4.11.tgz#41a623afc9957dcf8b5425f74280ba7861e92b74"
integrity sha512-zqppuVu3VrsHpdcNBvu6G+qMMt0CBXi5wHBb/0ryiXdXD9dmQci5Qz8zoGx5syOupFMdUDNstmGOaFWbBOuvbg== integrity sha512-yiodc3YFWG+RGG+wjpTjYmNAECP/Nv244mVu8IGVtj8LZo02KC/LpNCgmMhGaK4ZcqVtxHv9t7OUCSEWZhSgOg==
dependencies: dependencies:
es6-promise "^4.2.4" es6-promise "^4.2.4"
url-parse "^1.4.3" url-parse "^1.4.3"
@ -8438,14 +8438,6 @@ buffer@5.6.0:
base64-js "^1.0.2" base64-js "^1.0.2"
ieee754 "^1.1.4" ieee754 "^1.1.4"
buffer@6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.2.1"
buffer@^4.3.0: buffer@^4.3.0:
version "4.9.2" version "4.9.2"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
@ -13304,7 +13296,7 @@ identity-obj-proxy@3.0.0:
dependencies: dependencies:
harmony-reflect "^1.4.6" harmony-reflect "^1.4.6"
ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1: ieee754@^1.1.13, ieee754@^1.1.4:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
@ -14693,7 +14685,7 @@ jest@27.2.3:
import-local "^3.0.2" import-local "^3.0.2"
jest-cli "^27.2.3" jest-cli "^27.2.3"
js-sha3@0.8.0: js-sha3@0.8.0, js-sha3@^0.8.0:
version "0.8.0" version "0.8.0"
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==
@ -19310,13 +19302,6 @@ sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8, sha.js@~2.4.4:
inherits "^2.0.1" inherits "^2.0.1"
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
sha3@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/sha3/-/sha3-2.1.4.tgz#000fac0fe7c2feac1f48a25e7a31b52a6492cc8f"
integrity sha512-S8cNxbyb0UGUM2VhRD4Poe5N58gJnJsLJ5vC7FYWGUmGhcsj4++WaIOBFVDxlG0W3To6xBuiRh+i0Qp2oNCOtg==
dependencies:
buffer "6.0.3"
shallow-clone@^3.0.0: shallow-clone@^3.0.0:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"