mirror of https://github.com/certusone/oyster.git
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:
parent
941170be1d
commit
43abf3ae50
|
@ -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 { useConnection } from '../contexts/connection';
|
||||||
import { useWallet } from '../contexts/wallet';
|
import { useWallet } from '../contexts/wallet';
|
||||||
import { AccountInfo, Connection, PublicKey } from '@solana/web3.js';
|
import { AccountInfo, Connection, PublicKey } from '@solana/web3.js';
|
||||||
|
@ -7,7 +13,11 @@ import { TokenAccount } from '../models';
|
||||||
import { chunks } from '../utils/utils';
|
import { chunks } from '../utils/utils';
|
||||||
import { EventEmitter } from '../utils/eventEmitter';
|
import { EventEmitter } from '../utils/eventEmitter';
|
||||||
import { useUserAccounts } from '../hooks/useUserAccounts';
|
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);
|
const AccountsContext = React.createContext<any>(null);
|
||||||
|
|
||||||
|
@ -22,7 +32,10 @@ export interface ParsedAccountBase {
|
||||||
info: any; // TODO: change to unkown
|
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 {
|
export interface ParsedAccount<T> extends ParsedAccountBase {
|
||||||
info: T;
|
info: T;
|
||||||
|
@ -55,7 +68,10 @@ export const MintParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
|
||||||
return details;
|
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 buffer = Buffer.from(info.data);
|
||||||
const data = deserializeAccount(buffer);
|
const data = deserializeAccount(buffer);
|
||||||
|
|
||||||
|
@ -70,7 +86,10 @@ export const TokenAccountParser = (pubKey: PublicKey, info: AccountInfo<Buffer>)
|
||||||
return details;
|
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 buffer = Buffer.from(info.data);
|
||||||
|
|
||||||
const details = {
|
const details = {
|
||||||
|
@ -88,7 +107,11 @@ export const keyToAccountParser = new Map<string, AccountParser>();
|
||||||
|
|
||||||
export const cache = {
|
export const cache = {
|
||||||
emitter: new EventEmitter(),
|
emitter: new EventEmitter(),
|
||||||
query: async (connection: Connection, pubKey: string | PublicKey, parser?: AccountParser) => {
|
query: async (
|
||||||
|
connection: Connection,
|
||||||
|
pubKey: string | PublicKey,
|
||||||
|
parser?: AccountParser,
|
||||||
|
) => {
|
||||||
let id: PublicKey;
|
let id: PublicKey;
|
||||||
if (typeof pubKey === 'string') {
|
if (typeof pubKey === 'string') {
|
||||||
id = new PublicKey(pubKey);
|
id = new PublicKey(pubKey);
|
||||||
|
@ -109,7 +132,7 @@ export const cache = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: refactor to use multiple accounts query with flush like behavior
|
// 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) {
|
if (!data) {
|
||||||
throw new Error('Account not found');
|
throw new Error('Account not found');
|
||||||
}
|
}
|
||||||
|
@ -120,7 +143,11 @@ export const cache = {
|
||||||
|
|
||||||
return query;
|
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) {
|
if (obj.data.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -128,7 +155,9 @@ export const cache = {
|
||||||
const address = typeof id === 'string' ? id : id?.toBase58();
|
const address = typeof id === 'string' ? id : id?.toBase58();
|
||||||
const deserialize = parser ? parser : keyToAccountParser.get(address);
|
const deserialize = parser ? parser : keyToAccountParser.get(address);
|
||||||
if (!deserialize) {
|
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);
|
cache.registerParser(id, deserialize);
|
||||||
|
@ -187,7 +216,52 @@ export const cache = {
|
||||||
}
|
}
|
||||||
|
|
||||||
return pubkey;
|
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 = () => {
|
export const useAccountsContext = () => {
|
||||||
|
@ -196,7 +270,10 @@ export const useAccountsContext = () => {
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
function wrapNativeAccount(pubkey: PublicKey, account?: AccountInfo<Buffer>): TokenAccount | undefined {
|
function wrapNativeAccount(
|
||||||
|
pubkey: PublicKey,
|
||||||
|
account?: AccountInfo<Buffer>,
|
||||||
|
): TokenAccount | undefined {
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return undefined;
|
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()) {
|
for (const account of genericCache.values()) {
|
||||||
if (predicate(account)) {
|
if (predicate(account)) {
|
||||||
return account as TokenAccount;
|
return account as TokenAccount;
|
||||||
|
@ -234,8 +313,8 @@ const UseNativeAccount = () => {
|
||||||
const [nativeAccount, setNativeAccount] = useState<AccountInfo<Buffer>>();
|
const [nativeAccount, setNativeAccount] = useState<AccountInfo<Buffer>>();
|
||||||
|
|
||||||
const updateCache = useCallback(
|
const updateCache = useCallback(
|
||||||
(account) => {
|
account => {
|
||||||
if(wallet && wallet.publicKey) {
|
if (wallet && wallet.publicKey) {
|
||||||
const wrapped = wrapNativeAccount(wallet.publicKey, account);
|
const wrapped = wrapNativeAccount(wallet.publicKey, account);
|
||||||
if (wrapped !== undefined && wallet) {
|
if (wrapped !== undefined && wallet) {
|
||||||
const id = wallet.publicKey?.toBase58();
|
const id = wallet.publicKey?.toBase58();
|
||||||
|
@ -245,7 +324,7 @@ const UseNativeAccount = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[wallet]
|
[wallet],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -253,13 +332,13 @@ const UseNativeAccount = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
connection.getAccountInfo(wallet.publicKey).then((acc) => {
|
connection.getAccountInfo(wallet.publicKey).then(acc => {
|
||||||
if (acc) {
|
if (acc) {
|
||||||
updateCache(acc);
|
updateCache(acc);
|
||||||
setNativeAccount(acc);
|
setNativeAccount(acc);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
connection.onAccountChange(wallet.publicKey, (acc) => {
|
connection.onAccountChange(wallet.publicKey, acc => {
|
||||||
if (acc) {
|
if (acc) {
|
||||||
updateCache(acc);
|
updateCache(acc);
|
||||||
setNativeAccount(acc);
|
setNativeAccount(acc);
|
||||||
|
@ -271,7 +350,10 @@ const UseNativeAccount = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const PRECACHED_OWNERS = new Set<string>();
|
const PRECACHED_OWNERS = new Set<string>();
|
||||||
const precacheUserTokenAccounts = async (connection: Connection, owner?: PublicKey) => {
|
const precacheUserTokenAccounts = async (
|
||||||
|
connection: Connection,
|
||||||
|
owner?: PublicKey,
|
||||||
|
) => {
|
||||||
if (!owner) {
|
if (!owner) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -283,7 +365,7 @@ const precacheUserTokenAccounts = async (connection: Connection, owner?: PublicK
|
||||||
const accounts = await connection.getTokenAccountsByOwner(owner, {
|
const accounts = await connection.getTokenAccountsByOwner(owner, {
|
||||||
programId: programIds().token,
|
programId: programIds().token,
|
||||||
});
|
});
|
||||||
accounts.value.forEach((info) => {
|
accounts.value.forEach(info => {
|
||||||
cache.add(info.pubkey.toBase58(), info.account, TokenAccountParser);
|
cache.add(info.pubkey.toBase58(), info.account, TokenAccountParser);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -298,30 +380,34 @@ export function AccountsProvider({ children = null as any }) {
|
||||||
const selectUserAccounts = useCallback(() => {
|
const selectUserAccounts = useCallback(() => {
|
||||||
return cache
|
return cache
|
||||||
.byParser(TokenAccountParser)
|
.byParser(TokenAccountParser)
|
||||||
.map((id) => cache.get(id))
|
.map(id => cache.get(id))
|
||||||
.filter((a) => a && a.info.owner.toBase58() === wallet?.publicKey?.toBase58())
|
.filter(
|
||||||
.map((a) => a as TokenAccount);
|
a => a && a.info.owner.toBase58() === wallet?.publicKey?.toBase58(),
|
||||||
|
)
|
||||||
|
.map(a => a as TokenAccount);
|
||||||
}, [wallet]);
|
}, [wallet]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const accounts = selectUserAccounts().filter((a) => a !== undefined) as TokenAccount[];
|
const accounts = selectUserAccounts().filter(
|
||||||
|
a => a !== undefined,
|
||||||
|
) as TokenAccount[];
|
||||||
setUserAccounts(accounts);
|
setUserAccounts(accounts);
|
||||||
}, [nativeAccount, wallet, tokenAccounts, selectUserAccounts]);
|
}, [nativeAccount, wallet, tokenAccounts, selectUserAccounts]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subs: number[] = [];
|
const subs: number[] = [];
|
||||||
cache.emitter.onCache((args) => {
|
cache.emitter.onCache(args => {
|
||||||
if (args.isNew) {
|
if (args.isNew) {
|
||||||
let id = args.id;
|
let id = args.id;
|
||||||
let deserialize = args.parser;
|
let deserialize = args.parser;
|
||||||
connection.onAccountChange(new PublicKey(id), (info) => {
|
connection.onAccountChange(new PublicKey(id), info => {
|
||||||
cache.add(id, info, deserialize);
|
cache.add(id, info, deserialize);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
subs.forEach((id) => connection.removeAccountChangeListener(id));
|
subs.forEach(id => connection.removeAccountChangeListener(id));
|
||||||
};
|
};
|
||||||
}, [connection]);
|
}, [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
|
// this should use only filter syntax to only get accounts that are owned by user
|
||||||
const tokenSubID = connection.onProgramAccountChange(
|
const tokenSubID = connection.onProgramAccountChange(
|
||||||
programIds().token,
|
programIds().token,
|
||||||
(info) => {
|
info => {
|
||||||
// TODO: fix type in web3.js
|
// 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?)
|
// 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 () => {
|
return () => {
|
||||||
|
@ -382,16 +468,21 @@ 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(
|
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
|
const array = result
|
||||||
.map(
|
.map(
|
||||||
(a) =>
|
a =>
|
||||||
a.array
|
a.array.map(acc => {
|
||||||
.map((acc) => {
|
|
||||||
if (!acc) {
|
if (!acc) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -402,18 +493,24 @@ export const getMultipleAccounts = async (connection: any, keys: string[], commi
|
||||||
data: Buffer.from(data[0], 'base64'),
|
data: Buffer.from(data[0], 'base64'),
|
||||||
} as AccountInfo<Buffer>;
|
} as AccountInfo<Buffer>;
|
||||||
return obj;
|
return obj;
|
||||||
}) as AccountInfo<Buffer>[]
|
}) as AccountInfo<Buffer>[],
|
||||||
)
|
)
|
||||||
.flat();
|
.flat();
|
||||||
return { keys, array };
|
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 args = connection._buildArgs([keys], commitment, 'base64');
|
||||||
|
|
||||||
const unsafeRes = await connection._rpcRequest('getMultipleAccounts', args);
|
const unsafeRes = await connection._rpcRequest('getMultipleAccounts', args);
|
||||||
if (unsafeRes.error) {
|
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) {
|
if (unsafeRes.result.value) {
|
||||||
|
@ -438,13 +535,15 @@ export function useMint(key?: string | PublicKey) {
|
||||||
|
|
||||||
cache
|
cache
|
||||||
.query(connection, id, MintParser)
|
.query(connection, id, MintParser)
|
||||||
.then((acc) => setMint(acc.info as any))
|
.then(acc => setMint(acc.info as any))
|
||||||
.catch((err) => console.log(err));
|
.catch(err => console.log(err));
|
||||||
|
|
||||||
const dispose = cache.emitter.onCache((e) => {
|
const dispose = cache.emitter.onCache(e => {
|
||||||
const event = e;
|
const event = e;
|
||||||
if (event.id === id) {
|
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 () => {
|
return () => {
|
||||||
|
@ -467,7 +566,9 @@ export function useAccount(pubKey?: PublicKey) {
|
||||||
return;
|
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) {
|
if (acc) {
|
||||||
setAccount(acc);
|
setAccount(acc);
|
||||||
}
|
}
|
||||||
|
@ -478,7 +579,7 @@ export function useAccount(pubKey?: PublicKey) {
|
||||||
|
|
||||||
query();
|
query();
|
||||||
|
|
||||||
const dispose = cache.emitter.onCache((e) => {
|
const dispose = cache.emitter.onCache(e => {
|
||||||
const event = e;
|
const event = e;
|
||||||
if (event.id === key) {
|
if (event.id === key) {
|
||||||
query();
|
query();
|
||||||
|
@ -493,7 +594,7 @@ export function useAccount(pubKey?: PublicKey) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: expose in spl package
|
// TODO: expose in spl package
|
||||||
const deserializeAccount = (data: Buffer) => {
|
export const deserializeAccount = (data: Buffer) => {
|
||||||
const accountInfo = AccountLayout.decode(data);
|
const accountInfo = AccountLayout.decode(data);
|
||||||
accountInfo.mint = new PublicKey(accountInfo.mint);
|
accountInfo.mint = new PublicKey(accountInfo.mint);
|
||||||
accountInfo.owner = new PublicKey(accountInfo.owner);
|
accountInfo.owner = new PublicKey(accountInfo.owner);
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
import { ParsedAccount } from '@oyster/common';
|
import { ParsedAccount, TokenAccount } from '@oyster/common';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Modal,
|
Modal,
|
||||||
|
@ -9,6 +9,8 @@ import {
|
||||||
Col,
|
Col,
|
||||||
Row,
|
Row,
|
||||||
Space,
|
Space,
|
||||||
|
Switch,
|
||||||
|
Radio,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { TimelockSet } from '../../models/timelock';
|
import { TimelockSet } from '../../models/timelock';
|
||||||
|
@ -16,19 +18,22 @@ import { utils, contexts, hooks } from '@oyster/common';
|
||||||
import { addSigner } from '../../actions/addSigner';
|
import { addSigner } from '../../actions/addSigner';
|
||||||
import { PublicKey } from '@solana/web3.js';
|
import { PublicKey } from '@solana/web3.js';
|
||||||
import { LABELS } from '../../constants';
|
import { LABELS } from '../../constants';
|
||||||
|
import { removeSigner } from '../../actions/removeSigner';
|
||||||
|
import { AccountLayout } from '@solana/spl-token';
|
||||||
|
|
||||||
const { notify } = utils;
|
const { notify } = utils;
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
const { useWallet } = contexts.Wallet;
|
const { useWallet } = contexts.Wallet;
|
||||||
const { useConnection } = contexts.Connection;
|
const { useConnection } = contexts.Connection;
|
||||||
const { useAccountByMint } = hooks;
|
const { useAccountByMint } = hooks;
|
||||||
|
const { deserializeAccount } = contexts.Accounts;
|
||||||
|
|
||||||
const layout = {
|
const layout = {
|
||||||
labelCol: { span: 5 },
|
labelCol: { span: 5 },
|
||||||
wrapperCol: { span: 19 },
|
wrapperCol: { span: 19 },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AddSigners({
|
export default function EditSigners({
|
||||||
proposal,
|
proposal,
|
||||||
}: {
|
}: {
|
||||||
proposal: ParsedAccount<TimelockSet>;
|
proposal: ParsedAccount<TimelockSet>;
|
||||||
|
@ -38,6 +43,7 @@ export default function AddSigners({
|
||||||
const adminAccount = useAccountByMint(proposal.info.adminMint);
|
const adminAccount = useAccountByMint(proposal.info.adminMint);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
|
const PROGRAM_IDS = utils.programIds();
|
||||||
|
|
||||||
const [savePerc, setSavePerc] = useState(0);
|
const [savePerc, setSavePerc] = useState(0);
|
||||||
const [failedSigners, setFailedSigners] = useState<string[]>([]);
|
const [failedSigners, setFailedSigners] = useState<string[]>([]);
|
||||||
|
@ -46,19 +52,20 @@ export default function AddSigners({
|
||||||
const onSubmit = async (values: {
|
const onSubmit = async (values: {
|
||||||
signers: string;
|
signers: string;
|
||||||
failedSigners: string;
|
failedSigners: string;
|
||||||
|
type: string;
|
||||||
}) => {
|
}) => {
|
||||||
const signers = values.signers.split(',').map(s => s.trim());
|
const signers = values.signers.split(',').map(s => s.trim());
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
if (!adminAccount) {
|
if (!adminAccount) {
|
||||||
notify({
|
notify({
|
||||||
message: 'Admin account is not defined',
|
message: LABELS.ADMIN_ACCOUNT_NOT_DEFINED,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (signers.length == 0 || (signers.length == 1 && !signers[0])) {
|
if (signers.length == 0 || (signers.length == 1 && !signers[0])) {
|
||||||
notify({
|
notify({
|
||||||
message: 'Please enter at least one pub key.',
|
message: LABELS.ENTER_AT_LEAST_ONE_PUB_KEY,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
@ -68,6 +75,7 @@ export default function AddSigners({
|
||||||
|
|
||||||
for (let i = 0; i < signers.length; i++) {
|
for (let i = 0; i < signers.length; i++) {
|
||||||
try {
|
try {
|
||||||
|
if (values.type == LABELS.ADD)
|
||||||
await addSigner(
|
await addSigner(
|
||||||
connection,
|
connection,
|
||||||
wallet.wallet,
|
wallet.wallet,
|
||||||
|
@ -75,12 +83,34 @@ export default function AddSigners({
|
||||||
adminAccount.pubkey,
|
adminAccount.pubkey,
|
||||||
new PublicKey(signers[i]),
|
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)));
|
setSavePerc(Math.round(100 * ((i + 1) / signers.length)));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
failedSignersHold.push(signers[i]);
|
failedSignersHold.push(signers[i]);
|
||||||
notify({
|
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',
|
type: 'error',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -99,11 +129,11 @@ export default function AddSigners({
|
||||||
setIsModalVisible(true);
|
setIsModalVisible(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{LABELS.ADD_SIGNERS}
|
{LABELS.EDIT_SIGNERS}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
<Modal
|
<Modal
|
||||||
title={LABELS.ADD_SIGNERS}
|
title={LABELS.EDIT_SIGNERS}
|
||||||
visible={isModalVisible}
|
visible={isModalVisible}
|
||||||
destroyOnClose={true}
|
destroyOnClose={true}
|
||||||
onOk={form.submit}
|
onOk={form.submit}
|
||||||
|
@ -120,6 +150,20 @@ export default function AddSigners({
|
||||||
name="control-hooks"
|
name="control-hooks"
|
||||||
>
|
>
|
||||||
{!saving && (
|
{!saving && (
|
||||||
|
<>
|
||||||
|
<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
|
<Form.Item
|
||||||
name="signers"
|
name="signers"
|
||||||
label={LABELS.SIGNERS}
|
label={LABELS.SIGNERS}
|
||||||
|
@ -130,6 +174,7 @@ export default function AddSigners({
|
||||||
placeholder={LABELS.COMMA_SEPARATED_KEYS}
|
placeholder={LABELS.COMMA_SEPARATED_KEYS}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
{saving && <Progress percent={savePerc} status="active" />}
|
{saving && <Progress percent={savePerc} status="active" />}
|
|
@ -26,5 +26,12 @@ export const LABELS = {
|
||||||
FAILED_SIGNERS_COPIED_TO_CLIPBOARD: 'Failed signers copied to clipboard!',
|
FAILED_SIGNERS_COPIED_TO_CLIPBOARD: 'Failed signers copied to clipboard!',
|
||||||
COMMA_SEPARATED_KEYS: 'Comma separated base58 pubkeys',
|
COMMA_SEPARATED_KEYS: 'Comma separated base58 pubkeys',
|
||||||
SIGNERS: 'Signers',
|
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',
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
||||||
|
};
|
|
@ -12,6 +12,7 @@ export const TRANSACTION_SLOTS = 10;
|
||||||
export enum TimelockInstruction {
|
export enum TimelockInstruction {
|
||||||
InitTimelockSet = 1,
|
InitTimelockSet = 1,
|
||||||
AddSigner = 2,
|
AddSigner = 2,
|
||||||
|
RemoveSigner = 3,
|
||||||
addCustomSingleSignerTransaction = 4,
|
addCustomSingleSignerTransaction = 4,
|
||||||
Sign = 8,
|
Sign = 8,
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { MintInfo } from '@solana/spl-token';
|
||||||
import { InstructionCard } from '../../components/Proposal/InstructionCard';
|
import { InstructionCard } from '../../components/Proposal/InstructionCard';
|
||||||
import { NewInstructionCard } from '../../components/Proposal/NewInstructionCard';
|
import { NewInstructionCard } from '../../components/Proposal/NewInstructionCard';
|
||||||
import SignButton from '../../components/Proposal/SignButton';
|
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()@:%_\+.~#?&//=]*)/;
|
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 { useMint } = contexts.Accounts;
|
||||||
const { useAccountByMint } = hooks;
|
const { useAccountByMint } = hooks;
|
||||||
|
@ -163,7 +163,7 @@ function InnerProposalView({
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{adminAccount && adminAccount.info.amount.toNumber() === 1 && (
|
{adminAccount && adminAccount.info.amount.toNumber() === 1 && (
|
||||||
<AddSigners proposal={proposal} />
|
<EditSigners proposal={proposal} />
|
||||||
)}
|
)}
|
||||||
{sigAccount && sigAccount.info.amount.toNumber() === 1 && (
|
{sigAccount && sigAccount.info.amount.toNumber() === 1 && (
|
||||||
<SignButton proposal={proposal} />
|
<SignButton proposal={proposal} />
|
||||||
|
|
Loading…
Reference in New Issue