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 (
setMultisigAddress(e.target.value as string)} onKeyPress={e => { if (e.key === 'Enter') { searchFn(); } }} />
{multisig && }
); } 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); }} >
); } 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 ? : }
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()} ))}
); } 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); }} />
); } 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); }} >
); } 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)} />
); } 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)} />
); } // @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, }); }