This commit marks a working remove signer - if only you could burn other people's tokens. Going to remove this functionality now.

This commit is contained in:
Dummy Tester 123 2021-02-28 13:15:31 -06:00
parent 941170be1d
commit 43abf3ae50
7 changed files with 376 additions and 79 deletions

View File

@ -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<any>(null);
@ -22,7 +32,10 @@ export interface ParsedAccountBase {
info: any; // TODO: change to unkown
}
export type AccountParser = (pubkey: PublicKey, data: AccountInfo<Buffer>) => ParsedAccountBase | undefined;
export type AccountParser = (
pubkey: PublicKey,
data: AccountInfo<Buffer>,
) => ParsedAccountBase | undefined;
export interface ParsedAccount<T> extends ParsedAccountBase {
info: T;
@ -55,7 +68,10 @@ export const MintParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
return details;
};
export const TokenAccountParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
export const TokenAccountParser = (
pubKey: PublicKey,
info: AccountInfo<Buffer>,
) => {
const buffer = Buffer.from(info.data);
const data = deserializeAccount(buffer);
@ -70,7 +86,10 @@ export const TokenAccountParser = (pubKey: PublicKey, info: AccountInfo<Buffer>)
return details;
};
export const GenericAccountParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
export const GenericAccountParser = (
pubKey: PublicKey,
info: AccountInfo<Buffer>,
) => {
const buffer = Buffer.from(info.data);
const details = {
@ -88,7 +107,11 @@ export const keyToAccountParser = new Map<string, AccountParser>();
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<Buffer>, parser?: AccountParser) => {
add: (
id: PublicKey | string,
obj: AccountInfo<Buffer>,
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<MintInfo>;
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<Buffer>) => {
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<Buffer>): TokenAccount | undefined {
function wrapNativeAccount(
pubkey: PublicKey,
account?: AccountInfo<Buffer>,
): TokenAccount | undefined {
if (!account) {
return undefined;
}
@ -219,7 +296,9 @@ function wrapNativeAccount(pubkey: PublicKey, account?: AccountInfo<Buffer>): 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<AccountInfo<Buffer>>();
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<string>();
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<Buffer>;
return obj;
}) as AccountInfo<Buffer>[]
const { data, ...rest } = acc;
const obj = {
...rest,
data: Buffer.from(data[0], 'base64'),
} as AccountInfo<Buffer>;
return obj;
}) as AccountInfo<Buffer>[],
)
.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);

View File

@ -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<TimelockSet>,
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();
}
};

View File

@ -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<TimelockSet>;
@ -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<string[]>([]);
@ -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}
</Button>
) : null}
<Modal
title={LABELS.ADD_SIGNERS}
title={LABELS.EDIT_SIGNERS}
visible={isModalVisible}
destroyOnClose={true}
onOk={form.submit}
@ -120,16 +150,31 @@ export default function AddSigners({
name="control-hooks"
>
{!saving && (
<Form.Item
name="signers"
label={LABELS.SIGNERS}
rules={[{ required: true }]}
>
<TextArea
id="signers"
placeholder={LABELS.COMMA_SEPARATED_KEYS}
/>
</Form.Item>
<>
<Form.Item
name="type"
label={LABELS.ADDING_OR_REMOVING}
initialValue={LABELS.ADD}
rules={[{ required: true }]}
>
<Radio.Group value={layout}>
<Radio.Button value={LABELS.REMOVE}>
{LABELS.REMOVE}
</Radio.Button>
<Radio.Button value={LABELS.ADD}>{LABELS.ADD}</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item
name="signers"
label={LABELS.SIGNERS}
rules={[{ required: true }]}
>
<TextArea
id="signers"
placeholder={LABELS.COMMA_SEPARATED_KEYS}
/>
</Form.Item>
</>
)}
</Form>
{saving && <Progress percent={savePerc} status="active" />}

View File

@ -26,5 +26,12 @@ export const LABELS = {
FAILED_SIGNERS_COPIED_TO_CLIPBOARD: 'Failed signers copied to clipboard!',
COMMA_SEPARATED_KEYS: 'Comma separated base58 pubkeys',
SIGNERS: 'Signers',
ADD_SIGNERS: 'Add Signers',
EDIT_SIGNERS: 'Edit Signers',
ADMIN_ACCOUNT_NOT_DEFINED: 'Admin account is not defined',
ENTER_AT_LEAST_ONE_PUB_KEY: 'Please enter at least one pub key.',
PUB_KEY_FAILED:
" Pub key 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.",
ADD: 'Add',
REMOVE: 'Remove',
ADDING_OR_REMOVING: 'Type',
};

View File

@ -0,0 +1,60 @@
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
import { utils } from '@oyster/common';
import * as BufferLayout from 'buffer-layout';
import { TimelockInstruction } from './timelock';
/// [Requires Admin token]
/// Removes a signer from the set.
///
/// 0. `[writable]` Signatory account to remove token from.
/// 1. `[writable]` Signatory mint account.
/// 2. `[writable]` Admin account.
/// 3. `[writable]` Admin validation account.
/// 4. `[]` Timelock set account.
/// 5. `[]` Transfer authority
/// 5. `[]` Timelock program mint authority
/// 6. `[]` Timelock program account.
/// 7. '[]` Token program id.
export const removeSignerInstruction = (
signatoryAccount: PublicKey,
signatoryMintAccount: PublicKey,
adminAccount: PublicKey,
adminValidationAccount: PublicKey,
timelockSetAccount: PublicKey,
transferAuthority: PublicKey,
mintAuthority: PublicKey,
): TransactionInstruction => {
const PROGRAM_IDS = utils.programIds();
const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: TimelockInstruction.RemoveSigner,
},
data,
);
const keys = [
{ pubkey: signatoryAccount, isSigner: false, isWritable: true },
{ pubkey: signatoryMintAccount, isSigner: false, isWritable: true },
{ pubkey: adminAccount, isSigner: false, isWritable: true },
{ pubkey: adminValidationAccount, isSigner: false, isWritable: true },
{ pubkey: timelockSetAccount, isSigner: false, isWritable: true },
{ pubkey: transferAuthority, isSigner: true, isWritable: false },
{ pubkey: mintAuthority, isSigner: false, isWritable: false },
{
pubkey: PROGRAM_IDS.timelock.programAccountId,
isSigner: false,
isWritable: false,
},
{ pubkey: PROGRAM_IDS.token, isSigner: false, isWritable: false },
];
return new TransactionInstruction({
keys,
programId: PROGRAM_IDS.timelock.programId,
data,
});
};

View File

@ -12,6 +12,7 @@ export const TRANSACTION_SLOTS = 10;
export enum TimelockInstruction {
InitTimelockSet = 1,
AddSigner = 2,
RemoveSigner = 3,
addCustomSingleSignerTransaction = 4,
Sign = 8,
}

View File

@ -17,7 +17,7 @@ import { MintInfo } from '@solana/spl-token';
import { InstructionCard } from '../../components/Proposal/InstructionCard';
import { NewInstructionCard } from '../../components/Proposal/NewInstructionCard';
import SignButton from '../../components/Proposal/SignButton';
import AddSigners from '../../components/Proposal/AddSigners';
import EditSigners from '../../components/Proposal/EditSigners';
export const urlRegex = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
const { useMint } = contexts.Accounts;
const { useAccountByMint } = hooks;
@ -163,7 +163,7 @@ function InnerProposalView({
}
>
{adminAccount && adminAccount.info.amount.toNumber() === 1 && (
<AddSigners proposal={proposal} />
<EditSigners proposal={proposal} />
)}
{sigAccount && sigAccount.info.amount.toNumber() === 1 && (
<SignButton proposal={proposal} />