mirror of https://github.com/certusone/oyster.git
Working execute command, lots of cleanup to do, lots of test code lying around
This commit is contained in:
parent
243bb0a321
commit
9ce5b9644c
|
@ -90,9 +90,9 @@ export const PROGRAM_IDS = [
|
|||
name: 'devnet',
|
||||
timelock: () => ({
|
||||
programAccountId: new PublicKey(
|
||||
'BNRKDb6vrbfYE4hyALrVLa9V38U2YE9cHMc1RpazG2EG',
|
||||
'rgq8xnCzKtGcaWCqbb9nAiJkk7vgjohTbJVgPRQoxQc',
|
||||
),
|
||||
programId: new PublicKey('CcaR57vwHPJ2BJdErjQ42dchFFuvhnxG1jeTxWZwAjjs'),
|
||||
programId: new PublicKey('DwFgNNwigPgAiiexQXcXnKi4JU7UUtfk9vfcFrQ5sDTc'),
|
||||
}),
|
||||
wormhole: () => ({
|
||||
pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'),
|
||||
|
|
|
@ -4,3 +4,4 @@ export * as Layout from './layout';
|
|||
export * from './notifications';
|
||||
export * from './utils';
|
||||
export * from './strings';
|
||||
export * as shortvec from './shortvec';
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
export function decodeLength(bytes: Array<number>): number {
|
||||
let len = 0;
|
||||
let size = 0;
|
||||
for (;;) {
|
||||
let elem = bytes.shift();
|
||||
//@ts-ignore
|
||||
len |= (elem & 0x7f) << (size * 7);
|
||||
size += 1;
|
||||
//@ts-ignore
|
||||
if ((elem & 0x80) === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
export function encodeLength(bytes: Array<number>, len: number) {
|
||||
let rem_len = len;
|
||||
for (;;) {
|
||||
let elem = rem_len & 0x7f;
|
||||
rem_len >>= 7;
|
||||
if (rem_len == 0) {
|
||||
bytes.push(elem);
|
||||
break;
|
||||
} else {
|
||||
elem |= 0x80;
|
||||
bytes.push(elem);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,20 +1,27 @@
|
|||
import {
|
||||
Account,
|
||||
CompiledInstruction,
|
||||
Connection,
|
||||
Message,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
Transaction,
|
||||
TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
import { contexts, utils, models, ParsedAccount } from '@oyster/common';
|
||||
|
||||
import bs58 from 'bs58';
|
||||
import {
|
||||
CustomSingleSignerTimelockTransactionLayout,
|
||||
INSTRUCTION_LIMIT,
|
||||
TimelockSet,
|
||||
} from '../models/timelock';
|
||||
import { addCustomSingleSignerTransactionInstruction } from '../models/addCustomSingleSignerTransaction';
|
||||
import { pingInstruction } from '../models/ping';
|
||||
import * as BufferLayout from 'buffer-layout';
|
||||
import { signInstruction } from '../models/sign';
|
||||
|
||||
const { sendTransaction } = contexts.Connection;
|
||||
const { notify } = utils;
|
||||
const { notify, shortvec, toUTF8Array, fromUTF8Array } = utils;
|
||||
const { approve } = models;
|
||||
|
||||
export const addCustomSingleSignerTransaction = async (
|
||||
|
@ -34,6 +41,7 @@ export const addCustomSingleSignerTransaction = async (
|
|||
const rentExempt = await connection.getMinimumBalanceForRentExemption(
|
||||
CustomSingleSignerTimelockTransactionLayout.span,
|
||||
);
|
||||
|
||||
const txnKey = new Account();
|
||||
|
||||
const uninitializedTxnInstruction = SystemProgram.createAccount({
|
||||
|
@ -61,7 +69,11 @@ export const addCustomSingleSignerTransaction = async (
|
|||
1,
|
||||
);
|
||||
signers.push(transferAuthority);
|
||||
|
||||
instruction = await serializeInstruction2({
|
||||
connection,
|
||||
wallet,
|
||||
instr: pingInstruction(),
|
||||
});
|
||||
instructions.push(
|
||||
addCustomSingleSignerTransactionInstruction(
|
||||
txnKey.publicKey,
|
||||
|
@ -101,3 +113,135 @@ export const addCustomSingleSignerTransaction = async (
|
|||
throw new Error();
|
||||
}
|
||||
};
|
||||
|
||||
async function serializeInstruction2({
|
||||
connection,
|
||||
wallet,
|
||||
instr,
|
||||
}: {
|
||||
connection: Connection;
|
||||
wallet: any;
|
||||
instr: TransactionInstruction;
|
||||
}): Promise<string> {
|
||||
const PROGRAM_IDS = utils.programIds();
|
||||
let instructionTransaction = new Transaction();
|
||||
instructionTransaction.add(instr);
|
||||
instructionTransaction.recentBlockhash = (
|
||||
await connection.getRecentBlockhash('max')
|
||||
).blockhash;
|
||||
const [authority] = await PublicKey.findProgramAddress(
|
||||
[PROGRAM_IDS.timelock.programAccountId.toBuffer()],
|
||||
PROGRAM_IDS.timelock.programId,
|
||||
);
|
||||
instructionTransaction.setSigners(authority);
|
||||
const msg: Message = instructionTransaction.compileMessage();
|
||||
|
||||
console.log('message', msg);
|
||||
console.log('from', Message.from(msg.serialize()));
|
||||
console.log(
|
||||
msg.serialize(),
|
||||
toUTF8Array(
|
||||
atob(fromUTF8Array(toUTF8Array(msg.serialize().toString('base64')))),
|
||||
),
|
||||
);
|
||||
let binary_string = atob(msg.serialize().toString('base64'));
|
||||
let len = binary_string.length;
|
||||
let bytes = new Uint8Array(len);
|
||||
for (var i = 0; i < len; i++) {
|
||||
bytes[i] = binary_string.charCodeAt(i);
|
||||
}
|
||||
console.log('from again', Message.from(bytes));
|
||||
return msg.serialize().toString('base64');
|
||||
}
|
||||
|
||||
async function serializeInstruction({
|
||||
connection,
|
||||
wallet,
|
||||
instr,
|
||||
}: {
|
||||
connection: Connection;
|
||||
wallet: any;
|
||||
instr: TransactionInstruction;
|
||||
}): Promise<string> {
|
||||
let instructionTransaction = new Transaction();
|
||||
instructionTransaction.add(instr);
|
||||
instructionTransaction.recentBlockhash = (
|
||||
await connection.getRecentBlockhash('max')
|
||||
).blockhash;
|
||||
// We dont actually signed, we just set this to get past a throw condition in compileMessage
|
||||
instructionTransaction.setSigners(
|
||||
// fee payied by the wallet owner
|
||||
wallet.publicKey,
|
||||
);
|
||||
const msg: Message = instructionTransaction.compileMessage();
|
||||
|
||||
const numKeys = msg.accountKeys.length;
|
||||
|
||||
let keyCount: number[] = [];
|
||||
shortvec.encodeLength(keyCount, numKeys);
|
||||
|
||||
const instruction = msg.instructions[0];
|
||||
const { accounts, programIdIndex } = instruction;
|
||||
const data = bs58.decode(instruction.data);
|
||||
|
||||
let keyIndicesCount: number[] = [];
|
||||
shortvec.encodeLength(keyIndicesCount, accounts.length);
|
||||
|
||||
let dataCount: number[] = [];
|
||||
shortvec.encodeLength(dataCount, data.length);
|
||||
|
||||
const instructionMeta = {
|
||||
programIdIndex,
|
||||
keyIndicesCount: Buffer.from(keyIndicesCount),
|
||||
keyIndices: Buffer.from(accounts),
|
||||
dataLength: Buffer.from(dataCount),
|
||||
data,
|
||||
};
|
||||
|
||||
let instructionBuffer = Buffer.alloc(100);
|
||||
|
||||
const instructionLayout = BufferLayout.struct([
|
||||
BufferLayout.u8('programIdIndex'),
|
||||
|
||||
BufferLayout.blob(
|
||||
instructionMeta.keyIndicesCount.length,
|
||||
'keyIndicesCount',
|
||||
),
|
||||
BufferLayout.seq(
|
||||
BufferLayout.u8('keyIndex'),
|
||||
instructionMeta.keyIndices.length,
|
||||
'keyIndices',
|
||||
),
|
||||
BufferLayout.blob(instructionMeta.dataLength.length, 'dataLength'),
|
||||
BufferLayout.seq(
|
||||
BufferLayout.u8('userdatum'),
|
||||
instruction.data.length,
|
||||
'data',
|
||||
),
|
||||
]);
|
||||
instructionLayout.encode(instructionMeta, instructionBuffer);
|
||||
console.log(instruction);
|
||||
console.log(instructionBuffer);
|
||||
console.log(instructionBuffer.length);
|
||||
|
||||
console.log(instructionBuffer.toString('base64'));
|
||||
console.log(instructionBuffer.toString('base64').length);
|
||||
console.log(decodeBufferIntoInstruction(instructionBuffer));
|
||||
|
||||
return instructionBuffer.toString('base64');
|
||||
}
|
||||
|
||||
// For testing, eventually can be used agains tbase64 string (turn into bytes) to figure out accounts and
|
||||
// stuff, maybe display something to user. Decode.
|
||||
function decodeBufferIntoInstruction(instructionBuffer: Buffer) {
|
||||
let byteArray = [...instructionBuffer];
|
||||
let decodedInstruction: Partial<CompiledInstruction> = {};
|
||||
decodedInstruction.programIdIndex = byteArray.shift();
|
||||
const accountCount = shortvec.decodeLength(byteArray);
|
||||
decodedInstruction.accounts = byteArray.slice(0, accountCount);
|
||||
byteArray = byteArray.slice(accountCount);
|
||||
const dataLength = shortvec.decodeLength(byteArray);
|
||||
const data = byteArray.slice(0, dataLength);
|
||||
decodedInstruction.data = bs58.encode(Buffer.from(data));
|
||||
return decodedInstruction;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
import {
|
||||
Account,
|
||||
Connection,
|
||||
Message,
|
||||
PublicKey,
|
||||
TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
import { contexts, utils, ParsedAccount } from '@oyster/common';
|
||||
|
||||
import { TimelockSet, TimelockTransaction } from '../models/timelock';
|
||||
import { executeInstruction } from '../models/execute';
|
||||
import { LABELS } from '../constants';
|
||||
const { sendTransaction } = contexts.Connection;
|
||||
const { notify } = utils;
|
||||
|
||||
export const execute = async (
|
||||
connection: Connection,
|
||||
wallet: any,
|
||||
proposal: ParsedAccount<TimelockSet>,
|
||||
transaction: ParsedAccount<TimelockTransaction>,
|
||||
) => {
|
||||
const PROGRAM_IDS = utils.programIds();
|
||||
|
||||
let signers: Account[] = [];
|
||||
let instructions: TransactionInstruction[] = [];
|
||||
|
||||
const [authority] = await PublicKey.findProgramAddress(
|
||||
[PROGRAM_IDS.timelock.programAccountId.toBuffer()],
|
||||
PROGRAM_IDS.timelock.programId,
|
||||
);
|
||||
|
||||
const actualMessage = decodeBufferIntoMessage(transaction.info.instruction);
|
||||
console.log(actualMessage);
|
||||
instructions.push(
|
||||
executeInstruction(
|
||||
transaction.pubkey,
|
||||
proposal.pubkey,
|
||||
actualMessage.accountKeys[actualMessage.instructions[0].programIdIndex],
|
||||
authority,
|
||||
),
|
||||
);
|
||||
|
||||
notify({
|
||||
message: LABELS.ADDING_VOTES_TO_VOTER,
|
||||
description: LABELS.PLEASE_WAIT,
|
||||
type: 'warn',
|
||||
});
|
||||
|
||||
try {
|
||||
let tx = await sendTransaction(
|
||||
connection,
|
||||
wallet,
|
||||
instructions,
|
||||
signers,
|
||||
true,
|
||||
);
|
||||
|
||||
notify({
|
||||
message: LABELS.VOTES_ADDED,
|
||||
type: 'success',
|
||||
description: LABELS.TRANSACTION + ` ${tx}`,
|
||||
});
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
throw new Error();
|
||||
}
|
||||
};
|
||||
|
||||
function decodeBufferIntoMessage(instruction: string): Message {
|
||||
// stored as a base64, we need to convert back from base64(via atob), then convert that decoded
|
||||
// to a utf8 array, then decode that buffer into instruction
|
||||
|
||||
let binaryString = atob(instruction);
|
||||
let len = binaryString.length;
|
||||
let byteArray = new Uint8Array(len);
|
||||
for (var i = 0; i < len; i++) {
|
||||
byteArray[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return Message.from(byteArray);
|
||||
}
|
|
@ -1,20 +1,45 @@
|
|||
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
|
||||
import { ParsedAccount } from '@oyster/common';
|
||||
import { Card } from 'antd';
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
LoadingOutlined,
|
||||
PlayCircleOutlined,
|
||||
RedoOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { ParsedAccount, contexts } from '@oyster/common';
|
||||
import { Card, Spin } from 'antd';
|
||||
import Meta from 'antd/lib/card/Meta';
|
||||
import React, { useState } from 'react';
|
||||
import { TimelockTransaction } from '../../models/timelock';
|
||||
import { execute } from '../../actions/execute';
|
||||
import {
|
||||
TimelockSet,
|
||||
TimelockStateStatus,
|
||||
TimelockTransaction,
|
||||
} from '../../models/timelock';
|
||||
import './style.less';
|
||||
|
||||
const { useWallet } = contexts.Wallet;
|
||||
const { useConnection } = contexts.Connection;
|
||||
|
||||
enum Playstate {
|
||||
Played,
|
||||
Unplayed,
|
||||
Playing,
|
||||
Error,
|
||||
}
|
||||
export function InstructionCard({
|
||||
instruction,
|
||||
proposal,
|
||||
position,
|
||||
}: {
|
||||
instruction: ParsedAccount<TimelockTransaction>;
|
||||
proposal: ParsedAccount<TimelockSet>;
|
||||
position: number;
|
||||
}) {
|
||||
const [tabKey, setTabKey] = useState('info');
|
||||
|
||||
const [playing, setPlaying] = useState(
|
||||
instruction.info.executed === 1 ? Playstate.Played : Playstate.Unplayed,
|
||||
);
|
||||
const contentList: Record<string, JSX.Element> = {
|
||||
info: (
|
||||
<Meta
|
||||
|
@ -32,6 +57,14 @@ export function InstructionCard({
|
|||
|
||||
return (
|
||||
<Card
|
||||
extra={
|
||||
<PlayStatusButton
|
||||
playing={playing}
|
||||
setPlaying={setPlaying}
|
||||
proposal={proposal}
|
||||
instruction={instruction}
|
||||
/>
|
||||
}
|
||||
tabList={[
|
||||
{ key: 'info', tab: 'Info' },
|
||||
{ key: 'data', tab: 'Data' },
|
||||
|
@ -45,3 +78,51 @@ export function InstructionCard({
|
|||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function PlayStatusButton({
|
||||
proposal,
|
||||
playing,
|
||||
setPlaying,
|
||||
instruction,
|
||||
}: {
|
||||
proposal: ParsedAccount<TimelockSet>;
|
||||
instruction: ParsedAccount<TimelockTransaction>;
|
||||
playing: Playstate;
|
||||
setPlaying: React.Dispatch<React.SetStateAction<Playstate>>;
|
||||
}) {
|
||||
const wallet = useWallet();
|
||||
const connection = useConnection();
|
||||
const [currSlot, setCurrSlot] = useState(0);
|
||||
connection.getSlot().then(setCurrSlot);
|
||||
|
||||
const run = async () => {
|
||||
setPlaying(Playstate.Playing);
|
||||
try {
|
||||
await execute(connection, wallet.wallet, proposal, instruction);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setPlaying(Playstate.Error);
|
||||
return;
|
||||
}
|
||||
setPlaying(Playstate.Played);
|
||||
};
|
||||
|
||||
if (proposal.info.state.status != TimelockStateStatus.Executing) return null;
|
||||
if (currSlot < instruction.info.slot.toNumber()) return null;
|
||||
|
||||
if (playing === Playstate.Unplayed)
|
||||
return (
|
||||
<a onClick={run}>
|
||||
<PlayCircleOutlined style={{ color: 'green' }} key="play" />
|
||||
</a>
|
||||
);
|
||||
else if (playing === Playstate.Playing)
|
||||
return <LoadingOutlined style={{ color: 'orange' }} key="loading" />;
|
||||
else if (playing === Playstate.Error)
|
||||
return (
|
||||
<a onClick={run}>
|
||||
<RedoOutlined style={{ color: 'orange' }} key="play" />
|
||||
</a>
|
||||
);
|
||||
else return <CheckCircleOutlined style={{ color: 'green' }} key="played" />;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
|
||||
import { Message, PublicKey, TransactionInstruction } from '@solana/web3.js';
|
||||
import { utils } from '@oyster/common';
|
||||
import * as Layout from '../utils/layout';
|
||||
|
||||
|
@ -10,6 +10,7 @@ import {
|
|||
} from './timelock';
|
||||
import BN from 'bn.js';
|
||||
import { toUTF8Array } from '@oyster/common/dist/lib/utils';
|
||||
import { pingInstruction } from './ping';
|
||||
|
||||
/// [Requires Signatory token]
|
||||
/// Adds a Transaction to the Timelock Set. Max of 10 of any Transaction type. More than 10 will throw error.
|
||||
|
@ -35,6 +36,7 @@ export const addCustomSingleSignerTransactionInstruction = (
|
|||
position: number,
|
||||
): TransactionInstruction => {
|
||||
const PROGRAM_IDS = utils.programIds();
|
||||
// need to get a pda, move blockhash out of here...
|
||||
|
||||
const instructionAsBytes = toUTF8Array(instruction);
|
||||
if (instructionAsBytes.length > INSTRUCTION_LIMIT) {
|
||||
|
@ -54,12 +56,15 @@ export const addCustomSingleSignerTransactionInstruction = (
|
|||
Layout.uint64('slot'),
|
||||
BufferLayout.seq(BufferLayout.u8(), INSTRUCTION_LIMIT, 'instructions'),
|
||||
BufferLayout.u8('position'),
|
||||
BufferLayout.u8('instructionEndIndex'),
|
||||
]);
|
||||
|
||||
const data = Buffer.alloc(dataLayout.span);
|
||||
const instructionEndIndex = instructionAsBytes.length - 1;
|
||||
for (let i = instructionAsBytes.length; i <= INSTRUCTION_LIMIT - 1; i++) {
|
||||
instructionAsBytes.push(0);
|
||||
}
|
||||
console.log('Instruction end index', instructionEndIndex);
|
||||
|
||||
dataLayout.encode(
|
||||
{
|
||||
|
@ -67,6 +72,7 @@ export const addCustomSingleSignerTransactionInstruction = (
|
|||
slot: new BN(slot),
|
||||
instructions: instructionAsBytes,
|
||||
position: position,
|
||||
instructionEndIndex: instructionEndIndex,
|
||||
},
|
||||
data,
|
||||
);
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
|
||||
import { utils } from '@oyster/common';
|
||||
import * as BufferLayout from 'buffer-layout';
|
||||
import { TimelockInstruction } from './timelock';
|
||||
|
||||
/// Executes a command in the timelock set.
|
||||
///
|
||||
/// 0. `[writable]` Transaction account you wish to execute.
|
||||
/// 1. `[]` Timelock set account.
|
||||
/// 2. `[]` Program being invoked account
|
||||
/// 3. `[]` Timelock program authority
|
||||
/// 4. `[]` Timelock program account pub key.
|
||||
export const executeInstruction = (
|
||||
transactionAccount: PublicKey,
|
||||
timelockSetAccount: PublicKey,
|
||||
programBeingInvokedAccount: PublicKey,
|
||||
timelockAuthority: PublicKey,
|
||||
): TransactionInstruction => {
|
||||
const PROGRAM_IDS = utils.programIds();
|
||||
|
||||
const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]);
|
||||
|
||||
const data = Buffer.alloc(dataLayout.span);
|
||||
|
||||
dataLayout.encode(
|
||||
{
|
||||
instruction: TimelockInstruction.Execute,
|
||||
},
|
||||
data,
|
||||
);
|
||||
|
||||
const keys = [
|
||||
{ pubkey: transactionAccount, isSigner: false, isWritable: true },
|
||||
{ pubkey: timelockSetAccount, isSigner: false, isWritable: true },
|
||||
{ pubkey: programBeingInvokedAccount, isSigner: false, isWritable: true },
|
||||
{ pubkey: timelockAuthority, isSigner: false, isWritable: true },
|
||||
{
|
||||
pubkey: PROGRAM_IDS.timelock.programAccountId,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
];
|
||||
return new TransactionInstruction({
|
||||
keys,
|
||||
programId: PROGRAM_IDS.timelock.programId,
|
||||
data,
|
||||
});
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
import { TransactionInstruction } from '@solana/web3.js';
|
||||
import { utils } from '@oyster/common';
|
||||
import * as BufferLayout from 'buffer-layout';
|
||||
import { TimelockInstruction } from './timelock';
|
||||
|
||||
export const pingInstruction = (): TransactionInstruction => {
|
||||
const PROGRAM_IDS = utils.programIds();
|
||||
|
||||
const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]);
|
||||
|
||||
const data = Buffer.alloc(dataLayout.span);
|
||||
|
||||
dataLayout.encode(
|
||||
{
|
||||
instruction: TimelockInstruction.Ping,
|
||||
},
|
||||
data,
|
||||
);
|
||||
|
||||
const keys: never[] = [];
|
||||
return new TransactionInstruction({
|
||||
keys,
|
||||
programId: PROGRAM_IDS.timelock.programId,
|
||||
data,
|
||||
});
|
||||
};
|
|
@ -17,6 +17,8 @@ export enum TimelockInstruction {
|
|||
Sign = 8,
|
||||
Vote = 9,
|
||||
MintVotingTokens = 10,
|
||||
Ping = 11,
|
||||
Execute = 12,
|
||||
}
|
||||
|
||||
export interface TimelockConfig {
|
||||
|
@ -189,10 +191,12 @@ export const CustomSingleSignerTimelockTransactionParser = (
|
|||
info: {
|
||||
version: data.version,
|
||||
slot: data.slot,
|
||||
instruction: utils
|
||||
.fromUTF8Array(data.instruction)
|
||||
.replaceAll('\u0000', ''),
|
||||
instruction: utils.fromUTF8Array(
|
||||
data.instruction.slice(0, data.instructionEndIndex),
|
||||
),
|
||||
authorityKey: data.authorityKey,
|
||||
executed: data.executed,
|
||||
instructionEndIndex: data.instructionEndIndex,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -205,6 +209,8 @@ export const CustomSingleSignerTimelockTransactionLayout: typeof BufferLayout.St
|
|||
Layout.uint64('slot'),
|
||||
BufferLayout.seq(BufferLayout.u8(), INSTRUCTION_LIMIT, 'instruction'),
|
||||
Layout.publicKey('authorityKey'),
|
||||
BufferLayout.u8('executed'),
|
||||
BufferLayout.u8('instructionEndIndex'),
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -214,6 +220,10 @@ export interface TimelockTransaction {
|
|||
slot: BN;
|
||||
|
||||
instruction: string;
|
||||
|
||||
executed: number;
|
||||
|
||||
instructionEndIndex: number;
|
||||
}
|
||||
export interface CustomSingleSignerTimelockTransaction
|
||||
extends TimelockTransaction {
|
||||
|
|
|
@ -225,6 +225,7 @@ function InnerProposalView({
|
|||
{instructionsForProposal.map((instruction, position) => (
|
||||
<Col xs={24} sm={24} md={12} lg={8} key={position}>
|
||||
<InstructionCard
|
||||
proposal={proposal}
|
||||
position={position + 1}
|
||||
instruction={instruction}
|
||||
/>
|
||||
|
|
Loading…
Reference in New Issue