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 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 "./WalletProvider"; import { ViewTransactionOnExplorerButton } from "./Notification"; import * as idl from "../utils/idl"; import { networks } from "../store/reducer"; export default function Multisig({ multisig }: { multisig?: PublicKey }) { return (
{multisig && }
); } function NewMultisigButton() { const [open, setOpen] = useState(false); return (
setOpen(true)} > setOpen(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.fetch(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)} /> )}
); } export 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("11111111111111111111111111111111").toString(); const [participants, setParticipants] = useState([zeroAddr]); const _onClose = () => { onClose(); setThreshold(2); setParticipants([zeroAddr, zeroAddr]); }; const [maxParticipantLength, setMaxParticipantLength] = useState(10); const disableCreate = maxParticipantLength < participants.length; const createMultisig = async () => { enqueueSnackbar("Creating multisig", { variant: "info", }); const multisig = new Account(); // Disc. + threshold + nonce. const baseSize = 8 + 8 + 1 + 4; // Add enough for 2 more participants, in case the user changes one's /// mind later. const fudge = 64; // Can only grow the participant set by 2x the initialized value. const ownerSize = maxParticipantLength * 32 + 8; const multisigSize = baseSize + ownerSize + fudge; 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.publicKey.toString()}`); }; return ( New Multisig setThreshold(parseInt(e.target.value) as number)} /> setMaxParticipantLength(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("11111111111111111111111111111111").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 {multisig?.equals(networks.mainnet.multisigUpgradeAuthority!) && ( This multisig is the upgrade authority for the multisig program itself. )} 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("11111111111111111111111111111111").toString(); const [participants, setParticipants] = useState([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("11111111111111111111111111111111").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 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, }); } // Deterministic IDL address as a function of the program id. async function idlAddress(programId: PublicKey): Promise { const base = (await PublicKey.findProgramAddress([], programId))[0]; return await PublicKey.createWithSeed(base, seed(), programId); } // Seed for generating the idlAddress. function seed(): string { return "anchor:idl"; } // The