At great expense, we have upgradeable programs working. Still eneds a lot of beautifciation.

This commit is contained in:
Dummy Tester 123 2021-03-10 16:17:55 -06:00
parent 9ce5b9644c
commit 04d2f640a6
12 changed files with 319 additions and 195 deletions

View File

@ -206,7 +206,10 @@ export function useSlippageConfig() {
return { slippage, setSlippage };
}
const getErrorForTransaction = async (connection: Connection, txid: string) => {
export const getErrorForTransaction = async (
connection: Connection,
txid: string,
) => {
// wait for all confirmation before geting transaction
await connection.confirmTransaction(txid, 'max');
@ -239,6 +242,7 @@ export const sendTransaction = async (
instructions: TransactionInstruction[],
signers: Account[],
awaitConfirmation = true,
commitment = 'singleGossip',
) => {
let transaction = new Transaction();
instructions.forEach(instruction => transaction.add(instruction));
@ -257,7 +261,7 @@ export const sendTransaction = async (
const rawTransaction = transaction.serialize();
let options = {
skipPreflight: true,
commitment: 'singleGossip',
commitment,
};
const txid = await connection.sendRawTransaction(rawTransaction, options);

View File

@ -15,6 +15,9 @@ export let LENDING_PROGRAM_ID = new PublicKey(
export let SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = new PublicKey(
'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL',
);
export let BPF_UPGRADE_LOADER_ID = new PublicKey(
'BPFLoaderUpgradeab1e11111111111111111111111',
);
let WORMHOLE_BRIDGE: {
pubkey: PublicKey;
@ -90,9 +93,9 @@ export const PROGRAM_IDS = [
name: 'devnet',
timelock: () => ({
programAccountId: new PublicKey(
'rgq8xnCzKtGcaWCqbb9nAiJkk7vgjohTbJVgPRQoxQc',
'FNsF5k1dGz8mrq7unFeeNx8LqFn9bhKg6n6N5DLgQmfb',
),
programId: new PublicKey('DwFgNNwigPgAiiexQXcXnKi4JU7UUtfk9vfcFrQ5sDTc'),
programId: new PublicKey('6FyMHpXABKVSt4DmqUYLMgWChJV8HFXJuF6CsgucbZ3G'),
}),
wormhole: () => ({
pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'),
@ -163,5 +166,6 @@ export const programIds = () => {
wormhole: WORMHOLE_BRIDGE,
timelock: TIMELOCK,
associatedToken: SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID,
bpf_upgrade_loader: BPF_UPGRADE_LOADER_ID,
};
};

View File

@ -247,3 +247,7 @@ export function convert(
return result;
}
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@ -1,24 +1,19 @@
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';
import { serializeInstruction } from '../utils/serialize';
const { sendTransaction } = contexts.Connection;
const { notify, shortvec, toUTF8Array, fromUTF8Array } = utils;
@ -69,11 +64,25 @@ export const addCustomSingleSignerTransaction = async (
1,
);
signers.push(transferAuthority);
instruction = await serializeInstruction2({
connection,
wallet,
instr: pingInstruction(),
});
/*instruction = (
await serializeInstruction({
connection,
instr: pingInstruction(),
})
).base64;
console.log(pingInstruction());
const asArr = (
await serializeInstruction({
connection,
instr: pingInstruction(),
})
).byteArray;
console.log(asArr);
console.log('Message', Message.from(asArr));*/
instructions.push(
addCustomSingleSignerTransactionInstruction(
txnKey.publicKey,
@ -113,135 +122,3 @@ 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;
}

View File

@ -30,18 +30,21 @@ export const execute = async (
);
const actualMessage = decodeBufferIntoMessage(transaction.info.instruction);
console.log(actualMessage);
console.log('Actual message', actualMessage);
const accountInfos = getAccountInfos(actualMessage);
instructions.push(
executeInstruction(
transaction.pubkey,
proposal.pubkey,
actualMessage.accountKeys[actualMessage.instructions[0].programIdIndex],
authority,
accountInfos,
),
);
notify({
message: LABELS.ADDING_VOTES_TO_VOTER,
message: LABELS.EXECUTING,
description: LABELS.PLEASE_WAIT,
type: 'warn',
});
@ -56,7 +59,7 @@ export const execute = async (
);
notify({
message: LABELS.VOTES_ADDED,
message: LABELS.EXECUTED,
type: 'success',
description: LABELS.TRANSACTION + ` ${tx}`,
});
@ -66,15 +69,68 @@ export const execute = async (
}
};
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);
function decodeBufferIntoMessage(instruction: number[]): Message {
return Message.from(instruction);
}
function getAccountInfos(
actualMessage: Message,
): { pubkey: PublicKey; isSigner: boolean; isWritable: boolean }[] {
console.log(actualMessage);
// From Solana docs:
/*
The addresses that require signatures appear at the beginning of the account address array,
with addresses requesting write access first and read-only accounts following.
The addresses that do not require signatures follow the addresses that do,
again with read-write accounts first and read-only accounts following.
*/
const accountInfosInOrder = actualMessage.instructions[0].accounts.map(
a => actualMessage.accountKeys[a],
);
const requireSigsOnlyNotWritable =
actualMessage.header.numReadonlySignedAccounts;
const requireNietherSigsNorWrite =
actualMessage.header.numReadonlyUnsignedAccounts;
const writableOnly =
accountInfosInOrder.length -
requireSigsOnlyNotWritable -
requireNietherSigsNorWrite;
const readOnly = requireSigsOnlyNotWritable + requireNietherSigsNorWrite;
let position = 0;
let finalArray: {
pubkey: PublicKey;
isSigner: boolean;
isWritable: boolean;
}[] = [];
for (let i = 0; i < writableOnly; i++) {
finalArray.push({
pubkey: accountInfosInOrder[position],
isWritable: true,
isSigner: false, // We force signer to false because you realistically as executor wont
// have any of these keys present unless it happens to be your own
// WE dont care about required signers or not
});
position++;
}
for (let i = 0; i < readOnly; i++) {
finalArray.push({
pubkey: accountInfosInOrder[position],
isWritable: false,
isSigner: false,
});
position++;
}
for (; position < accountInfosInOrder.length; position++) {
finalArray.push({
pubkey: accountInfosInOrder[position],
isWritable: false,
isSigner: false,
});
}
return finalArray;
}

View File

@ -4,13 +4,7 @@ import {
PublicKey,
TransactionInstruction,
} from '@solana/web3.js';
import {
contexts,
utils,
models,
ParsedAccount,
actions,
} from '@oyster/common';
import { contexts, utils, models, ParsedAccount } from '@oyster/common';
import { TimelockSet } from '../models/timelock';
import { removeSignerInstruction } from '../models/removeSigner';

View File

@ -1,10 +1,13 @@
import React from 'react';
import { Card, Spin } from 'antd';
import React, { useState } from 'react';
import { Card, Progress, Spin } from 'antd';
import { Form, Input } from 'antd';
import { INSTRUCTION_LIMIT, TimelockSet } from '../../models/timelock';
import { contexts, ParsedAccount, hooks, utils } from '@oyster/common';
import { addCustomSingleSignerTransaction } from '../../actions/addCustomSingleSignerTransaction';
import { SaveOutlined } from '@ant-design/icons';
import { Connection, PublicKey } from '@solana/web3.js';
import { initializeBuffer } from '../../actions/initializeBuffer';
import { loadBufferAccount } from '../../actions/loadBufferAccount';
const { useWallet } = contexts.Wallet;
const { useConnection } = contexts.Connection;
@ -16,6 +19,11 @@ const layout = {
wrapperCol: { span: 16 },
};
enum UploadType {
Base64 = 'Base64',
Upgrade = 'Upgrade',
}
export function NewInstructionCard({
proposal,
position,
@ -27,7 +35,16 @@ export function NewInstructionCard({
const wallet = useWallet();
const connection = useConnection();
const sigAccount = useAccountByMint(proposal.info.signatoryMint);
const onFinish = async (values: { slot: string; instruction: string }) => {
const [tabKey, setTabKey] = useState<UploadType>(UploadType.Base64);
const [inputRef, setInputRef] = useState<Input | null>(null);
const [savePerc, setSavePerc] = useState(0);
const [saving, setSaving] = useState(false);
const onFinish = async (values: {
slot: string;
instruction: string;
destination?: string;
}) => {
if (!values.slot.match(/^\d*$/)) {
notify({
message: 'Slot can only be numeric',
@ -35,6 +52,24 @@ export function NewInstructionCard({
});
return;
}
let instruction = values.instruction;
if (inputRef?.input?.files) {
// Crap, we need to fully upload first...
await handleUploadBpf({
inputRef,
connection,
wallet,
proposal,
sigAccountKey: sigAccount?.pubkey,
setSavePerc,
setSaving,
});
// return for now...
return;
}
if (sigAccount) {
await addCustomSingleSignerTransaction(
connection,
@ -42,17 +77,15 @@ export function NewInstructionCard({
proposal,
sigAccount.pubkey,
values.slot,
values.instruction,
instruction,
position,
);
form.resetFields();
}
};
return !sigAccount ? null : (
<Card
title="New Instruction"
actions={[<SaveOutlined key="save" onClick={form.submit} />]}
>
const content = {
[UploadType.Base64]: (
<Form {...layout} form={form} name="control-hooks" onFinish={onFinish}>
<Form.Item name="slot" label="Slot" rules={[{ required: true }]}>
<Input maxLength={64} />
@ -64,10 +97,102 @@ export function NewInstructionCard({
>
<Input
maxLength={INSTRUCTION_LIMIT}
placeholder={'Base58 encoded instruction'}
placeholder={
"Base64 encoded Solana Message object with single instruction (call message.serialize().toString('base64')) no more than 255 characters"
}
/>
</Form.Item>
</Form>
),
[UploadType.Upgrade]: (
<Form {...layout} form={form} name="control-hooks" onFinish={onFinish}>
<Form.Item name="slot" label="Slot" rules={[{ required: true }]}>
<Input maxLength={64} />
</Form.Item>
<Form.Item
name="destination"
label="Program Address"
rules={[{ required: true }]}
>
<Input
maxLength={INSTRUCTION_LIMIT}
placeholder={'Program Address to Update (Base 58)'}
/>
</Form.Item>
<Form.Item
name="instruction"
label="Instruction"
rules={[{ required: true }]}
>
<Input type="file" ref={ref => setInputRef(ref)} />
</Form.Item>
</Form>
),
};
return !sigAccount ? null : (
<Card
title="New Instruction"
tabList={[
{ key: UploadType.Base64, tab: 'Custom Instruction' },
/*{ key: UploadType.Upgrade, tab: 'Program Upgrade' },*/
]}
activeTabKey={tabKey}
onTabChange={key =>
setTabKey(key === 'Base64' ? UploadType.Base64 : UploadType.Upgrade)
}
actions={[<SaveOutlined key="save" onClick={form.submit} />]}
>
{saving && <Progress percent={savePerc} status="active" />}
{content[tabKey]}
</Card>
);
}
async function handleUploadBpf({
inputRef,
connection,
wallet,
proposal,
sigAccountKey,
setSavePerc,
setSaving,
}: {
inputRef: Input;
connection: Connection;
wallet: any;
proposal: ParsedAccount<TimelockSet>;
sigAccountKey: PublicKey | undefined;
setSavePerc: React.Dispatch<React.SetStateAction<number>>;
setSaving: React.Dispatch<React.SetStateAction<boolean>>;
}) {
if (sigAccountKey)
return new Promise(res => {
const reader = new FileReader();
reader.onload = async function () {
const bytes = new Uint8Array(reader.result as ArrayBuffer);
const len = bytes.byteLength;
setSaving(true);
try {
const tempFile = await initializeBuffer(
connection,
wallet.wallet,
len,
);
await loadBufferAccount(
connection,
wallet.wallet,
tempFile,
bytes,
setSavePerc,
);
} catch (e) {
console.error(e);
}
setSaving(false);
setSavePerc(0);
res(true);
};
reader.readAsArrayBuffer((inputRef?.input?.files || [])[0]);
});
}

View File

@ -55,4 +55,6 @@ export const LABELS = {
BURNING_VOTES: 'Burning your votes...',
VOTES_BURNED: 'Votes burned',
VOTE: 'Vote',
EXECUTING: 'Executing...',
EXECUTED: 'Executed.',
};

View File

@ -32,13 +32,19 @@ export const addCustomSingleSignerTransactionInstruction = (
transferAuthority: PublicKey,
authority: PublicKey,
slot: string,
instruction: string,
instruction: string, // base64 encoded
position: number,
): TransactionInstruction => {
const PROGRAM_IDS = utils.programIds();
// need to get a pda, move blockhash out of here...
const instructionAsBytes = toUTF8Array(instruction);
let binaryString = atob(instruction);
let len = binaryString.length;
let bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
const instructionAsBytes = [...bytes];
if (instructionAsBytes.length > INSTRUCTION_LIMIT) {
throw new Error(
'Instruction length in bytes is more than ' + INSTRUCTION_LIMIT,
@ -56,7 +62,7 @@ export const addCustomSingleSignerTransactionInstruction = (
Layout.uint64('slot'),
BufferLayout.seq(BufferLayout.u8(), INSTRUCTION_LIMIT, 'instructions'),
BufferLayout.u8('position'),
BufferLayout.u8('instructionEndIndex'),
BufferLayout.u16('instructionEndIndex'),
]);
const data = Buffer.alloc(dataLayout.span);
@ -64,7 +70,6 @@ export const addCustomSingleSignerTransactionInstruction = (
for (let i = instructionAsBytes.length; i <= INSTRUCTION_LIMIT - 1; i++) {
instructionAsBytes.push(0);
}
console.log('Instruction end index', instructionEndIndex);
dataLayout.encode(
{

View File

@ -1,4 +1,8 @@
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
import {
PublicKey,
SYSVAR_CLOCK_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js';
import { utils } from '@oyster/common';
import * as BufferLayout from 'buffer-layout';
import { TimelockInstruction } from './timelock';
@ -10,24 +14,35 @@ import { TimelockInstruction } from './timelock';
/// 2. `[]` Program being invoked account
/// 3. `[]` Timelock program authority
/// 4. `[]` Timelock program account pub key.
/// 5. `[]` Clock sysvar.
/// 6+ Any extra accounts that are part of the instruction, in order
export const executeInstruction = (
transactionAccount: PublicKey,
timelockSetAccount: PublicKey,
programBeingInvokedAccount: PublicKey,
timelockAuthority: PublicKey,
accountInfos: { pubkey: PublicKey; isWritable: boolean; isSigner: boolean }[],
): TransactionInstruction => {
const PROGRAM_IDS = utils.programIds();
const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]);
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
BufferLayout.u8('numberOfExtraAccounts'),
]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: TimelockInstruction.Execute,
numberOfExtraAccounts: accountInfos.length,
},
data,
);
console.log(
'Acohjnt',
accountInfos.map(a => console.log(a.pubkey.toBase58(), a.isWritable)),
);
const keys = [
{ pubkey: transactionAccount, isSigner: false, isWritable: true },
@ -39,6 +54,8 @@ export const executeInstruction = (
isSigner: false,
isWritable: false,
},
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
...accountInfos,
];
return new TransactionInstruction({
keys,

View File

@ -6,8 +6,9 @@ import { utils } from '@oyster/common';
export const DESC_SIZE = 200;
export const NAME_SIZE = 32;
export const INSTRUCTION_LIMIT = 255;
export const INSTRUCTION_LIMIT = 500;
export const TRANSACTION_SLOTS = 10;
export const TEMP_FILE_TXN_SIZE = 1000;
export enum TimelockInstruction {
InitTimelockSet = 1,
@ -19,6 +20,7 @@ export enum TimelockInstruction {
MintVotingTokens = 10,
Ping = 11,
Execute = 12,
UploadTempFile = 13,
}
export interface TimelockConfig {
@ -191,10 +193,8 @@ export const CustomSingleSignerTimelockTransactionParser = (
info: {
version: data.version,
slot: data.slot,
instruction: utils.fromUTF8Array(
data.instruction.slice(0, data.instructionEndIndex),
),
authorityKey: data.authorityKey,
instruction: data.instruction.slice(0, data.instructionEndIndex + 1),
executed: data.executed,
instructionEndIndex: data.instructionEndIndex,
},
@ -208,9 +208,8 @@ export const CustomSingleSignerTimelockTransactionLayout: typeof BufferLayout.St
BufferLayout.u8('version'),
Layout.uint64('slot'),
BufferLayout.seq(BufferLayout.u8(), INSTRUCTION_LIMIT, 'instruction'),
Layout.publicKey('authorityKey'),
BufferLayout.u8('executed'),
BufferLayout.u8('instructionEndIndex'),
BufferLayout.u16('instructionEndIndex'),
],
);
@ -219,13 +218,11 @@ export interface TimelockTransaction {
slot: BN;
instruction: string;
instruction: number[];
executed: number;
instructionEndIndex: number;
}
export interface CustomSingleSignerTimelockTransaction
extends TimelockTransaction {
authorityKey: PublicKey;
}
extends TimelockTransaction {}

View File

@ -0,0 +1,39 @@
import { utils } from '@oyster/common';
import {
Connection,
TransactionInstruction,
Transaction,
PublicKey,
Message,
} from '@solana/web3.js';
export async function serializeInstruction({
connection,
instr,
}: {
connection: Connection;
instr: TransactionInstruction;
}): Promise<{ base64: string; byteArray: Uint8Array }> {
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();
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);
}
return {
base64: msg.serialize().toString('base64'),
byteArray: bytes,
};
}