diff --git a/packages/common/src/utils/ids.ts b/packages/common/src/utils/ids.ts index 144ea08..cb033c0 100644 --- a/packages/common/src/utils/ids.ts +++ b/packages/common/src/utils/ids.ts @@ -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'), diff --git a/packages/common/src/utils/index.tsx b/packages/common/src/utils/index.tsx index 82aa080..ffb0edf 100644 --- a/packages/common/src/utils/index.tsx +++ b/packages/common/src/utils/index.tsx @@ -4,3 +4,4 @@ export * as Layout from './layout'; export * from './notifications'; export * from './utils'; export * from './strings'; +export * as shortvec from './shortvec'; diff --git a/packages/common/src/utils/shortvec.ts b/packages/common/src/utils/shortvec.ts new file mode 100644 index 0000000..14b539d --- /dev/null +++ b/packages/common/src/utils/shortvec.ts @@ -0,0 +1,30 @@ +export function decodeLength(bytes: Array): 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, 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); + } + } +} diff --git a/packages/proposals/src/actions/addCustomSingleSignerTransaction.ts b/packages/proposals/src/actions/addCustomSingleSignerTransaction.ts index 53a7bdf..c0736ef 100644 --- a/packages/proposals/src/actions/addCustomSingleSignerTransaction.ts +++ b/packages/proposals/src/actions/addCustomSingleSignerTransaction.ts @@ -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 { + 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 { + 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 = {}; + 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; +} diff --git a/packages/proposals/src/actions/execute.ts b/packages/proposals/src/actions/execute.ts new file mode 100644 index 0000000..1900b4f --- /dev/null +++ b/packages/proposals/src/actions/execute.ts @@ -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, + transaction: ParsedAccount, +) => { + 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); +} diff --git a/packages/proposals/src/components/Proposal/InstructionCard.tsx b/packages/proposals/src/components/Proposal/InstructionCard.tsx index f7db7fe..75d83a7 100644 --- a/packages/proposals/src/components/Proposal/InstructionCard.tsx +++ b/packages/proposals/src/components/Proposal/InstructionCard.tsx @@ -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; + proposal: ParsedAccount; position: number; }) { const [tabKey, setTabKey] = useState('info'); - + const [playing, setPlaying] = useState( + instruction.info.executed === 1 ? Playstate.Played : Playstate.Unplayed, + ); const contentList: Record = { info: ( + } tabList={[ { key: 'info', tab: 'Info' }, { key: 'data', tab: 'Data' }, @@ -45,3 +78,51 @@ export function InstructionCard({ ); } + +function PlayStatusButton({ + proposal, + playing, + setPlaying, + instruction, +}: { + proposal: ParsedAccount; + instruction: ParsedAccount; + playing: Playstate; + setPlaying: React.Dispatch>; +}) { + 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 ( + + + + ); + else if (playing === Playstate.Playing) + return ; + else if (playing === Playstate.Error) + return ( + + + + ); + else return ; +} diff --git a/packages/proposals/src/models/addCustomSingleSignerTransaction.ts b/packages/proposals/src/models/addCustomSingleSignerTransaction.ts index edca786..e3e774f 100644 --- a/packages/proposals/src/models/addCustomSingleSignerTransaction.ts +++ b/packages/proposals/src/models/addCustomSingleSignerTransaction.ts @@ -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, ); diff --git a/packages/proposals/src/models/execute.ts b/packages/proposals/src/models/execute.ts new file mode 100644 index 0000000..e8663f3 --- /dev/null +++ b/packages/proposals/src/models/execute.ts @@ -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, + }); +}; diff --git a/packages/proposals/src/models/ping.ts b/packages/proposals/src/models/ping.ts new file mode 100644 index 0000000..10c6dac --- /dev/null +++ b/packages/proposals/src/models/ping.ts @@ -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, + }); +}; diff --git a/packages/proposals/src/models/timelock.ts b/packages/proposals/src/models/timelock.ts index cbd9bff..0cf9600 100644 --- a/packages/proposals/src/models/timelock.ts +++ b/packages/proposals/src/models/timelock.ts @@ -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 { diff --git a/packages/proposals/src/views/proposal/index.tsx b/packages/proposals/src/views/proposal/index.tsx index 4be4f06..6388a2e 100644 --- a/packages/proposals/src/views/proposal/index.tsx +++ b/packages/proposals/src/views/proposal/index.tsx @@ -225,6 +225,7 @@ function InnerProposalView({ {instructionsForProposal.map((instruction, position) => (