Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions Anchor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ addressToPublicKey = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/addressT
publicKeyToAddress = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/publicKeyToAddress.ts"
findFillStatusPdaFromEvent = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/findFillStatusPdaFromEvent.ts"
findFillStatusFromFillStatusPda = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/findFillStatusFromFillStatusPda.ts"
nativeDeposit = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/nativeDeposit.ts"

[test.validator]
url = "https://api.mainnet-beta.solana.com"
Expand Down
196 changes: 196 additions & 0 deletions scripts/svm/nativeDeposit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// This script is used to initiate a native token Solana deposit. useful in testing.

import * as anchor from "@coral-xyz/anchor";
import { AnchorProvider, BN } from "@coral-xyz/anchor";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
NATIVE_MINT,
TOKEN_PROGRAM_ID,
createApproveCheckedInstruction,
createAssociatedTokenAccountIdempotentInstruction,
createCloseAccountInstruction,
createSyncNativeInstruction,
getAssociatedTokenAddressSync,
getMinimumBalanceForRentExemptAccount,
getMint,
} from "@solana/spl-token";
import {
PublicKey,
Transaction,
sendAndConfirmTransaction,
TransactionInstruction,
SystemProgram,
} from "@solana/web3.js";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { getSpokePoolProgram, SOLANA_SPOKE_STATE_SEED } from "../../src/svm/web3-v1";

// Set up the provider
const provider = AnchorProvider.env();
anchor.setProvider(provider);
const program = getSpokePoolProgram(provider);
const programId = program.programId;
console.log("SVM-Spoke Program ID:", programId.toString());

// Parse arguments
const argv = yargs(hideBin(process.argv))
.option("recipient", { type: "string", demandOption: true, describe: "Recipient public key" })
.option("outputToken", { type: "string", demandOption: true, describe: "Output token public key" })
.option("inputAmount", { type: "number", demandOption: true, describe: "Input amount" })
.option("outputAmount", { type: "number", demandOption: true, describe: "Output amount" })
.option("destinationChainId", { type: "string", demandOption: true, describe: "Destination chain ID" })
.option("integratorId", { type: "string", demandOption: false, describe: "integrator ID" }).argv;

async function nativeDeposit(): Promise<void> {
const resolvedArgv = await argv;
const seed = SOLANA_SPOKE_STATE_SEED;
const recipient = new PublicKey(resolvedArgv.recipient);
const inputToken = NATIVE_MINT;
const outputToken = new PublicKey(resolvedArgv.outputToken);
const inputAmount = new BN(resolvedArgv.inputAmount);
const outputAmount = new BN(resolvedArgv.outputAmount);
const destinationChainId = new BN(resolvedArgv.destinationChainId);
const exclusiveRelayer = PublicKey.default;
const quoteTimestamp = Math.floor(Date.now() / 1000) - 1;
const fillDeadline = quoteTimestamp + 3600; // 1 hour from now
const exclusivityDeadline = 0;
const message = Buffer.from([]); // Convert to Buffer
const integratorId = resolvedArgv.integratorId || "";
// Define the state account PDA
const [statePda, _] = PublicKey.findProgramAddressSync(
[Buffer.from("state"), seed.toArrayLike(Buffer, "le", 8)],
programId
);

// Define the route account PDA
const [routePda] = PublicKey.findProgramAddressSync(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

will need to remove this once #939 is merged.

[
Buffer.from("route"),
inputToken.toBytes(),
seed.toArrayLike(Buffer, "le", 8),
destinationChainId.toArrayLike(Buffer, "le", 8),
],
programId
);

// Define the signer (replace with your actual signer)
const signer = (provider.wallet as anchor.Wallet).payer;

// Find ATA for the input token to be stored by state (vault). This was created when the route was enabled.
const vault = getAssociatedTokenAddressSync(
inputToken,
statePda,
true,
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
);

const userTokenAccount = getAssociatedTokenAddressSync(inputToken, signer.publicKey);
const userTokenAccountInfo = await provider.connection.getAccountInfo(userTokenAccount);
const existingTokenAccount = userTokenAccountInfo !== null && userTokenAccountInfo.owner.equals(TOKEN_PROGRAM_ID);

console.log("Depositing V3...");
console.table([
{ property: "seed", value: seed.toString() },
{ property: "recipient", value: recipient.toString() },
{ property: "inputToken", value: inputToken.toString() },
{ property: "outputToken", value: outputToken.toString() },
{ property: "inputAmount", value: inputAmount.toString() },
{ property: "outputAmount", value: outputAmount.toString() },
{ property: "destinationChainId", value: destinationChainId.toString() },
{ property: "quoteTimestamp", value: quoteTimestamp.toString() },
{ property: "fillDeadline", value: fillDeadline.toString() },
{ property: "exclusivityDeadline", value: exclusivityDeadline.toString() },
{ property: "message", value: message.toString("hex") },
{ property: "integratorId", value: integratorId },
{ property: "programId", value: programId.toString() },
{ property: "providerPublicKey", value: provider.wallet.publicKey.toString() },
{ property: "statePda", value: statePda.toString() },
{ property: "routePda", value: routePda.toString() },
{ property: "vault", value: vault.toString() },
{ property: "userTokenAccount", value: userTokenAccount.toString() },
{ property: "existingTokenAccount", value: existingTokenAccount },
]);

const tokenDecimals = (await getMint(provider.connection, inputToken, undefined, TOKEN_PROGRAM_ID)).decimals;

// Will need to add rent exemption to the deposit amount if the user token account does not exist.
const rentExempt = existingTokenAccount ? 0 : await getMinimumBalanceForRentExemptAccount(provider.connection);
const transferIx = SystemProgram.transfer({
fromPubkey: signer.publicKey,
toPubkey: userTokenAccount,
lamports: BigInt(inputAmount.toString()) + BigInt(rentExempt),
});

// Create wSOL user account if it doesn't exist, otherwise sync its native balance.
const syncOrCreateIx = existingTokenAccount
? createSyncNativeInstruction(userTokenAccount)
: createAssociatedTokenAccountIdempotentInstruction(
signer.publicKey,
userTokenAccount,
signer.publicKey,
inputToken
);

// Close the user token account if it did not exist before.
const lastIxs = existingTokenAccount
? []
: [createCloseAccountInstruction(userTokenAccount, signer.publicKey, signer.publicKey)];

// Delegate state PDA to pull depositor tokens.
const approveIx = await createApproveCheckedInstruction(
userTokenAccount,
inputToken,
statePda,
signer.publicKey,
BigInt(inputAmount.toString()),
tokenDecimals,
undefined,
TOKEN_PROGRAM_ID
);

const depositIx = await (
program.methods.deposit(
signer.publicKey,
recipient,
inputToken,
outputToken,
inputAmount,
outputAmount,
destinationChainId,
exclusiveRelayer,
quoteTimestamp,
fillDeadline,
exclusivityDeadline,
message
) as any
)
.accounts({
state: statePda,
route: routePda,
signer: signer.publicKey,
userTokenAccount,
vault: vault,
tokenProgram: TOKEN_PROGRAM_ID,
mint: inputToken,
})
.instruction();

// Create the deposit transaction
const depositTx = new Transaction().add(transferIx, syncOrCreateIx, approveIx, depositIx, ...lastIxs);

if (integratorId !== "") {
const MemoIx = new TransactionInstruction({
keys: [{ pubkey: signer.publicKey, isSigner: true, isWritable: true }],
data: Buffer.from(integratorId, "utf-8"),
programId: new PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"), // Memo program ID
});
depositTx.add(MemoIx);
}

const tx = await sendAndConfirmTransaction(provider.connection, depositTx, [signer]);
console.log("Transaction signature:", tx);
}

// Run the nativeDeposit function
nativeDeposit();
120 changes: 119 additions & 1 deletion test/svm/SvmSpoke.Deposit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,23 @@ import {
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
ExtensionType,
NATIVE_MINT,
TOKEN_2022_PROGRAM_ID,
TOKEN_PROGRAM_ID,
createApproveCheckedInstruction,
createAssociatedTokenAccountIdempotentInstruction,
createCloseAccountInstruction,
createEnableCpiGuardInstruction,
createMint,
createReallocateInstruction,
createSyncNativeInstruction,
getAccount,
getAssociatedTokenAddressSync,
getMinimumBalanceForRentExemptAccount,
getOrCreateAssociatedTokenAccount,
mintTo,
} from "@solana/spl-token";
import { Keypair, PublicKey, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
import { Keypair, PublicKey, SystemProgram, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
import { BigNumber, ethers } from "ethers";
import { SvmSpokeClient } from "../../src/svm";
import { DepositInput } from "../../src/svm/clients/SvmSpoke";
Expand Down Expand Up @@ -713,6 +719,118 @@ describe("svm_spoke.deposit", () => {
}
});

it("Deposit native token, new token account", async () => {
// Fund depositor account with SOL.
const nativeAmount = 1_000_000_000; // 1 SOL
await connection.requestAirdrop(depositor.publicKey, nativeAmount * 2); // Add buffer for transaction fees.

// Setup wSOL as the input token.
inputToken = NATIVE_MINT;
const nativeDecimals = 9;
depositorTA = getAssociatedTokenAddressSync(inputToken, depositor.publicKey);
await enableRoute();

// Will need to add rent exemption to the deposit amount, will recover it at the end of the transaction.
const rentExempt = await getMinimumBalanceForRentExemptAccount(connection);
const transferIx = SystemProgram.transfer({
fromPubkey: depositor.publicKey,
toPubkey: depositorTA,
lamports: BigInt(nativeAmount) + BigInt(rentExempt),
});

// Create wSOL user account.
const createIx = createAssociatedTokenAccountIdempotentInstruction(
depositor.publicKey,
depositorTA,
depositor.publicKey,
inputToken
);

const approveIx = await createApproveCheckedInstruction(
depositAccounts.depositorTokenAccount,
depositAccounts.mint,
depositAccounts.state,
depositor.publicKey,
BigInt(nativeAmount),
nativeDecimals,
undefined,
tokenProgram
);

const nativeDepositData = { ...depositData, inputAmount: new BN(nativeAmount), outputAmount: new BN(nativeAmount) };
const depositDataValues = Object.values(nativeDepositData) as DepositDataValues;
const depositIx = await program.methods
.deposit(...depositDataValues)
.accounts(depositAccounts)
.instruction();

const closeIx = createCloseAccountInstruction(depositorTA, depositor.publicKey, depositor.publicKey);

const iVaultAmount = (await getAccount(connection, vault, undefined, tokenProgram)).amount;

const depositTx = new Transaction().add(transferIx, createIx, approveIx, depositIx, closeIx);
const tx = await sendAndConfirmTransaction(connection, depositTx, [depositor]);

const fVaultAmount = (await getAccount(connection, vault, undefined, tokenProgram)).amount;
assertSE(
fVaultAmount,
iVaultAmount + BigInt(nativeAmount),
"Vault balance should be increased by the deposited amount"
);
});

it("Deposit native token, existing token account", async () => {
// Fund depositor account with SOL.
const nativeAmount = 1_000_000_000; // 1 SOL
await connection.requestAirdrop(depositor.publicKey, nativeAmount * 2); // Add buffer for transaction fees.

// Setup wSOL as the input token, creating the associated token account for the user.
inputToken = NATIVE_MINT;
const nativeDecimals = 9;
depositorTA = (await getOrCreateAssociatedTokenAccount(connection, payer, inputToken, depositor.publicKey)).address;
await enableRoute();

// Transfer SOL to the user token account.
const transferIx = SystemProgram.transfer({
fromPubkey: depositor.publicKey,
toPubkey: depositorTA,
lamports: nativeAmount,
});

// Sync the user token account with the native balance.
const syncIx = createSyncNativeInstruction(depositorTA);

const approveIx = await createApproveCheckedInstruction(
depositAccounts.depositorTokenAccount,
depositAccounts.mint,
depositAccounts.state,
depositor.publicKey,
BigInt(nativeAmount),
nativeDecimals,
undefined,
tokenProgram
);

const nativeDepositData = { ...depositData, inputAmount: new BN(nativeAmount), outputAmount: new BN(nativeAmount) };
const depositDataValues = Object.values(nativeDepositData) as DepositDataValues;
const depositIx = await program.methods
.deposit(...depositDataValues)
.accounts(depositAccounts)
.instruction();

const iVaultAmount = (await getAccount(connection, vault, undefined, tokenProgram)).amount;

const depositTx = new Transaction().add(transferIx, syncIx, approveIx, depositIx);
const tx = await sendAndConfirmTransaction(connection, depositTx, [depositor]);

const fVaultAmount = (await getAccount(connection, vault, undefined, tokenProgram)).amount;
assertSE(
fVaultAmount,
iVaultAmount + BigInt(nativeAmount),
"Vault balance should be increased by the deposited amount"
);
});

describe("codama client and solana kit", () => {
it("Deposit with with solana kit and codama client", async () => {
// typescript is not happy with the depositData object
Expand Down
Loading