import React, { useState, useEffect } from 'react';
import { useHistory } from 'react-router';
import { useSnackbar } from 'notistack';
import { encode as encodeBase64 } from 'js-base64';
import Container from '@material-ui/core/Container';
import AppBar from '@material-ui/core/AppBar';
import GavelIcon from '@material-ui/icons/Gavel';
import DescriptionIcon from '@material-ui/icons/Description';
import Paper from '@material-ui/core/Paper';
import SupervisorAccountIcon from '@material-ui/icons/SupervisorAccount';
import CheckIcon from '@material-ui/icons/Check';
import ReceiptIcon from '@material-ui/icons/Receipt';
import RemoveIcon from '@material-ui/icons/Remove';
import Collapse from '@material-ui/core/Collapse';
import Toolbar from '@material-ui/core/Toolbar';
import InfoIcon from '@material-ui/icons/Info';
import Table from '@material-ui/core/Table';
import TableHead from '@material-ui/core/TableHead';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableRow from '@material-ui/core/TableRow';
import BuildIcon from '@material-ui/icons/Build';
import Tooltip from '@material-ui/core/Tooltip';
import CircularProgress from '@material-ui/core/CircularProgress';
import Typography from '@material-ui/core/Typography';
import Card from '@material-ui/core/Card';
import ExpandLess from '@material-ui/icons/ExpandLess';
import ExpandMore from '@material-ui/icons/ExpandMore';
import CardContent from '@material-ui/core/CardContent';
import TextField from '@material-ui/core/TextField';
import IconButton from '@material-ui/core/IconButton';
import Button from '@material-ui/core/Button';
import SearchIcon from '@material-ui/icons/Search';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogActions from '@material-ui/core/DialogActions';
import AddIcon from '@material-ui/icons/Add';
import List from '@material-ui/core/List';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import CheckCircleIcon from '@material-ui/icons/CheckCircle';
import BN from 'bn.js';
import {
Account,
PublicKey,
SYSVAR_RENT_PUBKEY,
SYSVAR_CLOCK_PUBKEY,
} from '@solana/web3.js';
import * as anchor from '@project-serum/anchor';
import { useWallet } from '../common/WalletProvider';
import { ViewTransactionOnExplorerButton } from '../common/Notification';
import * as idl from '../../utils/idl';
export default function Multisig({ multisig }: { multisig?: PublicKey }) {
const history = useHistory();
const [multisigAddress, setMultisigAddress] = useState('');
const disabled = !isValidPubkey(multisigAddress);
const searchFn = () => {
history.push(`/multisig/${multisigAddress}`);
};
return (
);
}
function isValidPubkey(addr: string): boolean {
try {
new PublicKey(addr);
return true;
} catch (_) {
return false;
}
}
export function MultisigInstance({ multisig }: { multisig: PublicKey }) {
const { multisigClient } = useWallet();
const [multisigAccount, setMultisigAccount] = useState(undefined);
const [transactions, setTransactions] = useState(null);
const [showSignerDialog, setShowSignerDialog] = useState(false);
const [showAddTransactionDialog, setShowAddTransactionDialog] = useState(
false,
);
const [forceRefresh, setForceRefresh] = useState(false);
useEffect(() => {
multisigClient.account
.multisig(multisig)
.then((account: any) => {
setMultisigAccount(account);
})
.catch((err: any) => {
console.error(err);
setMultisigAccount(null);
});
}, [multisig, multisigClient.account]);
useEffect(() => {
multisigClient.account.transaction.all(multisig.toBuffer()).then(txs => {
setTransactions(txs);
});
}, [multisigClient.account.transaction, multisig, forceRefresh]);
useEffect(() => {
multisigClient.account.multisig
.subscribe(multisig)
.on('change', account => {
setMultisigAccount(account);
});
}, [multisigClient, multisig]);
return (
{multisigAccount === undefined ? (
) : multisigAccount === null ? (
Multisig not found
) : (
<>>
)}
{multisigAccount && (
{multisig.toString()} | {multisigAccount.threshold.toString()}{' '}
of {multisigAccount.owners.length.toString()} Multisig
setShowSignerDialog(true)}>
setShowAddTransactionDialog(true)}>
{transactions === null ? (
) : transactions.length === 0 ? (
) : (
transactions.map((tx: any) => (
))
)}
)}
setShowAddTransactionDialog(false)}
didAddTransaction={() => setForceRefresh(!forceRefresh)}
/>
{multisigAccount && (
setShowSignerDialog(false)}
/>
)}
);
}
function NewMultisigButton() {
const [open, setOpen] = useState(false);
return (
setOpen(true)}>
setOpen(false)} />
);
}
function NewMultisigDialog({
open,
onClose,
}: {
open: boolean;
onClose: () => void;
}) {
const history = useHistory();
const { multisigClient } = useWallet();
const { enqueueSnackbar } = useSnackbar();
const [threshold, setThreshold] = useState(2);
// @ts-ignore
const zeroAddr = new PublicKey().toString();
const [participants, setParticipants] = useState([
multisigClient.provider.wallet.publicKey.toString(),
zeroAddr,
]);
const _onClose = () => {
onClose();
setThreshold(2);
setParticipants([zeroAddr, zeroAddr]);
};
const createMultisig = async () => {
enqueueSnackbar('Creating multisig', {
variant: 'info',
});
const multisig = new Account();
// Disc. + threshold + nonce.
const baseSize = 8 + 8 + 1;
// Can only grow the participant set by 2x the initialized value.
const ownerSize = participants.length * 2 * 32 + 8;
const multisigSize = baseSize + ownerSize;
const [, nonce] = await PublicKey.findProgramAddress(
[multisig.publicKey.toBuffer()],
multisigClient.programId,
);
const owners = participants.map(p => new PublicKey(p));
const tx = await multisigClient.rpc.createMultisig(
owners,
new BN(threshold),
nonce,
{
accounts: {
multisig: multisig.publicKey,
rent: SYSVAR_RENT_PUBKEY,
},
signers: [multisig],
instructions: [
await multisigClient.account.multisig.createInstruction(
multisig,
// @ts-ignore
multisigSize,
),
],
},
);
enqueueSnackbar(`Multisig created: ${multisig.publicKey.toString()}`, {
variant: 'success',
action: ,
});
_onClose();
history.push(`/multisig/${multisig.publicKey.toString()}`);
};
return (
New Multisig
setThreshold(parseInt(e.target.value) as number)}
/>
{participants.map((p, idx) => (
{
const p = [...participants];
p[idx] = e.target.value;
setParticipants(p);
}}
/>
))}
{
const p = [...participants];
// @ts-ignore
p.push(new PublicKey().toString());
setParticipants(p);
}}
>
Cancel
createMultisig().catch(err => {
const str = err ? err.toString() : '';
enqueueSnackbar(`Error creating multisig: ${str}`, {
variant: 'error',
});
})
}
>
Create
);
}
function TxListItem({
multisig,
multisigAccount,
tx,
}: {
multisig: PublicKey;
multisigAccount: any;
tx: any;
}) {
const { enqueueSnackbar } = useSnackbar();
const { multisigClient } = useWallet();
const [open, setOpen] = useState(false);
const [txAccount, setTxAccount] = useState(tx.account);
useEffect(() => {
multisigClient.account.transaction
.subscribe(tx.publicKey)
.on('change', account => {
setTxAccount(account);
});
}, [multisigClient, multisig, tx.publicKey]);
const rows = [
{
field: 'Program ID',
value: txAccount.programId.toString(),
},
{
field: 'Did execute',
value: txAccount.didExecute.toString(),
},
{
field: 'Instruction data',
value: (
{encodeBase64(txAccount.data)}
),
},
{
field: 'Multisig',
value: txAccount.multisig.toString(),
},
{
field: 'Transaction account',
value: tx.publicKey.toString(),
},
{
field: 'Owner set seqno',
value: txAccount.ownerSetSeqno.toString(),
},
];
const msAccountRows = multisigAccount.owners.map(
(owner: PublicKey, idx: number) => {
return {
field: owner.toString(),
value: txAccount.signers[idx] ? : ,
};
},
);
const approve = async () => {
enqueueSnackbar('Approving transaction', {
variant: 'info',
});
await multisigClient.rpc.approve({
accounts: {
multisig,
transaction: tx.publicKey,
owner: multisigClient.provider.wallet.publicKey,
},
});
enqueueSnackbar('Transaction approved', {
variant: 'success',
});
};
const execute = async () => {
enqueueSnackbar('Executing transaction', {
variant: 'info',
});
const [multisigSigner] = await PublicKey.findProgramAddress(
[multisig.toBuffer()],
multisigClient.programId,
);
await multisigClient.rpc.executeTransaction({
accounts: {
multisig,
multisigSigner,
transaction: tx.publicKey,
},
remainingAccounts: txAccount.accounts
.map((t: any) => {
if (t.pubkey.equals(multisigSigner)) {
return { ...t, isSigner: false };
}
return t;
})
.concat({
pubkey: txAccount.programId,
isWritable: false,
isSigner: false,
}),
});
enqueueSnackbar('Transaction executed', {
variant: 'success',
});
};
return (
<>
setOpen(!open)}>
{icon(tx, multisigClient)}
{ixLabel(tx, multisigClient)}
{txAccount.didExecute && (
)}
{open ? : }
approve().catch(err => {
let errStr = '';
if (err) {
errStr = err.toString();
}
enqueueSnackbar(`Unable to approve transaction: ${errStr}`, {
variant: 'error',
});
})
}
>
Approve
execute().catch(err => {
let errStr = '';
if (err) {
errStr = err.toString();
}
enqueueSnackbar(`Unable to execute transaction: ${errStr}`, {
variant: 'error',
});
})
}
>
Execute
Transaction Field
Value
{rows.map(r => (
{r.field}
{r.value}
))}
Multisig Owner
Did Sign
{txAccount.ownerSetSeqno === multisigAccount.ownerSetSeqno &&
msAccountRows.map((r: any) => (
{r.field}
{r.value}
))}
{txAccount.ownerSetSeqno !== multisigAccount.ownerSetSeqno && (
The owner set has changed since this transaction was created
)}
>
);
}
function ixLabel(tx: any, multisigClient: any) {
if (tx.account.programId.equals(BPF_LOADER_UPGRADEABLE_PID)) {
// Upgrade instruction.
if (tx.account.data.equals(Buffer.from([3, 0, 0, 0]))) {
return (
);
}
}
if (tx.account.programId.equals(multisigClient.programId)) {
const setThresholdSighash = multisigClient.coder.sighash(
'global',
'change_threshold',
);
if (setThresholdSighash.equals(tx.account.data.slice(0, 8))) {
return (
);
}
const setOwnersSighash = multisigClient.coder.sighash(
'global',
'set_owners',
);
if (setOwnersSighash.equals(tx.account.data.slice(0, 8))) {
return (
);
}
}
if (idl.IDL_TAG.equals(tx.account.data.slice(0, 8))) {
return (
);
}
return ;
}
function AccountsList({ accounts }: { accounts: any }) {
return (
Account
Writable
Signer
{accounts.map((r: any) => (
{r.pubkey.toString()}
{r.isWritable.toString()}
{r.isSigner.toString()}
))}
);
}
function SignerDialog({
multisig,
multisigAccount,
open,
onClose,
}: {
multisig: PublicKey;
multisigAccount: any;
open: boolean;
onClose: () => void;
}) {
const { multisigClient } = useWallet();
const [signer, setSigner] = useState(null);
useEffect(() => {
PublicKey.findProgramAddress(
[multisig.toBuffer()],
multisigClient.programId,
).then(addrNonce => setSigner(addrNonce[0].toString()));
}, [multisig, multisigClient.programId, setSigner]);
return (
Multisig Info
Program derived address : {signer}. This is the address one
should use as the authority for data governed by the multisig.
Owners
{multisigAccount.owners.map((r: any) => (
{r.toString()}
))}
Close
);
}
function AddTransactionDialog({
multisig,
open,
onClose,
didAddTransaction,
}: {
multisig: PublicKey;
open: boolean;
onClose: () => void;
didAddTransaction: (tx: PublicKey) => void;
}) {
return (
New Transaction
Create a new transaction to be signed by the multisig. This
transaction will not execute until enough owners have signed the
transaction.
);
}
function ChangeThresholdListItem({
multisig,
onClose,
didAddTransaction,
}: {
multisig: PublicKey;
onClose: Function;
didAddTransaction: (tx: PublicKey) => void;
}) {
const [open, setOpen] = useState(false);
return (
<>
setOpen(open => !open)}>
{open ? : }
>
);
}
function ChangeThresholdListItemDetails({
multisig,
onClose,
didAddTransaction,
}: {
multisig: PublicKey;
onClose: Function;
didAddTransaction: (tx: PublicKey) => void;
}) {
const [threshold, setThreshold] = useState(2);
const { multisigClient } = useWallet();
// @ts-ignore
const { enqueueSnackbar } = useSnackbar();
const changeThreshold = async () => {
enqueueSnackbar('Creating change threshold transaction', {
variant: 'info',
});
const data = changeThresholdData(multisigClient, threshold);
const [multisigSigner] = await PublicKey.findProgramAddress(
[multisig.toBuffer()],
multisigClient.programId,
);
const accounts = [
{
pubkey: multisig,
isWritable: true,
isSigner: false,
},
{
pubkey: multisigSigner,
isWritable: false,
isSigner: true,
},
];
const transaction = new Account();
const txSize = 1000; // todo
const tx = await multisigClient.rpc.createTransaction(
multisigClient.programId,
accounts,
data,
{
accounts: {
multisig,
transaction: transaction.publicKey,
proposer: multisigClient.provider.wallet.publicKey,
rent: SYSVAR_RENT_PUBKEY,
},
signers: [transaction],
instructions: [
await multisigClient.account.transaction.createInstruction(
transaction,
// @ts-ignore
txSize,
),
],
},
);
enqueueSnackbar('Transaction created', {
variant: 'success',
action: ,
});
didAddTransaction(transaction.publicKey);
onClose();
};
return (
{
// @ts-ignore
setThreshold(e.target.value);
}}
/>
changeThreshold()}>Change Threshold
);
}
function MultisigSetOwnersListItem({
multisig,
onClose,
didAddTransaction,
}: {
multisig: PublicKey;
onClose: Function;
didAddTransaction: (tx: PublicKey) => void;
}) {
const [open, setOpen] = useState(false);
return (
<>
setOpen(open => !open)}>
{open ? : }
>
);
}
function SetOwnersListItemDetails({
multisig,
onClose,
didAddTransaction,
}: {
multisig: PublicKey;
onClose: Function;
didAddTransaction: (tx: PublicKey) => void;
}) {
const { multisigClient } = useWallet();
// @ts-ignore
const zeroAddr = new PublicKey().toString();
const [participants, setParticipants] = useState([
multisigClient.provider.wallet.publicKey.toString(),
zeroAddr,
]);
const { enqueueSnackbar } = useSnackbar();
const setOwners = async () => {
enqueueSnackbar('Creating setOwners transaction', {
variant: 'info',
});
const owners = participants.map(p => new PublicKey(p));
const data = setOwnersData(multisigClient, owners);
const [multisigSigner] = await PublicKey.findProgramAddress(
[multisig.toBuffer()],
multisigClient.programId,
);
const accounts = [
{
pubkey: multisig,
isWritable: true,
isSigner: false,
},
{
pubkey: multisigSigner,
isWritable: false,
isSigner: true,
},
];
const transaction = new Account();
const txSize = 5000; // TODO: tighter bound.
const tx = await multisigClient.rpc.createTransaction(
multisigClient.programId,
accounts,
data,
{
accounts: {
multisig,
transaction: transaction.publicKey,
proposer: multisigClient.provider.wallet.publicKey,
rent: SYSVAR_RENT_PUBKEY,
},
signers: [transaction],
instructions: [
await multisigClient.account.transaction.createInstruction(
transaction,
// @ts-ignore
txSize,
),
],
},
);
enqueueSnackbar('Transaction created', {
variant: 'success',
action: ,
});
didAddTransaction(transaction.publicKey);
onClose();
};
return (
{participants.map((p, idx) => (
{
const p = [...participants];
p[idx] = e.target.value;
setParticipants(p);
}}
/>
))}
{
const p = [...participants];
// @ts-ignore
p.push(new PublicKey().toString());
setParticipants(p);
}}
>
setOwners()}>Set Owners
);
}
function IdlUpgradeListItem({
multisig,
onClose,
didAddTransaction,
}: {
multisig: PublicKey;
onClose: Function;
didAddTransaction: (tx: PublicKey) => void;
}) {
const [open, setOpen] = useState(false);
return (
<>
setOpen(open => !open)}>
{open ? : }
>
);
}
function UpgradeIdlListItemDetails({
multisig,
onClose,
didAddTransaction,
}: {
multisig: PublicKey;
onClose: Function;
didAddTransaction: (tx: PublicKey) => void;
}) {
const [programId, setProgramId] = useState(null);
const [buffer, setBuffer] = useState(null);
const { multisigClient } = useWallet();
const { enqueueSnackbar } = useSnackbar();
const createTransactionAccount = async () => {
enqueueSnackbar('Creating transaction', {
variant: 'info',
});
const programAddr = new PublicKey(programId as string);
const bufferAddr = new PublicKey(buffer as string);
const idlAddr = await anchor.utils.idlAddress(programAddr);
const [multisigSigner] = await PublicKey.findProgramAddress(
[multisig.toBuffer()],
multisigClient.programId,
);
const data = idl.encodeInstruction({ setBuffer: {} });
const accs = [
{
pubkey: bufferAddr,
isWritable: true,
isSigner: false,
},
{ pubkey: idlAddr, isWritable: true, isSigner: false },
{ pubkey: multisigSigner, isWritable: true, isSigner: false },
];
const txSize = 1000; // TODO: tighter bound.
const transaction = new Account();
const tx = await multisigClient.rpc.createTransaction(
programAddr,
accs,
data,
{
accounts: {
multisig,
transaction: transaction.publicKey,
proposer: multisigClient.provider.wallet.publicKey,
rent: SYSVAR_RENT_PUBKEY,
},
signers: [transaction],
instructions: [
await multisigClient.account.transaction.createInstruction(
transaction,
// @ts-ignore
txSize,
),
],
},
);
enqueueSnackbar('Transaction created', {
variant: 'success',
action: ,
});
didAddTransaction(transaction.publicKey);
onClose();
};
return (
setProgramId(e.target.value as string)}
/>
setBuffer(e.target.value as string)}
/>
createTransactionAccount()}>
Create upgrade
);
}
function ProgramUpdateListItem({
multisig,
onClose,
didAddTransaction,
}: {
multisig: PublicKey;
onClose: Function;
didAddTransaction: (tx: PublicKey) => void;
}) {
const [open, setOpen] = useState(false);
return (
<>
setOpen(open => !open)}>
{open ? : }
>
);
}
const BPF_LOADER_UPGRADEABLE_PID = new PublicKey(
'BPFLoaderUpgradeab1e11111111111111111111111',
);
function UpgradeProgramListItemDetails({
multisig,
onClose,
didAddTransaction,
}: {
multisig: PublicKey;
onClose: Function;
didAddTransaction: (tx: PublicKey) => void;
}) {
const [programId, setProgramId] = useState(null);
const [buffer, setBuffer] = useState(null);
const { multisigClient } = useWallet();
const { enqueueSnackbar } = useSnackbar();
const createTransactionAccount = async () => {
enqueueSnackbar('Creating transaction', {
variant: 'info',
});
const programAddr = new PublicKey(programId as string);
const bufferAddr = new PublicKey(buffer as string);
// Hard code serialization.
const data = Buffer.from([3, 0, 0, 0]);
const programAccount = await (async () => {
const programAccount = await multisigClient.provider.connection.getAccountInfo(
programAddr,
);
if (programAccount === null) {
throw new Error('Invalid program ID');
}
return {
// Hard code deserialization.
programdataAddress: new PublicKey(programAccount.data.slice(4)),
};
})();
const spill = multisigClient.provider.wallet.publicKey;
const [multisigSigner] = await PublicKey.findProgramAddress(
[multisig.toBuffer()],
multisigClient.programId,
);
const accs = [
{
pubkey: programAccount.programdataAddress,
isWritable: true,
isSigner: false,
},
{ pubkey: programAddr, isWritable: true, isSigner: false },
{ pubkey: bufferAddr, isWritable: true, isSigner: false },
{ pubkey: spill, isWritable: true, isSigner: false },
{ pubkey: SYSVAR_RENT_PUBKEY, isWritable: false, isSigner: false },
{ pubkey: SYSVAR_CLOCK_PUBKEY, isWritable: false, isSigner: false },
{ pubkey: multisigSigner, isWritable: false, isSigner: false },
];
const txSize = 1000; // TODO: tighter bound.
const transaction = new Account();
const tx = await multisigClient.rpc.createTransaction(
BPF_LOADER_UPGRADEABLE_PID,
accs,
data,
{
accounts: {
multisig,
transaction: transaction.publicKey,
proposer: multisigClient.provider.wallet.publicKey,
rent: SYSVAR_RENT_PUBKEY,
},
signers: [transaction],
instructions: [
await multisigClient.account.transaction.createInstruction(
transaction,
// @ts-ignore
txSize,
),
],
},
);
enqueueSnackbar('Transaction created', {
variant: 'success',
action: ,
});
didAddTransaction(transaction.publicKey);
onClose();
};
return (
setProgramId(e.target.value as string)}
/>
setBuffer(e.target.value as string)}
/>
createTransactionAccount()}>
Create upgrade
);
}
// @ts-ignore
function icon(tx, multisigClient) {
if (tx.account.programId.equals(BPF_LOADER_UPGRADEABLE_PID)) {
return ;
}
if (tx.account.programId.equals(multisigClient.programId)) {
const setThresholdSighash = multisigClient.coder.sighash(
'global',
'change_threshold',
);
if (setThresholdSighash.equals(tx.account.data.slice(0, 8))) {
return ;
}
const setOwnersSighash = multisigClient.coder.sighash(
'global',
'set_owners',
);
if (setOwnersSighash.equals(tx.account.data.slice(0, 8))) {
return ;
}
}
if (idl.IDL_TAG.equals(tx.account.data.slice(0, 8))) {
return ;
}
return ;
}
// @ts-ignore
function changeThresholdData(multisigClient, threshold) {
return multisigClient.coder.instruction.encode('change_threshold', {
threshold: new BN(threshold),
});
}
// @ts-ignore
function setOwnersData(multisigClient, owners) {
return multisigClient.coder.instruction.encode('set_owners', {
owners,
});
}