multisig-ui/src/components/Multisig.tsx

1377 lines
39 KiB
TypeScript

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 (
<div>
<Container fixed maxWidth="md">
<div
style={{
position: "fixed",
bottom: "75px",
right: "75px",
display: "flex",
flexDirection: "row-reverse",
}}
>
<NewMultisigButton />
</div>
</Container>
{multisig && <MultisigInstance multisig={multisig} />}
</div>
);
}
function NewMultisigButton() {
const [open, setOpen] = useState(false);
return (
<div style={{ display: "flex" }}>
<IconButton
style={{
border: "solid 1pt #ccc",
width: "60px",
height: "60px",
borderRadius: "30px",
}}
onClick={() => setOpen(true)}
>
<AddIcon />
</IconButton>
<NewMultisigDialog open={open} onClose={() => setOpen(false)} />
</div>
);
}
export function MultisigInstance({ multisig }: { multisig: PublicKey }) {
const { multisigClient } = useWallet();
const [multisigAccount, setMultisigAccount] = useState<any>(undefined);
const [transactions, setTransactions] = useState<any>(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 (
<Container fixed maxWidth="md" style={{ marginBottom: "16px" }}>
<div>
<Card style={{ marginTop: "24px" }}>
{multisigAccount === undefined ? (
<div style={{ padding: "16px" }}>
<CircularProgress
style={{
display: "block",
marginLeft: "auto",
marginRight: "auto",
}}
/>
</div>
) : multisigAccount === null ? (
<CardContent>
<Typography
color="textSecondary"
style={{
padding: "24px",
textAlign: "center",
}}
>
Multisig not found
</Typography>
</CardContent>
) : (
<></>
)}
</Card>
{multisigAccount && (
<Paper>
<AppBar
style={{ marginTop: "24px" }}
position="static"
color="default"
elevation={1}
>
<Toolbar>
<Typography variant="h6" style={{ flexGrow: 1 }} component="h2">
{multisig.toString()} | {multisigAccount.threshold.toString()}{" "}
of {multisigAccount.owners.length.toString()} Multisig
</Typography>
<Tooltip title="Signer" arrow>
<IconButton onClick={() => setShowSignerDialog(true)}>
<InfoIcon />
</IconButton>
</Tooltip>
<Tooltip title="Add" arrow>
<IconButton onClick={() => setShowAddTransactionDialog(true)}>
<AddIcon />
</IconButton>
</Tooltip>
</Toolbar>
</AppBar>
<List disablePadding>
{transactions === null ? (
<div style={{ padding: "16px" }}>
<CircularProgress
style={{
display: "block",
marginLeft: "auto",
marginRight: "auto",
}}
/>
</div>
) : transactions.length === 0 ? (
<ListItem>
<ListItemText primary="No transactions found" />
</ListItem>
) : (
transactions.map((tx: any) => (
<TxListItem
key={tx.publicKey.toString()}
multisig={multisig}
multisigAccount={multisigAccount}
tx={tx}
/>
))
)}
</List>
</Paper>
)}
</div>
<AddTransactionDialog
multisig={multisig}
open={showAddTransactionDialog}
onClose={() => setShowAddTransactionDialog(false)}
didAddTransaction={() => setForceRefresh(!forceRefresh)}
/>
{multisigAccount && (
<SignerDialog
multisig={multisig}
multisigAccount={multisigAccount}
open={showSignerDialog}
onClose={() => setShowSignerDialog(false)}
/>
)}
</Container>
);
}
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: <ViewTransactionOnExplorerButton signature={tx} />,
});
_onClose();
history.push(`/${multisig.publicKey.toString()}`);
};
return (
<Dialog fullWidth open={open} onClose={_onClose} maxWidth="md">
<DialogTitle>
<Typography variant="h4" component="h2">
New Multisig
</Typography>
</DialogTitle>
<DialogContent>
<TextField
fullWidth
label="Threshold"
value={threshold}
type="number"
onChange={(e) => setThreshold(parseInt(e.target.value) as number)}
/>
<TextField
fullWidth
label="Max Number of Participants (cannot grow the owner set past this)"
value={maxParticipantLength}
type="number"
onChange={(e) => setMaxParticipantLength(parseInt(e.target.value) as number)}
/>
{participants.map((p, idx) => (
<TextField
key={p}
fullWidth
label="Participant"
value={p}
onChange={(e) => {
const p = [...participants];
p[idx] = e.target.value;
setParticipants(p);
}}
/>
))}
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<IconButton
onClick={() => {
const p = [...participants];
// @ts-ignore
p.push(new PublicKey("11111111111111111111111111111111").toString());
setParticipants(p);
}}
>
<AddIcon />
</IconButton>
</div>
</DialogContent>
<DialogActions>
<Button onClick={_onClose}>Cancel</Button>
<Button
disabled={disableCreate}
variant="contained"
type="submit"
color="primary"
onClick={() =>
createMultisig().catch((err) => {
const str = err ? err.toString() : "";
enqueueSnackbar(`Error creating multisig: ${str}`, {
variant: "error",
});
})
}
>
Create
</Button>
</DialogActions>
</Dialog>
);
}
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: (
<code
style={{
wordBreak: "break-word",
width: "370px",
background: "black",
color: "#ffffff",
float: "right",
textAlign: "left",
}}
>
{encodeBase64(txAccount.data)}
</code>
),
},
{
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] ? <CheckIcon /> : <RemoveIcon />,
};
}
);
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 (
<>
<ListItem button onClick={() => setOpen(!open)}>
<ListItemIcon>{icon(tx, multisigClient)}</ListItemIcon>
{ixLabel(tx, multisigClient)}
{txAccount.didExecute && (
<CheckCircleIcon style={{ marginRight: "16px" }} />
)}
{open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<div style={{ background: "#ececec", padding: "10px" }}>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<Button
style={{ marginRight: "10px" }}
variant="contained"
color="primary"
onClick={() =>
approve().catch((err) => {
let errStr = "";
if (err) {
errStr = err.toString();
}
enqueueSnackbar(`Unable to approve transaction: ${errStr}`, {
variant: "error",
});
})
}
>
Approve
</Button>
<Button
variant="contained"
color="secondary"
onClick={() =>
execute().catch((err) => {
let errStr = "";
if (err) {
errStr = err.toString();
}
enqueueSnackbar(`Unable to execute transaction: ${errStr}`, {
variant: "error",
});
})
}
>
Execute
</Button>
</div>
<Card style={{ marginTop: "16px" }}>
<CardContent>
<Table>
<TableHead>
<TableRow>
<TableCell>Transaction Field</TableCell>
<TableCell align="right">Value</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((r) => (
<TableRow>
<TableCell>{r.field}</TableCell>
<TableCell align="right">{r.value}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card style={{ marginTop: "16px" }}>
<CardContent>
<Table>
<TableHead>
<TableRow>
<TableCell>Multisig Owner</TableCell>
<TableCell align="right">Did Sign</TableCell>
</TableRow>
</TableHead>
<TableBody>
{txAccount.ownerSetSeqno === multisigAccount.ownerSetSeqno &&
msAccountRows.map((r: any) => (
<TableRow>
<TableCell>{r.field}</TableCell>
<TableCell align="right">{r.value}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{txAccount.ownerSetSeqno !== multisigAccount.ownerSetSeqno && (
<div style={{ marginTop: "16px" }}>
<Typography
color="textSecondary"
style={{ textAlign: "center" }}
>
The owner set has changed since this transaction was created
</Typography>
</div>
)}
</CardContent>
</Card>
<Card style={{ marginTop: "16px" }}>
<CardContent>
<AccountsList accounts={txAccount.accounts} />
</CardContent>
</Card>
</div>
</Collapse>
</>
);
}
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 (
<ListItemText
primary="Program upgrade"
secondary={tx.publicKey.toString()}
/>
);
}
}
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 (
<ListItemText
primary="Set threshold"
secondary={tx.publicKey.toString()}
/>
);
}
const setOwnersSighash = multisigClient.coder.sighash(
"global",
"set_owners"
);
if (setOwnersSighash.equals(tx.account.data.slice(0, 8))) {
return (
<ListItemText
primary="Set owners"
secondary={tx.publicKey.toString()}
/>
);
}
}
if (idl.IDL_TAG.equals(tx.account.data.slice(0, 8))) {
return (
<ListItemText primary="Upgrade IDL" secondary={tx.publicKey.toString()} />
);
}
return <ListItemText primary={tx.publicKey.toString()} />;
}
function AccountsList({ accounts }: { accounts: any }) {
return (
<Table>
<TableHead>
<TableRow>
<TableCell>Account</TableCell>
<TableCell align="right">Writable</TableCell>
<TableCell align="right">Signer</TableCell>
</TableRow>
</TableHead>
<TableBody>
{accounts.map((r: any) => (
<TableRow>
<TableCell>{r.pubkey.toString()}</TableCell>
<TableCell align="right">{r.isWritable.toString()}</TableCell>
<TableCell align="right">{r.isSigner.toString()}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
function SignerDialog({
multisig,
multisigAccount,
open,
onClose,
}: {
multisig: PublicKey;
multisigAccount: any;
open: boolean;
onClose: () => void;
}) {
const { multisigClient } = useWallet();
const [signer, setSigner] = useState<null | string>(null);
useEffect(() => {
PublicKey.findProgramAddress(
[multisig.toBuffer()],
multisigClient.programId
).then((addrNonce) => setSigner(addrNonce[0].toString()));
}, [multisig, multisigClient.programId, setSigner]);
return (
<Dialog open={open} fullWidth onClose={onClose} maxWidth="md">
<DialogTitle>
<Typography variant="h4" component="h2">
Multisig Info
</Typography>
</DialogTitle>
<DialogContent style={{ paddingBottom: "16px" }}>
{multisig?.equals(networks.mainnet.multisigUpgradeAuthority!) && (
<DialogContentText>
This multisig is the upgrade authority for the multisig program
itself.
</DialogContentText>
)}
<DialogContentText>
<b>Program derived address</b>: {signer}. This is the address one
should use as the authority for data governed by the multisig.
</DialogContentText>
<Table>
<TableHead>
<TableRow>
<TableCell>Owners</TableCell>
</TableRow>
</TableHead>
<TableBody>
{multisigAccount.owners.map((r: any) => (
<TableRow>
<TableCell>{r.toString()}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Close</Button>
</DialogActions>
</Dialog>
);
}
function AddTransactionDialog({
multisig,
open,
onClose,
didAddTransaction,
}: {
multisig: PublicKey;
open: boolean;
onClose: () => void;
didAddTransaction: (tx: PublicKey) => void;
}) {
return (
<Dialog open={open} fullWidth onClose={onClose} maxWidth="md">
<DialogTitle>
<Typography variant="h4" component="h2">
New Transaction
</Typography>
</DialogTitle>
<DialogContent style={{ paddingBottom: "16px" }}>
<DialogContentText>
Create a new transaction to be signed by the multisig. This
transaction will not execute until enough owners have signed the
transaction.
</DialogContentText>
<List disablePadding>
<ProgramUpdateListItem
didAddTransaction={didAddTransaction}
multisig={multisig}
onClose={onClose}
/>
<IdlUpgradeListItem
didAddTransaction={didAddTransaction}
multisig={multisig}
onClose={onClose}
/>
<MultisigSetOwnersListItem
didAddTransaction={didAddTransaction}
multisig={multisig}
onClose={onClose}
/>
<ChangeThresholdListItem
didAddTransaction={didAddTransaction}
multisig={multisig}
onClose={onClose}
/>
</List>
</DialogContent>
</Dialog>
);
}
function ChangeThresholdListItem({
multisig,
onClose,
didAddTransaction,
}: {
multisig: PublicKey;
onClose: Function;
didAddTransaction: (tx: PublicKey) => void;
}) {
const [open, setOpen] = useState(false);
return (
<>
<ListItem button onClick={() => setOpen((open) => !open)}>
<ListItemIcon>
<GavelIcon />
</ListItemIcon>
<ListItemText primary={"Change threshold"} />
{open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<ChangeThresholdListItemDetails
didAddTransaction={didAddTransaction}
multisig={multisig}
onClose={onClose}
/>
</Collapse>
</>
);
}
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: <ViewTransactionOnExplorerButton signature={tx} />,
});
didAddTransaction(transaction.publicKey);
onClose();
};
return (
<div
style={{
background: "#f1f0f0",
paddingLeft: "24px",
paddingRight: "24px",
}}
>
<TextField
fullWidth
style={{ marginTop: "16px" }}
label="Threshold"
value={threshold}
type="number"
onChange={(e) => {
// @ts-ignore
setThreshold(e.target.value);
}}
/>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<Button onClick={() => changeThreshold()}>Change Threshold</Button>
</div>
</div>
);
}
function MultisigSetOwnersListItem({
multisig,
onClose,
didAddTransaction,
}: {
multisig: PublicKey;
onClose: Function;
didAddTransaction: (tx: PublicKey) => void;
}) {
const [open, setOpen] = useState(false);
return (
<>
<ListItem button onClick={() => setOpen((open) => !open)}>
<ListItemIcon>
<SupervisorAccountIcon />
</ListItemIcon>
<ListItemText primary={"Set owners"} />
{open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<SetOwnersListItemDetails
didAddTransaction={didAddTransaction}
multisig={multisig}
onClose={onClose}
/>
</Collapse>
</>
);
}
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: <ViewTransactionOnExplorerButton signature={tx} />,
});
didAddTransaction(transaction.publicKey);
onClose();
};
return (
<div
style={{
background: "#f1f0f0",
paddingLeft: "24px",
paddingRight: "24px",
}}
>
{participants.map((p, idx) => (
<TextField
fullWidth
style={{ marginTop: "16px" }}
label="Participant"
value={p}
onChange={(e) => {
const p = [...participants];
p[idx] = e.target.value;
setParticipants(p);
}}
/>
))}
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<IconButton
onClick={() => {
const p = [...participants];
// @ts-ignore
p.push(new PublicKey("11111111111111111111111111111111").toString());
setParticipants(p);
}}
>
<AddIcon />
</IconButton>
</div>
<div
style={{
display: "flex",
justifyContent: "flex-end",
marginTop: "16px",
paddingBottom: "16px",
}}
>
<Button onClick={() => setOwners()}>Set Owners</Button>
</div>
</div>
);
}
function IdlUpgradeListItem({
multisig,
onClose,
didAddTransaction,
}: {
multisig: PublicKey;
onClose: Function;
didAddTransaction: (tx: PublicKey) => void;
}) {
const [open, setOpen] = useState(false);
return (
<>
<ListItem button onClick={() => setOpen((open) => !open)}>
<ListItemIcon>
<DescriptionIcon />
</ListItemIcon>
<ListItemText primary={"Upgrade IDL"} />
{open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<UpgradeIdlListItemDetails
didAddTransaction={didAddTransaction}
multisig={multisig}
onClose={onClose}
/>
</Collapse>
</>
);
}
function UpgradeIdlListItemDetails({
multisig,
onClose,
didAddTransaction,
}: {
multisig: PublicKey;
onClose: Function;
didAddTransaction: (tx: PublicKey) => void;
}) {
const [programId, setProgramId] = useState<null | string>(null);
const [buffer, setBuffer] = useState<null | string>(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: <ViewTransactionOnExplorerButton signature={tx} />,
});
didAddTransaction(transaction.publicKey);
onClose();
};
return (
<div
style={{
background: "#f1f0f0",
paddingLeft: "24px",
paddingRight: "24px",
}}
>
<TextField
fullWidth
style={{ marginTop: "16px" }}
label="Program ID"
value={programId}
onChange={(e) => setProgramId(e.target.value as string)}
/>
<TextField
style={{ marginTop: "16px" }}
fullWidth
label="New IDL buffer"
value={buffer}
onChange={(e) => setBuffer(e.target.value as string)}
/>
<div
style={{
display: "flex",
justifyContent: "flex-end",
marginTop: "16px",
paddingBottom: "16px",
}}
>
<Button onClick={() => createTransactionAccount()}>
Create upgrade
</Button>
</div>
</div>
);
}
function ProgramUpdateListItem({
multisig,
onClose,
didAddTransaction,
}: {
multisig: PublicKey;
onClose: Function;
didAddTransaction: (tx: PublicKey) => void;
}) {
const [open, setOpen] = useState(false);
return (
<>
<ListItem button onClick={() => setOpen((open) => !open)}>
<ListItemIcon>
<BuildIcon />
</ListItemIcon>
<ListItemText primary={"Upgrade program"} />
{open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<UpgradeProgramListItemDetails
didAddTransaction={didAddTransaction}
multisig={multisig}
onClose={onClose}
/>
</Collapse>
</>
);
}
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 | string>(null);
const [buffer, setBuffer] = useState<null | string>(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: <ViewTransactionOnExplorerButton signature={tx} />,
});
didAddTransaction(transaction.publicKey);
onClose();
};
return (
<div
style={{
background: "#f1f0f0",
paddingLeft: "24px",
paddingRight: "24px",
}}
>
<TextField
fullWidth
style={{ marginTop: "16px" }}
label="Program ID"
value={programId}
onChange={(e) => setProgramId(e.target.value as string)}
/>
<TextField
style={{ marginTop: "16px" }}
fullWidth
label="New program buffer"
value={buffer}
onChange={(e) => setBuffer(e.target.value as string)}
/>
<div
style={{
display: "flex",
justifyContent: "flex-end",
marginTop: "16px",
paddingBottom: "16px",
}}
>
<Button onClick={() => createTransactionAccount()}>
Create upgrade
</Button>
</div>
</div>
);
}
// @ts-ignore
function icon(tx, multisigClient) {
if (tx.account.programId.equals(BPF_LOADER_UPGRADEABLE_PID)) {
return <BuildIcon />;
}
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 <GavelIcon />;
}
const setOwnersSighash = multisigClient.coder.sighash(
"global",
"set_owners"
);
if (setOwnersSighash.equals(tx.account.data.slice(0, 8))) {
return <SupervisorAccountIcon />;
}
}
if (idl.IDL_TAG.equals(tx.account.data.slice(0, 8))) {
return <DescriptionIcon />;
}
return <ReceiptIcon />;
}
// @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<PublicKey> {
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