Skip to content

Commit

Permalink
Merge pull request #15 from joeymeere/fix/legacy-message-handling
Browse files Browse the repository at this point in the history
fix(tx): handle generic messages as input
  • Loading branch information
joeymeere authored Nov 7, 2024
2 parents 212404d + 425f30f commit 0a8e6d8
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 19 deletions.
2 changes: 2 additions & 0 deletions app/(app)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ const AppLayout = async ({ children }: { children: React.ReactNode }) => {
<RenderMultisigRoute multisig={multisig} children={children} />
</div>
<Toaster
expand
visibleToasts={3}
icons={{
error: <AlertTriangle className="w-4 h-4 text-red-600" />,
success: <CheckSquare className="w-4 h-4 text-green-600" />,
Expand Down
19 changes: 12 additions & 7 deletions components/CreateTransactionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import * as multisig from "@sqds/multisig";
import { useWallet } from "@solana/wallet-adapter-react";
import {
Connection,
Message,
PublicKey,
TransactionInstruction,
TransactionMessage,
clusterApiUrl,
} from "@solana/web3.js";
import { Input } from "./ui/input";
Expand Down Expand Up @@ -54,7 +54,7 @@ const CreateTransaction = ({
programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID,
})[0];

const dummyMessage = new TransactionMessage({
const dummyMessage = Message.compile({
instructions: [
new TransactionInstruction({
keys: [
Expand All @@ -72,7 +72,7 @@ const CreateTransaction = ({
],
payerKey: vaultAddress,
recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
}).compileToLegacyMessage();
});

const encoded = bs58.default.encode(dummyMessage.serialize());

Expand Down Expand Up @@ -100,17 +100,22 @@ const CreateTransaction = ({
/>
<div className="flex gap-2 items-center justify-end">
<Button
onClick={() =>
onClick={() => {
toast("Note: Simulations may fail on alt-SVM", {
description: "Please verify via an explorer before submitting.",
});
toast.promise(
simulateEncodedTransaction(tx, connection, wallet),
{
id: "simulation",
loading: "Building simulation...",
success: "Simulation successful.",
error: (e) => `${e}`,
error: (e) => {
return `${e}`;
},
}
)
}
);
}}
>
Simulate
</Button>
Expand Down
64 changes: 56 additions & 8 deletions lib/transaction/decodeAndDeserialize.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,66 @@
import * as bs58 from "bs58";
import { VersionedMessage } from "@solana/web3.js";
import {
Message,
MessageAccountKeys,
MessageV0,
PublicKey,
Transaction,
TransactionMessage,
VersionedMessage,
VersionedTransaction,
} from "@solana/web3.js";

export function decodeAndDeserialize(tx: string): {
message: VersionedMessage;
interface DeserializedTransaction {
message: TransactionMessage;
version: number | "legacy";
} {
accountKeys: PublicKey[];
}

/**
* Decodes a base58 encoded transaction and deserializes it into a TransactionMessage
* @param tx - Base58 encoded transaction string
* @returns Object containing the deserialized message, version, and account keys
* @throws Error if deserialization fails
*/
export function decodeAndDeserialize(tx: string): DeserializedTransaction {
if (!tx) {
throw new Error("Transaction string is required");
}

try {
const messageBytes = bs58.default.decode(tx);
const version = VersionedMessage.deserializeMessageVersion(messageBytes);
const message = VersionedMessage.deserialize(messageBytes);
let message: TransactionMessage;
let accountKeys: PublicKey[];

if (version === "legacy") {
const legacyMessage = Message.from(messageBytes);
accountKeys = legacyMessage.accountKeys;

const intermediate = VersionedMessage.deserialize(
new MessageV0(legacyMessage).serialize()
);
message = TransactionMessage.decompile(intermediate, {
addressLookupTableAccounts: [],
});
} else {
const versionedMessage = VersionedMessage.deserialize(messageBytes);
accountKeys = versionedMessage.staticAccountKeys;

message = TransactionMessage.decompile(versionedMessage, {
addressLookupTableAccounts: [],
});
}

return { version, message };
return {
version,
message,
accountKeys,
};
} catch (error) {
console.error(error);
throw new Error("Failed to decode transaction.");
if (error instanceof Error) {
throw new Error(`Failed to decode transaction: ${error.message}`);
}
throw new Error("Failed to decode transaction: Unknown error");
}
}
7 changes: 6 additions & 1 deletion lib/transaction/getAccountsForSimulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ export async function getAccountsForSimulation(
const { staticAccountKeys, accountKeysFromLookups } =
tx.message.getAccountKeys({ addressLookupTableAccounts });

const staticAddresses = staticAccountKeys.map((k) => k.toString());
const staticAddresses = staticAccountKeys.reduce((acc, k) => {
if (!k.equals(SystemProgram.programId)) {
acc.push(k.toString());
}
return acc;
}, [] as string[]);

const addressesFromLookups = accountKeysFromLookups
? accountKeysFromLookups.writable.map((k) => k.toString())
Expand Down
3 changes: 2 additions & 1 deletion lib/transaction/importTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Connection,
PublicKey,
TransactionMessage,
VersionedMessage,
VersionedTransaction,
} from "@solana/web3.js";
import { decodeAndDeserialize } from "./decodeAndDeserialize";
Expand All @@ -29,7 +30,7 @@ export const importTransaction = async (
new PublicKey(multisigPda)
);

const transactionMessage = TransactionMessage.decompile(message);
const transactionMessage = new TransactionMessage(message);

const addressLookupTableAccounts =
version === 0
Expand Down
4 changes: 2 additions & 2 deletions lib/transaction/simulateEncodedTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ export const simulateEncodedTransaction = async (
try {
const { message, version } = decodeAndDeserialize(tx);

const transaction = new VersionedTransaction(message);
const transaction = new VersionedTransaction(message.compileToV0Message());

const keys = await getAccountsForSimulation(
connection,
transaction,
version === "legacy"
version === 0
);

toast.loading("Simulating...", {
Expand Down

0 comments on commit 0a8e6d8

Please sign in to comment.