Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions packages/client/src/solanaEncode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ export const solanaEncode = async ({
mint,
accounts = [],
}: EncodeOptions) => {
const connectedPdaAccount = new anchor.web3.PublicKey(connected);
const connectedProgramId = new anchor.web3.PublicKey(connected);
const [connectedPdaAccount] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("connected", "utf-8")],
connectedProgramId
);
const [pdaAccount] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("meta", "utf-8")],
new anchor.web3.PublicKey(gateway)
Expand All @@ -41,6 +45,11 @@ export const solanaEncode = async ({
publicKey: ethers.hexlify(anchor.web3.SystemProgram.programId.toBytes()),
};

const instructionSysvar = {
isWritable: false,
publicKey: ethers.hexlify(anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY.toBytes()),
};

let baseAccounts;
if (mint) {
const mintPubkey = new anchor.web3.PublicKey(mint);
Expand Down Expand Up @@ -72,9 +81,10 @@ export const solanaEncode = async ({
gatewayPda,
tokenProgram,
systemProgram,
instructionSysvar,
];
} else {
baseAccounts = [pda, gatewayPda, systemProgram];
baseAccounts = [pda, gatewayPda, systemProgram, instructionSysvar];
}

// Parse additional accounts using our utility function
Expand Down
2 changes: 1 addition & 1 deletion packages/commands/src/solana/encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const main = async (options: EncodeOptions) => {

export const encodeCommand = new Command("encode")
.summary("Encode payload data for Solana")
.requiredOption("--connected <address>", "Connected PDA account address")
.requiredOption("--connected <address>", "Connected program address")
.requiredOption("--data <data>", "Data to encode")
.requiredOption("--gateway <address>", "Gateway program address")
.option("--mint <address>", "Mint address for SPL token operations")
Expand Down
166 changes: 157 additions & 9 deletions test/solanaEncode.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
import * as anchor from "@coral-xyz/anchor";
import { getAssociatedTokenAddress } from "@solana/spl-token";
import { ethers } from "ethers";

import { solanaEncode } from "../packages/client/src/solanaEncode";

type EncodedResult = {
accounts: Array<{ isWritable: boolean; publicKey: string }>;
data: string;
};

const tupleSignature = [
"tuple(tuple(bytes32 publicKey, bool isWritable)[] accounts, bytes data)",
];

const decodeEncodedResult = (encoded: string): EncodedResult =>
ethers.AbiCoder.defaultAbiCoder().decode(
tupleSignature,
encoded
)[0] as unknown as EncodedResult;

const hexlifyPubkey = (pubkey: anchor.web3.PublicKey) =>
ethers.hexlify(pubkey.toBytes());

describe("solanaEncode", () => {
it("should encode basic data without mint", async () => {
const input = {
Expand All @@ -8,11 +30,37 @@ describe("solanaEncode", () => {
gateway: "94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d",
};

const expected =
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000030b650b5c456bbc1c19bb901bb1fe3538d91402596d1675db7e3d40fc2f98306a0000000000000000000000000000000000000000000000000000000000000001803e2c72756c303bd21a5b950e10ab343e82118c123dbf6e53e326c496649b4d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003736f6c0000000000000000000000000000000000000000000000000000000000";
const connectedProgramId = new anchor.web3.PublicKey(input.connected);
const [connectedPdaAccount] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("connected", "utf-8")],
connectedProgramId
);
const [gatewayPdaAccount] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("meta", "utf-8")],
new anchor.web3.PublicKey(input.gateway)
);

const result = await solanaEncode(input);
expect(result).toBe(expected);
const { accounts, data: encodedData } = decodeEncodedResult(result);

expect(encodedData).toBe(ethers.hexlify(ethers.toUtf8Bytes(input.data)));
expect(accounts).toHaveLength(4);

expect(accounts[0].publicKey).toBe(hexlifyPubkey(connectedPdaAccount));
expect(accounts[0].isWritable).toBe(true);

expect(accounts[1].publicKey).toBe(hexlifyPubkey(gatewayPdaAccount));
expect(accounts[1].isWritable).toBe(false);

expect(accounts[2].publicKey).toBe(
hexlifyPubkey(anchor.web3.SystemProgram.programId)
);
expect(accounts[2].isWritable).toBe(false);

expect(accounts[3].publicKey).toBe(
hexlifyPubkey(anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY)
);
expect(accounts[3].isWritable).toBe(false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we also add some remainingAccounts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand what you mean 🤔

});

it("should encode data with mint", async () => {
Expand All @@ -23,11 +71,54 @@ describe("solanaEncode", () => {
mint: "A4NAzqwdGRxDxbRfiX5uMPdia6RbGJ3U3W9gvutNzBay",
};

const expected =
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000000060b650b5c456bbc1c19bb901bb1fe3538d91402596d1675db7e3d40fc2f98306a0000000000000000000000000000000000000000000000000000000000000001fb0520680b6a72b1d16b8a6627f0651df92d4fcae652b9b6899f44552bfbb5a400000000000000000000000000000000000000000000000000000000000000018695dd4bb5e0900a15c95bd37fc7ac7f64dc6005ae131136ad36cd9967a11bb20000000000000000000000000000000000000000000000000000000000000000803e2c72756c303bd21a5b950e10ab343e82118c123dbf6e53e326c496649b4d000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003736f6c0000000000000000000000000000000000000000000000000000000000";
const connectedProgramId = new anchor.web3.PublicKey(input.connected);
const [connectedPdaAccount] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("connected", "utf-8")],
connectedProgramId
);
const [gatewayPdaAccount] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("meta", "utf-8")],
new anchor.web3.PublicKey(input.gateway)
);
const mintPubkey = new anchor.web3.PublicKey(input.mint);
const connectedPdaAta = await getAssociatedTokenAddress(
mintPubkey,
connectedPdaAccount,
true
);

const result = await solanaEncode(input);
expect(result).toBe(expected);
const { accounts, data: encodedData } = decodeEncodedResult(result);

expect(encodedData).toBe(ethers.hexlify(ethers.toUtf8Bytes(input.data)));
expect(accounts).toHaveLength(7);

expect(accounts[0].publicKey).toBe(hexlifyPubkey(connectedPdaAccount));
expect(accounts[0].isWritable).toBe(true);

expect(accounts[1].publicKey).toBe(hexlifyPubkey(connectedPdaAta));
expect(accounts[1].isWritable).toBe(true);

expect(accounts[2].publicKey).toBe(hexlifyPubkey(mintPubkey));
expect(accounts[2].isWritable).toBe(false);

expect(accounts[3].publicKey).toBe(hexlifyPubkey(gatewayPdaAccount));
expect(accounts[3].isWritable).toBe(false);

expect(accounts[4].publicKey).toBe(
hexlifyPubkey(anchor.utils.token.TOKEN_PROGRAM_ID)
);
expect(accounts[4].isWritable).toBe(false);

expect(accounts[5].publicKey).toBe(
hexlifyPubkey(anchor.web3.SystemProgram.programId)
);
expect(accounts[5].isWritable).toBe(false);

expect(accounts[6].publicKey).toBe(
hexlifyPubkey(anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY)
);
expect(accounts[6].isWritable).toBe(false);
});

it("should encode data with mint and additional accounts", async () => {
Expand All @@ -42,11 +133,68 @@ describe("solanaEncode", () => {
mint: "A4NAzqwdGRxDxbRfiX5uMPdia6RbGJ3U3W9gvutNzBay",
};

const expected =
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000080b650b5c456bbc1c19bb901bb1fe3538d91402596d1675db7e3d40fc2f98306a0000000000000000000000000000000000000000000000000000000000000001fb0520680b6a72b1d16b8a6627f0651df92d4fcae652b9b6899f44552bfbb5a400000000000000000000000000000000000000000000000000000000000000018695dd4bb5e0900a15c95bd37fc7ac7f64dc6005ae131136ad36cd9967a11bb20000000000000000000000000000000000000000000000000000000000000000803e2c72756c303bd21a5b950e10ab343e82118c123dbf6e53e326c496649b4d000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005500bd811d9233154acf43b49e6e76e71f7327b491f687ba1d443997506025010000000000000000000000000000000000000000000000000000000000000001ccbb0d078f40dfc4c229d95aea785bf159dff1459594e2879df26f2b26dbe98b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003736f6c0000000000000000000000000000000000000000000000000000000000";
const connectedProgramId = new anchor.web3.PublicKey(input.connected);
const [connectedPdaAccount] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("connected", "utf-8")],
connectedProgramId
);
const [gatewayPdaAccount] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("meta", "utf-8")],
new anchor.web3.PublicKey(input.gateway)
);
const mintPubkey = new anchor.web3.PublicKey(input.mint);
const connectedPdaAta = await getAssociatedTokenAddress(
mintPubkey,
connectedPdaAccount,
true
);

const result = await solanaEncode(input);
expect(result).toBe(expected);
const { accounts, data: encodedData } = decodeEncodedResult(result);

expect(encodedData).toBe(ethers.hexlify(ethers.toUtf8Bytes(input.data)));
expect(accounts).toHaveLength(9);

expect(accounts[0].publicKey).toBe(hexlifyPubkey(connectedPdaAccount));
expect(accounts[0].isWritable).toBe(true);

expect(accounts[1].publicKey).toBe(hexlifyPubkey(connectedPdaAta));
expect(accounts[1].isWritable).toBe(true);

expect(accounts[2].publicKey).toBe(hexlifyPubkey(mintPubkey));
expect(accounts[2].isWritable).toBe(false);

expect(accounts[3].publicKey).toBe(hexlifyPubkey(gatewayPdaAccount));
expect(accounts[3].isWritable).toBe(false);

expect(accounts[4].publicKey).toBe(
hexlifyPubkey(anchor.utils.token.TOKEN_PROGRAM_ID)
);
expect(accounts[4].isWritable).toBe(false);

expect(accounts[5].publicKey).toBe(
hexlifyPubkey(anchor.web3.SystemProgram.programId)
);
expect(accounts[5].isWritable).toBe(false);

expect(accounts[6].publicKey).toBe(
hexlifyPubkey(anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY)
);
expect(accounts[6].isWritable).toBe(false);

const additionalAccounts = input.accounts.map((account) => {
const [publicKey, isWritable] = account.split(":");
return {
isWritable: isWritable === "true",
publicKey: hexlifyPubkey(new anchor.web3.PublicKey(publicKey)),
};
});

additionalAccounts.forEach((expectedAccount, index) => {
const account = accounts[index + 7];
expect(account.publicKey).toBe(expectedAccount.publicKey);
expect(account.isWritable).toBe(expectedAccount.isWritable);
});
});

it("should throw error for invalid account format", async () => {
Expand Down