diff --git a/README.md b/README.md index b309ef52..ea275636 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,44 @@ Then, test that css transpiling is working: ## ⚠️ Warning Any content produced by Solana, or developer resources that Solana provides, are for educational and inspiration purposes only. Solana does not encourage, induce or sanction the deployment of any such applications in violation of applicable laws or regulations. + +# Disclaimer + +All claims, content, designs, algorithms, estimates, roadmaps, +specifications, and performance measurements described in this project +are done with the Solana Foundation's ("SF") best efforts. It is up to +the reader to check and validate their accuracy and truthfulness. +Furthermore nothing in this project constitutes a solicitation for +investment. + +Any content produced by SF or developer resources that SF provides, are +for educational and inspiration purposes only. SF does not encourage, +induce or sanction the deployment, integration or use of any such +applications (including the code comprising the Solana blockchain +protocol) in violation of applicable laws or regulations and hereby +prohibits any such deployment, integration or use. This includes use of +any such applications by the reader (a) in violation of export control +or sanctions laws of the United States or any other applicable +jurisdiction, (b) if the reader is located in or ordinarily resident in +a country or territory subject to comprehensive sanctions administered +by the U.S. Office of Foreign Assets Control (OFAC), or (c) if the +reader is or is working on behalf of a Specially Designated National +(SDN) or a person subject to similar blocking or denied party +prohibitions. + +The reader should be aware that U.S. export control and sanctions laws +prohibit U.S. persons (and other persons that are subject to such laws) +from transacting with persons in certain countries and territories or +that are on the SDN list. As a project based primarily on open-source +software, it is possible that such sanctioned persons may nevertheless +bypass prohibitions, obtain the code comprising the Solana blockchain +protocol (or other project code or applications) and deploy, integrate, +or otherwise use it. Accordingly, there is a risk to individuals that +other persons using the Solana blockchain protocol may be sanctioned +persons and that transactions with such persons would be a violation of +U.S. export controls and sanctions law. This risk applies to +individuals, organizations, and other ecosystem participants that +deploy, integrate, or use the Solana blockchain protocol code directly +(e.g., as a node operator), and individuals that transact on the Solana +blockchain through light clients, third party interfaces, and/or wallet +software. diff --git a/packages/arweave-push/package.json b/packages/arweave-push/package.json index 55b0c41e..a806db64 100644 --- a/packages/arweave-push/package.json +++ b/packages/arweave-push/package.json @@ -24,7 +24,7 @@ "busboy": "^0.3.0", "escape-html": "^1.0.3", "arweave": "1.10.13", - "@solana/web3.js": "^1.5.0", + "@solana/web3.js": "^1.22.0", "mime-types": "2.1.30", "node-fetch": "2.6.1", "coingecko-api": "1.0.10" diff --git a/packages/bridge-sdk/package.json b/packages/bridge-sdk/package.json index 8edf4fbc..9398a730 100644 --- a/packages/bridge-sdk/package.json +++ b/packages/bridge-sdk/package.json @@ -16,12 +16,10 @@ }, "dependencies": { "@babel/preset-typescript": "^7.13.0", - "@oyster/common": "0.0.1", + "@oyster/common": "0.0.2", "@solana/spl-token": "0.0.13", "@solana/spl-token-swap": "0.1.0", - "@solana/wallet-base": "0.0.1", - "@solana/wallet-ledger": "0.0.1", - "@solana/web3.js": "^1.5.0", + "@solana/web3.js": "^1.22.0", "bignumber.js": "^9.0.1", "bn.js": "^5.1.3", "bs58": "^4.0.1", diff --git a/packages/bridge-sdk/src/bridge/transfer/fromSolana.ts b/packages/bridge-sdk/src/bridge/transfer/fromSolana.ts index 4c231794..a932089a 100644 --- a/packages/bridge-sdk/src/bridge/transfer/fromSolana.ts +++ b/packages/bridge-sdk/src/bridge/transfer/fromSolana.ts @@ -1,10 +1,14 @@ -import { programIds, sendTransactionWithRetry, sleep } from '@oyster/common'; -import { WalletAdapter } from '@solana/wallet-base'; +import { + programIds, + sendTransactionWithRetry, + sleep, + WalletSigner, +} from '@oyster/common'; import { ethers } from 'ethers'; import { WormholeFactory } from '../../contracts/WormholeFactory'; import { bridgeAuthorityKey } from './../helpers'; import { Connection, PublicKey, SystemProgram } from '@solana/web3.js'; -import { Token } from '@solana/spl-token'; +import { Token, u64 } from '@solana/spl-token'; import { ProgressUpdate, TransferRequest } from './interface'; import BN from 'bn.js'; import { createLockAssetInstruction } from '../lock'; @@ -13,7 +17,7 @@ import { SolanaBridge } from '../../core'; export const fromSolana = async ( connection: Connection, - wallet: WalletAdapter, + wallet: WalletSigner, request: TransferRequest, provider: ethers.providers.Web3Provider, setProgress: (update: ProgressUpdate) => void, @@ -62,20 +66,23 @@ export const fromSolana = async ( return; } + const amountBN = ethers.utils.parseUnits( + request.amount.toString(), + request.info.decimals || 0, + ); + const amountBI = BigInt(amountBN.toString()) + let group = 'Initiate transfer'; const programs = programIds(); const bridgeId = programs.wormhole.pubkey; const authorityKey = await bridgeAuthorityKey(bridgeId); - const precision = Math.pow(10, request.info?.decimals || 0); - const amount = Math.floor(request.amount * precision); - let { ix: lock_ix, transferKey } = await createLockAssetInstruction( authorityKey, wallet.publicKey, new PublicKey(request.info.address), new PublicKey(request.info.mint), - new BN(amount), + new BN(amountBN.toString()), request.to, request.recipient, { @@ -93,7 +100,7 @@ export const fromSolana = async ( authorityKey, wallet.publicKey, [], - amount, + new u64(amountBI.toString(16), 16), ); setProgress({ diff --git a/packages/bridge-sdk/src/bridge/transfer/toSolana.ts b/packages/bridge-sdk/src/bridge/transfer/toSolana.ts index 57be58aa..e48430f2 100644 --- a/packages/bridge-sdk/src/bridge/transfer/toSolana.ts +++ b/packages/bridge-sdk/src/bridge/transfer/toSolana.ts @@ -6,6 +6,7 @@ import { TokenAccountParser, ParsedAccount, createAssociatedTokenAccountInstruction, + WalletSigner, } from '@oyster/common'; import { ethers } from 'ethers'; import { ERC20Factory } from '../../contracts/ERC20Factory'; @@ -20,17 +21,21 @@ import { } from '@solana/web3.js'; import { AccountInfo } from '@solana/spl-token'; import { TransferRequest, ProgressUpdate } from './interface'; -import { WalletAdapter } from '@solana/wallet-base'; import { BigNumber } from 'bignumber.js'; export const toSolana = async ( connection: Connection, - wallet: WalletAdapter, + wallet: WalletSigner, request: TransferRequest, provider: ethers.providers.Web3Provider, setProgress: (update: ProgressUpdate) => void, ) => { - if (!request.asset || !request.amount || !request.info) { + if ( + !request.asset || + !request.amount || + !request.info || + !request.info.address + ) { return; } const walletName = 'MetaMask'; @@ -68,6 +73,8 @@ export const toSolana = async ( const group = 'Initiate transfer'; try { + let mintKey: PublicKey; + const bridgeId = programIds().wormhole.pubkey; const authority = await bridgeAuthorityKey(bridgeId); const meta: AssetMeta = { @@ -75,14 +82,21 @@ export const toSolana = async ( address: request.info?.assetAddress, chain: request.from, }; - const mintKey = await wrappedAssetMintKey(bridgeId, authority, meta); + if (request.info.mint) { + mintKey = new PublicKey(request.info.mint); + } else { + mintKey = await wrappedAssetMintKey(bridgeId, authority, meta); + } const recipientKey = cache .byParser(TokenAccountParser) .map(key => { let account = cache.get(key) as ParsedAccount; - if (account?.info.mint.toBase58() === mintKey.toBase58()) { + if ( + account?.info.mint.toBase58() === mintKey.toBase58() && + account?.info.owner.toBase58() === wallet?.publicKey?.toBase58() + ) { return key; } @@ -114,6 +128,7 @@ export const toSolana = async ( if (!accounts.array[0]) { // create mint using wormhole instruction + instructions.push( await createWrappedAssetInstruction( meta, @@ -164,14 +179,14 @@ export const toSolana = async ( }, // approves assets for transfer approve: async (request: TransferRequest) => { - if (!request.asset) { + if (!request.info?.address) { return; } const group = 'Approve assets'; try { if (request.info?.allowance.lt(amountBN)) { - let e = ERC20Factory.connect(request.asset, signer); + let e = ERC20Factory.connect(request.info.address, signer); setProgress({ message: `Waiting for ${walletName} approval`, type: 'user', @@ -180,7 +195,8 @@ export const toSolana = async ( }); let res = await e.approve(programIds().wormhole.bridge, amountBN); setProgress({ - message: 'Waiting for ETH transaction to be mined... (Up to few min.)', + message: + 'Waiting for ETH transaction to be mined... (Up to few min.)', type: 'wait', group, step: counter++, @@ -216,7 +232,7 @@ export const toSolana = async ( lock: async (request: TransferRequest) => { if ( !amountBN || - !request.asset || + !request.info?.address || !request.recipient || !request.to || !request.info @@ -235,7 +251,7 @@ export const toSolana = async ( step: counter++, }); let res = await wh.lockAssets( - request.asset, + request.info.address, amountBN, request.recipient, request.to, @@ -243,7 +259,8 @@ export const toSolana = async ( false, ); setProgress({ - message: 'Waiting for ETH transaction to be mined... (Up to few min.)', + message: + 'Waiting for ETH transaction to be mined... (Up to few min.)', type: 'wait', group, step: counter++, diff --git a/packages/bridge/package.json b/packages/bridge/package.json index 0e876ece..ea55c651 100644 --- a/packages/bridge/package.json +++ b/packages/bridge/package.json @@ -5,17 +5,14 @@ "@ant-design/icons": "^4.4.0", "@babel/preset-typescript": "^7.12.13", "@craco/craco": "^5.7.0", - "@oyster/common": "0.0.1", + "@oyster/common": "0.0.2", "@project-serum/serum": "^0.13.11", - "@project-serum/sol-wallet-adapter": "^0.1.4", "@react-three/drei": "^3.8.0", "@solana/spl-token": "0.0.13", "@solana/spl-token-registry": "^0.2.0", "@solana/spl-token-swap": "0.1.0", - "@solana/wallet-base": "0.0.1", - "@solana/wallet-ledger": "0.0.1", "@solana/bridge-sdk": "0.0.1", - "@solana/web3.js": "^1.5.0", + "@solana/web3.js": "^1.22.0", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.5.0", "@testing-library/user-event": "^7.2.1", diff --git a/packages/bridge/public/CNAME b/packages/bridge/public/CNAME index b10007ab..9703c0dd 100644 --- a/packages/bridge/public/CNAME +++ b/packages/bridge/public/CNAME @@ -1 +1 @@ -www.wormholebridge.com +v1.wormholebridge.com diff --git a/packages/bridge/public/home/main-logo.svg b/packages/bridge/public/home/main-logo.svg index 226f4440..1153d677 100644 --- a/packages/bridge/public/home/main-logo.svg +++ b/packages/bridge/public/home/main-logo.svg @@ -12,7 +12,7 @@ - + diff --git a/packages/bridge/src/components/CurrentUserWalletBadge/index.tsx b/packages/bridge/src/components/CurrentUserWalletBadge/index.tsx index be4358b1..f7ed0917 100644 --- a/packages/bridge/src/components/CurrentUserWalletBadge/index.tsx +++ b/packages/bridge/src/components/CurrentUserWalletBadge/index.tsx @@ -1,12 +1,11 @@ import React from 'react'; -import { useWallet, WALLET_PROVIDERS } from '@oyster/common'; -import { shortenAddress } from '@oyster/common'; +import { shortenAddress, useWallet } from '@oyster/common'; export const CurrentUserWalletBadge = (props: { showDisconnect?: boolean }) => { - const { wallet, disconnect } = useWallet(); + const { wallet, publicKey, disconnect } = useWallet(); - if (!wallet || !wallet.publicKey) { + if (!wallet || !publicKey) { return null; } @@ -17,10 +16,10 @@ export const CurrentUserWalletBadge = (props: { showDisconnect?: boolean }) => { alt={'icon'} width={20} height={20} - src={WALLET_PROVIDERS.filter(p => p.name === 'Sollet')[0]?.icon} + src={wallet.icon} style={{ marginRight: 8 }} /> - {shortenAddress(`${wallet.publicKey}`)} + {shortenAddress(`${publicKey}`)} {props.showDisconnect && ( disconnect()}> X diff --git a/packages/bridge/src/components/Input/style.less b/packages/bridge/src/components/Input/style.less index 6333a1c5..b82b9d22 100644 --- a/packages/bridge/src/components/Input/style.less +++ b/packages/bridge/src/components/Input/style.less @@ -119,7 +119,6 @@ font-weight: normal; font-size: 16px; line-height: 21px; - color: @tungsten-40; } .input-chain{ margin-top: 13px; @@ -151,7 +150,9 @@ padding: 0 36px 0 36px; font-size: 32px; line-height: 39px; - color: @tungsten-80; + ::placeholder { + color: @tungsten-40; + } height: 100%; } .input-select { @@ -181,7 +182,7 @@ } } -.dashed-input-container.right { +.dashed-input-container { & > button.ant-btn:not(.ant-dropdown-trigger) { text-transform: uppercase; color: white; diff --git a/packages/bridge/src/components/Layout/index.tsx b/packages/bridge/src/components/Layout/index.tsx index 69034b88..02fe1828 100644 --- a/packages/bridge/src/components/Layout/index.tsx +++ b/packages/bridge/src/components/Layout/index.tsx @@ -24,9 +24,10 @@ export const AppLayout = React.memo((props: any) => { {props.children}
-
- -
+
diff --git a/packages/bridge/src/components/RecentTransactionsTable/index.tsx b/packages/bridge/src/components/RecentTransactionsTable/index.tsx index a11efe9e..86466f79 100644 --- a/packages/bridge/src/components/RecentTransactionsTable/index.tsx +++ b/packages/bridge/src/components/RecentTransactionsTable/index.tsx @@ -34,7 +34,9 @@ export const RecentTransactionsTable = (props: { showUserTransactions?: boolean; tokenAccounts: TokenAccount[]; }) => { - const { loading: loadingTransfers, transfers } = useWormholeTransactions(props.tokenAccounts); + const { loading: loadingTransfers, transfers } = useWormholeTransactions( + props.tokenAccounts, + ); const { provider } = useEthereum(); const bridge = useBridge(); @@ -338,7 +340,6 @@ export const RecentTransactionsTable = (props: { scrollToFirstRowOnChange: false, x: 900, }} - dataSource={transfers.sort((a, b) => b.date - a.date)} columns={userColumns} loading={loadingTransfers} diff --git a/packages/bridge/src/components/Settings/index.tsx b/packages/bridge/src/components/Settings/index.tsx index 542881d7..44ede051 100644 --- a/packages/bridge/src/components/Settings/index.tsx +++ b/packages/bridge/src/components/Settings/index.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { Button, Select } from 'antd'; -import { contexts } from '@oyster/common'; +import { contexts, useWallet } from '@oyster/common'; -const { useWallet, WALLET_PROVIDERS } = contexts.Wallet; const { ENDPOINTS, useConnectionConfig } = contexts.Connection; export const Settings = () => { diff --git a/packages/bridge/src/components/TokenSelectModal/index.tsx b/packages/bridge/src/components/TokenSelectModal/index.tsx index f31d86ce..ffd2070d 100644 --- a/packages/bridge/src/components/TokenSelectModal/index.tsx +++ b/packages/bridge/src/components/TokenSelectModal/index.tsx @@ -7,8 +7,12 @@ import { Input, Modal } from 'antd'; import { useEthereum } from '../../contexts'; import { TokenDisplay } from '../TokenDisplay'; import { ASSET_CHAIN } from '../../utils/assets'; -import { useConnectionConfig } from '@oyster/common'; -import { filterModalEthTokens, filterModalSolTokens } from '../../utils/assets'; +import { + useConnectionConfig, + useUserAccounts, + useWallet +} from '@oyster/common'; +import { TokenInfo } from '@solana/spl-token-registry'; export const TokenSelectModal = (props: { onSelectToken: (token: string) => void; @@ -17,31 +21,39 @@ export const TokenSelectModal = (props: { chain?: ASSET_CHAIN; showIconChain?: boolean; }) => { - const { tokens: ethTokens } = useEthereum(); - const { tokens: solTokens } = useConnectionConfig(); + const { tokenMap: ethTokenMap } = useEthereum(); + const { connected } = useWallet(); + const { tokenMap } = useConnectionConfig() + const {userAccounts} = useUserAccounts() const [isModalVisible, setIsModalVisible] = useState(false); const [search, setSearch] = useState(''); const inputRef = useRef(null); - const tokens = useMemo( - () => [ - ...filterModalEthTokens(ethTokens), - ...filterModalSolTokens(solTokens), - ], - [ethTokens, solTokens], - ); const tokenList = useMemo(() => { - if (tokens && search) { - return tokens.filter(token => { - return ( - (token.tags?.indexOf('longList') || -1) < 0 && - token.symbol.includes(search.toUpperCase()) - ); - }); + const tokens: any [] = []; + if (connected && userAccounts.length) { + userAccounts.forEach(async acc => { + const token = tokenMap.get(acc.info.mint.toBase58()) + if (token) { + if (!token.name.toLowerCase().includes("wormhole")){ + tokens.push({token, chain: ASSET_CHAIN.Ethereum}) + } else { + if (token.extensions?.address) { + const ethToken = ethTokenMap.get(token.extensions.address.toLowerCase()) + if (ethToken){ + const name = `${ethToken.name} (Wormhole)`; + tokens.push({token: {...ethToken, name}, chain: ASSET_CHAIN.Solana}) + } else { + console.log("Wormhole token without contract info: ", token) + } + } + } + } + }) } return tokens; - }, [tokens, search]); + }, [connected, userAccounts.length]); const showModal = () => { if (inputRef && inputRef.current) { @@ -54,73 +66,78 @@ export const TokenSelectModal = (props: { setIsModalVisible(false); }; const firstToken = useMemo(() => { - return tokens.find(el => el.address === props.asset); - }, [tokens, props.asset]); + return tokenList.find((el: any) => el.token.address === props.asset); + }, [tokenList, props.asset]); const delayedSearchChange = _.debounce(val => { setSearch(val); }); + const getTokenInfo = (token: TokenInfo | undefined, chain: ASSET_CHAIN | undefined) => { + let name = token?.name || ''; + let symbol = token?.symbol || ''; + return { name, symbol }; + } + const rowRender = (rowProps: { index: number; key: string; style: any }) => { - const token = tokenList[rowProps.index]; + const tokenObject = tokenList[rowProps.index] + const token = tokenObject.token; const mint = token.address; - return [ASSET_CHAIN.Solana, ASSET_CHAIN.Ethereum].map((chain, index) => { - return ( + const chain = tokenObject.chain + const { name , symbol } = getTokenInfo(token, chain); + return ( +
{ + props.onSelectToken(mint); + props.onChain(chain); + hideModal(); + }} + style={{ + ...rowProps.style, + cursor: 'pointer', + height: '70px', + top: `${rowProps.style.top}px`, + }} + >
{ - props.onSelectToken(mint); - props.onChain(chain); - hideModal(); - }} - style={{ - ...rowProps.style, - cursor: 'pointer', - height: '70px', - top: `${rowProps.style.top + 70 * index}px`, - }} + className="multichain-option-content" + style={{ position: 'relative' }} > +
- -
- {token.symbol} - {token.name} -
+ {symbol} + {name}
- ); - }); +
+ ); }; + const { name , symbol } = getTokenInfo(firstToken?.token, props.chain); return ( <> - {firstToken ? ( -
showModal()} - style={{ cursor: 'pointer' }} - > -
- -
-
{firstToken.symbol}
- +
showModal()} + style={{ cursor: 'pointer' }} + > +
+
- ) : null} +
{symbol}
+ +
hideModal()} @@ -144,7 +161,7 @@ export const TokenSelectModal = (props: { { const style: React.CSSProperties = { marginRight: 5 }; @@ -42,7 +42,7 @@ export const typeToIcon = (type: string, isLast: boolean) => { export const Transfer = () => { const connection = useConnection(); const bridge = useBridge(); - const { wallet, connected } = useWallet(); + const wallet = useWallet(); const { provider, tokenMap } = useEthereum(); const { userAccounts } = useUserAccounts(); const hasCorrespondingNetworks = useCorrectNetwork(); @@ -54,7 +54,7 @@ export const Transfer = () => { setLastTypedAccount, } = useTokenChainPairState(); - + const [popoverVisible, setPopoverVisible] = useState(true) const [request, setRequest] = useState({ from: ASSET_CHAIN.Ethereum, @@ -85,8 +85,11 @@ export const Transfer = () => { }); }, [A, B, mintAddress, A.info]); - - const tokenAccounts = useMemo(() => userAccounts.filter(u => u.info.mint.toBase58() === request.info?.mint), [request.info?.mint]) + const tokenAccounts = useMemo( + () => + userAccounts.filter(u => u.info.mint.toBase58() === request.info?.mint), + [request.info?.mint], + ); return ( <> @@ -111,22 +114,37 @@ export const Transfer = () => { }} className={'left'} /> - + + { className={'transfer-button'} type="primary" size="large" - disabled={!(A.amount && B.amount) || !connected || !provider} + disabled={!(A.amount && B.amount) || !wallet.connected || !provider} onClick={async () => { if (!wallet || !provider) { return; diff --git a/packages/bridge/src/components/Transfer/style.less b/packages/bridge/src/components/Transfer/style.less index e9825b9e..97796e84 100644 --- a/packages/bridge/src/components/Transfer/style.less +++ b/packages/bridge/src/components/Transfer/style.less @@ -6,16 +6,20 @@ align-items: center; margin-bottom: 10px; } - +.ant-popover-title { + text-align: right; +} +.ant-popover-inner-content { + text-align: center; +} .swap-button { border-radius: 2em; - width: 40px; - height: 40px; - font-size: 19px; - top: 253px; - color: black; + width: 60px; + height: 60px; + font-size: 30px; + top: 150px; & > span:after { - content: '⇆'; + content: '➤'; padding-left: 0; } } diff --git a/packages/bridge/src/contexts/chainPair.tsx b/packages/bridge/src/contexts/chainPair.tsx index e7da0d4f..6a176df8 100644 --- a/packages/bridge/src/contexts/chainPair.tsx +++ b/packages/bridge/src/contexts/chainPair.tsx @@ -31,6 +31,7 @@ import { import { useBridge } from './bridge'; import { PublicKey } from '@solana/web3.js'; import { ethers } from 'ethers'; +import { deriveERC20Address } from '../utils/helpers'; export interface TokenChainContextState { info?: TransferRequestInfo; @@ -71,7 +72,7 @@ export const toChainSymbol = (chain: number | null) => { }; function getDefaultTokens(tokens: TokenInfo[], search: string) { - let defaultChain = 'ETH'; + let defaultChain = 'SOL'; let defaultToken = tokens[0].symbol; const nameToToken = tokens.reduce((map, item) => { @@ -149,7 +150,7 @@ export const useCurrencyLeg = (mintAddress: string) => { solToken = solTokens.find(t => t.address === mintKeyAddress); if (!solToken) { symbol = ethToken.symbol; - decimals = ethToken.decimals; + decimals = Math.min(ethToken.decimals, 9); } } else { setInfo(defaultCoinInfo); @@ -167,6 +168,7 @@ export const useCurrencyLeg = (mintAddress: string) => { const currentAccount = userAccounts?.find( a => a.info.mint.toBase58() === (solToken?.address || mintKeyAddress), ); + const assetMeta = await bridge?.fetchAssetMeta( new PublicKey(solToken?.address || mintKeyAddress), ); @@ -186,25 +188,45 @@ export const useCurrencyLeg = (mintAddress: string) => { assetAddress: assetMeta.address, mint: solToken?.address || mintKeyAddress, }; + // console.log({ info }, 'sol'); setInfo(info); } if (chain === ASSET_CHAIN.Ethereum) { - if (!ethToken) { + if (!solToken && !ethToken) { setInfo(defaultCoinInfo); return; } + let derived = false; let signer = provider.getSigner(); - let e = WrappedAssetFactory.connect(mintAddress, provider); + const ethBridgeAddress = programIds().wormhole.bridge; + let b = WormholeFactory.connect(ethBridgeAddress, provider); + mintKeyAddress = mintAddress; + + if (!ethToken && solToken) { + mintKeyAddress = deriveERC20Address(new PublicKey(mintAddress)); + if (mintKeyAddress) { + mintKeyAddress = `0x${mintKeyAddress}`; + derived = true; + } + } + + let isWrapped = await b.isWrappedAsset(mintKeyAddress); + if (derived && !isWrapped) { + setInfo(defaultCoinInfo); + return; + } + + let e = WrappedAssetFactory.connect(mintKeyAddress, provider); + let addr = await signer.getAddress(); let decimals = await e.decimals(); let symbol = await e.symbol(); - const ethBridgeAddress = programIds().wormhole.bridge; let allowance = await e.allowance(addr, ethBridgeAddress); - const assetAddress = Buffer.from(mintAddress.slice(2), 'hex'); + const assetAddress = Buffer.from(mintKeyAddress.slice(2), 'hex'); let info = { - address: mintAddress, + address: mintKeyAddress, name: symbol, balance: new BigNumber(0), allowance, @@ -212,14 +234,11 @@ export const useCurrencyLeg = (mintAddress: string) => { isWrapped: false, chainID: ASSET_CHAIN.Ethereum, assetAddress, - mint: '', + mint: (solToken && derived && mintAddress) || '', }; - - let b = WormholeFactory.connect(ethBridgeAddress, provider); - let isWrapped = await b.isWrappedAsset(mintAddress); if (isWrapped) { info.chainID = await e.assetChain(); - info.assetAddress = Buffer.from(addr.slice(2), 'hex'); + // info.assetAddress = Buffer.from(addr.slice(2), 'hex'); info.isWrapped = true; } @@ -240,7 +259,7 @@ export const useCurrencyLeg = (mintAddress: string) => { }); } - //console.log({ info }); + // console.log({ info }, 'eth'); setInfo(info); } })(); diff --git a/packages/bridge/src/contexts/ethereum.tsx b/packages/bridge/src/contexts/ethereum.tsx index 6efd80c3..1b1b5df6 100644 --- a/packages/bridge/src/contexts/ethereum.tsx +++ b/packages/bridge/src/contexts/ethereum.tsx @@ -8,8 +8,7 @@ import React, { useState, } from 'react'; -import { useWallet, useLocalStorageState } from '@oyster/common'; -import { WalletAdapter } from '@solana/wallet-base'; +import { useLocalStorageState, useWallet, SignerWalletAdapter } from '@oyster/common'; import { TokenList, TokenInfo } from '@uniswap/token-lists'; import { ethers } from 'ethers'; import { MetamaskWalletAdapter } from '../wallet-adapters/metamask'; @@ -85,6 +84,7 @@ export const EthereumProvider: FunctionComponent = ({ children }) => { const wallet = useMemo( function () { if (walletProvider) { + // @ts-ignore return new walletProvider.adapter() as WalletAdapter; } }, @@ -158,6 +158,7 @@ export const EthereumProvider: FunctionComponent = ({ children }) => { setProvider(wallet.provider); setConnected(true); }); + // @ts-ignore wallet.on('disconnect', error => { setConnected(false); setAccounts([]); @@ -168,6 +169,7 @@ export const EthereumProvider: FunctionComponent = ({ children }) => { }); // @ts-ignore wallet.on('accountsChanged', accounts => { + // @ts-ignore if (!accounts || !accounts[0]) setConnected(false); }); // @ts-ignore diff --git a/packages/bridge/src/hooks/useWormholeAccounts.tsx b/packages/bridge/src/hooks/useWormholeAccounts.tsx index f192eb02..38a24892 100644 --- a/packages/bridge/src/hooks/useWormholeAccounts.tsx +++ b/packages/bridge/src/hooks/useWormholeAccounts.tsx @@ -156,9 +156,8 @@ const queryWrappedMetaAccounts = async ( if (asset.mint) { asset.amount = - asset.mint?.info.supply.toNumber() / - Math.pow(10, asset.mint?.info.decimals) || 0; - + parseInt(asset.mint?.info.supply.toString()) / + Math.pow(10, asset.mint?.info.decimals || 0); if (!asset.mint) { throw new Error('missing mint'); } @@ -167,7 +166,11 @@ const queryWrappedMetaAccounts = async ( connection.onAccountChange(asset.mint?.pubkey, acc => { cache.add(key, acc); asset.mint = cache.get(key); - asset.amount = asset.mint?.info.supply.toNumber() || 0; + if (asset.mint) { + asset.amount = + asset.mint?.info.supply.toNumber() / + Math.pow(10, asset.mint?.info.decimals || 0); + } setExternalAssets([...assets.values()]); }); diff --git a/packages/bridge/src/hooks/useWormholeTransactions.tsx b/packages/bridge/src/hooks/useWormholeTransactions.tsx index c5adadd3..60284c0d 100644 --- a/packages/bridge/src/hooks/useWormholeTransactions.tsx +++ b/packages/bridge/src/hooks/useWormholeTransactions.tsx @@ -6,7 +6,6 @@ import { useConnection, useConnectionConfig, useUserAccounts, - useWallet, } from '@oyster/common'; import { POSTVAA_INSTRUCTION, diff --git a/packages/bridge/src/routes.tsx b/packages/bridge/src/routes.tsx index ad27418d..ccdce6d4 100644 --- a/packages/bridge/src/routes.tsx +++ b/packages/bridge/src/routes.tsx @@ -11,6 +11,7 @@ import { HelpView, ProofOfAssetsView, FaqView, + RenbtcDebugView, } from './views'; import { CoingeckoProvider } from './contexts/coingecko'; import { BridgeProvider } from './contexts/bridge'; @@ -52,6 +53,11 @@ export function Routes() { path="/faucet" children={} /> + {/*}*/} + {/*/>*/} diff --git a/packages/bridge/src/types/sol-wallet-adapter.d.ts b/packages/bridge/src/types/sol-wallet-adapter.d.ts deleted file mode 100644 index 41acf5df..00000000 --- a/packages/bridge/src/types/sol-wallet-adapter.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '@project-serum/sol-wallet-adapter' { - const adapter: any; - export = adapter; -} diff --git a/packages/bridge/src/utils/assets.ts b/packages/bridge/src/utils/assets.ts index 9920dc11..0e9a3475 100644 --- a/packages/bridge/src/utils/assets.ts +++ b/packages/bridge/src/utils/assets.ts @@ -50,11 +50,21 @@ export const chainToName = (chain?: ASSET_CHAIN) => { }; const EXCLUDED_COMMON_TOKENS = ['usdt', 'usdc']; -const EXCLUDED_SPL_TOKENS = ['sol', 'srm', ...EXCLUDED_COMMON_TOKENS]; +const EXCLUDED_SPL_TOKENS = [ + 'sol', + 'srm', + 'ray', + 'oxy', + 'mer', + 'maps', + ...EXCLUDED_COMMON_TOKENS, +]; export const filterModalSolTokens = (tokens: TokenInfo[]) => { return tokens.filter( - token => EXCLUDED_SPL_TOKENS.indexOf(token.symbol.toLowerCase()) < 0, + token => + EXCLUDED_SPL_TOKENS.indexOf(token.symbol.toLowerCase()) < 0 && + !token.name.includes('(Sollet)'), ); }; const EXCLUDED_ETH_TOKENS = [...EXCLUDED_COMMON_TOKENS]; diff --git a/packages/bridge/src/utils/helpers.ts b/packages/bridge/src/utils/helpers.ts new file mode 100644 index 00000000..562ec9ce --- /dev/null +++ b/packages/bridge/src/utils/helpers.ts @@ -0,0 +1,19 @@ +import { PublicKey } from '@solana/web3.js'; +import { keccak256 } from 'ethers/utils'; +import { programIds } from '@oyster/common'; + +export function deriveERC20Address(key: PublicKey) { + const ethBridgeAddress = programIds().wormhole.bridge; + const ethWrappedMaster = programIds().wormhole.wrappedMaster; + let hashData = '0xff' + ethBridgeAddress.slice(2); + hashData += keccak256(Buffer.concat([new Buffer([1]), key.toBuffer()])).slice( + 2, + ); // asset_id + hashData += keccak256( + '0x3d602d80600a3d3981f3363d3d373d3d3d363d73' + + ethWrappedMaster + + '5af43d82803e903d91602b57fd5bf3', + ).slice(2); // Bytecode + + return keccak256(hashData).slice(26); +} diff --git a/packages/bridge/src/views/faucet/index.tsx b/packages/bridge/src/views/faucet/index.tsx index 01b3e1aa..e3833d75 100644 --- a/packages/bridge/src/views/faucet/index.tsx +++ b/packages/bridge/src/views/faucet/index.tsx @@ -2,29 +2,28 @@ import React, { useCallback } from 'react'; import { Card } from 'antd'; import { LAMPORTS_PER_SOL } from '@solana/web3.js'; import { LABELS } from '../../constants'; -import { contexts, utils, ConnectButton } from '@oyster/common'; +import { contexts, utils, ConnectButton, useWallet } from '@oyster/common'; const { useConnection } = contexts.Connection; -const { useWallet } = contexts.Wallet; const { notify } = utils; export const FaucetView = () => { const connection = useConnection(); - const { wallet } = useWallet(); + const { publicKey } = useWallet(); const airdrop = useCallback(() => { - if (!wallet?.publicKey) { + if (!publicKey) { return; } connection - .requestAirdrop(wallet.publicKey, 2 * LAMPORTS_PER_SOL) + .requestAirdrop(publicKey, 2 * LAMPORTS_PER_SOL) .then(() => { notify({ message: LABELS.ACCOUNT_FUNDED, type: 'success', }); }); - }, [wallet, wallet?.publicKey, connection]); + }, [publicKey, connection]); const bodyStyle: React.CSSProperties = { display: 'flex', diff --git a/packages/bridge/src/views/help/index.less b/packages/bridge/src/views/help/index.less index ce236e4b..a393bec1 100644 --- a/packages/bridge/src/views/help/index.less +++ b/packages/bridge/src/views/help/index.less @@ -24,5 +24,5 @@ max-width: 80%; margin: auto; margin-top: 20px; - text-align: justify; + text-align: center; } diff --git a/packages/bridge/src/views/help/index.tsx b/packages/bridge/src/views/help/index.tsx index 575f5ba5..c0ab9b84 100644 --- a/packages/bridge/src/views/help/index.tsx +++ b/packages/bridge/src/views/help/index.tsx @@ -29,8 +29,12 @@ export const HelpView = () => { - diff --git a/packages/bridge/src/views/home/index.tsx b/packages/bridge/src/views/home/index.tsx index 50c24fcd..de56636e 100644 --- a/packages/bridge/src/views/home/index.tsx +++ b/packages/bridge/src/views/home/index.tsx @@ -1,13 +1,8 @@ import anime from 'animejs'; import React from 'react'; -import { formatUSD, shortenAddress } from '@oyster/common'; import './itemStyle.less'; import './index.less'; import { Link } from 'react-router-dom'; -import { useWormholeAccounts } from '../../hooks/useWormholeAccounts'; -import { TokenDisplay } from '../../components/TokenDisplay'; -import { toChainSymbol } from '../../contexts/chainPair'; -import { AssetsTable } from '../../components/AssetsTable'; export const HomeView = () => { const handleDownArrow = () => { @@ -78,7 +73,7 @@ export const HomeView = () => {
- + {/* */} ); diff --git a/packages/bridge/src/views/index.tsx b/packages/bridge/src/views/index.tsx index 69437df5..2ed93914 100644 --- a/packages/bridge/src/views/index.tsx +++ b/packages/bridge/src/views/index.tsx @@ -4,3 +4,4 @@ export { TransferView } from './transfer'; export { HelpView } from './help'; export { FaqView } from './faq'; export { ProofOfAssetsView } from './proof-of-assets'; +export { RenbtcDebugView } from './renbtc-debug'; diff --git a/packages/bridge/src/views/renbtc-debug.tsx b/packages/bridge/src/views/renbtc-debug.tsx new file mode 100644 index 00000000..9be5b51a --- /dev/null +++ b/packages/bridge/src/views/renbtc-debug.tsx @@ -0,0 +1,185 @@ +import React, { useState } from 'react'; +import { + cache, + ConnectButton, + createAssociatedTokenAccountInstruction, + ExplorerLink, + getMultipleAccounts, + ParsedAccount, + programIds, + sendTransactionWithRetry, + TokenAccountParser, + useConnection, + useWallet, +} from '@oyster/common'; +import { PublicKey, TransactionInstruction, Account } from '@solana/web3.js'; +import { AccountInfo } from '@solana/spl-token'; + +export const RenbtcDebugView = () => { + const rentbtcMint = new PublicKey( + 'EK6iyvvqvQtsWYcySrZVHkXjCLX494r9PhnDWJaX1CPu', + ); + const wallet = useWallet(); + const connection = useConnection(); + const [ataa, setAtaa] = useState(''); + const [exists, setExists] = useState(false); + return ( + <> +
+
+ + + renBTC MintKey --{' '} + + + + Wallet Address -- {wallet.publicKey?.toBase58()} + + + + {ataa} : {exists ? 'Already Created' : 'Not created yet'} + + + +
+
+ + ); +}; diff --git a/packages/bridge/src/wallet-adapters/metamask.tsx b/packages/bridge/src/wallet-adapters/metamask.tsx index c2dee0fd..69419e86 100644 --- a/packages/bridge/src/wallet-adapters/metamask.tsx +++ b/packages/bridge/src/wallet-adapters/metamask.tsx @@ -1,12 +1,12 @@ import EventEmitter from 'eventemitter3'; import { PublicKey, Transaction } from '@solana/web3.js'; -import { notify } from '@oyster/common'; -import { WalletAdapter } from '@solana/wallet-base'; +import { notify, SignerWalletAdapter } from '@oyster/common'; import { ethers } from 'ethers'; +// @ts-ignore export class MetamaskWalletAdapter extends EventEmitter - implements WalletAdapter { + implements SignerWalletAdapter { _publicKey: PublicKey | null; _onProcess: boolean; _accounts: Array; @@ -42,7 +42,7 @@ export class MetamaskWalletAdapter return transactions; } - connect() { + async connect() { if (this._onProcess) { return; } @@ -94,7 +94,7 @@ export class MetamaskWalletAdapter }); } - disconnect() { + async disconnect() { if (this._provider) { this._publicKey = null; this._provider = null; diff --git a/packages/bridge/src/wallet-adapters/wallet-connect.tsx b/packages/bridge/src/wallet-adapters/wallet-connect.tsx index a5134289..2c2fa945 100644 --- a/packages/bridge/src/wallet-adapters/wallet-connect.tsx +++ b/packages/bridge/src/wallet-adapters/wallet-connect.tsx @@ -1,12 +1,13 @@ import EventEmitter from 'eventemitter3'; import { PublicKey, Transaction } from '@solana/web3.js'; -import { WalletAdapter } from '@solana/wallet-base'; +import { SignerWalletAdapter } from '@oyster/common'; import { ethers } from 'ethers'; import WalletConnectProvider from '@walletconnect/web3-provider'; +// @ts-ignore export class WalletConnectWalletAdapter extends EventEmitter - implements WalletAdapter { + implements SignerWalletAdapter { _publicKey: PublicKey | null; _onProcess: boolean; _accounts: Array; @@ -45,7 +46,7 @@ export class WalletConnectWalletAdapter return transactions; } - connect() { + async connect() { if (this._onProcess) { return; } @@ -96,7 +97,7 @@ export class WalletConnectWalletAdapter }); } - disconnect() { + async disconnect() { if (this._provider) { this._publicKey = null; this._walletProvider.disconnect(); diff --git a/packages/common/package.json b/packages/common/package.json index 2f90825f..921651d4 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@oyster/common", - "version": "0.0.1", + "version": "0.0.2", "description": "Oyster common utilities", "main": "dist/lib/index.js", "types": "dist/lib/index.d.ts", @@ -15,7 +15,7 @@ "node": ">=10" }, "scripts": { - "build": "tsc", + "build": "tsc && less-watch-compiler --run-once src/ dist/lib/", "start": "npm-run-all --parallel watch watch-css watch-css-src", "watch-css": "less-watch-compiler src/ dist/lib/", "watch-css-src": "less-watch-compiler src/ src/", @@ -26,12 +26,12 @@ }, "dependencies": { "@project-serum/serum": "^0.13.11", - "@project-serum/sol-wallet-adapter": "^0.1.4", "@solana/spl-token": "0.0.13", "@solana/spl-token-swap": "0.1.0", - "@solana/wallet-base": "0.0.1", - "@solana/wallet-ledger": "0.0.1", - "@solana/web3.js": "^1.5.0", + "@solana/wallet-adapter-base": "^0.6.0", + "@solana/wallet-adapter-react": "^0.12.6", + "@solana/wallet-adapter-wallets": "^0.10.1", + "@solana/web3.js": "^1.22.0", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.5.0", "@testing-library/user-event": "^7.2.1", diff --git a/packages/common/src/components/AppBar/index.tsx b/packages/common/src/components/AppBar/index.tsx index d1e79bf3..68c5e922 100644 --- a/packages/common/src/components/AppBar/index.tsx +++ b/packages/common/src/components/AppBar/index.tsx @@ -4,25 +4,24 @@ import { CurrentUserBadge } from '../CurrentUserBadge'; import { SettingOutlined } from '@ant-design/icons'; import { Settings } from '../Settings'; import { LABELS } from '../../constants/labels'; -import { ConnectButton } from '..'; -import { useWallet } from '../../contexts/wallet'; +import { ConnectButton } from '../ConnectButton'; +import { useWallet } from '../../contexts'; import './style.css'; + export const AppBar = (props: { left?: JSX.Element; right?: JSX.Element; useWalletBadge?: boolean; additionalSettings?: JSX.Element; }) => { - const { connected, wallet } = useWallet(); + const { connected } = useWallet(); const TopBar = (
{props.left} - {connected ? - ( - - ) - : ( + {connected ? ( + + ) : ( { +export interface ConnectButtonProps + extends ButtonProps, + React.RefAttributes { allowWalletChange?: boolean; } -export const ConnectButton = ( - props: ConnectButtonProps -) => { - const { connected, connect, select, provider } = useWallet(); +export const ConnectButton = (props: ConnectButtonProps) => { + const { wallet, connected, connect } = useWallet(); + const { setVisible } = useWalletModal(); + const open = useCallback(() => setVisible(true), [setVisible]); const { onClick, children, disabled, allowWalletChange, ...rest } = props; // only show if wallet selected or user connected - const menu = ( - - Change Wallet - - ); - - if(!provider || !allowWalletChange) { - return ; + if (!wallet || !allowWalletChange) { + return ( + + ); } return ( + onClick={connected ? onClick : connect} + disabled={connected && disabled} + overlay={ + + + Change Wallet + + + } + > Connect ); diff --git a/packages/common/src/components/CurrentUserBadge/index.tsx b/packages/common/src/components/CurrentUserBadge/index.tsx index e2908c79..62e56309 100644 --- a/packages/common/src/components/CurrentUserBadge/index.tsx +++ b/packages/common/src/components/CurrentUserBadge/index.tsx @@ -1,71 +1,71 @@ -import React from 'react'; - -import { Identicon } from '../Identicon'; +import React, { useMemo } from 'react'; import { LAMPORTS_PER_SOL } from '@solana/web3.js'; -import { useWallet } from '../../contexts/wallet'; -import { useNativeAccount } from '../../contexts/accounts'; -import { formatNumber, shortenAddress } from '../../utils'; +import { useNativeAccount, useWallet } from '../../contexts'; +import { formatNumber } from '../../utils'; import './styles.css'; import { Popover } from 'antd'; import { Settings } from '../Settings'; -export const CurrentUserBadge = (props: { showBalance?: boolean, showAddress?: boolean, iconSize?: number }) => { - const { wallet } = useWallet(); +export const CurrentUserBadge = (props: { + showBalance?: boolean; + showAddress?: boolean; + iconSize?: number; +}) => { + const { wallet, publicKey } = useWallet(); const { account } = useNativeAccount(); - if (!wallet || !wallet.publicKey) { + const address = useMemo(() => { + if (publicKey) { + const base58 = publicKey.toBase58(); + return `${base58.slice(0, 4)}...${base58.slice(-4)}`; + } + }, [publicKey]) + + if (!wallet || !address) { return null; } - const iconStyle: React.CSSProperties = props.showAddress ? - { - marginLeft: '0.5rem', - display: 'flex', - width: props.iconSize, - borderRadius: 50, + const iconStyle: React.CSSProperties = props.showAddress + ? { + marginLeft: '0.5rem', + display: 'flex', + width: props.iconSize || 20, + borderRadius: 50, + } + : { + display: 'flex', + width: props.iconSize || 20, + paddingLeft: 0, + borderRadius: 50, + }; - } :{ - display: 'flex', - width: props.iconSize, - paddingLeft: 0, - borderRadius: 50, + const baseWalletKey: React.CSSProperties = { + height: props.iconSize, + cursor: 'pointer', + userSelect: 'none', }; + const walletKeyStyle: React.CSSProperties = props.showAddress + ? baseWalletKey + : { ...baseWalletKey, paddingLeft: 0 }; - const baseWalletKey: React.CSSProperties = { height: props.iconSize, cursor: 'pointer', userSelect: 'none' }; - const walletKeyStyle: React.CSSProperties = props.showAddress ? - baseWalletKey - :{ ...baseWalletKey, paddingLeft: 0 }; - - let name = props.showAddress ? shortenAddress(`${wallet.publicKey}`) : ''; - const unknownWallet = wallet as any; - if(unknownWallet.name) { - name = unknownWallet.name; - } - - let image = ; - - if(unknownWallet.image) { - image = ; - } return (
- {props.showBalance && - {formatNumber.format((account?.lamports || 0) / LAMPORTS_PER_SOL)} SOL - } + {props.showBalance && ( + + {formatNumber.format((account?.lamports || 0) / LAMPORTS_PER_SOL)} SOL + + )} } - trigger="click" - > + placement="topRight" + title="Settings" + content={} + trigger="click" + >
- {name && ({name})} - {image} + {address} +
diff --git a/packages/common/src/components/EtherscanLink/index.tsx b/packages/common/src/components/EtherscanLink/index.tsx index 321febb8..745daf30 100644 --- a/packages/common/src/components/EtherscanLink/index.tsx +++ b/packages/common/src/components/EtherscanLink/index.tsx @@ -3,7 +3,7 @@ import { Typography } from 'antd'; import { shortenAddress } from '../../utils/utils'; export const EtherscanLink = (props: { - address: string ; + address: string; type: string; code?: boolean; style?: React.CSSProperties; diff --git a/packages/common/src/components/ExplorerLink/index.tsx b/packages/common/src/components/ExplorerLink/index.tsx index 32fcc069..f13ccc5c 100644 --- a/packages/common/src/components/ExplorerLink/index.tsx +++ b/packages/common/src/components/ExplorerLink/index.tsx @@ -1,7 +1,10 @@ import React from 'react'; import { Typography } from 'antd'; import { shortenAddress } from '../../utils/utils'; -import { PublicKey } from '@solana/web3.js'; +import { Connection, PublicKey } from '@solana/web3.js'; +import { useConnectionConfig } from '../../contexts'; + +import { getExplorerUrl } from '../../utils/explorer'; export const ExplorerLink = (props: { address: string | PublicKey; @@ -9,8 +12,11 @@ export const ExplorerLink = (props: { code?: boolean; style?: React.CSSProperties; length?: number; + short?: boolean; + connection?: Connection; }) => { - const { type, code } = props; + const { type, code, short } = props; + let { endpoint } = useConnectionConfig(); const address = typeof props.address === 'string' @@ -21,11 +27,14 @@ export const ExplorerLink = (props: { return null; } - const length = props.length ?? 9; + const displayAddress = + short || props.length + ? shortenAddress(address, props.length ?? 9) + : address; return ( {code ? ( - {shortenAddress(address, length)} + {displayAddress} ) : ( - shortenAddress(address, length) + displayAddress )} ); diff --git a/packages/common/src/components/Icons/info.tsx b/packages/common/src/components/Icons/info.tsx index 4bb0e3a0..6ef9758e 100644 --- a/packages/common/src/components/Icons/info.tsx +++ b/packages/common/src/components/Icons/info.tsx @@ -1,7 +1,7 @@ -import { Button, Popover } from "antd"; -import React from "react"; +import { Button, Popover } from 'antd'; +import React from 'react'; -import { InfoCircleOutlined } from "@ant-design/icons"; +import { InfoCircleOutlined } from '@ant-design/icons'; export const Info = (props: { text: React.ReactElement; diff --git a/packages/common/src/components/Identicon/index.tsx b/packages/common/src/components/Identicon/index.tsx index 0c9f4764..55d824c9 100644 --- a/packages/common/src/components/Identicon/index.tsx +++ b/packages/common/src/components/Identicon/index.tsx @@ -20,7 +20,6 @@ export const Identicon = (props: { useEffect(() => { if (address && ref.current) { try { - ref.current.innerHTML = ''; ref.current.className = className || ''; ref.current.appendChild( @@ -29,9 +28,8 @@ export const Identicon = (props: { parseInt(bs58.decode(address).toString('hex').slice(5, 15), 16), ), ); - } catch (err) { - // TODO + // TODO } } }, [address, style, className]); diff --git a/packages/common/src/components/Input/numeric.tsx b/packages/common/src/components/Input/numeric.tsx index c307e3e9..84ebc771 100644 --- a/packages/common/src/components/Input/numeric.tsx +++ b/packages/common/src/components/Input/numeric.tsx @@ -1,11 +1,11 @@ -import React from "react"; -import { Input } from "antd"; +import React from 'react'; +import { Input } from 'antd'; export class NumericInput extends React.Component { onChange = (e: any) => { const { value } = e.target; const reg = /^-?\d*(\.\d*)?$/; - if (reg.test(value) || value === "" || value === "-") { + if (reg.test(value) || value === '' || value === '-') { this.props.onChange(value); } }; @@ -17,14 +17,14 @@ export class NumericInput extends React.Component { if (value === undefined || value === null) return; if ( value.charAt && - (value.charAt(value.length - 1) === "." || value === "-") + (value.charAt(value.length - 1) === '.' || value === '-') ) { valueTemp = value.slice(0, -1); } - if (value.startsWith && (value.startsWith(".") || value.startsWith("-."))) { - valueTemp = valueTemp.replace(".", "0."); + if (value.startsWith && (value.startsWith('.') || value.startsWith('-.'))) { + valueTemp = valueTemp.replace('.', '0.'); } - if (valueTemp.replace) onChange?.(valueTemp.replace(/0*(\d+)/, "$1")); + if (valueTemp.replace) onChange?.(valueTemp.replace(/0*(\d+)/, '$1')); if (onBlur) { onBlur(); } diff --git a/packages/common/src/components/Settings/index.tsx b/packages/common/src/components/Settings/index.tsx index 6e97706d..03660223 100644 --- a/packages/common/src/components/Settings/index.tsx +++ b/packages/common/src/components/Settings/index.tsx @@ -1,19 +1,18 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { Button, Select } from 'antd'; -import { useWallet } from '../../contexts/wallet'; -import { ENDPOINTS, useConnectionConfig } from '../../contexts/connection'; -import { shortenAddress } from '../../utils'; -import { - CopyOutlined -} from '@ant-design/icons'; +import { ENDPOINTS, useConnectionConfig, useWallet, useWalletModal } from '../../contexts'; +import { notify, shortenAddress } from '../../utils'; +import { CopyOutlined } from '@ant-design/icons'; export const Settings = ({ additionalSettings, }: { additionalSettings?: JSX.Element; }) => { - const { connected, disconnect, select, wallet } = useWallet(); + const { connected, disconnect, publicKey } = useWallet(); const { endpoint, setEndpoint } = useConnectionConfig(); + const { setVisible } = useWalletModal(); + const open = useCallback(() => setVisible(true), [setVisible]); return ( <> @@ -33,18 +32,32 @@ export const Settings = ({ {connected && ( <> Wallet: - {wallet?.publicKey && ()} + {publicKey && ( + + )} - - diff --git a/packages/common/src/components/TokenIcon/index.tsx b/packages/common/src/components/TokenIcon/index.tsx index 21da7ecd..d6a205a3 100644 --- a/packages/common/src/components/TokenIcon/index.tsx +++ b/packages/common/src/components/TokenIcon/index.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useState } from 'react'; import { PublicKey } from '@solana/web3.js'; -import {getTokenIcon, KnownTokenMap} from '../../utils'; +import { getTokenIcon, KnownTokenMap } from '../../utils'; import { useConnectionConfig } from '../../contexts/connection'; import { Identicon } from '../Identicon'; @@ -9,8 +9,10 @@ export const TokenIcon = (props: { style?: React.CSSProperties; size?: number; className?: string; - tokenMap?: KnownTokenMap, + tokenMap?: KnownTokenMap; }) => { + const [showIcon, setShowIcon] = useState(true); + let icon: string | undefined = ''; if (props.tokenMap) { icon = getTokenIcon(props.tokenMap, props.mintAddress); @@ -21,7 +23,7 @@ export const TokenIcon = (props: { const size = props.size || 20; - if (icon) { + if (showIcon && icon) { return ( Token icon setShowIcon(false)} /> ); } diff --git a/packages/common/src/constants/math.ts b/packages/common/src/constants/math.ts index d0b48657..6e062935 100644 --- a/packages/common/src/constants/math.ts +++ b/packages/common/src/constants/math.ts @@ -1,4 +1,4 @@ -import BN from "bn.js"; +import BN from 'bn.js'; export const TEN = new BN(10); export const HALF_WAD = TEN.pow(new BN(18)); diff --git a/packages/common/src/contexts/accounts.tsx b/packages/common/src/contexts/accounts.tsx index f4e84f95..dc0f32e6 100644 --- a/packages/common/src/contexts/accounts.tsx +++ b/packages/common/src/contexts/accounts.tsx @@ -5,8 +5,7 @@ import React, { useMemo, useState, } from 'react'; -import { useConnection } from '../contexts/connection'; -import { useWallet } from '../contexts/wallet'; +import { useConnection, useWallet } from '../contexts'; import { AccountInfo, Connection, PublicKey } from '@solana/web3.js'; import { AccountLayout, MintInfo, MintLayout, u64 } from '@solana/spl-token'; import { TokenAccount } from '../models'; @@ -126,6 +125,9 @@ export const cache = { return account; } + // Note: If the request to get the account fails the error is captured as a rejected Promise and would stay in pendingCalls forever + // It means if the first request fails for a transient reason it would never recover from the state and account would never be returned + // TODO: add logic to detect transient errors and remove the Promises from pendingCalls let query = pendingCalls.get(address); if (query) { return query; @@ -134,7 +136,7 @@ export const cache = { // TODO: refactor to use multiple accounts query with flush like behavior query = connection.getAccountInfo(id).then(data => { if (!data) { - throw new Error('Account not found'); + throw new Error(`Account ${id.toBase58()} not found`); } return cache.add(id, data, parser); @@ -231,6 +233,9 @@ export const cache = { return mint; } + // Note: If the request to get the mint fails the error is captured as a rejected Promise and would stay in pendingMintCalls forever + // It means if the first request fails for a transient reason it would never recover from the state and mint would never be returned + // TODO: add logic to detect transient errors and remove the Promises from pendingMintCalls let query = pendingMintCalls.get(address); if (query) { return query; @@ -308,23 +313,21 @@ export const getCachedAccount = ( const UseNativeAccount = () => { const connection = useConnection(); - const { wallet } = useWallet(); + const { publicKey } = useWallet(); const [nativeAccount, setNativeAccount] = useState>(); const updateCache = useCallback( account => { - if (wallet && wallet.publicKey) { - const wrapped = wrapNativeAccount(wallet.publicKey, account); - if (wrapped !== undefined && wallet) { - const id = wallet.publicKey?.toBase58(); - cache.registerParser(id, TokenAccountParser); - genericCache.set(id, wrapped as TokenAccount); - cache.emitter.raiseCacheUpdated(id, false, TokenAccountParser); - } + if (publicKey) { + const wrapped = wrapNativeAccount(publicKey, account); + const id = publicKey.toBase58(); + cache.registerParser(id, TokenAccountParser); + genericCache.set(id, wrapped as TokenAccount); + cache.emitter.raiseCacheUpdated(id, false, TokenAccountParser); } }, - [wallet], + [publicKey], ); useEffect(() => { @@ -337,22 +340,22 @@ const UseNativeAccount = () => { }; (async () => { - if (!connection || !wallet?.publicKey) { + if (!connection || !publicKey) { return; } - const account = await connection.getAccountInfo(wallet.publicKey) + const account = await connection.getAccountInfo(publicKey); updateAccount(account); - subId = connection.onAccountChange(wallet.publicKey, updateAccount); + subId = connection.onAccountChange(publicKey, updateAccount); })(); return () => { if (subId) { connection.removeAccountChangeListener(subId); } - } - }, [setNativeAccount, wallet, wallet?.publicKey, connection, updateCache]); + }; + }, [setNativeAccount, publicKey, connection, updateCache]); return { nativeAccount }; }; @@ -380,7 +383,7 @@ const precacheUserTokenAccounts = async ( export function AccountsProvider({ children = null as any }) { const connection = useConnection(); - const { wallet, connected } = useWallet(); + const { publicKey } = useWallet(); const [tokenAccounts, setTokenAccounts] = useState([]); const [userAccounts, setUserAccounts] = useState([]); const { nativeAccount } = UseNativeAccount(); @@ -390,17 +393,17 @@ export function AccountsProvider({ children = null as any }) { .byParser(TokenAccountParser) .map(id => cache.get(id)) .filter( - a => a && a.info.owner.toBase58() === wallet?.publicKey?.toBase58(), + a => a && a.info.owner.toBase58() === publicKey?.toBase58(), ) .map(a => a as TokenAccount); - }, [wallet]); + }, [publicKey]); useEffect(() => { const accounts = selectUserAccounts().filter( a => a !== undefined, ) as TokenAccount[]; setUserAccounts(accounts); - }, [nativeAccount, wallet, tokenAccounts, selectUserAccounts]); + }, [nativeAccount, tokenAccounts, selectUserAccounts]); useEffect(() => { const subs: number[] = []; @@ -419,7 +422,6 @@ export function AccountsProvider({ children = null as any }) { }; }, [connection]); - const publicKey = wallet?.publicKey; useEffect(() => { if (!connection || !publicKey) { setTokenAccounts([]); @@ -437,7 +439,7 @@ export function AccountsProvider({ children = null as any }) { programIds().token, info => { // TODO: fix type in web3.js - const id = (info.accountId as unknown) as string; + const id = info.accountId as unknown as string; // TODO: do we need a better way to identify layout (maybe a enum identifing type?) if (info.accountInfo.data.length === AccountLayout.span) { const data = deserializeAccount(info.accountInfo.data); @@ -455,7 +457,7 @@ export function AccountsProvider({ children = null as any }) { connection.removeProgramAccountChangeListener(tokenSubID); }; } - }, [connection, connected, publicKey, selectUserAccounts]); + }, [connection, publicKey, selectUserAccounts]); return ( ({ tokenMap: new Map(), }); +enum ASSET_CHAIN { + Solana = 1, + Ethereum = 2, +} + export function ConnectionProvider({ children = undefined as any }) { const [endpoint, setEndpoint] = useLocalStorageState( 'connectionEndpoint', @@ -107,12 +96,14 @@ export function ConnectionProvider({ children = undefined as any }) { DEFAULT_SLIPPAGE.toString(), ); - const connection = useMemo(() => new Connection(endpoint, 'recent'), [ - endpoint, - ]); - const sendConnection = useMemo(() => new Connection(endpoint, 'recent'), [ - endpoint, - ]); + const connection = useMemo( + () => new Connection(endpoint, 'recent'), + [endpoint], + ); + const sendConnection = useMemo( + () => new Connection(endpoint, 'recent'), + [endpoint], + ); const env = ENDPOINTS.find(end => end.endpoint === endpoint)?.name || ENDPOINTS[0].name; @@ -130,6 +121,19 @@ export function ConnectionProvider({ children = undefined as any }) { ) .getList(); + // WORMHOLE TOKEN NEEDED + list.push({ + address: '66CgfJQoZkpkrEgC1z4vFJcSFc4V6T5HqbjSSNuqcNJz', + chainId: ASSET_CHAIN.Solana, + decimals: 9, + logoURI: + 'https://assets.coingecko.com/coins/images/15500/thumb/ibbtc.png?1621077589', + name: 'Interest Bearing Bitcoin (Wormhole)', + symbol: 'IBBTC', + extensions: { + address: '0xc4e15973e6ff2a35cc804c2cf9d2a1b817a8b40f', + }, + }); const knownMints = [...list].reduce((map, item) => { map.set(item.address, item); return map; @@ -224,6 +228,7 @@ export const getErrorForTransaction = async ( txid: string, ) => { // wait for all confirmation before geting transaction + await connection.confirmTransaction(txid, 'max'); const tx = await connection.getParsedConfirmedTransaction(txid); @@ -257,7 +262,7 @@ export enum SequenceType { export const sendTransactions = async ( connection: Connection, - wallet: any, + wallet: WalletSigner, instructionSet: TransactionInstruction[][], signersSet: Account[][], sequenceType: SequenceType = SequenceType.Parallel, @@ -266,6 +271,8 @@ export const sendTransactions = async ( failCallback: (reason: string, ind: number) => boolean = (txid, ind) => false, block?: BlockhashAndFeeCalculator, ): Promise => { + if (!wallet.publicKey) throw new WalletNotConnectedError(); + const unsignedTxns: Transaction[] = []; if (!block) { @@ -312,6 +319,7 @@ export const sendTransactions = async ( successCallback(txid, i); }) .catch(reason => { + // @ts-ignore failCallback(signedTxns[i], i); if (sequenceType == SequenceType.StopOnFailure) { breakEarlyObject.breakEarly = true; @@ -337,7 +345,7 @@ export const sendTransactions = async ( export const sendTransaction = async ( connection: Connection, - wallet: any, + wallet: WalletSigner, instructions: TransactionInstruction[], signers: Account[], awaitConfirmation = true, @@ -345,6 +353,8 @@ export const sendTransaction = async ( includesFeePayer: boolean = false, block?: BlockhashAndFeeCalculator, ) => { + if (!wallet.publicKey) throw new WalletNotConnectedError(); + let transaction = new Transaction(); instructions.forEach(instruction => transaction.add(instruction)); transaction.recentBlockhash = ( @@ -364,8 +374,13 @@ export const sendTransaction = async ( if (signers.length > 0) { transaction.partialSign(...signers); } + if (!includesFeePayer) { - transaction = await wallet.signTransaction(transaction); + try { + transaction = await wallet.signTransaction(transaction); + } catch (ex) { + throw new SignTransactionError(ex); + } } const rawTransaction = transaction.serialize(); @@ -378,32 +393,67 @@ export const sendTransaction = async ( let slot = 0; if (awaitConfirmation) { - const confirmation = await awaitTransactionSignatureConfirmation( + const confirmationStatus = await awaitTransactionSignatureConfirmation( txid, DEFAULT_TIMEOUT, connection, commitment, ); - slot = confirmation?.slot || 0; + slot = confirmationStatus?.slot || 0; + + if (confirmationStatus?.err) { + let errors: string[] = []; + try { + // TODO: This call always throws errors and delays error feedback + // It needs to be investigated but for now I'm commenting it out + // errors = await getErrorForTransaction(connection, txid); + } catch (ex) { + console.error('getErrorForTransaction() error', ex); + } + + if ('timeout' in confirmationStatus.err) { + notify({ + message: `Transaction hasn't been confirmed within ${ + DEFAULT_TIMEOUT / 1000 + }s. Please check on Solana Explorer`, + description: ( + <> + + + ), + type: 'warn', + }); + throw new TransactionTimeoutError(txid); + } - if (confirmation?.err) { - const errors = await getErrorForTransaction(connection, txid); notify({ - message: 'Transaction failed...', + message: 'Transaction error', description: ( <> {errors.map(err => (
{err}
))} - + ), type: 'error', }); - throw new Error( - `Raw transaction ${txid} failed (${JSON.stringify(status)})`, + throw new SendTransactionError( + `Transaction ${txid} failed (${JSON.stringify(confirmationStatus)})`, + txid, + confirmationStatus.err, ); } } @@ -413,7 +463,7 @@ export const sendTransaction = async ( export const sendTransactionWithRetry = async ( connection: Connection, - wallet: any, + wallet: WalletSigner, instructions: TransactionInstruction[], signers: Account[], commitment: Commitment = 'singleGossip', @@ -421,6 +471,8 @@ export const sendTransactionWithRetry = async ( block?: BlockhashAndFeeCalculator, beforeSend?: () => void, ) => { + if (!wallet.publicKey) throw new WalletNotConnectedError(); + let transaction = new Transaction(); instructions.forEach(instruction => transaction.add(instruction)); transaction.recentBlockhash = ( @@ -460,7 +512,7 @@ export const getUnixTs = () => { return new Date().getTime() / 1000; }; -const DEFAULT_TIMEOUT = 15000; +const DEFAULT_TIMEOUT = 30000; export async function sendSignedTransaction({ signedTransaction, @@ -542,7 +594,7 @@ export async function sendSignedTransaction({ return { txid, slot }; } -async function simulateTransaction( +export async function simulateTransaction( connection: Connection, transaction: Transaction, commitment: Commitment, @@ -649,6 +701,10 @@ async function awaitTransactionSignatureConfirmation( })(); }) .catch(err => { + if (err.timeout && status) { + status.err = { timeout: true }; + } + //@ts-ignore if (connection._signatureSubscriptions[subId]) connection.removeSignatureListener(subId); diff --git a/packages/common/src/contexts/index.tsx b/packages/common/src/contexts/index.tsx index e8082d5b..c155b7e5 100644 --- a/packages/common/src/contexts/index.tsx +++ b/packages/common/src/contexts/index.tsx @@ -1,8 +1,6 @@ export * as Accounts from './accounts'; +export * from './accounts'; export * as Connection from './connection'; +export * from './connection'; export * as Wallet from './wallet'; -export { ParsedAccount, ParsedAccountBase } from './accounts'; - -export * from './accounts'; export * from './wallet'; -export * from './connection'; diff --git a/packages/common/src/contexts/wallet.tsx b/packages/common/src/contexts/wallet.tsx index 8a506e82..926a0753 100644 --- a/packages/common/src/contexts/wallet.tsx +++ b/packages/common/src/contexts/wallet.tsx @@ -1,202 +1,209 @@ -import { WalletAdapter } from "@solana/wallet-base"; - -import Wallet from "@project-serum/sol-wallet-adapter"; +import { + MessageSignerWalletAdapterProps, + SignerWalletAdapter, + SignerWalletAdapterProps, + WalletAdapterNetwork, + WalletAdapterProps, + WalletError, + WalletNotConnectedError, +} from '@solana/wallet-adapter-base'; +import { + useWallet as useWalletBase, + WalletProvider as BaseWalletProvider +} from '@solana/wallet-adapter-react'; +import { + getLedgerWallet, + getPhantomWallet, + getSlopeWallet, + getSolflareWallet, + getSolletWallet, + getSolletExtensionWallet, + getTorusWallet, + Wallet, + WalletName, +} from '@solana/wallet-adapter-wallets'; import { Button, Modal } from "antd"; -import React, { useCallback, useContext, useEffect, useMemo, useState } from "react"; -import { notify } from "./../utils/notifications"; +import React, { createContext, FC, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { notify } from "../utils"; import { useConnectionConfig } from "./connection"; -import { useLocalStorageState } from "../utils/utils"; -import { LedgerProvider } from "@solana/wallet-ledger"; -import { SolongWalletAdapter } from "../wallet-adapters/solong"; -import { PhantomWalletAdapter } from "../wallet-adapters/phantom"; -import { TorusWalletAdapter } from "../wallet-adapters/torus"; -import { useLocation } from "react-router"; - -const ASSETS_URL = 'https://raw.githubusercontent.com/solana-labs/oyster/main/assets/wallets/'; -export const WALLET_PROVIDERS = [ - { - name: "Phantom", - url: "https://www.phantom.app", - icon: `https://www.phantom.app/img/logo.png`, - adapter: PhantomWalletAdapter, - }, - LedgerProvider, - { - name: "Sollet", - url: "https://www.sollet.io", - icon: `${ASSETS_URL}sollet.svg`, - }, { - name: "Solong", - url: "https://solongwallet.com", - icon: `${ASSETS_URL}solong.png`, - adapter: SolongWalletAdapter, - }, - // TODO: enable when fully functional - // { - // name: "MathWallet", - // url: "https://mathwallet.org", - // icon: `${ASSETS_URL}mathwallet.svg`, - // }, - // { - // name: 'Torus', - // url: 'https://tor.us', - // icon: `${ASSETS_URL}torus.svg`, - // adapter: TorusWalletAdapter, - // } - - // Solflare doesnt allow external connections for all apps - // { - // name: "Solflare", - // url: "https://solflare.com/access-wallet", - // icon: `${ASSETS_URL}solflare.svg`, - // }, -]; - -const WalletContext = React.createContext<{ - wallet: WalletAdapter | undefined, - connected: boolean, - select: () => void, - provider: typeof WALLET_PROVIDERS[number] | undefined, -}>({ - wallet: undefined, - connected: false, - select() { }, - provider: undefined, -}); - -export function WalletProvider({ children = null as any }) { - const { endpoint } = useConnectionConfig(); - const location = useLocation(); - const [autoConnect, setAutoConnect] = useState(location.pathname.indexOf('result=') >= 0 || false); - const [providerUrl, setProviderUrl] = useLocalStorageState("walletProvider"); - - const provider = useMemo(() => WALLET_PROVIDERS.find(({ url }) => url === providerUrl), [providerUrl]); - - const wallet = useMemo(function () { - if (provider) { - return new (provider.adapter || Wallet)(providerUrl, endpoint) as WalletAdapter; - } - }, [provider, providerUrl, endpoint]); - const [connected, setConnected] = useState(false); +export interface WalletContextState extends WalletAdapterProps { + wallets: Wallet[]; + autoConnect: boolean; - useEffect(() => { - if (wallet) { - wallet.on("connect", () => { - if (wallet.publicKey) { - setConnected(true); - const walletPublicKey = wallet.publicKey.toBase58(); - const keyToDisplay = - walletPublicKey.length > 20 - ? `${walletPublicKey.substring(0, 7)}.....${walletPublicKey.substring( - walletPublicKey.length - 7, - walletPublicKey.length - )}` - : walletPublicKey; - - notify({ - message: "Wallet update", - description: "Connected to wallet " + keyToDisplay, - }); - } - }); + wallet: Wallet | null; + adapter: SignerWalletAdapter | MessageSignerWalletAdapterProps | null; + disconnecting: boolean; - wallet.on("disconnect", () => { - setConnected(false); - // setProviderUrl(null) - notify({ - message: "Wallet update", - description: "Disconnected from wallet", - }); - }); - } + select(walletName: WalletName): void; - return () => { - setConnected(false); - // setProviderUrl(null) - if (wallet) { - wallet.disconnect(); - } - }; - }, [wallet]); + signTransaction: SignerWalletAdapterProps['signTransaction']; + signAllTransactions: SignerWalletAdapterProps['signAllTransactions']; - useEffect(() => { - if (wallet && autoConnect) { - wallet.connect(); - setAutoConnect(false); - } + signMessage: MessageSignerWalletAdapterProps['signMessage'] | undefined; +} + +export function useWallet (): WalletContextState { + return useWalletBase() as WalletContextState; +} + +export { SignerWalletAdapter, WalletNotConnectedError }; + +export type WalletSigner = Pick; + +export interface WalletModalContextState { + visible: boolean; + setVisible: (open: boolean) => void; +} + +export const WalletModalContext = createContext({} as WalletModalContextState); + +export function useWalletModal(): WalletModalContextState { + return useContext(WalletModalContext); +} - return () => { } - }, [wallet, autoConnect]); +export const WalletModal = () => { + const { wallets, wallet: selected, select } = useWallet(); + const { visible, setVisible } = useWalletModal(); + const close = useCallback(() => setVisible(false), [setVisible]); - const [isModalVisible, setIsModalVisible] = useState(false); + return ( + + {wallets.map((wallet) => { + return ( + + ); + })} + + ); +}; - const select = useCallback(() => setIsModalVisible(true), []); - const close = useCallback(() => setIsModalVisible(false), []); +export const WalletModalProvider = ({ children }: { children: ReactNode }) => { + const { publicKey } = useWallet(); + const [connected, setConnected] = useState(!!publicKey); + const [visible, setVisible] = useState(false); + + useEffect(() => { + if (publicKey) { + const base58 = publicKey.toBase58(); + const keyToDisplay = + base58.length > 20 + ? `${base58.substring( + 0, + 7, + )}.....${base58.substring( + base58.length - 7, + base58.length, + )}` + : base58; + + notify({ + message: 'Wallet update', + description: 'Connected to wallet ' + keyToDisplay, + }); + } + }, [publicKey]); + + useEffect(() => { + if (!publicKey && connected) { + notify({ + message: 'Wallet update', + description: 'Disconnected from wallet', + }); + } + setConnected(!!publicKey); + }, [publicKey, connected, setConnected]); return ( - {children} - - {WALLET_PROVIDERS.map((provider, idx) => { - const onClick = function () { - setProviderUrl(provider.url); - setAutoConnect(true); - close(); - } - - return ( - - ) - })} - - + + + ); +}; + +export const WalletProvider = ({ children }: {children: ReactNode }) => { + const { env } = useConnectionConfig(); + + const network = useMemo(() => { + switch (env) { + case "mainnet-beta": + return WalletAdapterNetwork.Mainnet; + case "testnet": + return WalletAdapterNetwork.Testnet; + case "devnet": + case "localnet": + default: + return WalletAdapterNetwork.Devnet; + } + }, [env]); + + const wallets = useMemo( + () => [ + getPhantomWallet(), + getSlopeWallet(), + getSolflareWallet(), + getTorusWallet({ + options: { clientId: 'Get a client ID @ https://developer.tor.us' } + }), + getLedgerWallet(), + getSolletWallet({ network }), + getSolletExtensionWallet({ network }), + ], + [] ); -} -export const useWallet = () => { - const { wallet, connected, provider, select } = useContext(WalletContext); - return { - wallet, - connected, - provider, - select, - connect() { - wallet ? wallet.connect() : select(); - }, - disconnect() { - wallet?.disconnect(); - }, - }; + const onError = useCallback((error: WalletError) => { + console.error(error); + notify({ + message: 'Wallet error', + description: error.message, + }); + }, []); + + return ( + + + {children} + + + ); } diff --git a/packages/common/src/contracts/token.ts b/packages/common/src/contracts/token.ts index 7463f85d..03d7297f 100644 --- a/packages/common/src/contracts/token.ts +++ b/packages/common/src/contracts/token.ts @@ -1,4 +1,8 @@ import { MintLayout, AccountLayout, Token } from '@solana/spl-token'; +import { + SignerWalletAdapter, + WalletNotConnectedError, +} from '../contexts/wallet'; import { Connection, PublicKey, @@ -9,13 +13,12 @@ import { export const mintNFT = async ( connection: Connection, - wallet: { - publicKey: PublicKey; - signTransaction: (tx: Transaction) => Transaction; - }, + wallet: SignerWalletAdapter, // SOL account owner: PublicKey, ) => { + if (!wallet.publicKey) throw new WalletNotConnectedError(); + const TOKEN_PROGRAM_ID = new PublicKey( 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', ); diff --git a/packages/common/src/hooks/useAccountByMint.ts b/packages/common/src/hooks/useAccountByMint.ts index e0ae165b..0271d943 100644 --- a/packages/common/src/hooks/useAccountByMint.ts +++ b/packages/common/src/hooks/useAccountByMint.ts @@ -5,7 +5,9 @@ export const useAccountByMint = (mint?: string | PublicKey) => { const { userAccounts } = useUserAccounts(); const mintAddress = typeof mint === 'string' ? mint : mint?.toBase58(); - const index = userAccounts.findIndex((acc) => acc.info.mint.toBase58() === mintAddress); + const index = userAccounts.findIndex( + acc => acc.info.mint.toBase58() === mintAddress, + ); if (index !== -1) { return userAccounts[index]; diff --git a/packages/common/src/hooks/useTokenName.ts b/packages/common/src/hooks/useTokenName.ts index 2bcdcc5d..b4a6b60a 100644 --- a/packages/common/src/hooks/useTokenName.ts +++ b/packages/common/src/hooks/useTokenName.ts @@ -4,6 +4,7 @@ import { getTokenName } from '../utils/utils'; export function useTokenName(mintAddress?: string | PublicKey) { const { tokenMap } = useConnectionConfig(); - const address = typeof mintAddress === 'string' ? mintAddress : mintAddress?.toBase58(); + const address = + typeof mintAddress === 'string' ? mintAddress : mintAddress?.toBase58(); return getTokenName(tokenMap, address); } diff --git a/packages/common/src/index.tsx b/packages/common/src/index.tsx index d0985cef..dbff96b9 100644 --- a/packages/common/src/index.tsx +++ b/packages/common/src/index.tsx @@ -1,17 +1,14 @@ export * as actions from './actions'; export * from './actions'; export * as components from './components'; -export * from './components'; // Allow direct exports too +export * from './components'; export * as constants from './constants'; +export * from './constants'; export * as hooks from './hooks'; export * from './hooks'; export * as contexts from './contexts'; export * from './contexts'; export * as models from './models'; +export * from './models'; export * as utils from './utils'; export * from './utils'; -export * as walletAdapters from './wallet-adapters'; - -export { TokenAccount } from './models'; -export { ParsedAccount, ParsedAccountBase } from './contexts'; -export { KnownTokenMap, EventEmitter, Layout } from './utils'; diff --git a/packages/common/src/models/account.ts b/packages/common/src/models/account.ts index df0473c5..00d0b524 100644 --- a/packages/common/src/models/account.ts +++ b/packages/common/src/models/account.ts @@ -5,9 +5,10 @@ import { TransactionInstruction, } from '@solana/web3.js'; -import { AccountInfo as TokenAccountInfo, Token } from '@solana/spl-token'; +import { AccountInfo as TokenAccountInfo, Token, u64 } from '@solana/spl-token'; import { TOKEN_PROGRAM_ID } from '../utils/ids'; import BufferLayout from 'buffer-layout'; +import BN from 'bn.js'; export interface TokenAccount { pubkey: PublicKey; @@ -53,7 +54,7 @@ export function approve( cleanupInstructions: TransactionInstruction[], account: PublicKey, owner: PublicKey, - amount: number, + amount: number | u64, autoRevoke = true, // if delegate is not passed ephemeral transfer authority is used @@ -65,6 +66,12 @@ export function approve( const transferAuthority = existingTransferAuthority || new Account(); const delegateKey = delegate ?? transferAuthority.publicKey; + // Coerce amount to u64 in case it's deserialized as BN which differs by buffer conversion functions only + // Without the coercion createApproveInstruction would fail because it won't be able to serialize it + if (typeof amount !== 'number') { + amount = new u64(amount.toArray()); + } + instructions.push( Token.createApproveInstruction( tokenProgram, diff --git a/packages/common/src/models/tokenSwap.ts b/packages/common/src/models/tokenSwap.ts index e095ddda..a5017abb 100644 --- a/packages/common/src/models/tokenSwap.ts +++ b/packages/common/src/models/tokenSwap.ts @@ -14,7 +14,7 @@ const FEE_LAYOUT = BufferLayout.struct( BufferLayout.nu64('hostFeeNumerator'), BufferLayout.nu64('hostFeeDenominator'), ], - 'fees' + 'fees', ); export const TokenSwapLayoutLegacyV0 = BufferLayout.struct([ @@ -27,42 +27,58 @@ export const TokenSwapLayoutLegacyV0 = BufferLayout.struct([ uint64('feesDenominator'), ]); -export const TokenSwapLayoutV1: typeof BufferLayout.Structure = BufferLayout.struct([ - BufferLayout.u8('isInitialized'), - BufferLayout.u8('nonce'), - publicKey('tokenProgramId'), - publicKey('tokenAccountA'), - publicKey('tokenAccountB'), - publicKey('tokenPool'), - publicKey('mintA'), - publicKey('mintB'), - publicKey('feeAccount'), - BufferLayout.u8('curveType'), - uint64('tradeFeeNumerator'), - uint64('tradeFeeDenominator'), - uint64('ownerTradeFeeNumerator'), - uint64('ownerTradeFeeDenominator'), - uint64('ownerWithdrawFeeNumerator'), - uint64('ownerWithdrawFeeDenominator'), - BufferLayout.blob(16, 'padding'), -]); +export const TokenSwapLayoutV1: typeof BufferLayout.Structure = BufferLayout.struct( + [ + BufferLayout.u8('isInitialized'), + BufferLayout.u8('nonce'), + publicKey('tokenProgramId'), + publicKey('tokenAccountA'), + publicKey('tokenAccountB'), + publicKey('tokenPool'), + publicKey('mintA'), + publicKey('mintB'), + publicKey('feeAccount'), + BufferLayout.u8('curveType'), + uint64('tradeFeeNumerator'), + uint64('tradeFeeDenominator'), + uint64('ownerTradeFeeNumerator'), + uint64('ownerTradeFeeDenominator'), + uint64('ownerWithdrawFeeNumerator'), + uint64('ownerWithdrawFeeDenominator'), + BufferLayout.blob(16, 'padding'), + ], +); -const CURVE_NODE = BufferLayout.union(BufferLayout.u8(), BufferLayout.blob(32), 'curve'); +const CURVE_NODE = BufferLayout.union( + BufferLayout.u8(), + BufferLayout.blob(32), + 'curve', +); CURVE_NODE.addVariant(0, BufferLayout.struct([]), 'constantProduct'); -CURVE_NODE.addVariant(1, BufferLayout.struct([BufferLayout.nu64('token_b_price')]), 'constantPrice'); +CURVE_NODE.addVariant( + 1, + BufferLayout.struct([BufferLayout.nu64('token_b_price')]), + 'constantPrice', +); CURVE_NODE.addVariant(2, BufferLayout.struct([]), 'stable'); -CURVE_NODE.addVariant(3, BufferLayout.struct([BufferLayout.nu64('token_b_offset')]), 'offset'); +CURVE_NODE.addVariant( + 3, + BufferLayout.struct([BufferLayout.nu64('token_b_offset')]), + 'offset', +); -export const TokenSwapLayout: typeof BufferLayout.Structure = BufferLayout.struct([ - BufferLayout.u8('isInitialized'), - BufferLayout.u8('nonce'), - publicKey('tokenProgramId'), - publicKey('tokenAccountA'), - publicKey('tokenAccountB'), - publicKey('tokenPool'), - publicKey('mintA'), - publicKey('mintB'), - publicKey('feeAccount'), - FEE_LAYOUT, - CURVE_NODE, -]); +export const TokenSwapLayout: typeof BufferLayout.Structure = BufferLayout.struct( + [ + BufferLayout.u8('isInitialized'), + BufferLayout.u8('nonce'), + publicKey('tokenProgramId'), + publicKey('tokenAccountA'), + publicKey('tokenAccountB'), + publicKey('tokenPool'), + publicKey('mintA'), + publicKey('mintB'), + publicKey('feeAccount'), + FEE_LAYOUT, + CURVE_NODE, + ], +); diff --git a/packages/common/src/types/buffer-layout.d.ts b/packages/common/src/types/buffer-layout.d.ts index ded59a53..32e44d0e 100644 --- a/packages/common/src/types/buffer-layout.d.ts +++ b/packages/common/src/types/buffer-layout.d.ts @@ -1,9 +1,9 @@ -declare module "buffer-layout" { +declare module 'buffer-layout' { const bl: any; export = bl; } -declare module "jazzicon" { +declare module 'jazzicon' { const jazzicon: any; export = jazzicon; } diff --git a/packages/common/src/types/sol-wallet-adapter.d.ts b/packages/common/src/types/sol-wallet-adapter.d.ts deleted file mode 100644 index 6464233b..00000000 --- a/packages/common/src/types/sol-wallet-adapter.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "@project-serum/sol-wallet-adapter" { - const adapter: any; - export = adapter; -} diff --git a/packages/common/src/utils/errors.ts b/packages/common/src/utils/errors.ts new file mode 100644 index 00000000..93d27b3b --- /dev/null +++ b/packages/common/src/utils/errors.ts @@ -0,0 +1,45 @@ +import { TransactionError } from '@solana/web3.js'; + +export class SendTransactionError extends Error { + txError: TransactionError | undefined; + txId: string; + constructor(message: string, txId: string, txError?: TransactionError) { + super(message); + + this.txError = txError; + this.txId = txId; + } +} + +export function isSendTransactionError( + error: any, +): error is SendTransactionError { + return error instanceof SendTransactionError; +} + +export class SignTransactionError extends Error { + constructor(message: string) { + super(message); + } +} + +export function isSignTransactionError( + error: any, +): error is SignTransactionError { + return error instanceof SignTransactionError; +} + +export class TransactionTimeoutError extends Error { + txId: string; + constructor(txId: string) { + super(`Transaction has timed out`); + + this.txId = txId; + } +} + +export function isTransactionTimeoutError( + error: any, +): error is TransactionTimeoutError { + return error instanceof TransactionTimeoutError; +} diff --git a/packages/common/src/utils/eventEmitter.ts b/packages/common/src/utils/eventEmitter.ts index 1111bee6..ee31a717 100644 --- a/packages/common/src/utils/eventEmitter.ts +++ b/packages/common/src/utils/eventEmitter.ts @@ -1,7 +1,7 @@ -import { EventEmitter as Emitter } from "eventemitter3"; +import { EventEmitter as Emitter } from 'eventemitter3'; export class CacheUpdateEvent { - static type = "CacheUpdate"; + static type = 'CacheUpdate'; id: string; parser: any; isNew: boolean; @@ -13,7 +13,7 @@ export class CacheUpdateEvent { } export class CacheDeleteEvent { - static type = "CacheUpdate"; + static type = 'CacheUpdate'; id: string; constructor(id: string) { this.id = id; @@ -21,7 +21,7 @@ export class CacheDeleteEvent { } export class MarketUpdateEvent { - static type = "MarketUpdate"; + static type = 'MarketUpdate'; ids: Set; constructor(ids: Set) { this.ids = ids; @@ -50,7 +50,7 @@ export class EventEmitter { raiseCacheUpdated(id: string, isNew: boolean, parser: any) { this.emitter.emit( CacheUpdateEvent.type, - new CacheUpdateEvent(id, isNew, parser) + new CacheUpdateEvent(id, isNew, parser), ); } diff --git a/packages/common/src/utils/explorer.ts b/packages/common/src/utils/explorer.ts new file mode 100644 index 00000000..57274eb0 --- /dev/null +++ b/packages/common/src/utils/explorer.ts @@ -0,0 +1,64 @@ +import { Connection, PublicKey, Transaction } from '@solana/web3.js'; +import { ENDPOINTS } from '../contexts'; +import { ENV as ChainId } from '@solana/spl-token-registry'; +import base58 from 'bs58'; + +export function getExplorerUrl( + viewTypeOrItemAddress: string | PublicKey, + endpoint: string, + itemType: string = 'address', + connection?: Connection, +) { + const getClusterUrlParam = () => { + // If ExplorerLink (or any other component)is used outside of ConnectionContext, ex. in notifications, then useConnectionConfig() won't return the current endpoint + // It would instead return the default ENDPOINT which is not that useful to us + // If connection is provided then we can use it instead of the hook to resolve the endpoint + if (connection) { + // Endpoint is stored as internal _rpcEndpoint prop + endpoint = (connection as any)._rpcEndpoint ?? endpoint; + } + + const env = ENDPOINTS.find(end => end.endpoint === endpoint); + + let cluster; + + if (env?.ChainId == ChainId.Testnet) { + cluster = 'testnet'; + } else if (env?.ChainId == ChainId.Devnet) { + if (env?.name === 'localnet') { + cluster = `custom&customUrl=${encodeURIComponent( + 'http://127.0.0.1:8899', + )}`; + } else { + cluster = 'devnet'; + } + } + + return cluster ? `?cluster=${cluster}` : ''; + }; + + return `https://explorer.solana.com/${itemType}/${viewTypeOrItemAddress}${getClusterUrlParam()}`; +} + +/// Returns explorer inspector URL for the given transaction +export function getExplorerInspectorUrl( + endpoint: string, + transaction: Transaction, + connection?: Connection, +) { + const SIGNATURE_LENGTH = 64; + + const explorerUrl = new URL( + getExplorerUrl('inspector', endpoint, 'tx', connection), + ); + + const signatures = transaction.signatures.map(s => + base58.encode(s.signature ?? Buffer.alloc(SIGNATURE_LENGTH)), + ); + explorerUrl.searchParams.append('signatures', JSON.stringify(signatures)); + + const message = transaction.serializeMessage(); + explorerUrl.searchParams.append('message', message.toString('base64')); + + return explorerUrl.toString(); +} diff --git a/packages/common/src/utils/ids.ts b/packages/common/src/utils/ids.ts index 37697a3f..91106273 100644 --- a/packages/common/src/utils/ids.ts +++ b/packages/common/src/utils/ids.ts @@ -46,10 +46,6 @@ let WORMHOLE_BRIDGE: { wrappedMaster: string; }; -let GOVERNANCE: { - programId: PublicKey; -}; - let SWAP_PROGRAM_ID: PublicKey; let SWAP_PROGRAM_LEGACY_IDS: PublicKey[]; let SWAP_PROGRAM_LAYOUT: any; @@ -66,9 +62,7 @@ export const ENABLE_FEES_INPUT = false; export const PROGRAM_IDS = [ { name: 'mainnet-beta', - governance: () => ({ - programId: new PublicKey('9iAeqqppjn7g1Jn8o2cQCqU5aQVV3h4q9bbWdKRbeC2w'), - }), + wormhole: () => ({ pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'), bridge: '0xf92cD566Ea4864356C5491c177A430C222d7e678', @@ -87,9 +81,7 @@ export const PROGRAM_IDS = [ }, { name: 'testnet', - governance: () => ({ - programId: new PublicKey('A9KW1nhwZUr1kMX8C6rgzZvAE9AwEEUi2C77SiVvEiuN'), - }), + wormhole: () => ({ pubkey: new PublicKey('5gQf5AUhAgWYgUCt9ouShm9H7dzzXUsLdssYwe5krKhg'), bridge: '0x251bBCD91E84098509beaeAfF0B9951859af66D3', @@ -106,9 +98,7 @@ export const PROGRAM_IDS = [ { name: 'devnet', - governance: () => ({ - programId: new PublicKey('A9KW1nhwZUr1kMX8C6rgzZvAE9AwEEUi2C77SiVvEiuN'), - }), + wormhole: () => ({ pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'), bridge: '0xf92cD566Ea4864356C5491c177A430C222d7e678', @@ -124,9 +114,7 @@ export const PROGRAM_IDS = [ }, { name: 'localnet', - governance: () => ({ - programId: new PublicKey('2uWrXQ3tMurqTLe3Dmue6DzasUGV9UPqK7AK7HzS7v3D'), - }), + wormhole: () => ({ pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'), bridge: '0xf92cD566Ea4864356C5491c177A430C222d7e678', @@ -156,8 +144,6 @@ export const setProgramIds = (envName: string) => { SWAP_PROGRAM_LAYOUT = swap.current.layout; SWAP_PROGRAM_LEGACY_IDS = swap.legacy; - GOVERNANCE = instance.governance(); - if (envName === 'mainnet-beta') { LENDING_PROGRAM_ID = new PublicKey( 'LendZqTs7gn5CTSJU1jWKhKuVpjJGom45nnwPb2AMTi', @@ -173,7 +159,7 @@ export const programIds = () => { swapLayout: SWAP_PROGRAM_LAYOUT, lending: LENDING_PROGRAM_ID, wormhole: WORMHOLE_BRIDGE, - governance: GOVERNANCE, + associatedToken: SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, bpf_upgrade_loader: BPF_UPGRADE_LOADER_ID, system: SYSTEM, diff --git a/packages/common/src/utils/index.tsx b/packages/common/src/utils/index.tsx index dbe5b1e5..8464d615 100644 --- a/packages/common/src/utils/index.tsx +++ b/packages/common/src/utils/index.tsx @@ -6,3 +6,5 @@ export * from './utils'; export * from './strings'; export * as shortvec from './shortvec'; export * from './borsh'; +export * from './errors'; +export * from './explorer'; diff --git a/packages/common/src/utils/notifications.tsx b/packages/common/src/utils/notifications.tsx index ae1dc158..a6d2ad81 100644 --- a/packages/common/src/utils/notifications.tsx +++ b/packages/common/src/utils/notifications.tsx @@ -1,13 +1,13 @@ -import React from "react"; -import { notification } from "antd"; +import React from 'react'; +import { notification } from 'antd'; // import Link from '../components/Link'; export function notify({ - message = "", + message = '', description = undefined as any, - txid = "", - type = "info", - placement = "bottomLeft", + txid = '', + type = 'info', + placement = 'bottomLeft', }) { if (txid) { // ; } (notification as any)[type]({ - message: {message}, + message: {message}, description: ( - {description} + {description} ), placement, style: { - backgroundColor: "white", + backgroundColor: 'white', }, }); } diff --git a/packages/common/src/wallet-adapters/index.tsx b/packages/common/src/wallet-adapters/index.tsx deleted file mode 100644 index 73ba6ecc..00000000 --- a/packages/common/src/wallet-adapters/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * as solong_adapter from './solong_adapter'; diff --git a/packages/common/src/wallet-adapters/phantom/index.tsx b/packages/common/src/wallet-adapters/phantom/index.tsx deleted file mode 100644 index 6618192f..00000000 --- a/packages/common/src/wallet-adapters/phantom/index.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import EventEmitter from 'eventemitter3'; -import { PublicKey, Transaction } from '@solana/web3.js'; -import { notify } from '../../utils/notifications'; -import { WalletAdapter } from '@solana/wallet-base'; - -type PhantomEvent = 'disconnect' | 'connect'; -type PhantomRequestMethod = - | 'connect' - | 'disconnect' - | 'signTransaction' - | 'signAllTransactions'; - -interface PhantomProvider { - publicKey?: PublicKey; - isConnected?: boolean; - autoApprove?: boolean; - signTransaction: (transaction: Transaction) => Promise; - signAllTransactions: (transactions: Transaction[]) => Promise; - connect: () => Promise; - disconnect: () => Promise; - on: (event: PhantomEvent, handler: (args: any) => void) => void; - request: (method: PhantomRequestMethod, params: any) => Promise; -} - -export class PhantomWalletAdapter - extends EventEmitter - implements WalletAdapter { - _provider: PhantomProvider | undefined; - _cachedCorrectKey?: PublicKey; - constructor() { - super(); - this.connect = this.connect.bind(this); - } - - get connected() { - return this._provider?.isConnected || false; - } - - get autoApprove() { - return this._provider?.autoApprove || false; - } - - async signAllTransactions( - transactions: Transaction[], - ): Promise { - if (!this._provider) { - return transactions; - } - - return this._provider.signAllTransactions(transactions); - } - - get publicKey() { - // Due to weird phantom bug where their public key isnt quite like ours - if (!this._cachedCorrectKey && this._provider?.publicKey) - this._cachedCorrectKey = new PublicKey( - this._provider.publicKey.toBase58(), - ); - - return this._cachedCorrectKey || null; - } - - async signTransaction(transaction: Transaction) { - if (!this._provider) { - return transaction; - } - - return this._provider.signTransaction(transaction); - } - - connect = async () => { - if (this._provider) { - return; - } - - let provider: PhantomProvider; - if ((window as any)?.solana?.isPhantom) { - provider = (window as any).solana; - } else { - window.open('https://phantom.app/', '_blank'); - notify({ - message: 'Phantom Error', - description: 'Please install Phantom wallet from Chrome ', - }); - return; - } - - provider.on('connect', () => { - this._provider = provider; - this.emit('connect'); - }); - - if (!provider.isConnected) { - await provider.connect(); - } - - this._provider = provider; - this.emit('connect'); - }; - - disconnect() { - if (this._provider) { - this._provider.disconnect(); - this._provider = undefined; - this.emit('disconnect'); - } - } -} diff --git a/packages/common/src/wallet-adapters/solong/index.tsx b/packages/common/src/wallet-adapters/solong/index.tsx deleted file mode 100644 index 4f2bcc66..00000000 --- a/packages/common/src/wallet-adapters/solong/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import EventEmitter from "eventemitter3"; -import {PublicKey, Transaction} from "@solana/web3.js"; -import { WalletAdapter } from "@solana/wallet-base"; -import { notify } from "../../utils/notifications"; - -export class SolongWalletAdapter extends EventEmitter implements WalletAdapter { - _publicKey: PublicKey | null; - _onProcess: boolean; - constructor() { - super(); - this._publicKey = null; - this._onProcess = false; - this.connect = this.connect.bind(this); - } - - get publicKey() { - return this._publicKey; - } - - async signTransaction(transaction: Transaction) { - return (window as any).solong.signTransaction(transaction); - } - - async signAllTransactions(transactions: Transaction[]) { - return transactions; - } - - connect() { - if (this._onProcess) { - return; - } - - if ((window as any).solong === undefined) { - notify({ - message: "Solong Error", - description: "Please install solong wallet from Chrome ", - }); - return; - } - - this._onProcess = true; - (window as any).solong - .selectAccount() - .then((account: any) => { - this._publicKey = new PublicKey(account); - this.emit("connect", this._publicKey); - }) - .catch(() => { - this.disconnect(); - }) - .finally(() => { - this._onProcess = false; - }); - } - - disconnect() { - if (this._publicKey) { - this._publicKey = null; - this.emit("disconnect"); - } - } -} diff --git a/packages/common/src/wallet-adapters/solong_adapter.tsx b/packages/common/src/wallet-adapters/solong_adapter.tsx deleted file mode 100644 index aef7dd64..00000000 --- a/packages/common/src/wallet-adapters/solong_adapter.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import EventEmitter from "eventemitter3"; -import { PublicKey } from "@solana/web3.js"; -import { notify } from "../utils/notifications"; - -export class SolongAdapter extends EventEmitter { - _publicKey: any; - _onProcess: boolean; - constructor(providerUrl: string, network: string) { - super(); - this._publicKey = null; - this._onProcess = false; - this.connect = this.connect.bind(this); - } - - get publicKey() { - return this._publicKey; - } - - async signTransaction(transaction: any) { - return (window as any).solong.signTransaction(transaction); - } - - connect() { - if (this._onProcess) { - return; - } - - if ((window as any).solong === undefined) { - notify({ - message: "Solong Error", - description: "Please install solong wallet from Chrome ", - }); - return; - } - - this._onProcess = true; - (window as any).solong - .selectAccount() - .then((account: any) => { - this._publicKey = new PublicKey(account); - this.emit("connect", this._publicKey); - }) - .catch(() => { - this.disconnect(); - }) - .finally(() => { - this._onProcess = false; - }); - } - - disconnect() { - if (this._publicKey) { - this._publicKey = null; - this.emit("disconnect"); - } - } -} diff --git a/packages/common/src/wallet-adapters/torus/index.tsx b/packages/common/src/wallet-adapters/torus/index.tsx deleted file mode 100644 index 7d1e1578..00000000 --- a/packages/common/src/wallet-adapters/torus/index.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import EventEmitter from "eventemitter3" -import { Account, PublicKey, Transaction } from "@solana/web3.js" -import { WalletAdapter } from "@solana/wallet-base" -import OpenLogin from "@toruslabs/openlogin" -import { getED25519Key } from "@toruslabs/openlogin-ed25519" - -const getSolanaPrivateKey = (openloginKey: string)=>{ - const { sk } = getED25519Key(openloginKey) - return sk -} - -export class TorusWalletAdapter extends EventEmitter implements WalletAdapter { - _provider: OpenLogin | undefined; - endpoint: string; - providerUrl: string; - account: Account | undefined; - image: string = ''; - name: string = ''; - - constructor(providerUrl: string, endpoint: string) { - super() - this.connect = this.connect.bind(this) - this.endpoint = endpoint; - this.providerUrl = providerUrl; - } - - async signAllTransactions(transactions: Transaction[]): Promise { - if(this.account) { - let account = this.account; - transactions.forEach(t => t.partialSign(account)); - } - - return transactions - } - - get publicKey() { - return this.account?.publicKey || null; - } - - async signTransaction(transaction: Transaction) { - if(this.account) { - transaction.partialSign(this.account) - } - - return transaction - } - - connect = async () => { - const clientId = process.env.REACT_APP_CLIENT_ID || 'BNxdRWx08cSTPlzMAaShlM62d4f8Tp6racfnCg_gaH0XQ1NfSGo3h5B_IkLtgSnPMhlxsSvhqugWm0x8x-VkUXA'; - this._provider = new OpenLogin({ - clientId, - network: "testnet", // mainnet, testnet, development - uxMode: 'popup' - }); - - try { - await this._provider.init(); - } catch (ex) { - console.error('init failed', ex) - } - - console.error(this._provider?.state.store); - - if (this._provider.privKey) { - const privateKey = this._provider.privKey; - const secretKey = getSolanaPrivateKey(privateKey); - this.account = new Account(secretKey); - } else { - try { - const { privKey } = await this._provider.login({ loginProvider: "unselected"} as any); - const secretKey = getSolanaPrivateKey(privKey); - this.account = new Account(secretKey); - } catch(ex) { - console.error('login failed', ex); - } - } - - this.name = this._provider?.state.store.get('name');; - this.image = this._provider?.state.store.get('profileImage'); - debugger; - - this.emit("connect"); - } - - disconnect = async () => { - console.log("Disconnecting...") - if (this._provider) { - await this._provider.logout(); - await this._provider._cleanup(); - this._provider = undefined; - this.emit("disconnect"); - } - } -} diff --git a/packages/governance/README.md b/packages/governance/README.md new file mode 100644 index 00000000..6fd40aec --- /dev/null +++ b/packages/governance/README.md @@ -0,0 +1,63 @@ +# Governance UI + +The Governance package is MVP implementation for spl-governance program. + +## Setup + +Be sure to be running Node v12.16.2 and yarn version 1.22.10. + +In order to build the common package and governance run: + +`yarn && yarn bootstrap && yarn build --scope @oyster/common --scope governance` + +Then run: + +`yarn start governance` + +## ⚠️ Warning + +Any content produced by Solana, or developer resources that Solana provides, +are for educational and inspiration purposes only. +Solana does not encourage, +induce or sanction the deployment of any such applications in violation of applicable laws or regulations. + +## Disclaimer + +All claims, content, designs, algorithms, estimates, roadmaps, +specifications, and performance measurements described in this project +are done with the Solana Foundation's ("SF") best efforts. It is up to +the reader to check and validate their accuracy and truthfulness. +Furthermore nothing in this project constitutes a solicitation for +investment. + +Any content produced by SF or developer resources that SF provides, are +for educational and inspiration purposes only. SF does not encourage, +induce or sanction the deployment, integration or use of any such +applications (including the code comprising the Solana blockchain +protocol) in violation of applicable laws or regulations and hereby +prohibits any such deployment, integration or use. This includes use of +any such applications by the reader (a) in violation of export control +or sanctions laws of the United States or any other applicable +jurisdiction, (b) if the reader is located in or ordinarily resident in +a country or territory subject to comprehensive sanctions administered +by the U.S. Office of Foreign Assets Control (OFAC), or (c) if the +reader is or is working on behalf of a Specially Designated National +(SDN) or a person subject to similar blocking or denied party +prohibitions. + +The reader should be aware that U.S. export control and sanctions laws +prohibit U.S. persons (and other persons that are subject to such laws) +from transacting with persons in certain countries and territories or +that are on the SDN list. As a project based primarily on open-source +software, it is possible that such sanctioned persons may nevertheless +bypass prohibitions, obtain the code comprising the Solana blockchain +protocol (or other project code or applications) and deploy, integrate, +or otherwise use it. Accordingly, there is a risk to individuals that +other persons using the Solana blockchain protocol may be sanctioned +persons and that transactions with such persons would be a violation of +U.S. export controls and sanctions law. This risk applies to +individuals, organizations, and other ecosystem participants that +deploy, integrate, or use the Solana blockchain protocol code directly +(e.g., as a node operator), and individuals that transact on the Solana +blockchain through light clients, third party interfaces, and/or wallet +software. diff --git a/packages/governance/package.json b/packages/governance/package.json index b7a3fede..d785939e 100644 --- a/packages/governance/package.json +++ b/packages/governance/package.json @@ -6,14 +6,12 @@ "@ant-design/pro-layout": "^6.7.0", "@babel/preset-typescript": "^7.12.13", "@craco/craco": "^5.7.0", - "@oyster/common": "0.0.1", + "@oyster/common": "0.0.2", + "@project-serum/borsh": "^0.2.2", "@project-serum/serum": "^0.13.11", - "@project-serum/sol-wallet-adapter": "^0.1.4", "@solana/spl-token": "0.0.13", "@solana/spl-token-swap": "0.1.0", - "@solana/wallet-base": "0.0.1", - "@solana/wallet-ledger": "0.0.1", - "@solana/web3.js": "^1.5.0", + "@solana/web3.js": "^1.22.0", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.5.0", "@testing-library/user-event": "^7.2.1", @@ -26,13 +24,13 @@ "antd": "^4.6.6", "bn.js": "^5.1.3", "bs58": "^4.0.1", - "d3": "6.6.0", "buffer-layout": "^1.2.0", "cannon": "^0.6.2", "chart.js": "^2.9.4", "craco-alias": "^2.1.1", "craco-babel-loader": "^0.1.4", "craco-less": "^1.17.0", + "d3": "6.6.0", "echarts": "^4.9.0", "eventemitter3": "^4.0.7", "identicon.js": "^2.3.3", @@ -40,12 +38,14 @@ "lodash": "^4.17.20", "react": "16.13.1", "react-dom": "16.13.1", + "react-error-boundary": "^3.1.3", "react-github-btn": "^1.2.0", "react-intl": "^5.10.2", "react-markdown": "5.0.3", "react-router-dom": "^5.2.0", "react-scripts": "3.4.3", "react-three-fiber": "^5.3.19", + "superstruct": "^0.15.2", "typescript": "^4.1.3" }, "scripts": { @@ -60,7 +60,7 @@ "localnet:down": "solana-localnet down", "localnet:logs": "solana-localnet logs -f", "predeploy": "git pull --ff-only && yarn && yarn build", - "deploy": "gh-pages -d ../../build/governance --repo https://github.com/solana-labs/oyster-gov", + "deploy": "gh-pages -d ../../build/governance --repo https://github.com/UXDProtocol/oyster-uxd", "deploy:ar": "arweave deploy-dir ../../build/governance --key-file ", "format:fix": "prettier --write \"**/*.+(js|jsx|ts|tsx|json|css|md)\"" }, @@ -81,17 +81,18 @@ }, "repository": { "type": "git", - "url": "https://github.com/solana-labs/oyster" + "url": "https://github.com/UXDProtocol/oyster" }, "homepage": ".", "devDependencies": { "@types/bn.js": "^5.1.0", "@types/bs58": "^4.0.1", + "@types/d3": "6.3.0", "@types/identicon.js": "^2.3.0", "@types/jest": "^24.9.1", "@types/node": "^12.12.62", - "@types/d3": "6.3.0", "arweave-deploy": "^1.9.1", + "eslint-config-react-app": "^6.0.0", "gh-pages": "^3.1.0", "npm-link-shared": "0.5.6", "prettier": "^2.1.2" diff --git a/packages/governance/src/actions/addCustomSingleSignerTransaction.ts b/packages/governance/src/actions/addCustomSingleSignerTransaction.ts deleted file mode 100644 index 5fd87ecb..00000000 --- a/packages/governance/src/actions/addCustomSingleSignerTransaction.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { - Account, - Connection, - PublicKey, - SystemProgram, - TransactionInstruction, -} from '@solana/web3.js'; -import { contexts, utils, models, ParsedAccount } from '@oyster/common'; -import { - GOVERNANCE_AUTHORITY_SEED, - CustomSingleSignerTransactionLayout, - Proposal, - ProposalState, -} from '../models/governance'; -import { addCustomSingleSignerTransactionInstruction } from '../models/addCustomSingleSignerTransaction'; - -const { sendTransaction } = contexts.Connection; -const { notify } = utils; -const { approve } = models; - -export const addCustomSingleSignerTransaction = async ( - connection: Connection, - wallet: any, - proposal: ParsedAccount, - state: ParsedAccount, - sigAccount: PublicKey, - slot: string, - instruction: string, - position: number, -) => { - const PROGRAM_IDS = utils.programIds(); - - let signers: Account[] = []; - let instructions: TransactionInstruction[] = []; - - const rentExempt = await connection.getMinimumBalanceForRentExemption( - CustomSingleSignerTransactionLayout.span, - ); - - const txnKey = new Account(); - - const uninitializedTxnInstruction = SystemProgram.createAccount({ - fromPubkey: wallet.publicKey, - newAccountPubkey: txnKey.publicKey, - lamports: rentExempt, - space: CustomSingleSignerTransactionLayout.span, - programId: PROGRAM_IDS.governance.programId, - }); - - const [authority] = await PublicKey.findProgramAddress( - [Buffer.from(GOVERNANCE_AUTHORITY_SEED), proposal.pubkey.toBuffer()], - PROGRAM_IDS.governance.programId, - ); - - signers.push(txnKey); - - instructions.push(uninitializedTxnInstruction); - - const transferAuthority = approve( - instructions, - [], - sigAccount, - wallet.publicKey, - 1, - ); - signers.push(transferAuthority); - - /*instruction = ( - await serializeInstruction({ - connection, - instr: pingInstruction(), - proposal - }) - ).base64; - - console.log(pingInstruction()); - const asArr = ( - await serializeInstruction({ - connection, - instr: pingInstruction(), - proposal - }) - ).byteArray; - - console.log(asArr); - console.log('Message', Message.from(asArr));*/ - - instructions.push( - addCustomSingleSignerTransactionInstruction( - txnKey.publicKey, - state.pubkey, - sigAccount, - proposal.info.signatoryValidation, - proposal.pubkey, - proposal.info.config, - transferAuthority.publicKey, - authority, - slot, - instruction, - position, - ), - ); - - notify({ - message: 'Adding transaction...', - description: 'Please wait...', - type: 'warn', - }); - - try { - let tx = await sendTransaction( - connection, - wallet, - instructions, - signers, - true, - ); - - notify({ - message: 'Transaction added.', - type: 'success', - description: `Transaction - ${tx}`, - }); - } catch (ex) { - console.error(ex); - throw new Error(); - } -}; diff --git a/packages/governance/src/actions/addSigner.ts b/packages/governance/src/actions/addSigner.ts deleted file mode 100644 index 9263bd3f..00000000 --- a/packages/governance/src/actions/addSigner.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { - Account, - Connection, - PublicKey, - TransactionInstruction, -} from '@solana/web3.js'; -import { - contexts, - utils, - models, - ParsedAccount, - actions, -} from '@oyster/common'; - -import { - GOVERNANCE_AUTHORITY_SEED, - Proposal, - ProposalState, -} from '../models/governance'; -import { AccountLayout } from '@solana/spl-token'; -import { addSignerInstruction } from '../models/addSigner'; -const { createTokenAccount } = actions; -const { sendTransaction } = contexts.Connection; -const { notify } = utils; -const { approve } = models; - -export const addSigner = async ( - connection: Connection, - wallet: any, - proposal: ParsedAccount, - state: ParsedAccount, - adminAccount: PublicKey, - newSignatoryAccountOwner: PublicKey, -) => { - const PROGRAM_IDS = utils.programIds(); - - let signers: Account[] = []; - let instructions: TransactionInstruction[] = []; - - const accountRentExempt = await connection.getMinimumBalanceForRentExemption( - AccountLayout.span, - ); - - const newSignerAccount = createTokenAccount( - instructions, - wallet.publicKey, - accountRentExempt, - proposal.info.signatoryMint, - newSignatoryAccountOwner, - signers, - ); - - const [mintAuthority] = await PublicKey.findProgramAddress( - [Buffer.from(GOVERNANCE_AUTHORITY_SEED), proposal.pubkey.toBuffer()], - PROGRAM_IDS.governance.programId, - ); - - const transferAuthority = approve( - instructions, - [], - adminAccount, - wallet.publicKey, - 1, - ); - signers.push(transferAuthority); - - instructions.push( - addSignerInstruction( - newSignerAccount, - proposal.info.signatoryMint, - adminAccount, - proposal.info.adminValidation, - state.pubkey, - proposal.pubkey, - transferAuthority.publicKey, - mintAuthority, - ), - ); - - notify({ - message: 'Adding signer...', - description: 'Please wait...', - type: 'warn', - }); - - try { - let tx = await sendTransaction( - connection, - wallet, - instructions, - signers, - true, - ); - - notify({ - message: 'Signer added.', - type: 'success', - description: `Transaction - ${tx}`, - }); - } catch (ex) { - console.error(ex); - throw new Error(); - } -}; diff --git a/packages/governance/src/actions/cancelProposal.ts b/packages/governance/src/actions/cancelProposal.ts new file mode 100644 index 00000000..1dbd219c --- /dev/null +++ b/packages/governance/src/actions/cancelProposal.ts @@ -0,0 +1,37 @@ +import { Account, TransactionInstruction } from '@solana/web3.js'; +import { ParsedAccount } from '@oyster/common'; + +import { Proposal } from '../models/accounts'; + +import { withCancelProposal } from '../models/withCancelProposal'; +import { sendTransactionWithNotifications } from '../tools/transactions'; +import { RpcContext } from '../models/core/api'; + +export const cancelProposal = async ( + { connection, wallet, programId, programVersion, walletPubkey }: RpcContext, + proposal: ParsedAccount, +) => { + let governanceAuthority = walletPubkey; + + let signers: Account[] = []; + let instructions: TransactionInstruction[] = []; + + withCancelProposal( + instructions, + programId, + programVersion, + proposal.pubkey, + proposal.info.tokenOwnerRecord, + governanceAuthority, + proposal.info.governance, + ); + + await sendTransactionWithNotifications( + connection, + wallet, + instructions, + signers, + 'Cancelling proposal', + 'Proposal cancelled', + ); +}; diff --git a/packages/governance/src/actions/castVote.ts b/packages/governance/src/actions/castVote.ts new file mode 100644 index 00000000..7e3bcc66 --- /dev/null +++ b/packages/governance/src/actions/castVote.ts @@ -0,0 +1,46 @@ +import { Account, PublicKey, TransactionInstruction } from '@solana/web3.js'; +import { ParsedAccount } from '@oyster/common'; + +import { Proposal } from '../models/accounts'; +import { withCastVote } from '../models/withCastVote'; +import { Vote, YesNoVote } from '../models/instructions'; +import { sendTransactionWithNotifications } from '../tools/transactions'; +import { RpcContext } from '../models/core/api'; + +export const castVote = async ( + { connection, wallet, programId, programVersion, walletPubkey }: RpcContext, + realm: PublicKey, + proposal: ParsedAccount, + tokeOwnerRecord: PublicKey, + yesNoVote: YesNoVote, +) => { + let signers: Account[] = []; + let instructions: TransactionInstruction[] = []; + + let governanceAuthority = walletPubkey; + let payer = walletPubkey; + + await withCastVote( + instructions, + programId, + programVersion, + realm, + proposal.info.governance, + proposal.pubkey, + proposal.info.tokenOwnerRecord, + tokeOwnerRecord, + governanceAuthority, + proposal.info.governingTokenMint, + Vote.fromYesNoVote(yesNoVote), + payer, + ); + + await sendTransactionWithNotifications( + connection, + wallet, + instructions, + signers, + 'Voting on proposal', + 'Proposal voted on', + ); +}; diff --git a/packages/governance/src/actions/chat/postChatMessage.ts b/packages/governance/src/actions/chat/postChatMessage.ts new file mode 100644 index 00000000..2f80803b --- /dev/null +++ b/packages/governance/src/actions/chat/postChatMessage.ts @@ -0,0 +1,45 @@ +import { Account, PublicKey, TransactionInstruction } from '@solana/web3.js'; +import { ParsedAccount } from '@oyster/common'; + +import { Proposal } from '../../models/accounts'; + +import { sendTransactionWithNotifications } from '../../tools/transactions'; +import { RpcContext } from '../../models/core/api'; +import { withPostChatMessage } from '../../models/chat/withPostChatMessage'; +import { ChatMessageBody } from '../../models/chat/accounts'; + +export const postChatMessage = async ( + { connection, wallet, programId, walletPubkey }: RpcContext, + proposal: ParsedAccount, + tokeOwnerRecord: PublicKey, + replyTo: PublicKey | undefined, + body: ChatMessageBody, +) => { + let signers: Account[] = []; + let instructions: TransactionInstruction[] = []; + + let governanceAuthority = walletPubkey; + let payer = walletPubkey; + + await withPostChatMessage( + instructions, + signers, + programId, + proposal.info.governance, + proposal.pubkey, + tokeOwnerRecord, + governanceAuthority, + payer, + replyTo, + body, + ); + + await sendTransactionWithNotifications( + connection, + wallet, + instructions, + signers, + 'Posting comment', + 'Comment post', + ); +}; diff --git a/packages/governance/src/actions/createProposal.ts b/packages/governance/src/actions/createProposal.ts index 742e81b5..f9057298 100644 --- a/packages/governance/src/actions/createProposal.ts +++ b/packages/governance/src/actions/createProposal.ts @@ -1,363 +1,70 @@ -import { - Account, - Connection, - PublicKey, - SystemProgram, - TransactionInstruction, -} from '@solana/web3.js'; -import { - contexts, - utils, - actions, - ParsedAccount, - SequenceType, -} from '@oyster/common'; +import { PublicKey, TransactionInstruction } from '@solana/web3.js'; -import { AccountLayout, MintLayout } from '@solana/spl-token'; -import { initProposalInstruction } from '../models/initProposal'; -import { - GOVERNANCE_AUTHORITY_SEED, - Governance, - ProposalLayout, - ProposalStateLayout, -} from '../models/governance'; - -const { cache } = contexts.Accounts; -const { sendTransactions } = contexts.Connection; -const { createMint, createTokenAccount } = actions; -const { notify } = utils; +import { withCreateProposal } from '../models/withCreateProposal'; +import { withAddSignatory } from '../models/withAddSignatory'; +import { sendTransactionWithNotifications } from '../tools/transactions'; +import { RpcContext } from '../models/core/api'; +import { VoteType } from '../models/accounts'; export const createProposal = async ( - connection: Connection, - wallet: any, + { connection, wallet, programId, programVersion, walletPubkey }: RpcContext, + realm: PublicKey, + governance: PublicKey, + tokenOwnerRecord: PublicKey, name: string, - description: string, - useGovernance: boolean, - governance: ParsedAccount, -): Promise => { - const PROGRAM_IDS = utils.programIds(); - - let signers: Account[] = []; + descriptionLink: string, + governingTokenMint: PublicKey, + proposalIndex: number, +): Promise => { let instructions: TransactionInstruction[] = []; - const mintRentExempt = await connection.getMinimumBalanceForRentExemption( - MintLayout.span, - ); - const accountRentExempt = await connection.getMinimumBalanceForRentExemption( - AccountLayout.span, - ); + let governanceAuthority = walletPubkey; + let signatory = walletPubkey; + let payer = walletPubkey; - const sourceMintDecimals = ( - await cache.queryMint( - connection, - useGovernance - ? governance.info.governanceMint - : governance.info.councilMint!, - ) - ).decimals; + // V2 Approve/Deny configuration + const voteType = VoteType.SINGLE_CHOICE; + const options = ['Approve']; + const useDenyOption = true; - const proposalKey = new Account(); - - const { - sigMint, - voteMint, - yesVoteMint, - noVoteMint, - adminMint, - voteValidationAccount, - sigValidationAccount, - adminValidationAccount, - adminDestinationAccount, - sigDestinationAccount, - sourceHoldingAccount, - authority, - instructions: associatedInstructions, - signers: associatedSigners, - } = await getAssociatedAccountsAndInstructions( - wallet, - accountRentExempt, - mintRentExempt, + const proposalAddress = await withCreateProposal( + instructions, + programId, + programVersion, + realm, governance, - useGovernance, - sourceMintDecimals, - proposalKey, - ); - - let createGovernanceAccountsSigners: Account[] = []; - let createGovernanceAccountsInstructions: TransactionInstruction[] = []; - - const proposalRentExempt = await connection.getMinimumBalanceForRentExemption( - ProposalLayout.span, - ); - - const proposalStateRentExempt = await connection.getMinimumBalanceForRentExemption( - ProposalStateLayout.span, - ); - - const proposalStateKey = new Account(); - - const uninitializedProposalStateInstruction = SystemProgram.createAccount({ - fromPubkey: wallet.publicKey, - newAccountPubkey: proposalStateKey.publicKey, - lamports: proposalStateRentExempt, - space: ProposalStateLayout.span, - programId: PROGRAM_IDS.governance.programId, - }); - signers.push(proposalStateKey); - createGovernanceAccountsSigners.push(proposalStateKey); - createGovernanceAccountsInstructions.push( - uninitializedProposalStateInstruction, - ); - - const uninitializedProposalInstruction = SystemProgram.createAccount({ - fromPubkey: wallet.publicKey, - newAccountPubkey: proposalKey.publicKey, - lamports: proposalRentExempt, - space: ProposalLayout.span, - programId: PROGRAM_IDS.governance.programId, - }); - signers.push(proposalKey); - createGovernanceAccountsSigners.push(proposalKey); - createGovernanceAccountsInstructions.push(uninitializedProposalInstruction); - - instructions.push( - initProposalInstruction( - proposalStateKey.publicKey, - proposalKey.publicKey, - governance.pubkey, - sigMint, - adminMint, - voteMint, - yesVoteMint, - noVoteMint, - sigValidationAccount, - adminValidationAccount, - voteValidationAccount, - adminDestinationAccount, - sigDestinationAccount, - sourceHoldingAccount, - useGovernance - ? governance.info.governanceMint - : governance.info.councilMint!, - authority, - description, - name, - ), + tokenOwnerRecord, + name, + descriptionLink, + governingTokenMint, + + governanceAuthority, + proposalIndex, + voteType, + options, + useDenyOption, + payer, + ); + + // Add the proposal creator as the default signatory + await withAddSignatory( + instructions, + programId, + proposalAddress, + tokenOwnerRecord, + governanceAuthority, + signatory, + payer, + ); + + await sendTransactionWithNotifications( + connection, + wallet, + instructions, + [], + 'Creating proposal', + 'Proposal has been created', ); - notify({ - message: 'Initializing Proposal...', - description: 'Please wait...', - type: 'warn', - }); - - try { - let tx = await sendTransactions( - connection, - wallet, - [ - ...associatedInstructions, - createGovernanceAccountsInstructions, - instructions, - ], - [...associatedSigners, createGovernanceAccountsSigners, signers], - ); - - notify({ - message: 'Proposal created.', - type: 'success', - description: `Transaction - ${tx}`, - }); - - return proposalKey; - } catch (ex) { - console.error(ex); - throw new Error(); - } + return proposalAddress; }; - -interface ValidationReturn { - sigMint: PublicKey; - voteMint: PublicKey; - yesVoteMint: PublicKey; - noVoteMint: PublicKey; - adminMint: PublicKey; - voteValidationAccount: PublicKey; - sigValidationAccount: PublicKey; - adminValidationAccount: PublicKey; - adminDestinationAccount: PublicKey; - sigDestinationAccount: PublicKey; - sourceHoldingAccount: PublicKey; - authority: PublicKey; - signers: Account[][]; - instructions: TransactionInstruction[][]; -} - -async function getAssociatedAccountsAndInstructions( - wallet: any, - accountRentExempt: number, - mintRentExempt: number, - governance: ParsedAccount, - useGovernance: boolean, - sourceMintDecimals: number, - newProposalKey: Account, -): Promise { - const PROGRAM_IDS = utils.programIds(); - - const [authority] = await PublicKey.findProgramAddress( - [ - Buffer.from(GOVERNANCE_AUTHORITY_SEED), - newProposalKey.publicKey.toBuffer(), - ], - PROGRAM_IDS.governance.programId, - ); - - let mintSigners: Account[] = []; - let mintInstructions: TransactionInstruction[] = []; - - const adminMint = createMint( - mintInstructions, - wallet.publicKey, - mintRentExempt, - 0, - authority, - authority, - mintSigners, - ); - - const sigMint = createMint( - mintInstructions, - wallet.publicKey, - mintRentExempt, - 0, - authority, - authority, - mintSigners, - ); - - let voteMintSigners: Account[] = []; - let voteMintInstructions: TransactionInstruction[] = []; - - const voteMint = createMint( - voteMintInstructions, - wallet.publicKey, - mintRentExempt, - sourceMintDecimals, - authority, - authority, - voteMintSigners, - ); - - const yesVoteMint = createMint( - voteMintInstructions, - wallet.publicKey, - mintRentExempt, - sourceMintDecimals, - authority, - authority, - voteMintSigners, - ); - - const noVoteMint = createMint( - voteMintInstructions, - wallet.publicKey, - mintRentExempt, - sourceMintDecimals, - authority, - authority, - voteMintSigners, - ); - - let validationSigners: Account[] = []; - let validationInstructions: TransactionInstruction[] = []; - - const adminValidationAccount = createTokenAccount( - validationInstructions, - wallet.publicKey, - accountRentExempt, - adminMint, - authority, - validationSigners, - ); - - const sigValidationAccount = createTokenAccount( - validationInstructions, - wallet.publicKey, - accountRentExempt, - sigMint, - authority, - validationSigners, - ); - - const voteValidationAccount = createTokenAccount( - validationInstructions, - wallet.publicKey, - accountRentExempt, - voteMint, - authority, - validationSigners, - ); - - let destinationSigners: Account[] = []; - let destinationInstructions: TransactionInstruction[] = []; - - const adminDestinationAccount = createTokenAccount( - destinationInstructions, - wallet.publicKey, - accountRentExempt, - adminMint, - wallet.publicKey, - destinationSigners, - ); - const sigDestinationAccount = createTokenAccount( - destinationInstructions, - wallet.publicKey, - accountRentExempt, - sigMint, - wallet.publicKey, - destinationSigners, - ); - - let holdingSigners: Account[] = []; - let holdingInstructions: TransactionInstruction[] = []; - - const sourceHoldingAccount = createTokenAccount( - holdingInstructions, - wallet.publicKey, - accountRentExempt, - useGovernance - ? governance.info.governanceMint - : governance.info.councilMint!, - authority, - holdingSigners, - ); - - return { - sigMint, - voteMint, - adminMint, - yesVoteMint, - noVoteMint, - voteValidationAccount, - sigValidationAccount, - adminValidationAccount, - adminDestinationAccount, - sigDestinationAccount, - sourceHoldingAccount, - authority, - signers: [ - mintSigners, - voteMintSigners, - validationSigners, - destinationSigners, - holdingSigners, - ], - instructions: [ - mintInstructions, - voteMintInstructions, - validationInstructions, - destinationInstructions, - holdingInstructions, - ], - }; -} diff --git a/packages/governance/src/actions/createTreasuryAccount.ts b/packages/governance/src/actions/createTreasuryAccount.ts new file mode 100644 index 00000000..27c3c14d --- /dev/null +++ b/packages/governance/src/actions/createTreasuryAccount.ts @@ -0,0 +1,58 @@ +import { Account, PublicKey, TransactionInstruction } from '@solana/web3.js'; + +import { GovernanceConfig } from '../models/accounts'; + +import { sendTransactionWithNotifications } from '../tools/transactions'; + +import { withCreateTokenGovernance } from '../models/withCreateTokenGovernance'; +import { RpcContext } from '../models/core/api'; +import { withCreateSplTokenAccount } from '../models/splToken/withCreateSplTokenAccount'; + +export const createTreasuryAccount = async ( + { connection, wallet, programId, walletPubkey }: RpcContext, + realm: PublicKey, + mint: PublicKey, + config: GovernanceConfig, + tokenOwnerRecord: PublicKey, +): Promise => { + let instructions: TransactionInstruction[] = []; + let signers: Account[] = []; + + const tokenAccount = await withCreateSplTokenAccount( + instructions, + signers, + connection, + mint, + walletPubkey, + walletPubkey, + ); + + let governanceAddress; + let governanceAuthority = walletPubkey; + + governanceAddress = ( + await withCreateTokenGovernance( + instructions, + programId, + realm, + tokenAccount, + config, + true, + walletPubkey, + tokenOwnerRecord, + walletPubkey, + governanceAuthority, + ) + ).governanceAddress; + + await sendTransactionWithNotifications( + connection, + wallet, + instructions, + signers, + 'Creating treasury account', + 'Treasury account has been created', + ); + + return governanceAddress; +}; diff --git a/packages/governance/src/actions/depositGoverningTokens.ts b/packages/governance/src/actions/depositGoverningTokens.ts new file mode 100644 index 00000000..5dafc8cf --- /dev/null +++ b/packages/governance/src/actions/depositGoverningTokens.ts @@ -0,0 +1,51 @@ +import { PublicKey, TransactionInstruction, Account } from '@solana/web3.js'; +import { models, TokenAccount } from '@oyster/common'; +import { withDepositGoverningTokens } from '../models/withDepositGoverningTokens'; +import { sendTransactionWithNotifications } from '../tools/transactions'; +import { RpcContext } from '../models/core/api'; + +const { approve } = models; + +export const depositGoverningTokens = async ( + { connection, wallet, programId, programVersion, walletPubkey }: RpcContext, + realm: PublicKey, + governingTokenSource: TokenAccount, + governingTokenMint: PublicKey, +) => { + let instructions: TransactionInstruction[] = []; + let signers: Account[] = []; + + const amount = governingTokenSource.info.amount; + + const transferAuthority = approve( + instructions, + [], + governingTokenSource.pubkey, + walletPubkey, + amount, + ); + + signers.push(transferAuthority); + + await withDepositGoverningTokens( + instructions, + programId, + programVersion, + realm, + governingTokenSource.pubkey, + governingTokenMint, + walletPubkey, + transferAuthority.publicKey, + walletPubkey, + amount, + ); + + await sendTransactionWithNotifications( + connection, + wallet, + instructions, + signers, + 'Depositing governing tokens', + 'Tokens have been deposited', + ); +}; diff --git a/packages/governance/src/actions/depositSourceTokensAndVote.ts b/packages/governance/src/actions/depositSourceTokensAndVote.ts deleted file mode 100644 index 608d431c..00000000 --- a/packages/governance/src/actions/depositSourceTokensAndVote.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { - Account, - Connection, - PublicKey, - TransactionInstruction, -} from '@solana/web3.js'; -import { - contexts, - utils, - models, - ParsedAccount, - actions, -} from '@oyster/common'; - -import { - GOVERNANCE_AUTHORITY_SEED, - Governance, - Proposal, - ProposalState, -} from '../models/governance'; - -import { AccountLayout } from '@solana/spl-token'; - -import { LABELS } from '../constants'; - -import { depositSourceTokensInstruction } from '../models/depositSourceTokens'; -import { createEmptyGovernanceVotingRecordInstruction } from '../models/createEmptyGovernanceVotingRecord'; -import { voteInstruction } from '../models/vote'; - -const { createTokenAccount } = actions; -const { sendTransactions } = contexts.Connection; -const { notify } = utils; -const { approve } = models; - -export const depositSourceTokensAndVote = async ( - connection: Connection, - wallet: any, - proposal: ParsedAccount, - existingVoteAccount: PublicKey | undefined, - existingYesVoteAccount: PublicKey | undefined, - existingNoVoteAccount: PublicKey | undefined, - sourceAccount: PublicKey, - governance: ParsedAccount, - state: ParsedAccount, - yesVotingTokenAmount: number, - noVotingTokenAmount: number, -) => { - const votingTokenAmount = - yesVotingTokenAmount > 0 ? yesVotingTokenAmount : noVotingTokenAmount; - - const PROGRAM_IDS = utils.programIds(); - - let depositSigners: Account[] = []; - let depositInstructions: TransactionInstruction[] = []; - - const accountRentExempt = await connection.getMinimumBalanceForRentExemption( - AccountLayout.span, - ); - - let needToCreateGovAccountToo = !existingVoteAccount; - if (!existingVoteAccount) { - existingVoteAccount = createTokenAccount( - depositInstructions, - wallet.publicKey, - accountRentExempt, - proposal.info.votingMint, - wallet.publicKey, - depositSigners, - ); - } - - const [governanceVotingRecord] = await PublicKey.findProgramAddress( - [ - Buffer.from(GOVERNANCE_AUTHORITY_SEED), - PROGRAM_IDS.governance.programId.toBuffer(), - proposal.pubkey.toBuffer(), - existingVoteAccount.toBuffer(), - ], - PROGRAM_IDS.governance.programId, - ); - - if (needToCreateGovAccountToo) { - depositInstructions.push( - createEmptyGovernanceVotingRecordInstruction( - governanceVotingRecord, - proposal.pubkey, - existingVoteAccount, - wallet.publicKey, - ), - ); - } - - if (!existingYesVoteAccount) { - existingYesVoteAccount = createTokenAccount( - depositInstructions, - wallet.publicKey, - accountRentExempt, - proposal.info.yesVotingMint, - wallet.publicKey, - depositSigners, - ); - } - - if (!existingNoVoteAccount) { - existingNoVoteAccount = createTokenAccount( - depositInstructions, - wallet.publicKey, - accountRentExempt, - proposal.info.noVotingMint, - wallet.publicKey, - depositSigners, - ); - } - - const [mintAuthority] = await PublicKey.findProgramAddress( - [Buffer.from(GOVERNANCE_AUTHORITY_SEED), proposal.pubkey.toBuffer()], - PROGRAM_IDS.governance.programId, - ); - - const depositAuthority = approve( - depositInstructions, - [], - sourceAccount, - wallet.publicKey, - votingTokenAmount, - ); - - depositSigners.push(depositAuthority); - - depositInstructions.push( - depositSourceTokensInstruction( - governanceVotingRecord, - existingVoteAccount, - sourceAccount, - proposal.info.sourceHolding, - proposal.info.votingMint, - proposal.pubkey, - depositAuthority.publicKey, - mintAuthority, - votingTokenAmount, - ), - ); - - let voteSigners: Account[] = []; - let voteInstructions: TransactionInstruction[] = []; - - const voteAuthority = approve( - voteInstructions, - [], - existingVoteAccount, - wallet.publicKey, - yesVotingTokenAmount + noVotingTokenAmount, - ); - - voteSigners.push(voteAuthority); - - voteInstructions.push( - voteInstruction( - governanceVotingRecord, - state.pubkey, - existingVoteAccount, - existingYesVoteAccount, - existingNoVoteAccount, - proposal.info.votingMint, - proposal.info.yesVotingMint, - proposal.info.noVotingMint, - proposal.info.sourceMint, - proposal.pubkey, - governance.pubkey, - voteAuthority.publicKey, - mintAuthority, - yesVotingTokenAmount, - noVotingTokenAmount, - ), - ); - - const [votingMsg, votedMsg, voteTokensMsg] = - yesVotingTokenAmount > 0 - ? [ - LABELS.VOTING_YEAH, - LABELS.VOTED_YEAH, - `${yesVotingTokenAmount} ${LABELS.TOKENS_VOTED_FOR_THE_PROPOSAL}.`, - ] - : [ - LABELS.VOTING_NAY, - LABELS.VOTED_NAY, - `${noVotingTokenAmount} ${LABELS.TOKENS_VOTED_AGAINST_THE_PROPOSAL}.`, - ]; - - notify({ - message: votingMsg, - description: LABELS.PLEASE_WAIT, - type: 'warn', - }); - - try { - await sendTransactions( - connection, - wallet, - [depositInstructions, voteInstructions], - [depositSigners, voteSigners], - true, - true, - ); - - notify({ - message: votedMsg, - type: 'success', - description: voteTokensMsg, - }); - } catch (ex) { - console.error(ex); - throw new Error(); - } -}; diff --git a/packages/governance/src/actions/devtools/createAccount.ts b/packages/governance/src/actions/devtools/createAccount.ts new file mode 100644 index 00000000..5edfbc11 --- /dev/null +++ b/packages/governance/src/actions/devtools/createAccount.ts @@ -0,0 +1,59 @@ +import { sendTransaction, utils } from '@oyster/common'; +import { + Connection, + Account, + TransactionInstruction, + PublicKey, + SystemProgram, +} from '@solana/web3.js'; + +const { notify } = utils; + +export const createAccount = async ( + connection: Connection, + wallet: any, + size: number, + programId: PublicKey, +) => { + let signers: Account[] = []; + let instructions: TransactionInstruction[] = []; + + const account = new Account(); + + const mintRentExempt = await connection.getMinimumBalanceForRentExemption( + size, + ); + + instructions.push( + SystemProgram.createAccount({ + fromPubkey: wallet.publicKey, + newAccountPubkey: account.publicKey, + lamports: mintRentExempt, + space: size, + programId: programId, + }), + ); + + signers.push(account); + + console.log('ACCOUNT:', account.publicKey.toBase58()); + + notify({ + message: 'Creating account...', + description: 'Please wait...', + type: 'warn', + }); + + try { + let tx = await sendTransaction(connection, wallet, instructions, signers); + + notify({ + message: 'Governance artifacts created.', + type: 'success', + description: `Transaction - ${tx}`, + }); + } catch (ex) { + console.error(ex); + throw ex; + } +}; diff --git a/packages/governance/src/actions/devtools/generateGovernanceArtifacts.ts b/packages/governance/src/actions/devtools/generateGovernanceArtifacts.ts new file mode 100644 index 00000000..3dc9a1d8 --- /dev/null +++ b/packages/governance/src/actions/devtools/generateGovernanceArtifacts.ts @@ -0,0 +1,273 @@ +import { + Account, + Connection, + PublicKey, + SystemProgram, + TransactionInstruction, +} from '@solana/web3.js'; +import { + utils, + createMint, + createTokenAccount, + sendTransactions, + SequenceType, + WalletSigner, + WalletNotConnectedError, +} from '@oyster/common'; +import { AccountLayout, MintLayout, Token, u64 } from '@solana/spl-token'; + +const { notify } = utils; +export interface SourceEntryInterface { + owner: PublicKey; + sourceAccount: PublicKey | undefined; + tokenAmount: number; +} +export const generateGovernanceArtifacts = async ( + connection: Connection, + wallet: WalletSigner, +) => { + let communityMintSigners: Account[] = []; + let communityMintInstruction: TransactionInstruction[] = []; + + const otherOwnerWallet = new PublicKey( + 'ENmcpFCpxN1CqyUjuog9yyUVfdXBKF3LVCwLr7grJZpk', + ); + + // Setup community mint + const { mintAddress: communityMintAddress } = await withMint( + communityMintInstruction, + communityMintSigners, + connection, + wallet, + 3, + new u64('7000000'), + new u64('10000000'), + otherOwnerWallet, + ); + + let councilMinSigners: Account[] = []; + let councilMintInstructions: TransactionInstruction[] = []; + + // Setup council mint + const { mintAddress: councilMintAddress } = await withMint( + councilMintInstructions, + councilMinSigners, + connection, + wallet, + 0, + new u64(20), + new u64(55), + otherOwnerWallet, + ); + + // Setup Realm, Governance and Proposal instruction + let governanceSigners: Account[] = []; + let governanceInstructions: TransactionInstruction[] = []; + + // Token governance artifacts + const tokenGovernance = await withTokenGovernance( + governanceInstructions, + governanceSigners, + connection, + wallet, + 0, + new u64(200), + ); + + let realmName = `Realm-${communityMintAddress.toBase58().substring(0, 5)}`; + + notify({ + message: 'Creating Governance artifacts...', + description: 'Please wait...', + type: 'warn', + }); + + try { + let tx = await sendTransactions( + connection, + wallet, + [ + communityMintInstruction, + councilMintInstructions, + governanceInstructions, + ], + [communityMintSigners, councilMinSigners, governanceSigners], + SequenceType.Sequential, + ); + + notify({ + message: 'Governance artifacts created.', + type: 'success', + description: `Transaction - ${tx}`, + }); + + return { + realmName, + communityMintAddress, + councilMintAddress, + tokenGovernance, + }; + } catch (ex) { + console.error(ex); + throw ex; + } +}; + +const withTokenGovernance = async ( + instructions: TransactionInstruction[], + signers: Account[], + connection: Connection, + wallet: WalletSigner, + decimals: number, + amount: u64, +) => { + const { publicKey } = wallet; + if (!publicKey) throw new WalletNotConnectedError(); + + const { token: tokenId } = utils.programIds(); + + const mintRentExempt = await connection.getMinimumBalanceForRentExemption( + MintLayout.span, + ); + + const tokenAccountRentExempt = await connection.getMinimumBalanceForRentExemption( + AccountLayout.span, + ); + + const mintAddress = createMint( + instructions, + publicKey, + mintRentExempt, + decimals, + publicKey, + publicKey, + signers, + ); + + const tokenAccountAddress = createTokenAccount( + instructions, + publicKey, + tokenAccountRentExempt, + mintAddress, + publicKey, + signers, + ); + + instructions.push( + Token.createMintToInstruction( + tokenId, + mintAddress, + tokenAccountAddress, + publicKey, + [], + new u64(amount), + ), + ); + + const beneficiaryTokenAccountAddress = createTokenAccount( + instructions, + publicKey, + tokenAccountRentExempt, + mintAddress, + publicKey, + signers, + ); + + return { + tokenAccountAddress: tokenAccountAddress.toBase58(), + beneficiaryTokenAccountAddress: beneficiaryTokenAccountAddress.toBase58(), + }; +}; + +export const withMint = async ( + instructions: TransactionInstruction[], + signers: Account[], + connection: Connection, + wallet: WalletSigner, + decimals: number, + amount: u64, + supply: u64, + otherOwnerWallet: PublicKey, +) => { + const { publicKey } = wallet; + if (!publicKey) throw new WalletNotConnectedError(); + + const { system: systemId, token: tokenId } = utils.programIds(); + + const mintRentExempt = await connection.getMinimumBalanceForRentExemption( + MintLayout.span, + ); + + const tokenAccountRentExempt = await connection.getMinimumBalanceForRentExemption( + AccountLayout.span, + ); + + const accountRentExempt = await connection.getMinimumBalanceForRentExemption( + 0, + ); + + const mintAddress = createMint( + instructions, + publicKey, + mintRentExempt, + decimals, + publicKey, + publicKey, + signers, + ); + + const tokenAccountAddress = createTokenAccount( + instructions, + publicKey, + tokenAccountRentExempt, + mintAddress, + publicKey, + signers, + ); + + instructions.push( + Token.createMintToInstruction( + tokenId, + mintAddress, + tokenAccountAddress, + publicKey, + [], + new u64(amount), + ), + ); + + const otherOwner = new Account(); + instructions.push( + SystemProgram.createAccount({ + fromPubkey: publicKey, + newAccountPubkey: otherOwner.publicKey, + lamports: accountRentExempt, + space: 0, + programId: systemId, + }), + ); + + signers.push(otherOwner); + + const otherOwnerTokenAccount = createTokenAccount( + instructions, + publicKey, + tokenAccountRentExempt, + mintAddress, + otherOwnerWallet, + signers, + ); + + instructions.push( + Token.createMintToInstruction( + tokenId, + mintAddress, + otherOwnerTokenAccount, + publicKey, + [], + new u64(supply.sub(amount).toArray()), + ), + ); + + return { mintAddress, otherOwnerTokenAccount }; +}; diff --git a/packages/governance/src/actions/devtools/generateMint.ts b/packages/governance/src/actions/devtools/generateMint.ts new file mode 100644 index 00000000..a2b3612a --- /dev/null +++ b/packages/governance/src/actions/devtools/generateMint.ts @@ -0,0 +1,57 @@ +import { notify, sendTransaction, WalletSigner } from '@oyster/common'; +import { u64 } from '@solana/spl-token'; + +import { + Account, + Connection, + PublicKey, + TransactionInstruction, +} from '@solana/web3.js'; + +import { withMint } from './generateGovernanceArtifacts'; + +export const generateMint = async ( + connection: Connection, + wallet: WalletSigner, + decimals: number, + amount: u64, + supply: u64, + otherOwnerWallet: PublicKey, +) => { + let signers: Account[] = []; + let instructions: TransactionInstruction[] = []; + + const { mintAddress } = await withMint( + instructions, + signers, + connection, + wallet, + decimals, + amount, + supply, + otherOwnerWallet, + ); + + notify({ + message: 'Creating mint...', + description: 'Please wait...', + type: 'warn', + }); + + try { + let tx = await sendTransaction(connection, wallet, instructions, signers); + + notify({ + message: 'Mint created.', + type: 'success', + description: `Transaction - ${tx}`, + }); + + return { + mintAddress, + }; + } catch (ex) { + console.error(ex); + throw ex; + } +}; diff --git a/packages/governance/src/actions/dryRunInstruction.ts b/packages/governance/src/actions/dryRunInstruction.ts new file mode 100644 index 00000000..c4b7546c --- /dev/null +++ b/packages/governance/src/actions/dryRunInstruction.ts @@ -0,0 +1,24 @@ +import { InstructionData } from '../models/accounts'; +import { RpcContext } from '../models/core/api'; +import { simulateTransaction } from '@oyster/common'; +import { Transaction } from '@solana/web3.js'; + +export async function dryRunInstruction( + { connection, wallet }: RpcContext, + instructionData: InstructionData, +) { + let transaction = new Transaction({ feePayer: wallet!.publicKey }); + transaction.add({ + keys: instructionData.accounts, + programId: instructionData.programId, + data: Buffer.from(instructionData.data), + }); + + const result = await simulateTransaction( + connection, + transaction, + 'singleGossip', + ); + + return { response: result.value, transaction }; +} diff --git a/packages/governance/src/actions/execute.ts b/packages/governance/src/actions/execute.ts deleted file mode 100644 index e08b04bc..00000000 --- a/packages/governance/src/actions/execute.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - Account, - Connection, - Message, - TransactionInstruction, -} from '@solana/web3.js'; -import { contexts, utils, ParsedAccount } from '@oyster/common'; - -import { - Proposal, - ProposalState, - GovernanceTransaction, -} from '../models/governance'; -import { executeInstruction } from '../models/execute'; -import { LABELS } from '../constants'; -import { getMessageAccountInfos } from '../utils/transactions'; -const { sendTransaction } = contexts.Connection; -const { notify } = utils; - -export const execute = async ( - connection: Connection, - wallet: any, - proposal: ParsedAccount, - state: ParsedAccount, - transaction: ParsedAccount, -) => { - let signers: Account[] = []; - let instructions: TransactionInstruction[] = []; - const actualMessage = decodeBufferIntoMessage(transaction.info.instruction); - const accountInfos = getMessageAccountInfos(actualMessage); - - instructions.push( - executeInstruction( - transaction.pubkey, - state.pubkey, - proposal.pubkey, - actualMessage.accountKeys[actualMessage.instructions[0].programIdIndex], - proposal.info.config, - accountInfos, - ), - ); - - notify({ - message: LABELS.EXECUTING, - description: LABELS.PLEASE_WAIT, - type: 'warn', - }); - - try { - let tx = await sendTransaction( - connection, - wallet, - instructions, - signers, - true, - ); - - notify({ - message: LABELS.EXECUTED, - type: 'success', - description: LABELS.TRANSACTION + ` ${tx}`, - }); - } catch (ex) { - console.error(ex); - throw new Error(); - } -}; - -function decodeBufferIntoMessage(instruction: number[]): Message { - return Message.from(instruction); -} diff --git a/packages/governance/src/actions/executeInstruction.ts b/packages/governance/src/actions/executeInstruction.ts new file mode 100644 index 00000000..a3bd63a3 --- /dev/null +++ b/packages/governance/src/actions/executeInstruction.ts @@ -0,0 +1,35 @@ +import { Account, TransactionInstruction } from '@solana/web3.js'; +import { ParsedAccount } from '@oyster/common'; + +import { Proposal, ProposalInstruction } from '../models/accounts'; + +import { withExecuteInstruction } from '../models/withExecuteInstruction'; +import { sendTransactionWithNotifications } from '../tools/transactions'; +import { RpcContext } from '../models/core/api'; + +export const executeInstruction = async ( + { connection, wallet, programId }: RpcContext, + proposal: ParsedAccount, + instruction: ParsedAccount, +) => { + let signers: Account[] = []; + let instructions: TransactionInstruction[] = []; + + await withExecuteInstruction( + instructions, + programId, + proposal.info.governance, + proposal.pubkey, + instruction.pubkey, + instruction.info.instruction, + ); + + await sendTransactionWithNotifications( + connection, + wallet, + instructions, + signers, + 'Executing instruction', + 'Instruction executed', + ); +}; diff --git a/packages/governance/src/actions/finalizeVote.ts b/packages/governance/src/actions/finalizeVote.ts new file mode 100644 index 00000000..3a7425b5 --- /dev/null +++ b/packages/governance/src/actions/finalizeVote.ts @@ -0,0 +1,36 @@ +import { Account, PublicKey, TransactionInstruction } from '@solana/web3.js'; +import { ParsedAccount } from '@oyster/common'; + +import { Proposal } from '../models/accounts'; + +import { withFinalizeVote } from '../models/withFinalizeVote'; +import { sendTransactionWithNotifications } from '../tools/transactions'; +import { RpcContext } from '../models/core/api'; + +export const finalizeVote = async ( + { connection, wallet, programId }: RpcContext, + realm: PublicKey, + proposal: ParsedAccount, +) => { + let signers: Account[] = []; + let instructions: TransactionInstruction[] = []; + + withFinalizeVote( + instructions, + programId, + realm, + proposal.info.governance, + proposal.pubkey, + proposal.info.tokenOwnerRecord, + proposal.info.governingTokenMint, + ); + + await sendTransactionWithNotifications( + connection, + wallet, + instructions, + signers, + 'Finalizing vote', + 'Vote finalized', + ); +}; diff --git a/packages/governance/src/actions/flagInstructionError.ts b/packages/governance/src/actions/flagInstructionError.ts new file mode 100644 index 00000000..ec626445 --- /dev/null +++ b/packages/governance/src/actions/flagInstructionError.ts @@ -0,0 +1,37 @@ +import { Account, PublicKey, TransactionInstruction } from '@solana/web3.js'; +import { ParsedAccount } from '@oyster/common'; + +import { Proposal } from '../models/accounts'; + +import { withFlagInstructionError } from '../models/withFlagInstructionError'; +import { sendTransactionWithNotifications } from '../tools/transactions'; +import { RpcContext } from '../models/core/api'; + +export const flagInstructionError = async ( + { connection, wallet, programId, walletPubkey }: RpcContext, + proposal: ParsedAccount, + proposalInstruction: PublicKey, +) => { + let governanceAuthority = walletPubkey; + + let signers: Account[] = []; + let instructions: TransactionInstruction[] = []; + + withFlagInstructionError( + instructions, + programId, + proposal.pubkey, + proposal.info.tokenOwnerRecord, + governanceAuthority, + proposalInstruction, + ); + + await sendTransactionWithNotifications( + connection, + wallet, + instructions, + signers, + 'Flagging instruction as broken', + 'Instruction flagged as broken', + ); +}; diff --git a/packages/governance/src/actions/insertInstruction.ts b/packages/governance/src/actions/insertInstruction.ts new file mode 100644 index 00000000..30d632d1 --- /dev/null +++ b/packages/governance/src/actions/insertInstruction.ts @@ -0,0 +1,49 @@ +import { Account, PublicKey, TransactionInstruction } from '@solana/web3.js'; +import { ParsedAccount } from '@oyster/common'; + +import { InstructionData, Proposal } from '../models/accounts'; + +import { withInsertInstruction } from '../models/withInsertInstruction'; + +import { sendTransactionWithNotifications } from '../tools/transactions'; +import { RpcContext } from '../models/core/api'; + +export const insertInstruction = async ( + { connection, wallet, programId, programVersion, walletPubkey }: RpcContext, + proposal: ParsedAccount, + tokenOwnerRecord: PublicKey, + index: number, + holdUpTime: number, + instructionData: InstructionData, +) => { + let signers: Account[] = []; + let instructions: TransactionInstruction[] = []; + + const governanceAuthority = walletPubkey; + const payer = walletPubkey; + + const proposalInstructionAddress = await withInsertInstruction( + instructions, + programId, + programVersion, + proposal.info.governance, + proposal.pubkey, + tokenOwnerRecord, + governanceAuthority, + index, + holdUpTime, + instructionData, + payer, + ); + + await sendTransactionWithNotifications( + connection, + wallet, + instructions, + signers, + 'Adding instruction', + 'Instruction added', + ); + + return proposalInstructionAddress; +}; diff --git a/packages/governance/src/actions/mintSourceTokens.ts b/packages/governance/src/actions/mintSourceTokens.ts deleted file mode 100644 index 8c93ff2e..00000000 --- a/packages/governance/src/actions/mintSourceTokens.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { - Account, - Connection, - PublicKey, - TransactionInstruction, -} from '@solana/web3.js'; -import { - contexts, - utils, - ParsedAccount, - actions, - SequenceType, -} from '@oyster/common'; - -import { Governance } from '../models/governance'; -import { AccountLayout, Token } from '@solana/spl-token'; -import { LABELS } from '../constants'; -const { createTokenAccount } = actions; -const { sendTransactions } = contexts.Connection; -const { notify } = utils; -export interface SourceEntryInterface { - owner: PublicKey; - sourceAccount: PublicKey | undefined; - tokenAmount: number; -} -export const mintSourceTokens = async ( - connection: Connection, - wallet: any, - governance: ParsedAccount, - useGovernance: boolean, - entries: SourceEntryInterface[], - setSavePerc: (num: number) => void, - onFailedTxn: (index: number) => void, -) => { - const PROGRAM_IDS = utils.programIds(); - - let allSigners: Account[][] = []; - let allInstructions: TransactionInstruction[][] = []; - - const accountRentExempt = await connection.getMinimumBalanceForRentExemption( - AccountLayout.span, - ); - - entries.forEach(e => { - const signers: Account[] = []; - const instructions: TransactionInstruction[] = []; - if (!e.sourceAccount) - e.sourceAccount = createTokenAccount( - instructions, - wallet.publicKey, - accountRentExempt, - useGovernance - ? governance.info.governanceMint - : governance.info.councilMint!, - e.owner, - signers, - ); - - instructions.push( - Token.createMintToInstruction( - PROGRAM_IDS.token, - useGovernance - ? governance.info.governanceMint - : governance.info.councilMint!, - e.sourceAccount, - wallet.publicKey, - [], - e.tokenAmount, - ), - ); - - allSigners.push(signers); - allInstructions.push(instructions); - }); - - notify({ - message: LABELS.ADDING_GOVERNANCE_TOKENS, - description: LABELS.PLEASE_WAIT, - type: 'warn', - }); - - try { - await sendTransactions( - connection, - wallet, - allInstructions, - allSigners, - SequenceType.Sequential, - 'singleGossip', - (_txId: string, index: number) => { - setSavePerc(Math.round(100 * ((index + 1) / allInstructions.length))); - }, - (_reason: string, index: number) => { - setSavePerc(Math.round(100 * ((index + 1) / allInstructions.length))); - onFailedTxn(index); - return true; // keep going even on failed save - }, - ); - - notify({ - message: LABELS.GOVERNANCE_TOKENS_ADDED, - type: 'success', - }); - } catch (ex) { - console.error(ex); - throw new Error(); - } -}; diff --git a/packages/governance/src/actions/registerGovernance.ts b/packages/governance/src/actions/registerGovernance.ts new file mode 100644 index 00000000..26047e8a --- /dev/null +++ b/packages/governance/src/actions/registerGovernance.ts @@ -0,0 +1,110 @@ +import { PublicKey, TransactionInstruction } from '@solana/web3.js'; + +import { withCreateAccountGovernance } from '../models/withCreateAccountGovernance'; +import { GovernanceType } from '../models/enums'; +import { GovernanceConfig } from '../models/accounts'; +import { withCreateProgramGovernance } from '../models/withCreateProgramGovernance'; +import { sendTransactionWithNotifications } from '../tools/transactions'; +import { withCreateMintGovernance } from '../models/withCreateMintGovernance'; +import { withCreateTokenGovernance } from '../models/withCreateTokenGovernance'; +import { RpcContext } from '../models/core/api'; + +export const registerGovernance = async ( + { connection, wallet, programId, walletPubkey }: RpcContext, + governanceType: GovernanceType, + realm: PublicKey, + governedAccount: PublicKey, + config: GovernanceConfig, + transferAuthority: boolean, + tokenOwnerRecord: PublicKey, +): Promise => { + let instructions: TransactionInstruction[] = []; + + let governanceAddress; + let governanceAuthority = walletPubkey; + + switch (governanceType) { + case GovernanceType.Account: { + governanceAddress = ( + await withCreateAccountGovernance( + instructions, + programId, + realm, + governedAccount, + config, + tokenOwnerRecord, + walletPubkey, + governanceAuthority, + ) + ).governanceAddress; + break; + } + case GovernanceType.Program: { + governanceAddress = ( + await withCreateProgramGovernance( + instructions, + programId, + realm, + governedAccount, + config, + transferAuthority!, + walletPubkey, + tokenOwnerRecord, + walletPubkey, + governanceAuthority, + ) + ).governanceAddress; + break; + } + case GovernanceType.Mint: { + governanceAddress = ( + await withCreateMintGovernance( + instructions, + programId, + realm, + governedAccount, + config, + transferAuthority!, + walletPubkey, + tokenOwnerRecord, + walletPubkey, + governanceAuthority, + ) + ).governanceAddress; + break; + } + case GovernanceType.Token: { + governanceAddress = ( + await withCreateTokenGovernance( + instructions, + programId, + realm, + governedAccount, + config, + transferAuthority!, + walletPubkey, + tokenOwnerRecord, + walletPubkey, + governanceAuthority, + ) + ).governanceAddress; + break; + } + default: { + throw new Error( + `Governance type ${governanceType} is not supported yet.`, + ); + } + } + + await sendTransactionWithNotifications( + connection, + wallet, + instructions, + [], + 'Registering governance', + 'Governance has been registered', + ); + + return governanceAddress; +}; diff --git a/packages/governance/src/actions/registerProgramGovernance.ts b/packages/governance/src/actions/registerProgramGovernance.ts deleted file mode 100644 index 2ba9724c..00000000 --- a/packages/governance/src/actions/registerProgramGovernance.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { - Account, - Connection, - PublicKey, - TransactionInstruction, -} from '@solana/web3.js'; -import { contexts, utils, actions, SequenceType } from '@oyster/common'; - -import { AccountLayout, MintLayout, Token } from '@solana/spl-token'; -import { GOVERNANCE_AUTHORITY_SEED, Governance } from '../models/governance'; -import { createGovernanceInstruction } from '../models/createGovernance'; -import BN from 'bn.js'; - -const { sendTransactions } = contexts.Connection; -const { createMint, createTokenAccount } = actions; -const { notify } = utils; - -export const registerProgramGovernance = async ( - connection: Connection, - wallet: any, - uninitializedGovernance: Partial, - useCouncil: boolean, -): Promise => { - const PROGRAM_IDS = utils.programIds(); - let signers: Account[] = []; - let instructions: TransactionInstruction[] = []; - let mintSigners: Account[] = []; - let mintInstructions: TransactionInstruction[] = []; - - const mintRentExempt = await connection.getMinimumBalanceForRentExemption( - MintLayout.span, - ); - const accountRentExempt = await connection.getMinimumBalanceForRentExemption( - AccountLayout.span, - ); - - if (!uninitializedGovernance.program) - uninitializedGovernance.program = new Account().publicKey; // Random generation if none given - - if (!uninitializedGovernance.councilMint && useCouncil) { - // Initialize the mint, an account for the admin, and give them one council token - // to start their lives with. - uninitializedGovernance.councilMint = createMint( - mintInstructions, - wallet.publicKey, - mintRentExempt, - 0, - wallet.publicKey, - wallet.publicKey, - mintSigners, - ); - - const adminsCouncilToken = createTokenAccount( - mintInstructions, - wallet.publicKey, - accountRentExempt, - uninitializedGovernance.councilMint, - wallet.publicKey, - mintSigners, - ); - - mintInstructions.push( - Token.createMintToInstruction( - PROGRAM_IDS.token, - uninitializedGovernance.councilMint, - adminsCouncilToken, - wallet.publicKey, - [], - 1, - ), - ); - } - - if (!uninitializedGovernance.governanceMint) { - // Initialize the mint, an account for the admin, and give them one governance token - // to start their lives with. - uninitializedGovernance.governanceMint = createMint( - mintInstructions, - wallet.publicKey, - mintRentExempt, - 0, - wallet.publicKey, - wallet.publicKey, - mintSigners, - ); - - const adminsGovernanceToken = createTokenAccount( - mintInstructions, - wallet.publicKey, - accountRentExempt, - uninitializedGovernance.governanceMint, - wallet.publicKey, - mintSigners, - ); - - mintInstructions.push( - Token.createMintToInstruction( - PROGRAM_IDS.token, - uninitializedGovernance.governanceMint, - adminsGovernanceToken, - wallet.publicKey, - [], - 1, - ), - ); - } - - const [governanceKey] = await PublicKey.findProgramAddress( - [ - Buffer.from(GOVERNANCE_AUTHORITY_SEED), - uninitializedGovernance.program.toBuffer(), - ], - PROGRAM_IDS.governance.programId, - ); - - const [programDataAccount] = await PublicKey.findProgramAddress( - [uninitializedGovernance.program.toBuffer()], - PROGRAM_IDS.bpf_upgrade_loader, - ); - - instructions.push( - createGovernanceInstruction( - governanceKey, - uninitializedGovernance.program, - programDataAccount, - wallet.publicKey, - uninitializedGovernance.governanceMint, - - uninitializedGovernance.voteThreshold!, - uninitializedGovernance.minimumSlotWaitingPeriod || new BN(0), - uninitializedGovernance.timeLimit || new BN(0), - uninitializedGovernance.name || '', - wallet.publicKey, - uninitializedGovernance.councilMint, - ), - ); - - notify({ - message: 'Initializing governance of program...', - description: 'Please wait...', - type: 'warn', - }); - - try { - let tx = await sendTransactions( - connection, - wallet, - mintInstructions.length - ? [mintInstructions, instructions] - : [instructions], - mintInstructions.length ? [mintSigners, signers] : [signers], - SequenceType.Sequential, - ); - - notify({ - message: 'Program is now governed.', - type: 'success', - description: `Transaction - ${tx}`, - }); - - return governanceKey; - } catch (ex) { - console.error(ex); - throw new Error(); - } -}; diff --git a/packages/governance/src/actions/registerRealm.ts b/packages/governance/src/actions/registerRealm.ts new file mode 100644 index 00000000..054d3933 --- /dev/null +++ b/packages/governance/src/actions/registerRealm.ts @@ -0,0 +1,43 @@ +import { PublicKey, TransactionInstruction } from '@solana/web3.js'; +import BN from 'bn.js'; +import { MintMaxVoteWeightSource } from '../models/accounts'; +import { RpcContext } from '../models/core/api'; + +import { withCreateRealm } from '../models/withCreateRealm'; +import { sendTransactionWithNotifications } from '../tools/transactions'; + +export async function registerRealm( + { connection, wallet, programId, programVersion, walletPubkey }: RpcContext, + name: string, + communityMint: PublicKey, + councilMint: PublicKey | undefined, + communityMintMaxVoteWeightSource: MintMaxVoteWeightSource, + minCommunityTokensToCreateGovernance: BN, +) { + let instructions: TransactionInstruction[] = []; + + const realmAddress = await withCreateRealm( + instructions, + programId, + programVersion, + name, + walletPubkey, + communityMint, + walletPubkey, + councilMint, + communityMintMaxVoteWeightSource, + minCommunityTokensToCreateGovernance, + undefined, + ); + + await sendTransactionWithNotifications( + connection, + wallet, + instructions, + [], + 'Registering realm', + 'Realm has been registered', + ); + + return realmAddress; +} diff --git a/packages/governance/src/actions/relinquishVote.ts b/packages/governance/src/actions/relinquishVote.ts new file mode 100644 index 00000000..56718a39 --- /dev/null +++ b/packages/governance/src/actions/relinquishVote.ts @@ -0,0 +1,42 @@ +import { Account, PublicKey, TransactionInstruction } from '@solana/web3.js'; +import { ParsedAccount } from '@oyster/common'; + +import { Proposal } from '../models/accounts'; +import { withRelinquishVote } from '../models/withRelinquishVote'; +import { sendTransactionWithNotifications } from '../tools/transactions'; +import { RpcContext } from '../models/core/api'; + +export const relinquishVote = async ( + { connection, wallet, programId, walletPubkey }: RpcContext, + proposal: ParsedAccount, + tokenOwnerRecord: PublicKey, + voteRecord: PublicKey, + IsWithdrawal: boolean, +) => { + let signers: Account[] = []; + let instructions: TransactionInstruction[] = []; + + let governanceAuthority = walletPubkey; + let beneficiary = walletPubkey; + + withRelinquishVote( + instructions, + programId, + proposal.info.governance, + proposal.pubkey, + tokenOwnerRecord, + proposal.info.governingTokenMint, + voteRecord, + governanceAuthority, + beneficiary, + ); + + await sendTransactionWithNotifications( + connection, + wallet, + instructions, + signers, + IsWithdrawal ? 'Withdrawing vote from proposal' : 'Releasing voting tokens', + IsWithdrawal ? 'Vote withdrawn' : 'Tokens released', + ); +}; diff --git a/packages/governance/src/actions/removeInstruction.ts b/packages/governance/src/actions/removeInstruction.ts new file mode 100644 index 00000000..fb6acdb2 --- /dev/null +++ b/packages/governance/src/actions/removeInstruction.ts @@ -0,0 +1,39 @@ +import { Account, PublicKey, TransactionInstruction } from '@solana/web3.js'; +import { ParsedAccount } from '@oyster/common'; + +import { Proposal } from '../models/accounts'; + +import { withRemoveInstruction } from '../models/withRemoveInstruction'; +import { sendTransactionWithNotifications } from '../tools/transactions'; +import { RpcContext } from '../models/core/api'; + +export const removeInstruction = async ( + { connection, wallet, programId, walletPubkey }: RpcContext, + proposal: ParsedAccount, + proposalInstruction: PublicKey, +) => { + let signers: Account[] = []; + let instructions: TransactionInstruction[] = []; + + const governanceAuthority = walletPubkey; + const beneficiary = walletPubkey; + + await withRemoveInstruction( + instructions, + programId, + proposal.pubkey, + proposal.info.tokenOwnerRecord, + governanceAuthority, + proposalInstruction, + beneficiary, + ); + + await sendTransactionWithNotifications( + connection, + wallet, + instructions, + signers, + 'Removing instruction', + 'Instruction removed', + ); +}; diff --git a/packages/governance/src/actions/removeSigner.ts b/packages/governance/src/actions/removeSigner.ts deleted file mode 100644 index 9dd2747e..00000000 --- a/packages/governance/src/actions/removeSigner.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - Account, - Connection, - PublicKey, - TransactionInstruction, -} from '@solana/web3.js'; -import { contexts, utils, models, ParsedAccount } from '@oyster/common'; - -import { GOVERNANCE_AUTHORITY_SEED, Proposal } from '../models/governance'; -import { removeSignerInstruction } from '../models/removeSigner'; -const { sendTransaction } = contexts.Connection; -const { notify } = utils; -const { approve } = models; - -export const removeSigner = async ( - connection: Connection, - wallet: any, - proposal: ParsedAccount, - adminAccount: PublicKey, - sigAccount: PublicKey, -) => { - const PROGRAM_IDS = utils.programIds(); - - let signers: Account[] = []; - let instructions: TransactionInstruction[] = []; - - const [mintAuthority] = await PublicKey.findProgramAddress( - [Buffer.from(GOVERNANCE_AUTHORITY_SEED), proposal.pubkey.toBuffer()], - PROGRAM_IDS.governance.programId, - ); - - const transferAuthority = approve( - instructions, - [], - adminAccount, - wallet.publicKey, - 1, - ); - signers.push(transferAuthority); - - instructions.push( - removeSignerInstruction( - sigAccount, - proposal.info.signatoryMint, - adminAccount, - proposal.info.adminValidation, - proposal.pubkey, - transferAuthority.publicKey, - mintAuthority, - ), - ); - - notify({ - message: 'Removing signer...', - description: 'Please wait...', - type: 'warn', - }); - - try { - let tx = await sendTransaction( - connection, - wallet, - instructions, - signers, - true, - ); - - notify({ - message: 'Signer removed.', - type: 'success', - description: `Transaction - ${tx}`, - }); - } catch (ex) { - console.error(ex); - throw new Error(); - } -}; diff --git a/packages/governance/src/actions/setRealmAuthority.ts b/packages/governance/src/actions/setRealmAuthority.ts new file mode 100644 index 00000000..63117853 --- /dev/null +++ b/packages/governance/src/actions/setRealmAuthority.ts @@ -0,0 +1,35 @@ +import { Account, PublicKey, TransactionInstruction } from '@solana/web3.js'; +import { ParsedAccount } from '@oyster/common'; + +import { Realm } from '../models/accounts'; + +import { sendTransactionWithNotifications } from '../tools/transactions'; +import { RpcContext } from '../models/core/api'; +import { withSetRealmAuthority } from '../models/withSetRealmAuthority'; + +export const setRealmAuthority = async ( + { connection, wallet, programId }: RpcContext, + + realm: ParsedAccount, + newRealmAuthority: PublicKey, +) => { + let signers: Account[] = []; + let instructions: TransactionInstruction[] = []; + + withSetRealmAuthority( + instructions, + programId, + realm.pubkey, + realm.info.authority!, + newRealmAuthority, + ); + + await sendTransactionWithNotifications( + connection, + wallet, + instructions, + signers, + 'Setting realm authority', + 'Realm authority set', + ); +}; diff --git a/packages/governance/src/actions/sign.ts b/packages/governance/src/actions/sign.ts deleted file mode 100644 index 725deed8..00000000 --- a/packages/governance/src/actions/sign.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { - Account, - Connection, - PublicKey, - TransactionInstruction, -} from '@solana/web3.js'; -import { contexts, utils, models, ParsedAccount } from '@oyster/common'; - -import { - GOVERNANCE_AUTHORITY_SEED, - Proposal, - ProposalState, -} from '../models/governance'; -import { signInstruction } from '../models/sign'; - -const { sendTransaction } = contexts.Connection; -const { notify } = utils; -const { approve } = models; - -export const sign = async ( - connection: Connection, - wallet: any, - proposal: ParsedAccount, - state: ParsedAccount, - sigAccount: PublicKey, -) => { - const PROGRAM_IDS = utils.programIds(); - - let signers: Account[] = []; - let instructions: TransactionInstruction[] = []; - - const [mintAuthority] = await PublicKey.findProgramAddress( - [Buffer.from(GOVERNANCE_AUTHORITY_SEED), proposal.pubkey.toBuffer()], - PROGRAM_IDS.governance.programId, - ); - - const transferAuthority = approve( - instructions, - [], - sigAccount, - wallet.publicKey, - 1, - ); - signers.push(transferAuthority); - - instructions.push( - signInstruction( - state.pubkey, - sigAccount, - proposal.info.signatoryMint, - proposal.pubkey, - transferAuthority.publicKey, - mintAuthority, - ), - ); - - notify({ - message: 'Signing proposal...', - description: 'Please wait...', - type: 'warn', - }); - - try { - let tx = await sendTransaction( - connection, - wallet, - instructions, - signers, - true, - ); - - notify({ - message: 'Proposal signed.', - type: 'success', - description: `Transaction - ${tx}`, - }); - } catch (ex) { - console.error(ex); - throw new Error(); - } -}; diff --git a/packages/governance/src/actions/signOffProposal.ts b/packages/governance/src/actions/signOffProposal.ts new file mode 100644 index 00000000..4ee76f92 --- /dev/null +++ b/packages/governance/src/actions/signOffProposal.ts @@ -0,0 +1,34 @@ +import { Account, PublicKey, TransactionInstruction } from '@solana/web3.js'; +import { ParsedAccount } from '@oyster/common'; + +import { SignatoryRecord } from '../models/accounts'; +import { withSignOffProposal } from '../models/withSignOffProposal'; +import { sendTransactionWithNotifications } from '../tools/transactions'; +import { RpcContext } from '../models/core/api'; + +export const signOffProposal = async ( + { connection, wallet, programId }: RpcContext, + + signatoryRecord: ParsedAccount, + signatory: PublicKey, +) => { + let signers: Account[] = []; + let instructions: TransactionInstruction[] = []; + + withSignOffProposal( + instructions, + programId, + signatoryRecord.info.proposal, + signatoryRecord.pubkey, + signatory, + ); + + await sendTransactionWithNotifications( + connection, + wallet, + instructions, + signers, + 'Signing off proposal', + 'Proposal signed off', + ); +}; diff --git a/packages/governance/src/actions/withdrawGoverningTokens.ts b/packages/governance/src/actions/withdrawGoverningTokens.ts new file mode 100644 index 00000000..577337e0 --- /dev/null +++ b/packages/governance/src/actions/withdrawGoverningTokens.ts @@ -0,0 +1,32 @@ +import { PublicKey, TransactionInstruction } from '@solana/web3.js'; +import { RpcContext } from '../models/core/api'; + +import { withWithdrawGoverningTokens } from '../models/withWithdrawGoverningTokens'; +import { sendTransactionWithNotifications } from '../tools/transactions'; + +export const withdrawGoverningTokens = async ( + { connection, wallet, programId, walletPubkey }: RpcContext, + realm: PublicKey, + governingTokenDestination: PublicKey, + governingTokenMint: PublicKey, +) => { + let instructions: TransactionInstruction[] = []; + + await withWithdrawGoverningTokens( + instructions, + programId, + realm, + governingTokenDestination, + governingTokenMint, + walletPubkey, + ); + + await sendTransactionWithNotifications( + connection, + wallet, + instructions, + [], + 'Withdrawing governing tokens', + 'Tokens have been withdrawn', + ); +}; diff --git a/packages/governance/src/actions/withdrawVotingTokens.ts b/packages/governance/src/actions/withdrawVotingTokens.ts deleted file mode 100644 index b3cf6013..00000000 --- a/packages/governance/src/actions/withdrawVotingTokens.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { - Account, - Connection, - PublicKey, - TransactionInstruction, -} from '@solana/web3.js'; -import { - contexts, - utils, - models, - ParsedAccount, - actions, -} from '@oyster/common'; - -import { - GOVERNANCE_AUTHORITY_SEED, - Proposal, - ProposalState, - ProposalStateStatus, -} from '../models/governance'; -import { AccountLayout } from '@solana/spl-token'; -import { withdrawVotingTokensInstruction } from '../models/withdrawVotingTokens'; -import { LABELS } from '../constants'; -const { createTokenAccount } = actions; -const { sendTransaction } = contexts.Connection; -const { notify } = utils; -const { approve } = models; - -export const withdrawVotingTokens = async ( - connection: Connection, - wallet: any, - proposal: ParsedAccount, - state: ParsedAccount, - existingVoteAccount: PublicKey | undefined, - existingYesVoteAccount: PublicKey | undefined, - existingNoVoteAccount: PublicKey | undefined, - destinationAccount: PublicKey, - votingTokenAmount: number, -) => { - const PROGRAM_IDS = utils.programIds(); - - let signers: Account[] = []; - let instructions: TransactionInstruction[] = []; - - const accountRentExempt = await connection.getMinimumBalanceForRentExemption( - AccountLayout.span, - ); - - if (!existingVoteAccount) { - existingVoteAccount = createTokenAccount( - instructions, - wallet.publicKey, - accountRentExempt, - proposal.info.votingMint, - wallet.publicKey, - signers, - ); - } - - if (!existingYesVoteAccount) { - existingYesVoteAccount = createTokenAccount( - instructions, - wallet.publicKey, - accountRentExempt, - proposal.info.yesVotingMint, - wallet.publicKey, - signers, - ); - } - - if (!existingNoVoteAccount) { - existingNoVoteAccount = createTokenAccount( - instructions, - wallet.publicKey, - accountRentExempt, - proposal.info.noVotingMint, - wallet.publicKey, - signers, - ); - } - - const [mintAuthority] = await PublicKey.findProgramAddress( - [Buffer.from(GOVERNANCE_AUTHORITY_SEED), proposal.pubkey.toBuffer()], - PROGRAM_IDS.governance.programId, - ); - - // We dont know in this scope how much is in each account so we just ask for all in each. - // Should be alright, this is just permission, not actual moving. - const transferAuthority = approve( - instructions, - [], - existingVoteAccount, - wallet.publicKey, - votingTokenAmount, - ); - - approve( - instructions, - [], - existingYesVoteAccount, - wallet.publicKey, - votingTokenAmount, - undefined, - undefined, - transferAuthority, - ); - - approve( - instructions, - [], - existingNoVoteAccount, - wallet.publicKey, - votingTokenAmount, - undefined, - undefined, - transferAuthority, - ); - - const [governanceVotingRecord] = await PublicKey.findProgramAddress( - [ - Buffer.from(GOVERNANCE_AUTHORITY_SEED), - PROGRAM_IDS.governance.programId.toBuffer(), - proposal.pubkey.toBuffer(), - existingVoteAccount.toBuffer(), - ], - PROGRAM_IDS.governance.programId, - ); - - signers.push(transferAuthority); - - instructions.push( - withdrawVotingTokensInstruction( - governanceVotingRecord, - existingVoteAccount, - existingYesVoteAccount, - existingNoVoteAccount, - destinationAccount, - proposal.info.sourceHolding, - proposal.info.votingMint, - proposal.info.yesVotingMint, - proposal.info.noVotingMint, - state.pubkey, - proposal.pubkey, - transferAuthority.publicKey, - mintAuthority, - votingTokenAmount, - ), - ); - - const [msg, completedMsg] = - state.info.status === ProposalStateStatus.Voting - ? [LABELS.WITHDRAWING_YOUR_VOTE, LABELS.VOTE_WITHDRAWN] - : [LABELS.REFUNDING_YOUR_TOKENS, LABELS.TOKENS_REFUNDED]; - - notify({ - message: msg, - description: LABELS.PLEASE_WAIT, - type: 'warn', - }); - - try { - let tx = await sendTransaction( - connection, - wallet, - instructions, - signers, - true, - ); - - notify({ - message: completedMsg, - type: 'success', - description: LABELS.TRANSACTION + ` ${tx}`, - }); - } catch (ex) { - console.error(ex); - throw new Error(); - } -}; diff --git a/packages/governance/src/components/AccountFormItem/accountFormItem.tsx b/packages/governance/src/components/AccountFormItem/accountFormItem.tsx new file mode 100644 index 00000000..070fbe3a --- /dev/null +++ b/packages/governance/src/components/AccountFormItem/accountFormItem.tsx @@ -0,0 +1,57 @@ +import { Form, Input } from 'antd'; + +import React from 'react'; + +import { contexts, tryParseKey } from '@oyster/common'; +import { AccountInfo, ParsedAccountData } from '@solana/web3.js'; + +const { useConnection } = contexts.Connection; + +export function AccountFormItem({ + name, + label, + required = true, + accountInfoValidator, +}: { + name: string; + label: string; + required?: boolean; + accountInfoValidator?: ( + account: AccountInfo, + ) => void; +}) { + const connection = useConnection(); + + const accountValidator = async (rule: any, value: string) => { + if (rule.required && !value) { + throw new Error(`Please provide ${label}`); + } else { + const pubkey = tryParseKey(value); + + if (!pubkey) { + throw new Error('Provided value is not a valid account address'); + } + + // Note: Do not use the accounts cache here to always get most recent result + await connection.getParsedAccountInfo(pubkey).then(data => { + if (!data || !data.value) { + throw new Error('Account not found'); + } + + if (accountInfoValidator) { + accountInfoValidator(data.value); + } + }); + } + }; + + return ( + + + + ); +} diff --git a/packages/governance/src/components/Background/index.tsx b/packages/governance/src/components/Background/index.tsx index a521e356..b918296a 100644 --- a/packages/governance/src/components/Background/index.tsx +++ b/packages/governance/src/components/Background/index.tsx @@ -1,5 +1,5 @@ import * as CANNON from 'cannon'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { Canvas } from 'react-three-fiber'; import { useCannon, Provider } from './useCannon'; import './styles.less'; @@ -33,15 +33,11 @@ function Box({ position }: { position: any }) { } export const Background = () => { - const [showPlane, set] = useState(true); - // When React removes (unmounts) the upper plane after 5 sec, objects should drop ... - // This may seem like magic, but as the plane unmounts it removes itself from cannon and that's that - useEffect(() => void setTimeout(() => set(false), 3000), []); return ( @@ -54,14 +50,11 @@ export const Background = () => { /> - {showPlane && } - - - - - - - {!showPlane && } + + + + + ); diff --git a/packages/governance/src/components/GovernanceBadge/governanceBadge.tsx b/packages/governance/src/components/GovernanceBadge/governanceBadge.tsx new file mode 100644 index 00000000..742ac7ee --- /dev/null +++ b/packages/governance/src/components/GovernanceBadge/governanceBadge.tsx @@ -0,0 +1,100 @@ +import { + ParsedAccount, + TokenIcon, + useConnectionConfig, + useAccount, +} from '@oyster/common'; +import { Avatar, Badge, Tooltip } from 'antd'; +import React from 'react'; +import { Governance, ProposalState, Realm } from '../../models/accounts'; + +import { useProposalsByGovernance } from '../../hooks/apiHooks'; + +import './style.less'; +import { SafetyCertificateOutlined } from '@ant-design/icons'; + +export function GovernanceBadge({ + realm, + governance, + size = 40, + showVotingCount = true, +}: { + realm: ParsedAccount | undefined; + governance: ParsedAccount; + size?: number; + showVotingCount?: boolean; +}) { + const proposals = useProposalsByGovernance(governance?.pubkey); + const { tokenMap } = useConnectionConfig(); + const tokenAccount = useAccount(governance.info.governedAccount); + + const color = governance.info.isProgramGovernance() ? 'green' : 'gray'; + const useAvatar = + governance.info.isProgramGovernance() || + governance.info.isAccountGovernance(); + + const tokenMint = tokenAccount ? tokenAccount.info.mint : undefined; + + return ( + p.info.state === ProposalState.Voting).length + : 0 + } + > +
+ {governance.info.isMintGovernance() && ( + + )} + {governance.info.isTokenGovernance() && ( +
+ + + +
+ )} + {useAvatar && ( + + {governance.info.governedAccount.toBase58().slice(0, 5)} + + )} +
+ {realm?.info.authority?.toBase58() === governance.pubkey.toBase58() && ( + + + + )} +
+ ); +} diff --git a/packages/governance/src/components/GovernanceBadge/style.less b/packages/governance/src/components/GovernanceBadge/style.less new file mode 100644 index 00000000..4d55a1eb --- /dev/null +++ b/packages/governance/src/components/GovernanceBadge/style.less @@ -0,0 +1,6 @@ +@import '~antd/dist/antd.dark.less'; + +// add transparent shadow to token icons +.token-icon-container > div > div { + box-shadow: 0px 0px 0px 2px rgba(0, 0, 0, 0.3); +} diff --git a/packages/governance/src/components/Layout/index.tsx b/packages/governance/src/components/Layout/layout.tsx similarity index 75% rename from packages/governance/src/components/Layout/index.tsx rename to packages/governance/src/components/Layout/layout.tsx index f6b9baa9..52353bb6 100644 --- a/packages/governance/src/components/Layout/index.tsx +++ b/packages/governance/src/components/Layout/layout.tsx @@ -3,10 +3,11 @@ import './../../App.less'; import { Layout } from 'antd'; import { Link } from 'react-router-dom'; -import { LABELS } from '../../constants'; import { components } from '@oyster/common'; import { Content, Header } from 'antd/lib/layout/layout'; import Logo from './dark-horizontal-combined-rainbow.inline.svg'; +import { useRpcContext } from '../../hooks/useRpcContext'; +import { getHomeUrl } from '../../tools/routeTools'; const { AppBar } = components; @@ -41,12 +42,13 @@ export const AppLayout = React.memo((props: any) => { return (
- +
- - {`Solana - + + + Docs +
@@ -58,3 +60,13 @@ export const AppLayout = React.memo((props: any) => {
); }); + +const HomeLink = () => { + const { programId } = useRpcContext(); + + return ( + + {`Solana + + ); +}; diff --git a/packages/governance/src/components/MintFormItem/mintFormItem.tsx b/packages/governance/src/components/MintFormItem/mintFormItem.tsx new file mode 100644 index 00000000..dcbbd863 --- /dev/null +++ b/packages/governance/src/components/MintFormItem/mintFormItem.tsx @@ -0,0 +1,58 @@ +import { Form, Input } from 'antd'; + +import React from 'react'; + +import { contexts, MintParser, tryParseKey } from '@oyster/common'; +const { useConnection } = contexts.Connection; + +export function MintFormItem({ + name, + label, + required = true, + onChange, +}: { + name: string; + label: string; + required?: boolean; + onChange?: (mint: string) => void; +}) { + const connection = useConnection(); + + const mintValidator = async (rule: any, value: string) => { + if (rule.required && !value) { + throw new Error(`Please provide a ${label}`); + } else { + const pubkey = tryParseKey(value); + + if (!pubkey) { + throw new Error('Provided value is not a valid mint address'); + } + + // Note: Do not use the accounts cache here to always get most recent result + await connection.getAccountInfo(pubkey).then(data => { + if (!data) { + throw new Error('Account not found'); + } + + try { + MintParser(pubkey, data); + } catch { + throw new Error('Account is not a valid mint'); + } + }); + } + }; + + return ( + + onChange && onChange(e.target.value)} + /> + + ); +} diff --git a/packages/governance/src/components/ModalFormAction/modalFormAction.tsx b/packages/governance/src/components/ModalFormAction/modalFormAction.tsx new file mode 100644 index 00000000..f48841b1 --- /dev/null +++ b/packages/governance/src/components/ModalFormAction/modalFormAction.tsx @@ -0,0 +1,244 @@ +import React, { useState } from 'react'; +import { Alert, Button, ButtonProps, Modal, Space, Typography } from 'antd'; +import { Form } from 'antd'; +import './style.less'; +import { + ExplorerLink, + isTransactionTimeoutError, +} from '@oyster/common'; +import { formDefaults } from '../../tools/forms'; +import { isSendTransactionError, isSignTransactionError, useWallet } from '@oyster/common'; +import { + getTransactionErrorMsg, + isWalletNotConnectedError, +} from '../../models/errors'; + +const { Text } = Typography; + +/// ModalFormAction is a control displayed as a Button action which opens a Modal from +/// The ModalForm captures common form use cases: 1) Progress indicator, 2) Close/Cancel state management, 3) Submission errors +/// TODO: add version without TResult +export function ModalFormAction({ + label, + formTitle, + formAction, + formPendingAction, + isWalletRequired = true, + buttonProps, + onSubmit, + onComplete, + onReset, + children, + initialValues, +}: { + label: string; + formTitle: string; + formAction: string; + formPendingAction: string; + isWalletRequired?: boolean; + buttonProps?: ButtonProps; + onSubmit: (values: any) => Promise; + onComplete?: (result: TResult) => void; + onReset?: () => void; + children?: any; + initialValues?: any; +}) { + const [isModalVisible, setIsModalVisible] = useState(false); + const { connected } = useWallet(); + + const onFormSubmit = (result: TResult) => { + setIsModalVisible(false); + onComplete && onComplete(result); + }; + const onFormCancel = () => { + setIsModalVisible(false); + }; + + return ( + <> + + + + ); +} + +function ActionForm({ + onFormSubmit, + onFormCancel, + isModalVisible, + onSubmit, + onReset, + formTitle, + formAction, + formPendingAction, + children, + initialValues, +}: { + onFormSubmit: (a: TResult) => void; + onFormCancel: () => void; + isModalVisible: boolean; + onSubmit: (values: any) => Promise; + onReset?: () => void; + formTitle: string; + formAction: string; + formPendingAction: string; + children: any; + initialValues: any; +}) { + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState<{ + message?: string; + txId?: string; + recoveryAction: string; + header?: string; + } | null>(); + + const resetForm = () => { + form.resetFields(); + onReset && onReset(); + setError(null); + }; + + const closeForm = (reset = true) => { + onFormCancel(); + setLoading(false); + setError(null); + reset && resetForm(); + }; + + const onSubmitForm = async (values: any) => { + try { + setLoading(true); + setError(null); + + const result = await onSubmit(values); + onFormSubmit(result); + closeForm(); + } catch (ex) { + if (isSendTransactionError(ex)) { + setError({ + txId: ex.txId, + message: `${getTransactionErrorMsg(ex).toString()}`, + recoveryAction: + 'Please try to amend the inputs and submit the transaction again', + }); + } else if (isTransactionTimeoutError(ex)) { + setError({ + txId: ex.txId, + message: ex.message, + recoveryAction: 'Please try to submit the transaction again', + }); + } else if (isSignTransactionError(ex)) { + setError({ + header: "Couldn't sign the transaction", + message: ex.message, + recoveryAction: + 'Please try to submit and sign the transaction with your wallet again', + }); + } else if (isWalletNotConnectedError(ex)) { + setError({ + header: "Can't submit the transaction", + message: ex.message, + recoveryAction: + 'Please ensure your wallet is connected and submit the transaction again', + }); + } else { + setError({ + header: "Can't submit the transaction", + message: ex.toString(), + recoveryAction: + 'Please try to amend the inputs and submit the transaction again', + }); + } + } finally { + setLoading(false); + } + }; + + const ErrorMessageBanner = () => { + return error ? ( +
+ + {error.txId ? ( +
+ Transaction + + returned an error +
+ ) : ( + error?.header + )} + + } + description={ + <> + + {/* {error.message &&
{error.message}
} */} + {error.message && {error.message}} + + {error.recoveryAction} +
+ + } + type="error" + closable + banner + onClose={() => { + setError(null); + }} + /> +
+ ) : null; + }; + + return ( + closeForm(false)} + footer={[ + , + , + ]} + > + {error && } + +
+ {children} +
+
+ ); +} diff --git a/packages/governance/src/components/ModalFormAction/style.less b/packages/governance/src/components/ModalFormAction/style.less new file mode 100644 index 00000000..a865542f --- /dev/null +++ b/packages/governance/src/components/ModalFormAction/style.less @@ -0,0 +1,6 @@ +@import '~antd/dist/antd.dark.less'; + +.error-message-banner { + // Extend the banner to left, top and right edges of the Modal form with 24px padding + margin: -24px -24px 0px -24px; +} diff --git a/packages/governance/src/components/Proposal/InstructionCard.tsx b/packages/governance/src/components/Proposal/InstructionCard.tsx deleted file mode 100644 index a1c4f80d..00000000 --- a/packages/governance/src/components/Proposal/InstructionCard.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { - CheckCircleOutlined, - DeleteOutlined, - EditOutlined, - LoadingOutlined, - PlayCircleOutlined, - RedoOutlined, -} from '@ant-design/icons'; -import { ParsedAccount, contexts } from '@oyster/common'; -import { Message } from '@solana/web3.js'; -import { Card, Button } from 'antd'; -import Meta from 'antd/lib/card/Meta'; -import React, { useEffect, useMemo, useState } from 'react'; -import { execute } from '../../actions/execute'; -import { LABELS } from '../../constants'; -import { - Proposal, - ProposalState, - ProposalStateStatus, - GovernanceTransaction, -} from '../../models/governance'; - -import './style.less'; - -const { useWallet } = contexts.Wallet; -const { useConnection } = contexts.Connection; - -enum Playstate { - Played, - Unplayed, - Playing, - Error, -} -export function InstructionCard({ - instruction, - proposal, - state, - position, -}: { - instruction: ParsedAccount; - proposal: ParsedAccount; - state: ParsedAccount; - position: number; -}) { - const [tabKey, setTabKey] = useState('info'); - const [playing, setPlaying] = useState( - instruction.info.executed === 1 ? Playstate.Played : Playstate.Unplayed, - ); - - const instructionDetails = useMemo(() => { - const message = Message.from(instruction.info.instruction); - - return { - instructionProgramID: - message.accountKeys[message.instructions[0].programIdIndex], - instructionData: message.instructions[0].data, - }; - }, [instruction]); - - const contentList: Record = { - info: ( - -

{`${LABELS.INSTRUCTION}: ${instructionDetails.instructionData}`}

-

- {LABELS.DELAY}: {instruction.info.slot.toNumber()} -

- - } - /> - ), - data:

{instruction.info.instruction}

, - }; - - return ( - - } - tabList={[ - { key: 'info', tab: 'Info' }, - { key: 'data', tab: 'Data' }, - ]} - title={'Instruction #' + position} - activeTabKey={tabKey} - onTabChange={setTabKey} - actions={[, ]} - > - {contentList[tabKey]} - - ); -} - -function PlayStatusButton({ - proposal, - state, - playing, - setPlaying, - instruction, -}: { - proposal: ParsedAccount; - state: ParsedAccount; - instruction: ParsedAccount; - playing: Playstate; - setPlaying: React.Dispatch>; -}) { - const wallet = useWallet(); - const connection = useConnection(); - const [currSlot, setCurrSlot] = useState(0); - - const elapsedTime = currSlot - state.info.votingEndedAt.toNumber(); - const ineligibleToSee = elapsedTime < instruction.info.slot.toNumber(); - - useEffect(() => { - if (ineligibleToSee) { - const timer = setTimeout(() => { - connection.getSlot().then(setCurrSlot); - }, 5000); - - return () => { - clearTimeout(timer); - }; - } - }, [ineligibleToSee, connection, currSlot]); - - const run = async () => { - setPlaying(Playstate.Playing); - try { - await execute(connection, wallet.wallet, proposal, state, instruction); - } catch (e) { - console.error(e); - setPlaying(Playstate.Error); - return; - } - setPlaying(Playstate.Played); - }; - - if ( - state.info.status !== ProposalStateStatus.Executing && - state.info.status !== ProposalStateStatus.Completed - ) - return null; - if (ineligibleToSee) return null; - - if (playing === Playstate.Unplayed) - return ( - - ); - else if (playing === Playstate.Playing) - return ; - else if (playing === Playstate.Error) - return ( - - ); - else return ; -} diff --git a/packages/governance/src/components/Proposal/MintSourceTokens.tsx b/packages/governance/src/components/Proposal/MintSourceTokens.tsx deleted file mode 100644 index 291f1d60..00000000 --- a/packages/governance/src/components/Proposal/MintSourceTokens.tsx +++ /dev/null @@ -1,280 +0,0 @@ -import { ParsedAccount } from '@oyster/common'; -import { Button, Modal, Input, Form, Progress, InputNumber, Radio } from 'antd'; -import React, { useState } from 'react'; -import { Governance } from '../../models/governance'; -import { utils, contexts } from '@oyster/common'; -import { PublicKey } from '@solana/web3.js'; -import { LABELS } from '../../constants'; -import { - SourceEntryInterface, - mintSourceTokens, -} from '../../actions/mintSourceTokens'; - -const { notify } = utils; -const { TextArea } = Input; -const { useWallet } = contexts.Wallet; -const { useConnection } = contexts.Connection; -const { deserializeAccount, useMint } = contexts.Accounts; - -const layout = { - labelCol: { span: 5 }, - wrapperCol: { span: 19 }, -}; - -export default function MintSourceTokens({ - governance, - useGovernance, -}: { - governance: ParsedAccount; - useGovernance: boolean; -}) { - const PROGRAM_IDS = utils.programIds(); - const wallet = useWallet(); - const connection = useConnection(); - const mintKey = useGovernance - ? governance.info.governanceMint - : governance.info.councilMint!; - const mint = useMint(mintKey); - const [saving, setSaving] = useState(false); - - const [isModalVisible, setIsModalVisible] = useState(false); - const [bulkModeVisible, setBulkModeVisible] = useState(false); - const [savePerc, setSavePerc] = useState(0); - const [failedSources, setFailedSources] = useState([]); - const [form] = Form.useForm(); - - const onSubmit = async (values: { - sourceHolders: string; - failedSources: string; - singleSourceHolder: string; - singleSourceCount: number; - }) => { - const { singleSourceHolder, singleSourceCount } = values; - const sourceHoldersAndCounts = values.sourceHolders - ? values.sourceHolders.split(',').map(s => s.trim()) - : []; - const sourceHolders: SourceEntryInterface[] = []; - let failedSourcesHold: SourceEntryInterface[] = []; - const zeroKey = PROGRAM_IDS.system; - sourceHoldersAndCounts.forEach((value: string, index: number) => { - if (index % 2 === 0) - sourceHolders.push({ - owner: value ? new PublicKey(value) : zeroKey, - tokenAmount: 0, - sourceAccount: undefined, - }); - else - sourceHolders[sourceHolders.length - 1].tokenAmount = parseInt(value); - }); - //console.log(sourceHolders); - - if (singleSourceHolder) - sourceHolders.push({ - owner: singleSourceHolder ? new PublicKey(singleSourceHolder) : zeroKey, - tokenAmount: singleSourceCount, - sourceAccount: undefined, - }); - - if (!sourceHolders.find(v => v.owner !== zeroKey)) { - notify({ - message: LABELS.ENTER_AT_LEAST_ONE_PUB_KEY, - type: 'error', - }); - return; - } - - if (sourceHolders.find(v => v.tokenAmount === 0)) { - notify({ - message: LABELS.CANT_GIVE_ZERO_TOKENS, - type: 'error', - }); - setSaving(false); - return; - } - - setSaving(true); - - const failedSourceCatch = (index: number, error: any) => { - if (error) console.error(error); - failedSourcesHold.push(sourceHolders[index]); - notify({ - message: sourceHolders[index].owner?.toBase58() + LABELS.PUB_KEY_FAILED, - type: 'error', - }); - }; - - const sourceHoldersToRun = []; - for (let i = 0; i < sourceHolders.length; i++) { - try { - if (sourceHolders[i].owner) { - const tokenAccounts = await connection.getTokenAccountsByOwner( - sourceHolders[i].owner || PROGRAM_IDS.governance, - { - programId: PROGRAM_IDS.token, - }, - ); - const specificToThisMint = tokenAccounts.value.find( - a => - deserializeAccount(a.account.data).mint.toBase58() === - mintKey.toBase58(), - ); - sourceHolders[i].sourceAccount = specificToThisMint?.pubkey; - sourceHoldersToRun.push(sourceHolders[i]); - } - } catch (e) { - failedSourceCatch(i, e); - } - } - - try { - await mintSourceTokens( - connection, - wallet.wallet, - governance, - useGovernance, - sourceHoldersToRun, - setSavePerc, - index => failedSourceCatch(index, null), - ); - } catch (e) { - console.error(e); - failedSourcesHold = sourceHolders; - } - - setFailedSources(failedSourcesHold); - setSaving(false); - setSavePerc(0); - setIsModalVisible(failedSourcesHold.length > 0); - if (failedSourcesHold.length === 0) form.resetFields(); - }; - return ( - <> - {mint?.mintAuthority?.toBase58() === - wallet.wallet?.publicKey?.toBase58() ? ( - - ) : null} - { - if (!saving) setIsModalVisible(false); - }} - > -
- {!saving && ( - <> - - - setBulkModeVisible(e.target.value === LABELS.BULK) - } - > - {LABELS.BULK} - - {LABELS.SINGLE} - - - - {!bulkModeVisible && ( - <> - - - - - - - - )} - {bulkModeVisible && ( - -