Skip to content

Commit

Permalink
test: account creation in handler
Browse files Browse the repository at this point in the history
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
  • Loading branch information
Reinis-FRP committed Nov 14, 2024
1 parent 4bd28bb commit b1fab92
Show file tree
Hide file tree
Showing 2 changed files with 175 additions and 2 deletions.
2 changes: 2 additions & 0 deletions programs/svm-spoke/src/utils/message_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ pub fn invoke_handler<'info>(
}

// Transfer value amount from the relayer to the first account in the message accounts.
// Note that the depositor is responsible to make sure that after invoking the handler the recipient account will
// not hold any balance that is below its rent-exempt threshold, otherwise the fill would fail.
if message.value_amount > 0 {
let recipient_account = account_infos.get(0).ok_or(AcrossPlusError::MissingValueRecipientKey)?;
let transfer_ix = system_instruction::transfer(&relayer.key(), &recipient_account.key(), message.value_amount);
Expand Down
175 changes: 173 additions & 2 deletions test/svm/SvmSpoke.Fill.AcrossPlus.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as anchor from "@coral-xyz/anchor";
import { BN, Program } from "@coral-xyz/anchor";
import { BN, Program, web3 } from "@coral-xyz/anchor";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
Expand All @@ -8,8 +8,18 @@ import {
mintTo,
getAccount,
createTransferCheckedInstruction,
getAssociatedTokenAddressSync,
createAssociatedTokenAccountInstruction,
getMinimumBalanceForRentExemptAccount,
} from "@solana/spl-token";
import { PublicKey, Keypair, AccountMeta, TransactionMessage } from "@solana/web3.js";
import {
PublicKey,
Keypair,
AccountMeta,
TransactionMessage,
AddressLookupTableProgram,
VersionedTransaction,
} from "@solana/web3.js";
import { calculateRelayHashUint8Array, MulticallHandlerCoder, AcrossPlusMessageCoder } from "../../src/SvmUtils";
import { MulticallHandler } from "../../target/types/multicall_handler";
import { common } from "./SvmSpoke.common";
Expand Down Expand Up @@ -156,4 +166,165 @@ describe("svm_spoke.fill.across_plus", () => {
"Final recipient's balance should be increased by the relay amount"
);
});

it("Sends lamports from the relayer to value recipient", async () => {
const valueAmount = new BN(1_000_000_000);
const valueRecipient = Keypair.generate().publicKey;

const multicallHandlerCoder = new MulticallHandlerCoder([], valueRecipient);

const handlerMessage = multicallHandlerCoder.encode();

const message = new AcrossPlusMessageCoder({
handler: handlerProgram.programId,
readOnlyLen: multicallHandlerCoder.readOnlyLen,
valueAmount,
accounts: multicallHandlerCoder.compiledMessage.accountKeys,
handlerMessage,
});

const encodedMessage = message.encode();

// Update relay data with the encoded message.
const newRelayData = { ...relayData, message: encodedMessage };
updateRelayData(newRelayData);

const remainingAccounts: AccountMeta[] = [
{ pubkey: handlerProgram.programId, isSigner: false, isWritable: false },
...multicallHandlerCoder.compiledKeyMetas,
];

const relayHash = Array.from(calculateRelayHashUint8Array(newRelayData, chainId));

await program.methods
.fillV3Relay(relayHash, relayData, new BN(1), relayer.publicKey)
.accounts(accounts)
.remainingAccounts(remainingAccounts)
.signers([relayer])
.rpc();

// Verify value recipient balance.
const valueRecipientAccount = await connection.getAccountInfo(valueRecipient);
if (valueRecipientAccount === null) throw new Error("Account not found");
assertSE(
valueRecipientAccount.lamports,
valueAmount.toNumber(),
"Value recipient's balance should be increased by the value amount"
);
});

it("Creates new ATA when forwarding tokens within invoked message call", async () => {
// We need precise estimate of the minimum balance for ATA creation.
const valueAmount = await getMinimumBalanceForRentExemptAccount(connection);

const anotherRecipient = Keypair.generate().publicKey;
const anotherRecipientATA = getAssociatedTokenAddressSync(mint, anotherRecipient);

// Construct ix to create recipient ATA.
const createTokenAccountInstruction = createAssociatedTokenAccountInstruction(
handlerSigner,
anotherRecipientATA,
anotherRecipient,
mint
);

// Construct ix to transfer all tokens from handler to the recipient ATA.
const transferInstruction = createTransferCheckedInstruction(
handlerATA,
mint,
anotherRecipientATA,
handlerSigner,
relayData.outputAmount,
mintDecimals
);

// Encode both instructions with handler PDA as the payer for ATA initialization.
const multicallHandlerCoder = new MulticallHandlerCoder(
[createTokenAccountInstruction, transferInstruction],
handlerSigner
);
const handlerMessage = multicallHandlerCoder.encode();
const message = new AcrossPlusMessageCoder({
handler: handlerProgram.programId,
readOnlyLen: multicallHandlerCoder.readOnlyLen,
valueAmount: new BN(valueAmount), // Should exactly cover ATA creation.
accounts: multicallHandlerCoder.compiledMessage.accountKeys,
handlerMessage,
});
const encodedMessage = message.encode();

// Update relay data with the encoded message.
const newRelayData = { ...relayData, message: encodedMessage };
updateRelayData(newRelayData);

const remainingAccounts: AccountMeta[] = [
{ pubkey: handlerProgram.programId, isSigner: false, isWritable: false },
...multicallHandlerCoder.compiledKeyMetas,
];

const relayHash = Array.from(calculateRelayHashUint8Array(newRelayData, chainId));

// Prepare fill instruction as we will need to use Address Lookup Table (ALT).
const fillInstruction = await program.methods
.fillV3Relay(relayHash, relayData, new BN(1), relayer.publicKey)
.accounts(accounts)
.remainingAccounts(remainingAccounts)
.signers([relayer])
.instruction();

// Consolidate all above addresses into a single array for the ALT.
const lookupAddresses = [...Object.values(accounts), ...remainingAccounts.map((acc) => acc.pubkey)];

// Create instructions for creating and extending the ALT.
const [lookupTableInstruction, lookupTableAddress] = await AddressLookupTableProgram.createLookupTable({
authority: relayer.publicKey,
payer: relayer.publicKey,
recentSlot: await connection.getSlot(),
});

// Submit the ALT creation transaction
await web3.sendAndConfirmTransaction(connection, new web3.Transaction().add(lookupTableInstruction), [relayer], {
skipPreflight: true, // Avoids recent slot mismatch in simulation.
});

// Extend the ALT with all accounts
const extendInstruction = AddressLookupTableProgram.extendLookupTable({
lookupTable: lookupTableAddress,
authority: relayer.publicKey,
payer: relayer.publicKey,
addresses: lookupAddresses as PublicKey[],
});
await web3.sendAndConfirmTransaction(connection, new web3.Transaction().add(extendInstruction), [relayer], {
skipPreflight: true, // Avoids recent slot mismatch in simulation.
});

// Avoids invalid ALT index as ALT might not be active yet on the following tx.
await new Promise((resolve) => setTimeout(resolve, 1000));

// Fetch the AddressLookupTableAccount
const lookupTableAccount = (await connection.getAddressLookupTable(lookupTableAddress)).value;
if (lookupTableAccount === null) throw new Error("AddressLookupTableAccount not fetched");

// Create the versioned transaction
const versionedTx = new VersionedTransaction(
new TransactionMessage({
payerKey: relayer.publicKey,
recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
instructions: [fillInstruction],
}).compileToV0Message([lookupTableAccount])
);

// Sign and submit the versioned transaction.
versionedTx.sign([relayer]);
await connection.sendTransaction(versionedTx);

// Verify recipient's balance after the fill
await new Promise((resolve) => setTimeout(resolve, 500)); // Make sure token transfer gets processed.
const anotherRecipientAccount = await getAccount(connection, anotherRecipientATA);
assertSE(
anotherRecipientAccount.amount,
relayAmount,
"Recipient's balance should be increased by the relay amount"
);
});
});

0 comments on commit b1fab92

Please sign in to comment.