From 43abf3ae50f97498cca571fb657668abc2df5dcf Mon Sep 17 00:00:00 2001 From: Dummy Tester 123 Date: Sun, 28 Feb 2021 13:15:31 -0600 Subject: [PATCH] This commit marks a working remove signer - if only you could burn other people's tokens. Going to remove this functionality now. --- packages/common/src/contexts/accounts.tsx | 205 +++++++++++++----- .../proposals/src/actions/removeSigner.ts | 83 +++++++ .../{AddSigners.tsx => EditSigners.tsx} | 93 ++++++-- packages/proposals/src/constants/labels.ts | 9 +- packages/proposals/src/models/removeSigner.ts | 60 +++++ packages/proposals/src/models/timelock.ts | 1 + .../proposals/src/views/proposal/index.tsx | 4 +- 7 files changed, 376 insertions(+), 79 deletions(-) create mode 100644 packages/proposals/src/actions/removeSigner.ts rename packages/proposals/src/components/Proposal/{AddSigners.tsx => EditSigners.tsx} (61%) create mode 100644 packages/proposals/src/models/removeSigner.ts diff --git a/packages/common/src/contexts/accounts.tsx b/packages/common/src/contexts/accounts.tsx index 1aee897..1e81616 100644 --- a/packages/common/src/contexts/accounts.tsx +++ b/packages/common/src/contexts/accounts.tsx @@ -1,4 +1,10 @@ -import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; import { useConnection } from '../contexts/connection'; import { useWallet } from '../contexts/wallet'; import { AccountInfo, Connection, PublicKey } from '@solana/web3.js'; @@ -7,7 +13,11 @@ import { TokenAccount } from '../models'; import { chunks } from '../utils/utils'; import { EventEmitter } from '../utils/eventEmitter'; import { useUserAccounts } from '../hooks/useUserAccounts'; -import { WRAPPED_SOL_MINT, programIds, LEND_HOST_FEE_ADDRESS } from '../utils/ids'; +import { + WRAPPED_SOL_MINT, + programIds, + LEND_HOST_FEE_ADDRESS, +} from '../utils/ids'; const AccountsContext = React.createContext(null); @@ -22,7 +32,10 @@ export interface ParsedAccountBase { info: any; // TODO: change to unkown } -export type AccountParser = (pubkey: PublicKey, data: AccountInfo) => ParsedAccountBase | undefined; +export type AccountParser = ( + pubkey: PublicKey, + data: AccountInfo, +) => ParsedAccountBase | undefined; export interface ParsedAccount extends ParsedAccountBase { info: T; @@ -55,7 +68,10 @@ export const MintParser = (pubKey: PublicKey, info: AccountInfo) => { return details; }; -export const TokenAccountParser = (pubKey: PublicKey, info: AccountInfo) => { +export const TokenAccountParser = ( + pubKey: PublicKey, + info: AccountInfo, +) => { const buffer = Buffer.from(info.data); const data = deserializeAccount(buffer); @@ -70,7 +86,10 @@ export const TokenAccountParser = (pubKey: PublicKey, info: AccountInfo) return details; }; -export const GenericAccountParser = (pubKey: PublicKey, info: AccountInfo) => { +export const GenericAccountParser = ( + pubKey: PublicKey, + info: AccountInfo, +) => { const buffer = Buffer.from(info.data); const details = { @@ -88,7 +107,11 @@ export const keyToAccountParser = new Map(); export const cache = { emitter: new EventEmitter(), - query: async (connection: Connection, pubKey: string | PublicKey, parser?: AccountParser) => { + query: async ( + connection: Connection, + pubKey: string | PublicKey, + parser?: AccountParser, + ) => { let id: PublicKey; if (typeof pubKey === 'string') { id = new PublicKey(pubKey); @@ -109,7 +132,7 @@ export const cache = { } // TODO: refactor to use multiple accounts query with flush like behavior - query = connection.getAccountInfo(id).then((data) => { + query = connection.getAccountInfo(id).then(data => { if (!data) { throw new Error('Account not found'); } @@ -120,7 +143,11 @@ export const cache = { return query; }, - add: (id: PublicKey | string, obj: AccountInfo, parser?: AccountParser) => { + add: ( + id: PublicKey | string, + obj: AccountInfo, + parser?: AccountParser, + ) => { if (obj.data.length === 0) { return; } @@ -128,7 +155,9 @@ export const cache = { const address = typeof id === 'string' ? id : id?.toBase58(); const deserialize = parser ? parser : keyToAccountParser.get(address); if (!deserialize) { - throw new Error('Deserializer needs to be registered or passed as a parameter'); + throw new Error( + 'Deserializer needs to be registered or passed as a parameter', + ); } cache.registerParser(id, deserialize); @@ -187,7 +216,52 @@ export const cache = { } return pubkey; - } + }, + queryMint: async (connection: Connection, pubKey: string | PublicKey) => { + let id: PublicKey; + if (typeof pubKey === 'string') { + id = new PublicKey(pubKey); + } else { + id = pubKey; + } + + const address = id.toBase58(); + let mint = mintCache.get(address); + if (mint) { + return mint; + } + + let query = pendingMintCalls.get(address); + if (query) { + return query; + } + + query = getMintInfo(connection, id).then(data => { + pendingMintCalls.delete(address); + + mintCache.set(address, data); + return data; + }) as Promise; + pendingMintCalls.set(address, query as any); + + return query; + }, + getMint: (pubKey: string | PublicKey) => { + let key: string; + if (typeof pubKey !== 'string') { + key = pubKey.toBase58(); + } else { + key = pubKey; + } + + return mintCache.get(key); + }, + addMint: (pubKey: PublicKey, obj: AccountInfo) => { + const mint = deserializeMint(obj.data); + const id = pubKey.toBase58(); + mintCache.set(id, mint); + return mint; + }, }; export const useAccountsContext = () => { @@ -196,7 +270,10 @@ export const useAccountsContext = () => { return context; }; -function wrapNativeAccount(pubkey: PublicKey, account?: AccountInfo): TokenAccount | undefined { +function wrapNativeAccount( + pubkey: PublicKey, + account?: AccountInfo, +): TokenAccount | undefined { if (!account) { return undefined; } @@ -219,7 +296,9 @@ function wrapNativeAccount(pubkey: PublicKey, account?: AccountInfo): To }; } -export const getCachedAccount = (predicate: (account: TokenAccount) => boolean) => { +export const getCachedAccount = ( + predicate: (account: TokenAccount) => boolean, +) => { for (const account of genericCache.values()) { if (predicate(account)) { return account as TokenAccount; @@ -234,8 +313,8 @@ const UseNativeAccount = () => { const [nativeAccount, setNativeAccount] = useState>(); const updateCache = useCallback( - (account) => { - if(wallet && wallet.publicKey) { + account => { + if (wallet && wallet.publicKey) { const wrapped = wrapNativeAccount(wallet.publicKey, account); if (wrapped !== undefined && wallet) { const id = wallet.publicKey?.toBase58(); @@ -245,7 +324,7 @@ const UseNativeAccount = () => { } } }, - [wallet] + [wallet], ); useEffect(() => { @@ -253,13 +332,13 @@ const UseNativeAccount = () => { return; } - connection.getAccountInfo(wallet.publicKey).then((acc) => { + connection.getAccountInfo(wallet.publicKey).then(acc => { if (acc) { updateCache(acc); setNativeAccount(acc); } }); - connection.onAccountChange(wallet.publicKey, (acc) => { + connection.onAccountChange(wallet.publicKey, acc => { if (acc) { updateCache(acc); setNativeAccount(acc); @@ -271,7 +350,10 @@ const UseNativeAccount = () => { }; const PRECACHED_OWNERS = new Set(); -const precacheUserTokenAccounts = async (connection: Connection, owner?: PublicKey) => { +const precacheUserTokenAccounts = async ( + connection: Connection, + owner?: PublicKey, +) => { if (!owner) { return; } @@ -283,7 +365,7 @@ const precacheUserTokenAccounts = async (connection: Connection, owner?: PublicK const accounts = await connection.getTokenAccountsByOwner(owner, { programId: programIds().token, }); - accounts.value.forEach((info) => { + accounts.value.forEach(info => { cache.add(info.pubkey.toBase58(), info.account, TokenAccountParser); }); }; @@ -298,30 +380,34 @@ export function AccountsProvider({ children = null as any }) { const selectUserAccounts = useCallback(() => { return cache .byParser(TokenAccountParser) - .map((id) => cache.get(id)) - .filter((a) => a && a.info.owner.toBase58() === wallet?.publicKey?.toBase58()) - .map((a) => a as TokenAccount); + .map(id => cache.get(id)) + .filter( + a => a && a.info.owner.toBase58() === wallet?.publicKey?.toBase58(), + ) + .map(a => a as TokenAccount); }, [wallet]); useEffect(() => { - const accounts = selectUserAccounts().filter((a) => a !== undefined) as TokenAccount[]; + const accounts = selectUserAccounts().filter( + a => a !== undefined, + ) as TokenAccount[]; setUserAccounts(accounts); }, [nativeAccount, wallet, tokenAccounts, selectUserAccounts]); useEffect(() => { const subs: number[] = []; - cache.emitter.onCache((args) => { + cache.emitter.onCache(args => { if (args.isNew) { let id = args.id; let deserialize = args.parser; - connection.onAccountChange(new PublicKey(id), (info) => { + connection.onAccountChange(new PublicKey(id), info => { cache.add(id, info, deserialize); }); } }); return () => { - subs.forEach((id) => connection.removeAccountChangeListener(id)); + subs.forEach(id => connection.removeAccountChangeListener(id)); }; }, [connection]); @@ -341,7 +427,7 @@ export function AccountsProvider({ children = null as any }) { // this should use only filter syntax to only get accounts that are owned by user const tokenSubID = connection.onProgramAccountChange( programIds().token, - (info) => { + info => { // TODO: fix type in web3.js const id = (info.accountId as unknown) as string; // TODO: do we need a better way to identify layout (maybe a enum identifing type?) @@ -354,7 +440,7 @@ export function AccountsProvider({ children = null as any }) { } } }, - 'singleGossip' + 'singleGossip', ); return () => { @@ -382,38 +468,49 @@ export function useNativeAccount() { }; } -export const getMultipleAccounts = async (connection: any, keys: string[], commitment: string) => { +export const getMultipleAccounts = async ( + connection: any, + keys: string[], + commitment: string, +) => { const result = await Promise.all( - chunks(keys, 99).map((chunk) => getMultipleAccountsCore(connection, chunk, commitment)) + chunks(keys, 99).map(chunk => + getMultipleAccountsCore(connection, chunk, commitment), + ), ); const array = result .map( - (a) => - a.array - .map((acc) => { - if (!acc) { - return undefined; - } + a => + a.array.map(acc => { + if (!acc) { + return undefined; + } - const { data, ...rest } = acc; - const obj = { - ...rest, - data: Buffer.from(data[0], 'base64'), - } as AccountInfo; - return obj; - }) as AccountInfo[] + const { data, ...rest } = acc; + const obj = { + ...rest, + data: Buffer.from(data[0], 'base64'), + } as AccountInfo; + return obj; + }) as AccountInfo[], ) .flat(); return { keys, array }; }; -const getMultipleAccountsCore = async (connection: any, keys: string[], commitment: string) => { +const getMultipleAccountsCore = async ( + connection: any, + keys: string[], + commitment: string, +) => { const args = connection._buildArgs([keys], commitment, 'base64'); const unsafeRes = await connection._rpcRequest('getMultipleAccounts', args); if (unsafeRes.error) { - throw new Error('failed to get info about account ' + unsafeRes.error.message); + throw new Error( + 'failed to get info about account ' + unsafeRes.error.message, + ); } if (unsafeRes.result.value) { @@ -438,13 +535,15 @@ export function useMint(key?: string | PublicKey) { cache .query(connection, id, MintParser) - .then((acc) => setMint(acc.info as any)) - .catch((err) => console.log(err)); + .then(acc => setMint(acc.info as any)) + .catch(err => console.log(err)); - const dispose = cache.emitter.onCache((e) => { + const dispose = cache.emitter.onCache(e => { const event = e; if (event.id === id) { - cache.query(connection, id, MintParser).then((mint) => setMint(mint.info as any)); + cache + .query(connection, id, MintParser) + .then(mint => setMint(mint.info as any)); } }); return () => { @@ -467,7 +566,9 @@ export function useAccount(pubKey?: PublicKey) { return; } - const acc = await cache.query(connection, key, TokenAccountParser).catch((err) => console.log(err)); + const acc = await cache + .query(connection, key, TokenAccountParser) + .catch(err => console.log(err)); if (acc) { setAccount(acc); } @@ -478,7 +579,7 @@ export function useAccount(pubKey?: PublicKey) { query(); - const dispose = cache.emitter.onCache((e) => { + const dispose = cache.emitter.onCache(e => { const event = e; if (event.id === key) { query(); @@ -493,7 +594,7 @@ export function useAccount(pubKey?: PublicKey) { } // TODO: expose in spl package -const deserializeAccount = (data: Buffer) => { +export const deserializeAccount = (data: Buffer) => { const accountInfo = AccountLayout.decode(data); accountInfo.mint = new PublicKey(accountInfo.mint); accountInfo.owner = new PublicKey(accountInfo.owner); diff --git a/packages/proposals/src/actions/removeSigner.ts b/packages/proposals/src/actions/removeSigner.ts new file mode 100644 index 0000000..ed08a9e --- /dev/null +++ b/packages/proposals/src/actions/removeSigner.ts @@ -0,0 +1,83 @@ +import { + Account, + Connection, + PublicKey, + TransactionInstruction, +} from '@solana/web3.js'; +import { + contexts, + utils, + models, + ParsedAccount, + actions, +} from '@oyster/common'; + +import { TimelockSet } from '../models/timelock'; +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( + [PROGRAM_IDS.timelock.programAccountId.toBuffer()], + PROGRAM_IDS.timelock.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/proposals/src/components/Proposal/AddSigners.tsx b/packages/proposals/src/components/Proposal/EditSigners.tsx similarity index 61% rename from packages/proposals/src/components/Proposal/AddSigners.tsx rename to packages/proposals/src/components/Proposal/EditSigners.tsx index 0d01794..2a47548 100644 --- a/packages/proposals/src/components/Proposal/AddSigners.tsx +++ b/packages/proposals/src/components/Proposal/EditSigners.tsx @@ -1,4 +1,4 @@ -import { ParsedAccount } from '@oyster/common'; +import { ParsedAccount, TokenAccount } from '@oyster/common'; import { Button, Modal, @@ -9,6 +9,8 @@ import { Col, Row, Space, + Switch, + Radio, } from 'antd'; import React, { useState } from 'react'; import { TimelockSet } from '../../models/timelock'; @@ -16,19 +18,22 @@ import { utils, contexts, hooks } from '@oyster/common'; import { addSigner } from '../../actions/addSigner'; import { PublicKey } from '@solana/web3.js'; import { LABELS } from '../../constants'; +import { removeSigner } from '../../actions/removeSigner'; +import { AccountLayout } from '@solana/spl-token'; const { notify } = utils; const { TextArea } = Input; const { useWallet } = contexts.Wallet; const { useConnection } = contexts.Connection; const { useAccountByMint } = hooks; +const { deserializeAccount } = contexts.Accounts; const layout = { labelCol: { span: 5 }, wrapperCol: { span: 19 }, }; -export default function AddSigners({ +export default function EditSigners({ proposal, }: { proposal: ParsedAccount; @@ -38,6 +43,7 @@ export default function AddSigners({ const adminAccount = useAccountByMint(proposal.info.adminMint); const [saving, setSaving] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false); + const PROGRAM_IDS = utils.programIds(); const [savePerc, setSavePerc] = useState(0); const [failedSigners, setFailedSigners] = useState([]); @@ -46,19 +52,20 @@ export default function AddSigners({ const onSubmit = async (values: { signers: string; failedSigners: string; + type: string; }) => { const signers = values.signers.split(',').map(s => s.trim()); setSaving(true); if (!adminAccount) { notify({ - message: 'Admin account is not defined', + message: LABELS.ADMIN_ACCOUNT_NOT_DEFINED, type: 'error', }); return; } if (signers.length == 0 || (signers.length == 1 && !signers[0])) { notify({ - message: 'Please enter at least one pub key.', + message: LABELS.ENTER_AT_LEAST_ONE_PUB_KEY, type: 'error', }); return; @@ -68,19 +75,42 @@ export default function AddSigners({ for (let i = 0; i < signers.length; i++) { try { - await addSigner( - connection, - wallet.wallet, - proposal, - adminAccount.pubkey, - new PublicKey(signers[i]), - ); + if (values.type == LABELS.ADD) + await addSigner( + connection, + wallet.wallet, + proposal, + adminAccount.pubkey, + new PublicKey(signers[i]), + ); + else { + const tokenAccounts = await connection.getTokenAccountsByOwner( + new PublicKey(signers[i]), + { + programId: PROGRAM_IDS.token, + }, + ); + const specificToThisMint = tokenAccounts.value.filter( + a => + deserializeAccount(a.account.data).mint.toBase58() === + proposal.info.signatoryMint.toBase58(), + ); + for (let j = 0; j < specificToThisMint.length; j++) { + await removeSigner( + connection, + wallet.wallet, + proposal, + adminAccount.pubkey, + specificToThisMint[j].pubkey, + ); + } + } setSavePerc(Math.round(100 * ((i + 1) / signers.length))); } catch (e) { console.error(e); failedSignersHold.push(signers[i]); notify({ - message: `Pub key ${signers[i]} failed. Please check your inspector tab for more information. We'll continue onward and add this to a list for you to re-upload in a later save.`, + message: signers[i] + LABELS.PUB_KEY_FAILED, type: 'error', }); } @@ -99,11 +129,11 @@ export default function AddSigners({ setIsModalVisible(true); }} > - {LABELS.ADD_SIGNERS} + {LABELS.EDIT_SIGNERS} ) : null} {!saving && ( - -