Skip to content

Commit 58db641

Browse files
authored
[xc-admin] Batch instructions (#612)
* Checkpoint * Working * Remove console log * Restore send all * Fix tests
1 parent 8e11caa commit 58db641

File tree

3 files changed

+238
-33
lines changed

3 files changed

+238
-33
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { AnchorProvider, Wallet } from "@project-serum/anchor";
2+
import { pythOracleProgram } from "@pythnetwork/client";
3+
import {
4+
getPythClusterApiUrl,
5+
getPythProgramKeyForCluster,
6+
PythCluster,
7+
} from "@pythnetwork/client/lib/cluster";
8+
import {
9+
Connection,
10+
Keypair,
11+
PACKET_DATA_SIZE,
12+
PublicKey,
13+
Transaction,
14+
TransactionInstruction,
15+
} from "@solana/web3.js";
16+
import {
17+
batchIntoTransactions,
18+
getSizeOfCompressedU16,
19+
getSizeOfTransaction,
20+
MultisigInstructionProgram,
21+
MultisigParser,
22+
} from "..";
23+
import { PythMultisigInstruction } from "../multisig_transaction/PythMultisigInstruction";
24+
25+
it("Unit test compressed u16 size", async () => {
26+
expect(getSizeOfCompressedU16(127)).toBe(1);
27+
expect(getSizeOfCompressedU16(128)).toBe(2);
28+
expect(getSizeOfCompressedU16(16383)).toBe(2);
29+
expect(getSizeOfCompressedU16(16384)).toBe(3);
30+
});
31+
32+
it("Unit test for getSizeOfTransaction", async () => {
33+
jest.setTimeout(60000);
34+
35+
const cluster: PythCluster = "devnet";
36+
const pythProgram = pythOracleProgram(
37+
getPythProgramKeyForCluster(cluster),
38+
new AnchorProvider(
39+
new Connection(getPythClusterApiUrl(cluster)),
40+
new Wallet(new Keypair()),
41+
AnchorProvider.defaultOptions()
42+
)
43+
);
44+
45+
const payer = new Keypair();
46+
const productAccount = PublicKey.unique();
47+
48+
const ixsToSend: TransactionInstruction[] = [];
49+
50+
ixsToSend.push(
51+
await pythProgram.methods
52+
.addProduct({
53+
asset_type: "Crypto",
54+
base: "ETH",
55+
description: "ETH/USD",
56+
quote_currency: "USD",
57+
symbol: "Crypto.ETH/USD",
58+
generic_symbol: "ETHUSD",
59+
})
60+
.accounts({
61+
fundingAccount: payer.publicKey,
62+
productAccount,
63+
tailMappingAccount: PublicKey.unique(),
64+
})
65+
.instruction()
66+
);
67+
68+
ixsToSend.push(
69+
await pythProgram.methods
70+
.addPrice(-8, 1)
71+
.accounts({
72+
fundingAccount: payer.publicKey,
73+
productAccount,
74+
priceAccount: PublicKey.unique(),
75+
})
76+
.instruction()
77+
);
78+
79+
const transaction = new Transaction();
80+
for (let ix of ixsToSend) {
81+
transaction.add(ix);
82+
}
83+
84+
transaction.recentBlockhash = "GqdFtdM7zzWw33YyHtBNwPhyBsdYKcfm9gT47bWnbHvs"; // Mock blockhash from devnet
85+
transaction.feePayer = payer.publicKey;
86+
expect(transaction.serialize({ requireAllSignatures: false }).length).toBe(
87+
getSizeOfTransaction(ixsToSend)
88+
);
89+
});
90+
91+
it("Unit test for getSizeOfTransaction", async () => {
92+
jest.setTimeout(60000);
93+
94+
const cluster: PythCluster = "devnet";
95+
const pythProgram = pythOracleProgram(
96+
getPythProgramKeyForCluster(cluster),
97+
new AnchorProvider(
98+
new Connection(getPythClusterApiUrl(cluster)),
99+
new Wallet(new Keypair()),
100+
AnchorProvider.defaultOptions()
101+
)
102+
);
103+
const ixsToSend: TransactionInstruction[] = [];
104+
const payer = new Keypair();
105+
106+
for (let i = 0; i < 100; i++) {
107+
ixsToSend.push(
108+
await pythProgram.methods
109+
.addPublisher(PublicKey.unique())
110+
.accounts({
111+
fundingAccount: payer.publicKey,
112+
priceAccount: PublicKey.unique(),
113+
})
114+
.instruction()
115+
);
116+
}
117+
118+
const txToSend: Transaction[] = batchIntoTransactions(
119+
ixsToSend,
120+
payer.publicKey
121+
);
122+
expect(
123+
txToSend.map((tx) => tx.instructions.length).reduce((a, b) => a + b)
124+
).toBe(ixsToSend.length);
125+
expect(
126+
txToSend.every(
127+
(tx) => getSizeOfTransaction(tx.instructions) <= PACKET_DATA_SIZE
128+
)
129+
).toBeTruthy();
130+
131+
for (let tx of txToSend) {
132+
tx.recentBlockhash = "GqdFtdM7zzWw33YyHtBNwPhyBsdYKcfm9gT47bWnbHvs"; // Mock blockhash from devnet
133+
tx.feePayer = payer.publicKey;
134+
expect(tx.serialize({ requireAllSignatures: false }).length).toBe(
135+
getSizeOfTransaction(tx.instructions)
136+
);
137+
}
138+
});

governance/xc_admin/packages/xc_admin_common/src/propose.ts

Lines changed: 98 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
SYSVAR_RENT_PUBKEY,
77
SYSVAR_CLOCK_PUBKEY,
88
SystemProgram,
9+
PACKET_DATA_SIZE,
910
} from "@solana/web3.js";
1011
import { BN } from "bn.js";
1112
import { AnchorProvider } from "@project-serum/anchor";
@@ -41,8 +42,8 @@ export async function proposeInstructions(
4142
wormholeAddress?: PublicKey
4243
): Promise<PublicKey> {
4344
const msAccount = await squad.getMultisig(vault);
44-
let txToSend: Transaction[] = [];
45-
const createProposal = new Transaction().add(
45+
let ixToSend: TransactionInstruction[] = [];
46+
const createProposal = ixToSend.push(
4647
await squad.buildCreateTransaction(
4748
msAccount.publicKey,
4849
msAccount.authorityIndex,
@@ -54,7 +55,6 @@ export async function proposeInstructions(
5455
new BN(msAccount.transactionIndex + 1),
5556
squad.multisigProgramId
5657
)[0];
57-
txToSend.push(createProposal);
5858

5959
if (remote) {
6060
if (!wormholeAddress) {
@@ -69,47 +69,38 @@ export async function proposeInstructions(
6969
i + 1,
7070
wormholeAddress
7171
);
72-
txToSend.push(
73-
new Transaction().add(
74-
await squad.buildAddInstruction(
75-
vault,
76-
newProposalAddress,
77-
squadIx.instruction,
78-
i + 1,
79-
squadIx.authorityIndex,
80-
squadIx.authorityBump,
81-
squadIx.authorityType
82-
)
72+
ixToSend.push(
73+
await squad.buildAddInstruction(
74+
vault,
75+
newProposalAddress,
76+
squadIx.instruction,
77+
i + 1,
78+
squadIx.authorityIndex,
79+
squadIx.authorityBump,
80+
squadIx.authorityType
8381
)
8482
);
8583
}
8684
} else {
8785
for (let i = 0; i < instructions.length; i++) {
88-
txToSend.push(
89-
new Transaction().add(
90-
await squad.buildAddInstruction(
91-
vault,
92-
newProposalAddress,
93-
instructions[i],
94-
i + 1
95-
)
86+
ixToSend.push(
87+
await squad.buildAddInstruction(
88+
vault,
89+
newProposalAddress,
90+
instructions[i],
91+
i + 1
9692
)
9793
);
9894
}
9995
}
10096

101-
txToSend.push(
102-
new Transaction().add(
103-
await squad.buildActivateTransaction(vault, newProposalAddress)
104-
)
97+
ixToSend.push(
98+
await squad.buildActivateTransaction(vault, newProposalAddress)
10599
);
106100

107-
txToSend.push(
108-
new Transaction().add(
109-
await squad.buildApproveTransaction(vault, newProposalAddress)
110-
)
111-
);
101+
ixToSend.push(await squad.buildApproveTransaction(vault, newProposalAddress));
112102

103+
const txToSend = batchIntoTransactions(ixToSend, squad.wallet.publicKey);
113104
await new AnchorProvider(
114105
squad.connection,
115106
squad.wallet,
@@ -122,6 +113,82 @@ export async function proposeInstructions(
122113
return newProposalAddress;
123114
}
124115

116+
/**
117+
* Batch instructions into transactions
118+
*/
119+
export function batchIntoTransactions(
120+
instructions: TransactionInstruction[],
121+
feePayer: PublicKey
122+
): Transaction[] {
123+
let i = 0;
124+
const txToSend: Transaction[] = [];
125+
while (i < instructions.length) {
126+
let j = i + 2;
127+
while (
128+
j < instructions.length &&
129+
getSizeOfTransaction(instructions.slice(i, j)) <= PACKET_DATA_SIZE
130+
) {
131+
j += 1;
132+
}
133+
const tx = new Transaction();
134+
tx.feePayer = feePayer;
135+
for (let k = i; k < j - 1; k += 1) {
136+
tx.add(instructions[k]);
137+
}
138+
i = j - 1;
139+
txToSend.push(tx);
140+
}
141+
return txToSend;
142+
}
143+
144+
/**
145+
* Get the size of a transaction that would contain the provided array of instructions
146+
*/
147+
export function getSizeOfTransaction(
148+
instructions: TransactionInstruction[]
149+
): number {
150+
const signers = new Set<string>();
151+
const accounts = new Set<string>();
152+
153+
instructions.map((ix) => {
154+
accounts.add(ix.programId.toBase58()),
155+
ix.keys.map((key) => {
156+
if (key.isSigner) {
157+
signers.add(key.pubkey.toBase58());
158+
}
159+
accounts.add(key.pubkey.toBase58());
160+
});
161+
});
162+
163+
const instruction_sizes: number = instructions
164+
.map(
165+
(ix) =>
166+
1 +
167+
getSizeOfCompressedU16(ix.keys.length) +
168+
ix.keys.length +
169+
getSizeOfCompressedU16(ix.data.length) +
170+
ix.data.length
171+
)
172+
.reduce((a, b) => a + b, 0);
173+
return (
174+
1 +
175+
signers.size * 64 +
176+
3 +
177+
getSizeOfCompressedU16(accounts.size) +
178+
32 * accounts.size +
179+
32 +
180+
getSizeOfCompressedU16(instructions.length) +
181+
instruction_sizes
182+
);
183+
}
184+
185+
/**
186+
* Get the size of n in bytes when serialized as a CompressedU16
187+
*/
188+
export function getSizeOfCompressedU16(n: number) {
189+
return 1 + Number(n >= 128) + Number(n >= 16384);
190+
}
191+
125192
/**
126193
* Wrap `instruction` in a Wormhole message for remote execution
127194
* @param squad Squads client

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)