feat(wallet): metamask snaps (#4621)

Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
This commit is contained in:
Art 2023-08-29 03:51:58 +02:00 committed by GitHub
parent 4fe81cc4aa
commit 5c18c898b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 562 additions and 158 deletions

View File

@ -29,3 +29,4 @@ LC_ALL="en_US.UTF-8"
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true

View File

@ -30,3 +30,4 @@ CYPRESS_FAIRGROUND=false
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false

View File

@ -22,3 +22,4 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true

View File

@ -22,3 +22,4 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false

View File

@ -21,3 +21,4 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false

View File

@ -18,3 +18,4 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true

View File

@ -23,3 +23,4 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true

View File

@ -20,3 +20,4 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false

View File

@ -1,17 +1,26 @@
import { ENV } from '@vegaprotocol/environment';
import {
JsonRpcConnector,
ViewConnector,
InjectedConnector,
SnapConnector,
DEFAULT_SNAP_ID,
} from '@vegaprotocol/wallet';
const urlParams = new URLSearchParams(window.location.search);
export const injected = new InjectedConnector();
export const jsonRpc = new JsonRpcConnector();
export const injected = new InjectedConnector();
export const view = new ViewConnector(urlParams.get('address'));
export const snap = new SnapConnector(
ENV.VEGA_URL ? new URL(ENV.VEGA_URL).origin : undefined,
DEFAULT_SNAP_ID
);
export const Connectors = {
injected,
jsonRpc,
view,
snap,
};

View File

@ -19,6 +19,7 @@ NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket

View File

@ -23,6 +23,7 @@ NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket

View File

@ -21,6 +21,7 @@ NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket

View File

@ -23,6 +23,7 @@ NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket

View File

@ -23,6 +23,7 @@ NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket

View File

@ -21,3 +21,4 @@ NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true

View File

@ -22,6 +22,7 @@ NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket

View File

@ -23,6 +23,7 @@ NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket

View File

@ -1,7 +1,10 @@
import { ENV } from '@vegaprotocol/environment';
import {
JsonRpcConnector,
ViewConnector,
InjectedConnector,
SnapConnector,
DEFAULT_SNAP_ID,
} from '@vegaprotocol/wallet';
export const jsonRpc = new JsonRpcConnector();
@ -15,8 +18,14 @@ if (typeof window !== 'undefined') {
view = new ViewConnector();
}
export const snap = new SnapConnector(
ENV.VEGA_URL ? new URL(ENV.VEGA_URL).origin : undefined,
DEFAULT_SNAP_ID
);
export const Connectors = {
injected,
jsonRpc,
view,
snap,
};

View File

@ -408,6 +408,12 @@ function compileFeatureFlags(): FeatureFlags {
process.env['NX_PRODUCT_PERPETUALS']
) as string
),
METAMASK_SNAPS: TRUTHY.includes(
windowOrDefault(
'NX_METAMASK_SNAPS',
process.env['NX_METAMASK_SNAPS']
) as string
),
};
const EXPLORER_FLAGS = {
EXPLORER_ASSETS: TRUTHY.includes(

View File

@ -18,7 +18,11 @@ export type Environment = z.infer<typeof envSchema>;
export type FeatureFlags = z.infer<typeof featureFlagsSchema>;
export type CosmicElevatorFlags = Pick<
FeatureFlags,
'ICEBERG_ORDERS' | 'STOP_ORDERS' | 'SUCCESSOR_MARKETS' | 'PRODUCT_PERPETUALS'
| 'ICEBERG_ORDERS'
| 'STOP_ORDERS'
| 'SUCCESSOR_MARKETS'
| 'PRODUCT_PERPETUALS'
| 'METAMASK_SNAPS'
>;
export type Configuration = z.infer<typeof tomlConfigSchema>;
export const CUSTOM_NODE_KEY = 'custom' as const;

View File

@ -77,6 +77,7 @@ const COSMIC_ELEVATOR_FLAGS = {
STOP_ORDERS: z.optional(z.boolean()),
ICEBERG_ORDERS: z.optional(z.boolean()),
PRODUCT_PERPETUALS: z.optional(z.boolean()),
METAMASK_SNAPS: z.optional(z.boolean()),
};
const EXPLORER_FLAGS = {

View File

@ -0,0 +1,102 @@
export const IconMetaMask = ({ size = 16 }: { size: number }) => (
<svg viewBox="0 0 47 47" fill="none" height={size}>
<g>
<path
d="m40.632 6.969-14.136 10.62 2.628-6.259L40.632 6.97Z"
fill="#E17726"
stroke="#E17726"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m8.024 6.969 14.01 10.72-2.502-6.359L8.024 6.97ZM35.542 31.594l-3.761 5.834 8.054 2.251 2.307-7.958-6.6-.127ZM6.528 31.721 8.82 39.68l8.04-2.251-3.747-5.834-6.586.127Z"
fill="#E27625"
stroke="#E27625"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m16.428 21.738-2.237 3.427 7.97.368-.266-8.709-5.467 4.914ZM32.229 21.738l-5.552-5.012-.181 8.807 7.97-.368-2.237-3.427ZM16.861 37.428l4.824-2.365-4.152-3.285-.672 5.65ZM26.971 35.063l4.81 2.365-.657-5.65-4.153 3.285Z"
fill="#E27625"
stroke="#E27625"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m31.78 37.428-4.81-2.365.392 3.172-.042 1.345 4.46-2.152ZM16.861 37.428l4.475 2.152-.028-1.345.377-3.172-4.824 2.365Z"
fill="#D5BFB2"
stroke="#D5BFB2"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m21.42 29.682-4-1.19 2.825-1.316 1.174 2.506ZM27.236 29.682l1.175-2.506 2.838 1.317-4.013 1.19Z"
fill="#233447"
stroke="#233447"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m16.861 37.427.7-5.834-4.447.128 3.747 5.706ZM31.096 31.593l.685 5.834 3.761-5.706-4.446-.128ZM34.465 25.165l-7.97.368.741 4.15 1.175-2.507 2.838 1.317 3.216-3.328ZM17.42 28.493l2.825-1.317 1.175 2.506.74-4.149-7.97-.368 3.23 3.328Z"
fill="#CC6228"
stroke="#CC6228"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m14.19 25.165 3.343 6.613-.112-3.285-3.23-3.328ZM31.25 28.493l-.126 3.285 3.342-6.613-3.216 3.328ZM22.161 25.533l-.741 4.149.937 4.9.21-6.458-.406-2.591ZM26.495 25.533l-.391 2.577.196 6.471.937-4.9-.741-4.148Z"
fill="#E27525"
stroke="#E27525"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m27.237 29.682-.937 4.9.671.481 4.153-3.285.126-3.285-4.013 1.19ZM17.42 28.493l.112 3.285 4.153 3.285.671-.481-.937-4.9-3.999-1.19Z"
fill="#F5841F"
stroke="#F5841F"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m27.32 39.58.042-1.345-.363-.312h-5.342l-.35.312.029 1.345-4.475-2.152 1.566 1.303 3.175 2.223h5.439l3.188-2.224 1.552-1.302-4.46 2.152Z"
fill="#C0AC9D"
stroke="#C0AC9D"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m26.97 35.063-.67-.482h-3.944l-.67.482-.378 3.172.35-.312h5.34l.364.312-.391-3.172Z"
fill="#161616"
stroke="#161616"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m41.234 18.283 1.188-5.863-1.79-5.451-13.66 10.266 5.257 4.503 7.425 2.195 1.636-1.94-.713-.524 1.132-1.048-.867-.68 1.133-.878-.741-.58ZM6.234 12.42l1.203 5.863-.77.58 1.147.878-.867.68L8.08 21.47l-.713.524 1.636 1.94 7.425-2.195 5.257-4.503L8.025 6.97l-1.79 5.452Z"
fill="#763E1A"
stroke="#763E1A"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m39.654 23.933-7.425-2.195 2.237 3.427-3.342 6.613 4.419-.057h6.6l-2.49-7.788ZM16.428 21.738l-7.425 2.195-2.475 7.788h6.586l4.418.056-3.342-6.612 2.238-3.427ZM26.495 25.533l.476-8.298 2.153-5.905h-9.592l2.153 5.905.476 8.298.181 2.605.014 6.443H26.3l.014-6.443.182-2.605Z"
fill="#F5841F"
stroke="#F5841F"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
</g>
</svg>
);

View File

@ -1,8 +1,8 @@
import { IconArrowDown } from './svg-icons/icon-arrow-down';
import { IconArrowLeft } from './svg-icons/icon-arrow-left';
import { IconArrowUp } from './svg-icons/icon-arrow-up';
import { IconArrowRight } from './svg-icons/icon-arrow-right';
import { IconArrowTopRight } from './svg-icons/icon-arrow-top-right';
import { IconArrowUp } from './svg-icons/icon-arrow-up';
import { IconBreakdown } from './svg-icons/icon-breakdown';
import { IconBullet } from './svg-icons/icon-bullet';
import { IconChevronDown } from './svg-icons/icon-chevron-down';
@ -20,28 +20,29 @@ import { IconGlobe } from './svg-icons/icon-globe';
import { IconInfo } from './svg-icons/icon-info';
import { IconKebab } from './svg-icons/icon-kebab';
import { IconLinkedIn } from './svg-icons/icon-linkedin';
import { IconMetaMask } from './svg-icons/icon-metamask';
import { IconMinus } from './svg-icons/icon-minus';
import { IconMoon } from './svg-icons/icon-moon';
import { IconOpenExternal } from './svg-icons/icon-open-external';
import { IconQuestionMark } from './svg-icons/icon-question-mark';
import { IconPlus } from './svg-icons/icon-plus';
import { IconQuestionMark } from './svg-icons/icon-question-mark';
import { IconSearch } from './svg-icons/icon-search';
import { IconStar } from './svg-icons/icon-star';
import { IconTick } from './svg-icons/icon-tick';
import { IconTicket } from './svg-icons/icon-ticket';
import { IconTransfer } from './svg-icons/icon-transfer';
import { IconTrendUp } from './svg-icons/icon-trend-up';
import { IconTrendDown } from './svg-icons/icon-trend-down';
import { IconTrendUp } from './svg-icons/icon-trend-up';
import { IconTwitter } from './svg-icons/icon-twitter';
import { IconVote } from './svg-icons/icon-vote';
import { IconWithdraw } from './svg-icons/icon-withdraw';
import { IconSearch } from './svg-icons/icon-search';
export enum VegaIconNames {
ARROW_DOWN = 'arrow-down',
ARROW_LEFT = 'arrow-left',
ARROW_UP = 'arrow-up',
ARROW_RIGHT = 'arrow-right',
ARROW_TOP_RIGHT = 'arrow-top-right',
ARROW_UP = 'arrow-up',
BREAKDOWN = 'breakdown',
BULLET = 'bullet',
CHEVRON_DOWN = 'chevron-down',
@ -59,18 +60,19 @@ export enum VegaIconNames {
INFO = 'info',
KEBAB = 'kebab',
LINKEDIN = 'linkedin',
METAMASK = 'metamask',
MINUS = 'minus',
MOON = 'moon',
OPEN_EXTERNAL = 'open-external',
QUESTION_MARK = 'question-mark',
PLUS = 'plus',
QUESTION_MARK = 'question-mark',
SEARCH = 'search',
STAR = 'star',
TICK = 'tick',
TICKET = 'ticket',
TRANSFER = 'transfer',
TREND_UP = 'trend-up',
TREND_DOWN = 'trend-down',
TREND_UP = 'trend-up',
TWITTER = 'twitter',
VOTE = 'vote',
WITHDRAW = 'withdraw',
@ -82,38 +84,39 @@ export const VegaIconNameMap: Record<
> = {
'arrow-down': IconArrowDown,
'arrow-left': IconArrowLeft,
'arrow-up': IconArrowUp,
'arrow-right': IconArrowRight,
'arrow-top-right': IconArrowTopRight,
breakdown: IconBreakdown,
bullet: IconBullet,
'arrow-up': IconArrowUp,
'chevron-down': IconChevronDown,
'chevron-left': IconChevronLeft,
'chevron-up': IconChevronUp,
'exclaimation-mark': IconExclaimationMark,
'open-external': IconOpenExternal,
'question-mark': IconQuestionMark,
'trend-down': IconTrendDown,
'trend-up': IconTrendUp,
breakdown: IconBreakdown,
bullet: IconBullet,
cog: IconCog,
copy: IconCopy,
cross: IconCross,
deposit: IconDeposit,
edit: IconEdit,
'exclaimation-mark': IconExclaimationMark,
eye: IconEye,
forum: IconForum,
globe: IconGlobe,
info: IconInfo,
kebab: IconKebab,
linkedin: IconLinkedIn,
metamask: IconMetaMask,
minus: IconMinus,
moon: IconMoon,
'open-external': IconOpenExternal,
plus: IconPlus,
'question-mark': IconQuestionMark,
search: IconSearch,
star: IconStar,
tick: IconTick,
ticket: IconTicket,
transfer: IconTransfer,
'trend-up': IconTrendUp,
'trend-down': IconTrendDown,
twitter: IconTwitter,
vote: IconVote,
withdraw: IconWithdraw,

View File

@ -16,13 +16,16 @@ import type { WalletClientError } from '@vegaprotocol/wallet-client';
import { t } from '@vegaprotocol/i18n';
import type { VegaConnector } from '../connectors';
import {
DEFAULT_SNAP_ID,
InjectedConnector,
JsonRpcConnector,
SnapConnector,
ViewConnector,
requestSnap,
} from '../connectors';
import { JsonRpcConnectorForm } from './json-rpc-connector-form';
import { ViewConnectorForm } from './view-connector-form';
import { useEnvironment } from '@vegaprotocol/environment';
import { FLAGS, useEnvironment } from '@vegaprotocol/environment';
import {
BrowserIcon,
ConnectDialogContent,
@ -38,10 +41,11 @@ import { useVegaWallet } from '../use-vega-wallet';
import { InjectedConnectorForm } from './injected-connector-form';
import { isBrowserWalletInstalled } from '../utils';
import { useIsWalletServiceRunning } from '../use-is-wallet-service-running';
import { useIsSnapRunning } from '../use-is-snap-running';
export const CLOSE_DELAY = 1700;
type Connectors = { [key: string]: VegaConnector };
export type WalletType = 'injected' | 'jsonRpc' | 'view';
export type WalletType = 'injected' | 'jsonRpc' | 'view' | 'snap';
export interface VegaConnectDialogProps {
connectors: Connectors;
@ -156,7 +160,10 @@ const ConnectDialogContainer = ({
// for rest because we need to show an authentication form
if (connector instanceof JsonRpcConnector) {
jsonRpcConnect(connector, appChainId);
} else if (connector instanceof InjectedConnector) {
} else if (
connector instanceof InjectedConnector ||
connector instanceof SnapConnector
) {
injectedConnect(connector, appChainId);
}
};
@ -166,6 +173,8 @@ const ConnectDialogContainer = ({
appChainId
);
const isSnapRunning = useIsSnapRunning(DEFAULT_SNAP_ID);
return (
<>
<ConnectDialogContent>
@ -185,6 +194,7 @@ const ConnectDialogContainer = ({
setWalletUrl={setWalletUrl}
onSelect={handleSelect}
isDesktopWalletRunning={isDesktopWalletRunning}
isSnapRunning={isSnapRunning}
/>
)}
</ConnectDialogContent>
@ -198,11 +208,13 @@ const ConnectorList = ({
walletUrl,
setWalletUrl,
isDesktopWalletRunning,
isSnapRunning,
}: {
onSelect: (type: WalletType) => void;
walletUrl: string;
setWalletUrl: (value: string) => void;
isDesktopWalletRunning: boolean | null;
isSnapRunning: boolean | null;
}) => {
const { pubKey } = useVegaWallet();
const title = isBrowserWalletInstalled()
@ -238,6 +250,45 @@ const ConnectorList = ({
<GetWalletButton />
)}
</div>
{FLAGS.METAMASK_SNAPS ? (
<div>
{isSnapRunning ? (
<ConnectionOption
type="snap"
text={
<>
<div className="w-full h-full flex justify-center items-center gap-1 text-base">
{t('Connect via Vega MetaMask Snap')}
</div>
<div className="absolute right-1 top-0 h-8 flex items-center">
<VegaIcon name={VegaIconNames.METAMASK} size={24} />
</div>
</>
}
onClick={() => {
onSelect('snap');
}}
/>
) : (
<ConnectionOption
type="snap"
text={
<>
<div className="w-full h-full flex justify-center items-center gap-1 text-base">
{t('Install Vega MetaMask Snap')}
</div>
<div className="absolute right-1 top-0 h-8 flex items-center">
<VegaIcon name={VegaIconNames.METAMASK} size={24} />
</div>
</>
}
onClick={() => {
requestSnap(DEFAULT_SNAP_ID);
}}
/>
)}
</div>
) : null}
<div>
<ConnectionOption
type="view"
@ -282,7 +333,10 @@ const SelectedForm = ({
onConnect: () => void;
riskMessage?: ReactNode;
}) => {
if (connector instanceof InjectedConnector) {
if (
connector instanceof InjectedConnector ||
connector instanceof SnapConnector
) {
return (
<InjectedConnectorForm
status={injectedState.status}

View File

@ -11,6 +11,7 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { setAcknowledged } from '../storage';
import { useVegaWallet } from '../use-vega-wallet';
import { InjectedConnectorErrors, SnapConnectorErrors } from '../connectors';
export const InjectedConnectorForm = ({
status,
@ -20,7 +21,6 @@ export const InjectedConnectorForm = ({
reset,
error,
}: {
// connector: JsonRpcConnector;
appChainId: string;
status: Status;
error: Error | null;
@ -109,7 +109,7 @@ export const InjectedConnectorForm = ({
const Center = ({ children }: { children: ReactNode }) => {
return (
<div className="flex justify-center items-center my-6">{children}</div>
<div className="flex items-center justify-center my-6">{children}</div>
);
};
@ -131,22 +131,30 @@ const Error = ({
);
if (error) {
if (error.message === 'Invalid chain') {
if (error.message === InjectedConnectorErrors.INVALID_CHAIN.message) {
title = t('Wrong network');
text = t(
'To complete your wallet connection, set your wallet network in your app to "%s".',
appChainId
);
} else if (error.message === 'window.vega not found') {
} else if (
error.message === InjectedConnectorErrors.VEGA_UNDEFINED.message
) {
title = t('No wallet detected');
text = t('Vega browser extension not installed');
} else if (
error.message === SnapConnectorErrors.ETHEREUM_UNDEFINED.message ||
error.message === SnapConnectorErrors.NODE_ADDRESS_NOT_SET.message
) {
title = t('Snap failed');
text = t('Could not connect to Vega MetaMask Snap');
}
}
return (
<>
<ConnectDialogTitle>{title}</ConnectDialogTitle>
<p className="text-center mb-2 first-letter:uppercase">{text}</p>
<p className="mb-2 text-center first-letter:uppercase">{text}</p>
{tryAgain}
</>
);

View File

@ -1,4 +1,5 @@
export * from './vega-connector';
export * from './injected-connector';
export * from './json-rpc-connector';
export * from './snap-connector';
export * from './vega-connector';
export * from './view-connector';

View File

@ -41,6 +41,11 @@ declare global {
}
}
export const InjectedConnectorErrors = {
VEGA_UNDEFINED: new Error('window.vega not found'),
INVALID_CHAIN: new Error('Invalid chain'),
};
export class InjectedConnector implements VegaConnector {
description = 'Connects using the Vega wallet browser extension';

View File

@ -0,0 +1,236 @@
import {
WalletError,
type PubKey,
type Transaction,
type VegaConnector,
} from './vega-connector';
import { clearConfig, setConfig } from '../storage';
type RequestArguments = {
method: string;
params?: unknown[] | object;
};
type WindowEthereumProvider = {
isMetaMask: boolean;
request<T = unknown>(args: RequestArguments): Promise<T>;
};
export const SnapConnectorErrors = {
ETHEREUM_UNDEFINED: new Error('MetaMask extension could not be found'),
NODE_ADDRESS_NOT_SET: new Error('nodeAddress is not set'),
SNAP_ID_NOT_SET: new Error('snapId is not set'),
TRANSACTION_PARSE: new Error('could not parse transaction data'),
};
const ethereumRequest = <T>(args: RequestArguments): Promise<T> => {
// can't declare `EthereumProvider` here because of the conflict with
// type definitions of `@web3-react`
if (
'ethereum' in window &&
typeof window.ethereum === 'object' &&
window.ethereum &&
'request' in window.ethereum &&
'isMetaMask' in window.ethereum &&
window.ethereum.isMetaMask &&
typeof window.ethereum.request === 'function'
) {
return (window.ethereum as WindowEthereumProvider).request<T>(args);
}
throw SnapConnectorErrors.ETHEREUM_UNDEFINED;
};
export const LOCAL_SNAP_ID = 'local:http://localhost:8080';
export const DEFAULT_SNAP_ID = 'npm:@vegaprotocol/snap';
type GetSnapsResponse = Record<string, Snap>;
type Snap = {
id: string;
initialPermissions?: Record<string, unknown>;
version: string;
enables: boolean;
blocked: boolean;
};
type InvokeSnapRequest = {
method: string;
params?: object;
};
type SendTransactionResponse =
| {
transactionHash: string;
receivedAt: string;
sentAt: string;
transaction?: {
signature?: {
value: string;
};
};
}
| {
error: Error & {
code: number;
data: unknown;
};
};
type GetChainIdResponse = {
chainID: string;
};
type ListKeysResponse = { keys: PubKey[] };
/**
* Requests permission for a website to communicate with the specified snaps
* and attempts to install them if they're not already installed.
* If the installation of any snap fails, returns the error that caused the failure.
* More informations here: https://docs.metamask.io/snaps/reference/rpc-api/#wallet_requestsnaps
*/
export const requestSnap = async (
snapId: string,
params: Record<'version' | string, unknown> = {}
) => {
try {
await ethereumRequest({
method: 'wallet_requestSnaps',
params: {
[snapId]: params,
},
});
} catch (err) {
// NOOP - rejected by user
}
};
/**
* Gets the list of all installed snaps.
* More information here: https://docs.metamask.io/snaps/reference/rpc-api/#wallet_getsnaps
*/
export const getSnaps = async (): Promise<GetSnapsResponse> => {
return (await ethereumRequest({
method: 'wallet_getSnaps',
})) as GetSnapsResponse;
};
/**
* Gets the requested snap by `snapId` and an optional `version`
*/
export const getSnap = async (
snapId: string,
version?: string
): Promise<Snap | undefined> => {
try {
const snaps = await getSnaps();
return Object.values(snaps).find(
(snap) => snap.id === snapId && (!version || snap.version === version)
);
} catch (e) {
return undefined;
}
};
export const invokeSnap = async <T>(
snapId: string,
request: InvokeSnapRequest
) => {
const req = {
method: 'wallet_invokeSnap',
params: {
snapId,
request,
},
};
return await ethereumRequest<T>(req);
};
export class SnapConnector implements VegaConnector {
description = "Connects using Vega Protocol's MetaMask snap";
snapId: string | undefined = undefined;
nodeAddress: string | undefined = undefined;
constructor(nodeAddress?: string, snapId = DEFAULT_SNAP_ID) {
this.nodeAddress = nodeAddress;
this.snapId = snapId;
}
async listKeys() {
if (!this.snapId) throw SnapConnectorErrors.SNAP_ID_NOT_SET;
return await invokeSnap<ListKeysResponse>(this.snapId, {
method: 'client.list_keys',
});
}
async connect() {
const res = await this.listKeys();
setConfig({
connector: 'snap',
token: null, // no token required for snap
url: null, // no url required for snap
});
return res?.keys;
}
async sendTx(pubKey: string, transaction: Transaction) {
if (!this.nodeAddress) throw SnapConnectorErrors.NODE_ADDRESS_NOT_SET;
if (!this.snapId) throw SnapConnectorErrors.SNAP_ID_NOT_SET;
// This step is needed to strip the transaction object from any additional
// properties, such as `__proto__`, etc.
let txData = null;
try {
txData = JSON.parse(JSON.stringify(transaction));
} catch (err) {
throw SnapConnectorErrors.TRANSACTION_PARSE;
}
const payload = {
method: 'client.send_transaction',
params: {
sendingMode: 'TYPE_SYNC',
transaction: txData,
publicKey: pubKey,
networkEndpoints: [this.nodeAddress],
},
};
const result = await invokeSnap<SendTransactionResponse>(
this.snapId,
payload
);
if ('error' in result) {
const { message, code, data } = result.error;
throw new WalletError(
message,
code,
typeof data === 'string' ? data : ''
);
}
if (!result?.transaction?.signature) {
throw new Error('could not retrieve transaction siganture');
}
return {
transactionHash: result.transactionHash,
signature: result?.transaction?.signature?.value,
receivedAt: result.receivedAt,
sentAt: result.sentAt,
};
}
async getChainId(): Promise<GetChainIdResponse> {
if (!this.nodeAddress) throw SnapConnectorErrors.NODE_ADDRESS_NOT_SET;
if (!this.snapId) throw SnapConnectorErrors.SNAP_ID_NOT_SET;
const response = await invokeSnap<GetChainIdResponse>(this.snapId, {
method: 'client.get_chain_id',
params: { networkEndpoints: [this.nodeAddress] },
});
return response;
}
async disconnect() {
clearConfig();
}
}

View File

@ -2,7 +2,7 @@ import { LocalStorage } from '@vegaprotocol/utils';
interface ConnectorConfig {
token: string | null;
connector: 'injected' | 'jsonRpc' | 'view';
connector: 'injected' | 'jsonRpc' | 'view' | 'snap';
url: string | null;
}

View File

@ -1,11 +1,14 @@
import type { SnapConnector } from './';
import { useVegaWallet } from './';
import { useEffect, useState } from 'react';
import type { VegaConnector } from './connectors/vega-connector';
import { getConfig } from './storage';
import { useEnvironment } from '@vegaprotocol/environment';
export function useEagerConnect(Connectors: {
[connector: string]: VegaConnector;
}) {
const { VEGA_URL } = useEnvironment();
const [connecting, setConnecting] = useState(true);
const { connect, acknowledgeNeeded } = useVegaWallet();
@ -36,6 +39,12 @@ export function useEagerConnect(Connectors: {
// @ts-ignore only injected wallet has connectWallet method
await injectedInstance.connectWallet();
await connect(injectedInstance);
} else if (cfg.connector === 'snap') {
const snapInstance = Connectors[cfg.connector] as SnapConnector;
if (VEGA_URL) {
snapInstance.nodeAddress = new URL(VEGA_URL).origin;
await connect(snapInstance);
}
} else {
await connect(Connectors[cfg.connector]);
}
@ -49,7 +58,7 @@ export function useEagerConnect(Connectors: {
if (typeof window !== 'undefined') {
attemptConnect();
}
}, [connect, Connectors, acknowledgeNeeded]);
}, [connect, Connectors, acknowledgeNeeded, VEGA_URL]);
return connecting;
}

View File

@ -1,6 +1,12 @@
import { useCallback, useState } from 'react';
import type { InjectedConnector } from './connectors';
import {
InjectedConnectorErrors,
SnapConnector,
SnapConnectorErrors,
} from './connectors';
import { InjectedConnector } from './connectors';
import { useVegaWallet } from './use-vega-wallet';
import { useEnvironment } from '@vegaprotocol/environment';
export enum Status {
Idle = 'Idle',
@ -15,24 +21,39 @@ export const useInjectedConnector = (onConnect: () => void) => {
const { connect, acknowledgeNeeded } = useVegaWallet();
const [status, setStatus] = useState(Status.Idle);
const [error, setError] = useState<Error | null>(null);
const { VEGA_URL } = useEnvironment();
const attemptConnect = useCallback(
async (connector: InjectedConnector, appChainId: string) => {
async (
connector: InjectedConnector | SnapConnector,
appChainId: string
) => {
try {
if (!('vega' in window)) {
throw new Error('window.vega not found');
if (connector instanceof InjectedConnector && !('vega' in window)) {
throw InjectedConnectorErrors.VEGA_UNDEFINED;
}
if (connector instanceof SnapConnector) {
if (!('ethereum' in window)) {
throw SnapConnectorErrors.ETHEREUM_UNDEFINED;
}
if (!VEGA_URL) {
throw SnapConnectorErrors.NODE_ADDRESS_NOT_SET;
}
connector.nodeAddress = new URL(VEGA_URL).origin;
}
setStatus(Status.GettingChainId);
const { chainID } = await connector.getChainId();
if (chainID !== appChainId) {
throw new Error('Invalid chain');
throw InjectedConnectorErrors.INVALID_CHAIN;
}
setStatus(Status.Connecting);
await connector.connectWallet(); // authorize wallet
if (connector instanceof InjectedConnector) {
// extra step for injected connector - authorize wallet
await connector.connectWallet();
}
await connect(connector); // connect with keys
if (acknowledgeNeeded) {
@ -50,7 +71,7 @@ export const useInjectedConnector = (onConnect: () => void) => {
setStatus(Status.Error);
}
},
[acknowledgeNeeded, connect, onConnect]
[VEGA_URL, acknowledgeNeeded, connect, onConnect]
);
return {

View File

@ -0,0 +1,22 @@
import { useEffect, useState } from 'react';
import { getSnap } from './connectors';
const INTERVAL = 2_000;
export const useIsSnapRunning = (snapId: string) => {
const [running, setRunning] = useState(false);
useEffect(() => {
const checkState = async () => {
const snap = await getSnap(snapId);
setRunning(!!snap);
};
const i = setInterval(() => {
checkState();
}, INTERVAL);
checkState();
return () => {
clearInterval(i);
};
}, [snapId]);
return running;
};

View File

@ -73,6 +73,7 @@ export const useVegaTransactionStore = create<VegaTransactionStore>()(
order,
};
set({ transactions: transactions.concat(transaction) });
return transaction.id;
},
update: (index: number, update: Partial<VegaStoredTxState>) => {

View File

@ -9,7 +9,7 @@ import type { VegaTransactionContentMap } from './vega-transaction-dialog';
import { VegaTransactionDialog } from './vega-transaction-dialog';
import type { Intent } from '@vegaprotocol/ui-toolkit';
import type { Transaction } from './connectors';
import type { WalletError } from './connectors';
import { WalletError } from './connectors';
import { ClientErrors } from './connectors';
export interface DialogProps {
@ -44,7 +44,7 @@ export const initialState = {
};
export const orderErrorResolve = (err: Error | unknown): Error => {
if (err instanceof WalletClientError) {
if (err instanceof WalletClientError || err instanceof WalletError) {
return err;
} else if (err instanceof WalletHttpError) {
return ClientErrors.UNKNOWN;

View File

@ -758,11 +758,11 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
const VegaTxErrorToastContent = ({ tx }: VegaTxToastContentProps) => {
let label = t('Error occurred');
let errorMessage = `${tx.error?.message} ${
tx.error instanceof WalletError && tx.error?.data
? `: ${tx.error?.data}`
: ''
}`;
let errorMessage =
tx.error instanceof WalletError
? `${tx.error.title}: ${tx.error.data}`
: tx.error?.message;
const reconnectVegaWallet = useReconnectVegaWallet();
const orderRejection = tx.order && getRejectionReason(tx.order);
@ -773,6 +773,7 @@ const VegaTxErrorToastContent = ({ tx }: VegaTxToastContentProps) => {
const walletError =
tx.error instanceof WalletError &&
walletNoConnectionCodes.includes(tx.error.code);
if (orderRejection) {
label = getOrderToastTitle(tx.order?.status) || t('Order rejected');
errorMessage = t('Your order has been rejected because: %s', [

View File

@ -1,6 +1,11 @@
import { t } from '@vegaprotocol/i18n';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
import {
Dialog,
Intent,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { MetaMask } from '@web3-react/metamask';
import { WalletConnect } from '@web3-react/walletconnect-v2';
import { WalletConnect as WalletConnectLegacy } from '@web3-react/walletconnect';
@ -118,7 +123,7 @@ export const Web3ConnectUncontrolledDialog = () => {
function getConnectorInfo(connector: Connector) {
if (connector instanceof MetaMask) {
return {
icon: <MetaMaskIcon width={40} />,
icon: <VegaIcon name={VegaIconNames.METAMASK} size={32} />,
name: 'MetaMask',
text: t('MetaMask'),
alt: t('MetaMask, Brave or other injected web wallet'),
@ -126,14 +131,14 @@ function getConnectorInfo(connector: Connector) {
}
if (connector instanceof CoinbaseWallet) {
return {
icon: <CoinbaseWalletIcon width={40} />,
icon: <CoinbaseWalletIcon width={32} />,
name: 'CoinbaseWallet',
text: t('Coinbase'),
};
}
if (connector instanceof WalletConnect) {
return {
icon: <WalletConnectIcon width={40} />,
icon: <WalletConnectIcon width={32} />,
name: 'WalletConnect',
text: t('WalletConnect'),
alt: t('WalletConnect v2'),
@ -143,7 +148,7 @@ function getConnectorInfo(connector: Connector) {
return {
icon: (
<WalletConnectIcon
width={40}
width={32}
fillColor={theme.colors.vega.light[200]}
/>
),
@ -160,115 +165,6 @@ type IconProps = {
height?: number;
};
const MetaMaskIcon = ({ width, height }: IconProps) => (
<svg
viewBox="0 0 47 47"
fill="none"
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
>
<g>
<path
d="m40.632 6.969-14.136 10.62 2.628-6.259L40.632 6.97Z"
fill="#E17726"
stroke="#E17726"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m8.024 6.969 14.01 10.72-2.502-6.359L8.024 6.97ZM35.542 31.594l-3.761 5.834 8.054 2.251 2.307-7.958-6.6-.127ZM6.528 31.721 8.82 39.68l8.04-2.251-3.747-5.834-6.586.127Z"
fill="#E27625"
stroke="#E27625"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m16.428 21.738-2.237 3.427 7.97.368-.266-8.709-5.467 4.914ZM32.229 21.738l-5.552-5.012-.181 8.807 7.97-.368-2.237-3.427ZM16.861 37.428l4.824-2.365-4.152-3.285-.672 5.65ZM26.971 35.063l4.81 2.365-.657-5.65-4.153 3.285Z"
fill="#E27625"
stroke="#E27625"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m31.78 37.428-4.81-2.365.392 3.172-.042 1.345 4.46-2.152ZM16.861 37.428l4.475 2.152-.028-1.345.377-3.172-4.824 2.365Z"
fill="#D5BFB2"
stroke="#D5BFB2"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m21.42 29.682-4-1.19 2.825-1.316 1.174 2.506ZM27.236 29.682l1.175-2.506 2.838 1.317-4.013 1.19Z"
fill="#233447"
stroke="#233447"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m16.861 37.427.7-5.834-4.447.128 3.747 5.706ZM31.096 31.593l.685 5.834 3.761-5.706-4.446-.128ZM34.465 25.165l-7.97.368.741 4.15 1.175-2.507 2.838 1.317 3.216-3.328ZM17.42 28.493l2.825-1.317 1.175 2.506.74-4.149-7.97-.368 3.23 3.328Z"
fill="#CC6228"
stroke="#CC6228"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m14.19 25.165 3.343 6.613-.112-3.285-3.23-3.328ZM31.25 28.493l-.126 3.285 3.342-6.613-3.216 3.328ZM22.161 25.533l-.741 4.149.937 4.9.21-6.458-.406-2.591ZM26.495 25.533l-.391 2.577.196 6.471.937-4.9-.741-4.148Z"
fill="#E27525"
stroke="#E27525"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m27.237 29.682-.937 4.9.671.481 4.153-3.285.126-3.285-4.013 1.19ZM17.42 28.493l.112 3.285 4.153 3.285.671-.481-.937-4.9-3.999-1.19Z"
fill="#F5841F"
stroke="#F5841F"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m27.32 39.58.042-1.345-.363-.312h-5.342l-.35.312.029 1.345-4.475-2.152 1.566 1.303 3.175 2.223h5.439l3.188-2.224 1.552-1.302-4.46 2.152Z"
fill="#C0AC9D"
stroke="#C0AC9D"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m26.97 35.063-.67-.482h-3.944l-.67.482-.378 3.172.35-.312h5.34l.364.312-.391-3.172Z"
fill="#161616"
stroke="#161616"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m41.234 18.283 1.188-5.863-1.79-5.451-13.66 10.266 5.257 4.503 7.425 2.195 1.636-1.94-.713-.524 1.132-1.048-.867-.68 1.133-.878-.741-.58ZM6.234 12.42l1.203 5.863-.77.58 1.147.878-.867.68L8.08 21.47l-.713.524 1.636 1.94 7.425-2.195 5.257-4.503L8.025 6.97l-1.79 5.452Z"
fill="#763E1A"
stroke="#763E1A"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d="m39.654 23.933-7.425-2.195 2.237 3.427-3.342 6.613 4.419-.057h6.6l-2.49-7.788ZM16.428 21.738l-7.425 2.195-2.475 7.788h6.586l4.418.056-3.342-6.612 2.238-3.427ZM26.495 25.533l.476-8.298 2.153-5.905h-9.592l2.153 5.905.476 8.298.181 2.605.014 6.443H26.3l.014-6.443.182-2.605Z"
fill="#F5841F"
stroke="#F5841F"
strokeWidth="0.223"
strokeLinecap="round"
strokeLinejoin="round"
></path>
</g>
</svg>
);
const CoinbaseWalletIcon = ({ width, height }: IconProps) => (
<svg
width={width}