support multiple wallets

This commit is contained in:
Tyler Shipe 2021-04-14 01:23:50 -04:00
parent de620e9a3f
commit da3072b867
10 changed files with 269 additions and 35 deletions

View File

@ -1,4 +1,9 @@
import { AccountInfo, Connection, PublicKey } from '@solana/web3.js'
import {
AccountInfo,
Connection,
PublicKey,
Transaction,
} from '@solana/web3.js'
import Wallet from '@project-serum/sol-wallet-adapter'
import { Market, OpenOrders } from '@project-serum/serum'
import { Event } from '@project-serum/serum/lib/queue'
@ -186,7 +191,6 @@ export interface MarginAccountContextValues {
}
// Type declaration for the margin accounts for the mango group
export type mangoTokenAccounts = {
mango_group: string
accounts: TokenAccount[]
@ -199,3 +203,18 @@ export interface KnownToken {
icon?: string
mintAddress: string
}
export const DEFAULT_PUBLIC_KEY = new PublicKey(
'11111111111111111111111111111111'
)
export interface WalletAdapter {
publicKey: PublicKey
autoApprove: boolean
connected: boolean
signTransaction: (transaction: Transaction) => Promise<Transaction>
signAllTransactions: (transaction: Transaction[]) => Promise<Transaction[]>
connect: () => any
disconnect: () => any
on<T>(event: string, fn: () => void): this
}

View File

@ -6,6 +6,7 @@ import useWallet from '../hooks/useWallet'
import ThemeSwitch from './ThemeSwitch'
import UiLock from './UiLock'
import { useRouter } from 'next/router'
import WalletSelect from './WalletSelect'
const Code = styled.code`
border: 1px solid hsla(0, 0%, 39.2%, 0.2);
@ -44,14 +45,14 @@ const TopBar = () => {
<div className="flex items-center pr-1">
{asPath === '/' ? <UiLock className="mr-4" /> : null}
<ThemeSwitch />
<div className="hidden sm:ml-4 sm:flex sm:items-center">
<div className="border border-th-primary hover:bg-th-primary rounded-md ">
<div className="hidden sm:ml-4 sm:flex">
<div className="flex items-center border border-th-primary rounded-md">
<button
onClick={handleConnectDisconnect}
className="px-4 py-2 focus:outline-none text-th-primary hover:text-th-fgd-1 font-semibold text-bas"
className="p-2 focus:outline-none text-th-primary hover:text-th-fgd-1 hover:bg-th-primary font-semibold"
>
{connected ? (
<div onClick={wallet.disconnect}>
<div>
<span>Disconnect: </span>
<Code
className={`text-xs p-1 text-th-fgd-1 font-extralight`}
@ -65,6 +66,11 @@ const TopBar = () => {
'Connect Wallet'
)}
</button>
{!connected && (
<div className="relative h-full">
<WalletSelect />
</div>
)}
</div>
</div>
</div>
@ -76,11 +82,11 @@ const TopBar = () => {
aria-expanded="false"
onClick={() => setShowMenu((showMenu) => !showMenu)}
>
<span className={`sr-only`}>Open main menu</span>
<span className="sr-only">Open main menu</span>
{showMenu ? (
<XIcon className={`h-5 w-5 text-th-primary`} />
<XIcon className="h-5 w-5 text-th-primary" />
) : (
<MenuIcon className={`h-5 w-5 text-th-primary`} />
<MenuIcon className="h-5 w-5 text-th-primary" />
)}
</button>
</div>

View File

@ -18,7 +18,7 @@ import Switch from './Switch'
export default function TradeForm() {
const { baseCurrency, quoteCurrency, market, marketAddress } = useMarket()
const set = useMangoStore((s) => s.set)
const { connected } = useMangoStore((s) => s.wallet)
const connected = useMangoStore((s) => s.wallet.connected)
const actions = useMangoStore((s) => s.actions)
const { connection, cluster } = useConnection()
const { side, baseSize, quoteSize, price, tradeType } = useMangoStore(

View File

@ -0,0 +1,55 @@
import { Menu, Transition } from '@headlessui/react'
import { DotsHorizontalIcon, CheckCircleIcon } from '@heroicons/react/outline'
import useMangoStore from '../stores/useMangoStore'
import { WALLET_PROVIDERS } from '../hooks/useWallet'
export default function WalletSelect() {
const setMangoStore = useMangoStore((s) => s.set)
const { providerUrl } = useMangoStore((s) => s.wallet)
const handleSelectProvider = (url) => {
setMangoStore((state) => {
state.wallet.providerUrl = url
})
}
return (
<Menu>
{({ open }) => (
<>
<Menu.Button className="p-2 h-full border-l border-th-primary focus:outline-none text-th-primary hover:text-th-fgd-1 hover:bg-th-primary cursor-pointer">
<DotsHorizontalIcon className="h-5 w-5 text-th-primary" />
</Menu.Button>
<Transition
show={open}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
static
className="z-40 absolute right-0 w-48 mt-2 origin-top-right border border-th-primary bg-th-bkg-2 divide-y divide-th-fgd-1 rounded-md shadow-lg outline-none"
>
{WALLET_PROVIDERS.map(({ name, url }) => (
<Menu.Item key={name}>
<button
className="p-4 w-full text-left flex items-center hover:text-th-primary"
onClick={() => handleSelectProvider(url)}
>
{providerUrl === url ? (
<CheckCircleIcon className="h-4 w-4 mr-2" />
) : null}{' '}
{name}
</button>
</Menu.Item>
))}
</Menu.Items>
</Transition>
</>
)}
</Menu>
)
}

View File

@ -18,13 +18,9 @@ const WithdrawModal = ({ isOpen, onClose }) => {
const { connection, programId } = useConnection()
const walletAccounts = useMangoStore((s) => s.wallet.balances)
const actions = useMangoStore((s) => s.actions)
const withdrawAccounts = useMemo(
() =>
walletAccounts.filter((acc) =>
const withdrawAccounts = useMemo(() => walletAccounts.filter((acc) =>
Object.values(symbols).includes(acc.account.mint.toString())
),
[symbols, walletAccounts]
)
),[symbols, walletAccounts])
const [selectedAccount, setSelectedAccount] = useState(withdrawAccounts[0])
const mintAddress = useMemo(() => selectedAccount?.account.mint.toString(), [
selectedAccount,

View File

@ -1,39 +1,69 @@
import { useEffect } from 'react'
import { useEffect, useMemo } from 'react'
import Wallet from '@project-serum/sol-wallet-adapter'
import useLocalStorageState from './useLocalStorageState'
import useMangoStore, { INITIAL_STATE } from '../stores/useMangoStore'
import { notify } from '../utils/notifications'
import {
PhantomWalletAdapter,
SolletExtensionAdapter,
} from '../utils/wallet-adapters'
const ASSET_URL =
'https://cdn.jsdelivr.net/gh/solana-labs/oyster@main/assets/wallets'
export const WALLET_PROVIDERS = [
{ name: 'sollet.io', url: 'https://www.sollet.io' },
{
name: 'Sollet.io',
url: 'https://www.sollet.io',
icon: `${ASSET_URL}/sollet.svg`,
},
{
name: 'Sollet Extension',
url: 'https://www.sollet.io/extension',
icon: `${ASSET_URL}/sollet.svg`,
adapter: SolletExtensionAdapter as any,
},
{
name: 'Phantom',
url: 'https://www.phantom.app',
icon: `https://www.phantom.app/img/logo.png`,
adapter: PhantomWalletAdapter,
},
]
const ENDPOINT = process.env.CLUSTER ? process.env.CLUSTER : 'mainnet-beta'
export default function useWallet() {
const setMangoStore = useMangoStore((state) => state.set)
const { current: wallet, connected } = useMangoStore((state) => state.wallet)
const { current: wallet, connected, providerUrl } = useMangoStore(
(state) => state.wallet
)
const endpoint = useMangoStore((state) => state.connection.endpoint)
const fetchWalletBalances = useMangoStore(
(s) => s.actions.fetchWalletBalances
)
const [savedProviderUrl] = useLocalStorageState(
const [savedProviderUrl, setSavedProviderUrl] = useLocalStorageState(
'walletProvider',
'https://www.sollet.io'
)
const providerUrl = savedProviderUrl
? savedProviderUrl
: 'https://www.sollet.io'
useEffect(() => {
if (wallet) return
console.log('creating wallet', endpoint)
if (providerUrl !== savedProviderUrl) {
setSavedProviderUrl(providerUrl || savedProviderUrl)
setMangoStore((state) => {
state.wallet.providerUrl = providerUrl || savedProviderUrl
})
}
}, [providerUrl, savedProviderUrl, setSavedProviderUrl])
useEffect(() => {
if (!providerUrl) return
const provider = WALLET_PROVIDERS.find(({ url }) => url === providerUrl)
const newWallet = new (provider?.adapter || Wallet)(providerUrl, endpoint)
console.log('wallet', newWallet)
const newWallet = new Wallet(providerUrl, ENDPOINT)
setMangoStore((state) => {
state.wallet.current = newWallet
})
}, [endpoint, connected])
}, [endpoint, providerUrl, setMangoStore])
useEffect(() => {
if (!wallet) return
@ -64,16 +94,20 @@ export default function useWallet() {
})
})
return () => {
wallet.disconnect()
if (wallet && wallet.connected) {
console.log('DISCONNECTING')
wallet.disconnect()
}
setMangoStore((state) => {
state.wallet = INITIAL_STATE.WALLET
})
}
}, [wallet])
}, [wallet, setMangoStore])
useEffect(() => {
fetchWalletBalances()
}, [connected, fetchWalletBalances])
return { wallet, connected }
return { connected, wallet }
}

View File

@ -10,8 +10,7 @@ import {
} from '@blockworks-foundation/mango-client'
import { SRM_DECIMALS } from '@project-serum/serum/lib/token-instructions'
import { AccountInfo, Connection, PublicKey } from '@solana/web3.js'
import { Wallet } from '@project-serum/sol-wallet-adapter'
import { EndpointInfo } from '../@types/types'
import { EndpointInfo, WalletAdapter } from '../@types/types'
import { getOwnedTokenAccounts } from '../utils/tokens'
export const ENDPOINTS: EndpointInfo[] = [
@ -34,6 +33,7 @@ const DEFAULT_MANGO_GROUP_NAME = 'BTC_ETH_USDT'
export const INITIAL_STATE = {
WALLET: {
providerUrl: null,
connected: false,
current: null,
balances: [],
@ -95,8 +95,9 @@ interface MangoStore extends State {
tradeType: 'Market' | 'Limit'
}
wallet: {
providerUrl: string
connected: boolean
current: Wallet
current: WalletAdapter | undefined
balances: Array<{ account: any; publicKey: PublicKey }>
srmAccountsForOwner: any[]
contributedSrm: number

View File

@ -0,0 +1,2 @@
export * from './phantom'
export * from './sollet-extension'

View File

@ -0,0 +1,102 @@
import EventEmitter from 'eventemitter3'
import { PublicKey, Transaction } from '@solana/web3.js'
import { notify } from '../../utils/notifications'
import { DEFAULT_PUBLIC_KEY, WalletAdapter } from '../../@types/types'
type PhantomEvent = 'disconnect' | 'connect'
type PhantomRequestMethod =
| 'connect'
| 'disconnect'
| 'signTransaction'
| 'signAllTransactions'
interface PhantomProvider {
publicKey?: PublicKey
isConnected?: boolean
autoApprove?: boolean
signTransaction: (transaction: Transaction) => Promise<Transaction>
signAllTransactions: (transactions: Transaction[]) => Promise<Transaction[]>
connect: () => Promise<void>
disconnect: () => Promise<void>
on: (event: PhantomEvent, handler: (args: any) => void) => void
request: (method: PhantomRequestMethod, params: any) => Promise<any>
listeners: (event: PhantomEvent) => (() => void)[]
}
export class PhantomWalletAdapter
extends EventEmitter
implements WalletAdapter {
constructor() {
super()
this.connect = this.connect.bind(this)
}
private get _provider(): PhantomProvider | undefined {
if ((window as any)?.solana?.isPhantom) {
return (window as any).solana
}
return undefined
}
private _handleConnect = (...args) => {
this.emit('connect', ...args)
}
private _handleDisconnect = (...args) => {
this.emit('disconnect', ...args)
}
get connected() {
return this._provider?.isConnected || false
}
get autoApprove() {
return this._provider?.autoApprove || false
}
async signAllTransactions(
transactions: Transaction[]
): Promise<Transaction[]> {
if (!this._provider) {
return transactions
}
return this._provider.signAllTransactions(transactions)
}
get publicKey() {
return this._provider?.publicKey || DEFAULT_PUBLIC_KEY
}
async signTransaction(transaction: Transaction) {
if (!this._provider) {
return transaction
}
return this._provider.signTransaction(transaction)
}
connect() {
if (!this._provider) {
window.open('https://phantom.app/', '_blank')
notify({
message: 'Connection Error',
description: 'Please install Phantom wallet',
})
return
}
if (!this._provider.listeners('connect').length) {
this._provider?.on('connect', this._handleConnect)
}
if (!this._provider.listeners('disconnect').length) {
this._provider?.on('disconnect', this._handleDisconnect)
}
return this._provider?.connect()
}
disconnect() {
if (this._provider) {
this._provider.disconnect()
}
}
}

View File

@ -0,0 +1,19 @@
import Wallet from '@project-serum/sol-wallet-adapter'
import { notify } from '../../utils/notifications'
export function SolletExtensionAdapter(_, network) {
const sollet = (window as any).sollet
if (sollet) {
return new Wallet(sollet, network)
}
return {
on: () => {},
connect: () => {
notify({
message: 'Sollet Extension Error',
description: 'Please install the Sollet Extension for Chrome',
})
},
}
}