Compare commits

...
9 changed files with 811 additions and 2 deletions
+4 -1
View File
@@ -33,6 +33,7 @@ import { PartyActiveOrdersHandler } from './party-active-orders-handler';
import { MaybeConnectEagerly } from './maybe-connect-eagerly';
import { TransactionHandlers } from './transaction-handlers';
import { useT } from '../lib/use-t';
import { Test } from './test/test';
const Title = () => {
const t = useT();
@@ -135,4 +136,6 @@ function VegaTradingApp(props: AppProps) {
);
}
export default VegaTradingApp;
export default function () {
return <Test />;
}
+17
View File
@@ -0,0 +1,17 @@
export type Chain = {
id: string;
testnet: boolean;
name: string;
};
export const fairground = {
id: 'vega-fairground-202305051805',
testnet: true,
name: 'Fairground',
};
export const stagnet = {
id: 'vega-stagnet1-202307191148',
testnet: true,
name: 'Stagnet',
};
+47
View File
@@ -0,0 +1,47 @@
import {
type Transaction,
type TransactionResponse,
} from '@vegaprotocol/wallet';
export enum JsonRpcMethod {
ConnectWallet = 'client.connect_wallet',
DisconnectWallet = 'client.disconnect_wallet',
ListKeys = 'client.list_keys',
SignTransaction = 'client.sign_transaction',
SendTransaction = 'client.send_transaction',
GetChainId = 'client.get_chain_id',
}
export interface IWalletError {
error: string;
}
export interface TransactionParams {
publicKey: string;
transaction: Transaction;
sendingMode: 'TYPE_SYNC';
}
export interface Connector {
id: string;
connectWallet(chainId: string): Promise<{ success: boolean } | IWalletError>;
disconnectWallet(): Promise<{ success: boolean } | IWalletError>;
getChainId(): Promise<{ chainId: string } | IWalletError>;
listKeys(): Promise<
Array<{ publicKey: string; name: string }> | IWalletError
>;
isConnected(): Promise<{ connected: boolean } | IWalletError>;
sendTransaction(
params: TransactionParams
): Promise<TransactionResponse | IWalletError>;
on(event: string, callback: () => void): void;
off(event: string, callback: () => void): void;
}
@@ -0,0 +1,75 @@
import { TransactionParams, type Connector } from '.';
export class InjectedConnector implements Connector {
id = 'injected';
async connectWallet(chainId: string) {
try {
await window.vega.connectWallet({ chainId });
return { success: true };
} catch (err) {
return { error: 'failed to connect' };
}
}
async disconnectWallet() {
try {
await window.vega.disconnectWallet();
return { success: true };
} catch (err) {
return { error: 'failed to disconnect' };
}
}
// deprecated, pass chain on connect
async getChainId() {
try {
const res = await window.vega.getChainId();
return { chainId: res.chainID };
} catch (err) {
return { error: 'failed to get chain id' };
}
}
async listKeys() {
try {
const res = await window.vega.listKeys();
return res.keys;
} catch (err) {
return { error: 'failed to list keys' };
}
}
async isConnected() {
try {
const res = await window.vega.isConnected();
return { connected: res };
} catch (err) {
return { error: 'failed to check isConnected' };
}
}
async sendTransaction(params: TransactionParams) {
try {
const res = await window.vega.sendTransaction(params);
return {
transactionHash: res.transactionHash,
signature: res.transaction.signature.value,
receivedAt: res.receivedAt,
sentAt: res.sentAt,
};
} catch (err) {
console.error(err);
return { error: 'failed to send transaction' };
}
}
on(event: 'client.disconnected', callback: () => void) {
window.vega.on(event, callback);
}
off(event: 'client.disconnected') {
window.vega.off(event);
}
}
@@ -0,0 +1,126 @@
import { JsonRpcMethod, type Connector, type TransactionParams } from '.';
type JsonRpcConnectorConfig = { url: string };
export class JsonRpcConnector implements Connector {
id = 'json-rpc';
config: JsonRpcConnectorConfig;
requestId: number = 0;
token: string | null = null;
constructor(config: JsonRpcConnectorConfig) {
this.config = config;
}
async connectWallet(desiredChainId: string) {
try {
const { chainId } = await this.getChainId();
if (chainId !== desiredChainId) {
throw new Error('incorrect chain id');
}
const { response } = await this.request(JsonRpcMethod.ConnectWallet, {
hostname: window.location.hostname,
});
const token = response.headers.get('Authorization');
if (response.ok && token) {
this.token = token;
return { success: true };
}
return { error: 'failed to connect' };
} catch (err) {
return { error: 'failed to connect' };
}
}
async disconnectWallet() {
try {
await this.request(JsonRpcMethod.DisconnectWallet);
return { success: true };
} catch (err) {
return { error: 'failed to disconnect' };
}
}
// deprecated, pass chain on connect
async getChainId() {
try {
const { data } = await this.request(JsonRpcMethod.GetChainId);
return { chainId: data.result.chainID };
} catch (err) {
return { error: 'failed to get chain id' };
}
}
async listKeys() {
try {
const { data } = await this.request(JsonRpcMethod.ListKeys);
return data.result.keys as Array<{ publicKey: string; name: string }>;
} catch (err) {
return { error: 'failed to list keys' };
}
}
async isConnected() {
try {
await this.listKeys();
return { connected: true };
} catch (err) {
return { error: 'failed to check isConnected' };
}
}
async sendTransaction(params: TransactionParams) {
try {
const { data } = await this.request(
JsonRpcMethod.SendTransaction,
params
);
return {
transactionHash: data.result.transactionHash,
signature: data.result.transaction.signature.value,
receivedAt: data.result.receivedAt,
sentAt: data.result.sentAt,
};
} catch (err) {
return { error: 'failed to send transaction' };
}
}
private async request(method: JsonRpcMethod, params?: any) {
const headers = new Headers();
if (this.token) {
headers.set('Authorization', this.token);
}
const response = await fetch(this.config.url, {
method: 'POST',
headers,
body: JSON.stringify({
id: `${this.requestId++}`,
jsonrpc: '2.0',
method,
params,
}),
});
const data = await response.json();
return {
data,
response,
};
}
on() {
console.warn('events are not supported in json rpc wallet');
}
off() {
console.warn('events are not supported in json rpc wallet');
}
}
+141
View File
@@ -0,0 +1,141 @@
import { JsonRpcMethod, type Connector, type TransactionParams } from '.';
enum EthereumMethod {
RequestSnaps = 'wallet_requestSnaps',
GetSnaps = 'wallet_getSnaps',
InvokeSnap = 'wallet_invokeSnap',
}
type SnapConfig = {
id: string;
node: string;
version: string;
};
export class SnapConnector implements Connector {
id = 'snap';
config: SnapConfig;
constructor(config: SnapConfig) {
this.config = config;
}
async connectWallet(desiredChainId: string) {
try {
await this.requestSnap();
const { chainId } = await this.getChainId();
if (chainId !== desiredChainId) {
throw new Error('incorrect chain id');
}
return { success: true };
} catch (err) {
return { error: 'failed to connect' };
}
}
async disconnectWallet() {
console.warn('disconnectWallet not implemented');
return { error: 'failed to disconnect' };
}
// deprecated, pass chain on connect
async getChainId() {
try {
const res = await this.invokeSnap(JsonRpcMethod.GetChainId, {
networkEndpoints: [this.config.node],
});
return { chainId: res.chainID };
} catch (err) {
return { error: 'failed to get chain id' };
}
}
async listKeys() {
try {
const res = await this.invokeSnap(JsonRpcMethod.ListKeys);
return res.keys as Array<{ publicKey: string; name: string }>;
} catch (err) {
return { error: 'failed to list keys' };
}
}
async isConnected() {
console.warn('isConnected not implemented');
return { error: 'failed to check if connected' };
}
async sendTransaction(params: TransactionParams) {
try {
const res = await this.invokeSnap(JsonRpcMethod.SendTransaction, {
publicKey: params.publicKey,
sendingMode: params.sendingMode,
transaction: params.transaction,
networkEndpoints: [this.config.node],
});
return {
transactionHash: res.transactionHash,
signature: res.transaction.signature.value,
receivedAt: res.receivedAt,
sentAt: res.sentAt,
};
} catch (err) {
console.error(err);
return { error: 'failed to send transaction' };
}
}
////////////////////////////////////
// Snap methods
////////////////////////////////////
async requestSnap() {
const res = await this.request(EthereumMethod.RequestSnaps, {
[this.config.id]: {
version: this.config.version,
},
});
console.log(res);
}
async getSnap() {
const snaps = await this.request(EthereumMethod.GetSnaps);
return Object.values(snaps).find(
(s) => s.id === this.config.id && s.version === this.config.version
);
}
async invokeSnap(method: JsonRpcMethod, params?: any) {
return await this.request(EthereumMethod.InvokeSnap, {
snapId: this.config.id,
request: {
method: method,
params,
},
});
}
async request(method: EthereumMethod, params?: any) {
if (!window.ethereum) {
throw new Error('no window.ethereum');
}
// @ts-ignore ethereum types a bit mangled from web3-react
return window.ethereum.request({
method,
params,
});
}
on() {
console.warn('events are not supported in json rpc wallet');
}
off() {
console.warn('events are not supported in json rpc wallet');
}
}
+333
View File
@@ -0,0 +1,333 @@
import { StoreApi, createStore } from 'zustand/vanilla';
import { useStore } from 'zustand';
import { InjectedConnector } from './injected-connector';
import { JsonRpcConnector } from './json-rpc-connector';
import { SnapConnector } from './snap-connector';
import { Chain, fairground, stagnet } from './chains';
import { Connector, TransactionParams } from '.';
import {
PropsWithChildren,
createContext,
createElement,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import { Dialog } from '@vegaprotocol/ui-toolkit';
import { TransactionResponse } from '@vegaprotocol/wallet';
import { AccountType } from '@vegaprotocol/types';
const injected = new InjectedConnector();
const jsonRpc = new JsonRpcConnector({
url: 'http://localhost:1789/api/v2/requests',
});
const snap = new SnapConnector({
node: 'https://api.n08.testnet.vega.rocks',
id: 'npm:@vegaprotocol/snap',
version: '0.3.1',
});
type Key = {
publicKey: string;
name: string;
};
type Store = {
chainId: string;
status: 'disconnected' | 'connecting' | 'connected';
current: string | undefined;
keys: Key[];
setKeys: (keys: Key[]) => void;
};
type Config = { chains: Chain[]; connectors: Connector[] };
type Wallet = {
store: StoreApi<Store>;
connectors: Connector[];
connect: (id: string) => Promise<void>;
disconnect: () => Promise<void>;
sendTransaction: (params: TransactionParams) => Promise<TransactionResponse>;
};
function createConfig(cfg: Config): Wallet {
const connectors = createStore(() => cfg.connectors);
const store = createStore<Store>((set) => ({
chainId: cfg.chains[0].id,
status: 'disconnected',
current: undefined,
keys: [],
setKeys: (keys) => {
set({ keys });
},
}));
async function connect(id: string) {
if (store.getState().status === 'connecting') {
return;
}
const connector = connectors.getState().find((x) => x.id === id);
if (!connector) return;
try {
store.setState({ status: 'connecting', current: id });
const connectWalletRes = await connector.connectWallet(
store.getState().chainId
);
if ('error' in connectWalletRes) {
throw new Error('failed to connect');
}
const listKeysRes = await connector.listKeys();
if ('error' in listKeysRes) {
throw new Error('failed to get keys');
}
store.setState({
keys: listKeysRes,
status: 'connected',
});
} catch (err) {
console.error(err);
store.setState({ status: 'disconnected', current: undefined, keys: [] });
}
}
async function disconnect() {
const connector = connectors
.getState()
.find((x) => x.id === store.getState().current);
if (!connector) return;
store.setState({ status: 'disconnected', current: undefined, keys: [] });
}
async function sendTransaction(params: TransactionParams) {
const connector = connectors
.getState()
.find((x) => x.id === store.getState().current);
if (!connector) return;
try {
const res = await connector.sendTransaction(params);
return res;
console.log('res', res);
} catch (err) {
return {
error: 'failed to send',
};
console.error(err);
}
}
return {
store,
connect,
disconnect,
sendTransaction,
get connectors() {
return connectors.getState();
},
};
}
const config = createConfig({
chains: [fairground, stagnet],
connectors: [injected, jsonRpc, snap],
});
function useWallet<T>(selector: (store: Store) => T) {
const store = useStore(config.store, selector);
return store;
}
function useConfig() {
const context = useContext(VegaWalletContext);
if (context === undefined) {
throw new Error('must be used within VegaWalletProvider');
}
return context;
}
function useConnect() {
const config = useConfig();
return {
connectors: config.connectors,
connect: config.connect,
};
}
function useDisconnect() {
const config = useConfig();
return {
disconnect: config.disconnect,
};
}
function useSendTransaction() {
const config = useConfig();
return {
sendTransaction: config.sendTransaction,
};
}
const VegaWalletContext = createContext<Wallet | undefined>(undefined);
function VegaWalletProvider({
children,
config,
}: PropsWithChildren<{ config: Wallet }>) {
return createElement(VegaWalletContext.Provider, { value: config }, children);
}
export function Test() {
return (
<VegaWalletProvider config={config}>
<App />
</VegaWalletProvider>
);
}
function App() {
const [open, setOpen] = useState(false);
const { connectors, connect } = useConnect();
const { disconnect } = useDisconnect();
const { sendTransaction } = useSendTransaction();
const status = useWallet((store) => store.status);
return (
<div>
<button onClick={() => setOpen(true)}>Connect</button>
<Dialog open={open}>
<div>
{status === 'disconnected' && (
<div className="flex gap-2">
{connectors.map((c) => (
<button
key={c.id}
onClick={async () => {
try {
await connect(c.id);
setTimeout(() => {
setOpen(false);
}, 1000);
} catch (err) {}
}}
>
connect {c.id}
</button>
))}
</div>
)}
{status === 'connecting' && <div>Approve connection in wallet</div>}
{status === 'connected' && <div>Connected!</div>}
</div>
</Dialog>
<hr className="border-t pt-2 mt-2" />
<div className="flex flex-col items-start gap-2">
<button onClick={() => disconnect()}>diconnectWallet</button>
<button
onClick={async () => {
const fromKey =
'846b2ed2fff0ddf219a5302a3856f9ac279652c4d92e72e7df762117886e69cc'; // snap
const toKey =
'5a703397f6ac1f62513af1f5d0237d1b9f8b2c93ef19fbe9fa87771a2b7f55da'; // browser
const res = await sendTransaction({
publicKey: fromKey,
sendingMode: 'TYPE_SYNC',
transaction: {
transfer: {
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
to: toKey,
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
asset:
'16ae5dbb1fd7aa2ddef725703bfe66b3647a4da7b844bfdd04e985756f53d9d6',
amount: '100',
oneOff: {},
},
},
});
console.log(res);
}}
>
submit transfer
</button>
</div>
<div>{status}</div>
<Keys />
{/* <NoReact /> */}
</div>
);
}
const Keys = () => {
const keys = useWallet((store) => store.keys);
if (!keys.length) {
return <p>No keys</p>;
}
return (
<ul>
{keys.map((k) => {
return (
<li key={k.publicKey}>
{k.name} {k.publicKey}
</li>
);
})}
</ul>
);
};
const NoReact = () => {
const divRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!divRef.current) return;
const div = divRef.current;
const button = document.createElement('button');
button.addEventListener('click', () => {
config.connect('injected'); // should fail
});
button.textContent = 'Connect injected';
const status = document.createElement('div');
const keys = document.createElement('ul');
config.store.subscribe((state) => {
status.textContent = state.status;
state.keys.forEach((key) => {
const li = document.createElement('li');
li.textContent = `${key.name} ${key.publicKey}`;
keys.appendChild(li);
});
});
div.appendChild(button);
div.appendChild(status);
div.appendChild(keys);
}, []);
return (
<div>
<h1>No react</h1>
<div ref={divRef} />
</div>
);
};
+65
View File
@@ -0,0 +1,65 @@
import { InjectedConnector } from './injected-connector';
import { JsonRpcConnector } from './json-rpc-connector';
import { SnapConnector } from './snap-connector';
export class Wallet {
connector?: InjectedConnector | JsonRpcConnector | SnapConnector;
async connectWallet(
connector: InjectedConnector | JsonRpcConnector | SnapConnector
) {
this.connector = connector;
const res = await this.connector.connectWallet();
if (res.success !== true) {
// TODO: something better
return;
}
const keys = await this.connector.listKeys();
console.log(keys);
return keys;
}
async disconnectWallet() {
if (!this.connector) {
throw new Error('no connector');
}
await this.connector.disconnectWallet();
this.connector = undefined;
}
async listKeys() {
if (!this.connector) {
throw new Error('no connector');
}
return this.connector.listKeys();
}
async getChainId() {
if (!this.connector) {
throw new Error('no connector');
}
return this.connector.getChainId();
}
async isConnected() {
if (!this.connector) {
throw new Error('no connector');
}
return this.connector.isConnected();
}
// sendTransaction(...args: Parameters<typeof window.vega.sendTransaction>) {
// if (!this.connector) {
// throw new Error('no connector');
// }
//
// return this.connector.sendTransaction(...args);
// }
}
@@ -37,7 +37,9 @@ declare global {
}>;
on: (event: VegaWalletEvent, callback: () => void) => void;
isConnected?: () => Promise<boolean>;
off: (event: VegaWalletEvent) => void;
isConnected: () => Promise<boolean>;
getChainId: () => Promise<{ chainID: string }>;
}
interface Window {