Working execute command, lots of cleanup to do, lots of test code lying around

This commit is contained in:
Dummy Tester 123 2021-03-05 16:22:54 -06:00
parent 243bb0a321
commit 9ce5b9644c
11 changed files with 441 additions and 14 deletions

View File

@ -90,9 +90,9 @@ export const PROGRAM_IDS = [
name: 'devnet', name: 'devnet',
timelock: () => ({ timelock: () => ({
programAccountId: new PublicKey( programAccountId: new PublicKey(
'BNRKDb6vrbfYE4hyALrVLa9V38U2YE9cHMc1RpazG2EG', 'rgq8xnCzKtGcaWCqbb9nAiJkk7vgjohTbJVgPRQoxQc',
), ),
programId: new PublicKey('CcaR57vwHPJ2BJdErjQ42dchFFuvhnxG1jeTxWZwAjjs'), programId: new PublicKey('DwFgNNwigPgAiiexQXcXnKi4JU7UUtfk9vfcFrQ5sDTc'),
}), }),
wormhole: () => ({ wormhole: () => ({
pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'), pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'),

View File

@ -4,3 +4,4 @@ export * as Layout from './layout';
export * from './notifications'; export * from './notifications';
export * from './utils'; export * from './utils';
export * from './strings'; export * from './strings';
export * as shortvec from './shortvec';

View File

@ -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);
}
}
}

View File

@ -1,20 +1,27 @@
import { import {
Account, Account,
CompiledInstruction,
Connection, Connection,
Message,
PublicKey, PublicKey,
SystemProgram, SystemProgram,
Transaction,
TransactionInstruction, TransactionInstruction,
} from '@solana/web3.js'; } from '@solana/web3.js';
import { contexts, utils, models, ParsedAccount } from '@oyster/common'; import { contexts, utils, models, ParsedAccount } from '@oyster/common';
import bs58 from 'bs58';
import { import {
CustomSingleSignerTimelockTransactionLayout, CustomSingleSignerTimelockTransactionLayout,
INSTRUCTION_LIMIT,
TimelockSet, TimelockSet,
} from '../models/timelock'; } from '../models/timelock';
import { addCustomSingleSignerTransactionInstruction } from '../models/addCustomSingleSignerTransaction'; 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 { sendTransaction } = contexts.Connection;
const { notify } = utils; const { notify, shortvec, toUTF8Array, fromUTF8Array } = utils;
const { approve } = models; const { approve } = models;
export const addCustomSingleSignerTransaction = async ( export const addCustomSingleSignerTransaction = async (
@ -34,6 +41,7 @@ export const addCustomSingleSignerTransaction = async (
const rentExempt = await connection.getMinimumBalanceForRentExemption( const rentExempt = await connection.getMinimumBalanceForRentExemption(
CustomSingleSignerTimelockTransactionLayout.span, CustomSingleSignerTimelockTransactionLayout.span,
); );
const txnKey = new Account(); const txnKey = new Account();
const uninitializedTxnInstruction = SystemProgram.createAccount({ const uninitializedTxnInstruction = SystemProgram.createAccount({
@ -61,7 +69,11 @@ export const addCustomSingleSignerTransaction = async (
1, 1,
); );
signers.push(transferAuthority); signers.push(transferAuthority);
instruction = await serializeInstruction2({
connection,
wallet,
instr: pingInstruction(),
});
instructions.push( instructions.push(
addCustomSingleSignerTransactionInstruction( addCustomSingleSignerTransactionInstruction(
txnKey.publicKey, txnKey.publicKey,
@ -101,3 +113,135 @@ export const addCustomSingleSignerTransaction = async (
throw new Error(); 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;
}

View File

@ -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);
}

View File

@ -1,20 +1,45 @@
import { DeleteOutlined, EditOutlined } from '@ant-design/icons'; import {
import { ParsedAccount } from '@oyster/common'; CheckCircleOutlined,
import { Card } from 'antd'; 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 Meta from 'antd/lib/card/Meta';
import React, { useState } from 'react'; 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'; import './style.less';
const { useWallet } = contexts.Wallet;
const { useConnection } = contexts.Connection;
enum Playstate {
Played,
Unplayed,
Playing,
Error,
}
export function InstructionCard({ export function InstructionCard({
instruction, instruction,
proposal,
position, position,
}: { }: {
instruction: ParsedAccount<TimelockTransaction>; instruction: ParsedAccount<TimelockTransaction>;
proposal: ParsedAccount<TimelockSet>;
position: number; position: number;
}) { }) {
const [tabKey, setTabKey] = useState('info'); const [tabKey, setTabKey] = useState('info');
const [playing, setPlaying] = useState(
instruction.info.executed === 1 ? Playstate.Played : Playstate.Unplayed,
);
const contentList: Record<string, JSX.Element> = { const contentList: Record<string, JSX.Element> = {
info: ( info: (
<Meta <Meta
@ -32,6 +57,14 @@ export function InstructionCard({
return ( return (
<Card <Card
extra={
<PlayStatusButton
playing={playing}
setPlaying={setPlaying}
proposal={proposal}
instruction={instruction}
/>
}
tabList={[ tabList={[
{ key: 'info', tab: 'Info' }, { key: 'info', tab: 'Info' },
{ key: 'data', tab: 'Data' }, { key: 'data', tab: 'Data' },
@ -45,3 +78,51 @@ export function InstructionCard({
</Card> </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" />;
}

View File

@ -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 { utils } from '@oyster/common';
import * as Layout from '../utils/layout'; import * as Layout from '../utils/layout';
@ -10,6 +10,7 @@ import {
} from './timelock'; } from './timelock';
import BN from 'bn.js'; import BN from 'bn.js';
import { toUTF8Array } from '@oyster/common/dist/lib/utils'; import { toUTF8Array } from '@oyster/common/dist/lib/utils';
import { pingInstruction } from './ping';
/// [Requires Signatory token] /// [Requires Signatory token]
/// Adds a Transaction to the Timelock Set. Max of 10 of any Transaction type. More than 10 will throw error. /// 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, position: number,
): TransactionInstruction => { ): TransactionInstruction => {
const PROGRAM_IDS = utils.programIds(); const PROGRAM_IDS = utils.programIds();
// need to get a pda, move blockhash out of here...
const instructionAsBytes = toUTF8Array(instruction); const instructionAsBytes = toUTF8Array(instruction);
if (instructionAsBytes.length > INSTRUCTION_LIMIT) { if (instructionAsBytes.length > INSTRUCTION_LIMIT) {
@ -54,12 +56,15 @@ export const addCustomSingleSignerTransactionInstruction = (
Layout.uint64('slot'), Layout.uint64('slot'),
BufferLayout.seq(BufferLayout.u8(), INSTRUCTION_LIMIT, 'instructions'), BufferLayout.seq(BufferLayout.u8(), INSTRUCTION_LIMIT, 'instructions'),
BufferLayout.u8('position'), BufferLayout.u8('position'),
BufferLayout.u8('instructionEndIndex'),
]); ]);
const data = Buffer.alloc(dataLayout.span); const data = Buffer.alloc(dataLayout.span);
const instructionEndIndex = instructionAsBytes.length - 1;
for (let i = instructionAsBytes.length; i <= INSTRUCTION_LIMIT - 1; i++) { for (let i = instructionAsBytes.length; i <= INSTRUCTION_LIMIT - 1; i++) {
instructionAsBytes.push(0); instructionAsBytes.push(0);
} }
console.log('Instruction end index', instructionEndIndex);
dataLayout.encode( dataLayout.encode(
{ {
@ -67,6 +72,7 @@ export const addCustomSingleSignerTransactionInstruction = (
slot: new BN(slot), slot: new BN(slot),
instructions: instructionAsBytes, instructions: instructionAsBytes,
position: position, position: position,
instructionEndIndex: instructionEndIndex,
}, },
data, data,
); );

View File

@ -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,
});
};

View File

@ -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,
});
};

View File

@ -17,6 +17,8 @@ export enum TimelockInstruction {
Sign = 8, Sign = 8,
Vote = 9, Vote = 9,
MintVotingTokens = 10, MintVotingTokens = 10,
Ping = 11,
Execute = 12,
} }
export interface TimelockConfig { export interface TimelockConfig {
@ -189,10 +191,12 @@ export const CustomSingleSignerTimelockTransactionParser = (
info: { info: {
version: data.version, version: data.version,
slot: data.slot, slot: data.slot,
instruction: utils instruction: utils.fromUTF8Array(
.fromUTF8Array(data.instruction) data.instruction.slice(0, data.instructionEndIndex),
.replaceAll('\u0000', ''), ),
authorityKey: data.authorityKey, authorityKey: data.authorityKey,
executed: data.executed,
instructionEndIndex: data.instructionEndIndex,
}, },
}; };
@ -205,6 +209,8 @@ export const CustomSingleSignerTimelockTransactionLayout: typeof BufferLayout.St
Layout.uint64('slot'), Layout.uint64('slot'),
BufferLayout.seq(BufferLayout.u8(), INSTRUCTION_LIMIT, 'instruction'), BufferLayout.seq(BufferLayout.u8(), INSTRUCTION_LIMIT, 'instruction'),
Layout.publicKey('authorityKey'), Layout.publicKey('authorityKey'),
BufferLayout.u8('executed'),
BufferLayout.u8('instructionEndIndex'),
], ],
); );
@ -214,6 +220,10 @@ export interface TimelockTransaction {
slot: BN; slot: BN;
instruction: string; instruction: string;
executed: number;
instructionEndIndex: number;
} }
export interface CustomSingleSignerTimelockTransaction export interface CustomSingleSignerTimelockTransaction
extends TimelockTransaction { extends TimelockTransaction {

View File

@ -225,6 +225,7 @@ function InnerProposalView({
{instructionsForProposal.map((instruction, position) => ( {instructionsForProposal.map((instruction, position) => (
<Col xs={24} sm={24} md={12} lg={8} key={position}> <Col xs={24} sm={24} md={12} lg={8} key={position}>
<InstructionCard <InstructionCard
proposal={proposal}
position={position + 1} position={position + 1}
instruction={instruction} instruction={instruction}
/> />