[xc-admin] Batch instructions (#612)

* Checkpoint

* Working

* Remove console log

* Restore send all

* Fix tests
This commit is contained in:
guibescos 2023-02-20 15:58:31 +01:00 committed by GitHub
parent 8e11caa1ee
commit 58db641ddd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 238 additions and 33 deletions

View File

@ -0,0 +1,138 @@
import { AnchorProvider, Wallet } from "@project-serum/anchor";
import { pythOracleProgram } from "@pythnetwork/client";
import {
getPythClusterApiUrl,
getPythProgramKeyForCluster,
PythCluster,
} from "@pythnetwork/client/lib/cluster";
import {
Connection,
Keypair,
PACKET_DATA_SIZE,
PublicKey,
Transaction,
TransactionInstruction,
} from "@solana/web3.js";
import {
batchIntoTransactions,
getSizeOfCompressedU16,
getSizeOfTransaction,
MultisigInstructionProgram,
MultisigParser,
} from "..";
import { PythMultisigInstruction } from "../multisig_transaction/PythMultisigInstruction";
it("Unit test compressed u16 size", async () => {
expect(getSizeOfCompressedU16(127)).toBe(1);
expect(getSizeOfCompressedU16(128)).toBe(2);
expect(getSizeOfCompressedU16(16383)).toBe(2);
expect(getSizeOfCompressedU16(16384)).toBe(3);
});
it("Unit test for getSizeOfTransaction", async () => {
jest.setTimeout(60000);
const cluster: PythCluster = "devnet";
const pythProgram = pythOracleProgram(
getPythProgramKeyForCluster(cluster),
new AnchorProvider(
new Connection(getPythClusterApiUrl(cluster)),
new Wallet(new Keypair()),
AnchorProvider.defaultOptions()
)
);
const payer = new Keypair();
const productAccount = PublicKey.unique();
const ixsToSend: TransactionInstruction[] = [];
ixsToSend.push(
await pythProgram.methods
.addProduct({
asset_type: "Crypto",
base: "ETH",
description: "ETH/USD",
quote_currency: "USD",
symbol: "Crypto.ETH/USD",
generic_symbol: "ETHUSD",
})
.accounts({
fundingAccount: payer.publicKey,
productAccount,
tailMappingAccount: PublicKey.unique(),
})
.instruction()
);
ixsToSend.push(
await pythProgram.methods
.addPrice(-8, 1)
.accounts({
fundingAccount: payer.publicKey,
productAccount,
priceAccount: PublicKey.unique(),
})
.instruction()
);
const transaction = new Transaction();
for (let ix of ixsToSend) {
transaction.add(ix);
}
transaction.recentBlockhash = "GqdFtdM7zzWw33YyHtBNwPhyBsdYKcfm9gT47bWnbHvs"; // Mock blockhash from devnet
transaction.feePayer = payer.publicKey;
expect(transaction.serialize({ requireAllSignatures: false }).length).toBe(
getSizeOfTransaction(ixsToSend)
);
});
it("Unit test for getSizeOfTransaction", async () => {
jest.setTimeout(60000);
const cluster: PythCluster = "devnet";
const pythProgram = pythOracleProgram(
getPythProgramKeyForCluster(cluster),
new AnchorProvider(
new Connection(getPythClusterApiUrl(cluster)),
new Wallet(new Keypair()),
AnchorProvider.defaultOptions()
)
);
const ixsToSend: TransactionInstruction[] = [];
const payer = new Keypair();
for (let i = 0; i < 100; i++) {
ixsToSend.push(
await pythProgram.methods
.addPublisher(PublicKey.unique())
.accounts({
fundingAccount: payer.publicKey,
priceAccount: PublicKey.unique(),
})
.instruction()
);
}
const txToSend: Transaction[] = batchIntoTransactions(
ixsToSend,
payer.publicKey
);
expect(
txToSend.map((tx) => tx.instructions.length).reduce((a, b) => a + b)
).toBe(ixsToSend.length);
expect(
txToSend.every(
(tx) => getSizeOfTransaction(tx.instructions) <= PACKET_DATA_SIZE
)
).toBeTruthy();
for (let tx of txToSend) {
tx.recentBlockhash = "GqdFtdM7zzWw33YyHtBNwPhyBsdYKcfm9gT47bWnbHvs"; // Mock blockhash from devnet
tx.feePayer = payer.publicKey;
expect(tx.serialize({ requireAllSignatures: false }).length).toBe(
getSizeOfTransaction(tx.instructions)
);
}
});

View File

@ -6,6 +6,7 @@ import {
SYSVAR_RENT_PUBKEY,
SYSVAR_CLOCK_PUBKEY,
SystemProgram,
PACKET_DATA_SIZE,
} from "@solana/web3.js";
import { BN } from "bn.js";
import { AnchorProvider } from "@project-serum/anchor";
@ -41,8 +42,8 @@ export async function proposeInstructions(
wormholeAddress?: PublicKey
): Promise<PublicKey> {
const msAccount = await squad.getMultisig(vault);
let txToSend: Transaction[] = [];
const createProposal = new Transaction().add(
let ixToSend: TransactionInstruction[] = [];
const createProposal = ixToSend.push(
await squad.buildCreateTransaction(
msAccount.publicKey,
msAccount.authorityIndex,
@ -54,7 +55,6 @@ export async function proposeInstructions(
new BN(msAccount.transactionIndex + 1),
squad.multisigProgramId
)[0];
txToSend.push(createProposal);
if (remote) {
if (!wormholeAddress) {
@ -69,8 +69,7 @@ export async function proposeInstructions(
i + 1,
wormholeAddress
);
txToSend.push(
new Transaction().add(
ixToSend.push(
await squad.buildAddInstruction(
vault,
newProposalAddress,
@ -80,36 +79,28 @@ export async function proposeInstructions(
squadIx.authorityBump,
squadIx.authorityType
)
)
);
}
} else {
for (let i = 0; i < instructions.length; i++) {
txToSend.push(
new Transaction().add(
ixToSend.push(
await squad.buildAddInstruction(
vault,
newProposalAddress,
instructions[i],
i + 1
)
)
);
}
}
txToSend.push(
new Transaction().add(
ixToSend.push(
await squad.buildActivateTransaction(vault, newProposalAddress)
)
);
txToSend.push(
new Transaction().add(
await squad.buildApproveTransaction(vault, newProposalAddress)
)
);
ixToSend.push(await squad.buildApproveTransaction(vault, newProposalAddress));
const txToSend = batchIntoTransactions(ixToSend, squad.wallet.publicKey);
await new AnchorProvider(
squad.connection,
squad.wallet,
@ -122,6 +113,82 @@ export async function proposeInstructions(
return newProposalAddress;
}
/**
* Batch instructions into transactions
*/
export function batchIntoTransactions(
instructions: TransactionInstruction[],
feePayer: PublicKey
): Transaction[] {
let i = 0;
const txToSend: Transaction[] = [];
while (i < instructions.length) {
let j = i + 2;
while (
j < instructions.length &&
getSizeOfTransaction(instructions.slice(i, j)) <= PACKET_DATA_SIZE
) {
j += 1;
}
const tx = new Transaction();
tx.feePayer = feePayer;
for (let k = i; k < j - 1; k += 1) {
tx.add(instructions[k]);
}
i = j - 1;
txToSend.push(tx);
}
return txToSend;
}
/**
* Get the size of a transaction that would contain the provided array of instructions
*/
export function getSizeOfTransaction(
instructions: TransactionInstruction[]
): number {
const signers = new Set<string>();
const accounts = new Set<string>();
instructions.map((ix) => {
accounts.add(ix.programId.toBase58()),
ix.keys.map((key) => {
if (key.isSigner) {
signers.add(key.pubkey.toBase58());
}
accounts.add(key.pubkey.toBase58());
});
});
const instruction_sizes: number = instructions
.map(
(ix) =>
1 +
getSizeOfCompressedU16(ix.keys.length) +
ix.keys.length +
getSizeOfCompressedU16(ix.data.length) +
ix.data.length
)
.reduce((a, b) => a + b, 0);
return (
1 +
signers.size * 64 +
3 +
getSizeOfCompressedU16(accounts.size) +
32 * accounts.size +
32 +
getSizeOfCompressedU16(instructions.length) +
instruction_sizes
);
}
/**
* Get the size of n in bytes when serialized as a CompressedU16
*/
export function getSizeOfCompressedU16(n: number) {
return 1 + Number(n >= 128) + Number(n >= 16384);
}
/**
* Wrap `instruction` in a Wormhole message for remote execution
* @param squad Squads client

4
package-lock.json generated
View File

@ -85694,8 +85694,8 @@
"@coral-xyz/anchor": "^0.26.0",
"@headlessui/react": "^1.7.7",
"@pythnetwork/client": "^2.15.0",
"@solana/spl-token": "*",
"@radix-ui/react-tooltip": "*",
"@radix-ui/react-tooltip": "^1.0.3",
"@solana/spl-token": "^0.3.7",
"@solana/wallet-adapter-base": "^0.9.20",
"@solana/wallet-adapter-react": "^0.15.28",
"@solana/wallet-adapter-react-ui": "^0.9.27",