Skip to content

Commit 71e990b

Browse files
authored
feat(svm): test native sol deposits (#942)
* feat(svm): test native sol deposits Signed-off-by: Reinis Martinsons <reinis@umaproject.org> * fix: merge issues Signed-off-by: Reinis Martinsons <reinis@umaproject.org> --------- Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
1 parent b7ddacc commit 71e990b

File tree

3 files changed

+303
-2
lines changed

3 files changed

+303
-2
lines changed

Anchor.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ addressToPublicKey = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/addressT
5353
publicKeyToAddress = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/publicKeyToAddress.ts"
5454
findFillStatusPdaFromEvent = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/findFillStatusPdaFromEvent.ts"
5555
findFillStatusFromFillStatusPda = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/findFillStatusFromFillStatusPda.ts"
56+
nativeDeposit = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/nativeDeposit.ts"
5657

5758
[test.validator]
5859
url = "https://api.mainnet-beta.solana.com"

scripts/svm/nativeDeposit.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// This script is used to initiate a native token Solana deposit. useful in testing.
2+
3+
import * as anchor from "@coral-xyz/anchor";
4+
import { AnchorProvider, BN } from "@coral-xyz/anchor";
5+
import {
6+
ASSOCIATED_TOKEN_PROGRAM_ID,
7+
NATIVE_MINT,
8+
TOKEN_PROGRAM_ID,
9+
createApproveCheckedInstruction,
10+
createAssociatedTokenAccountIdempotentInstruction,
11+
createCloseAccountInstruction,
12+
createSyncNativeInstruction,
13+
getAssociatedTokenAddressSync,
14+
getMinimumBalanceForRentExemptAccount,
15+
getMint,
16+
} from "@solana/spl-token";
17+
import {
18+
PublicKey,
19+
Transaction,
20+
sendAndConfirmTransaction,
21+
TransactionInstruction,
22+
SystemProgram,
23+
} from "@solana/web3.js";
24+
import yargs from "yargs";
25+
import { hideBin } from "yargs/helpers";
26+
import { getSpokePoolProgram, SOLANA_SPOKE_STATE_SEED } from "../../src/svm/web3-v1";
27+
28+
// Set up the provider
29+
const provider = AnchorProvider.env();
30+
anchor.setProvider(provider);
31+
const program = getSpokePoolProgram(provider);
32+
const programId = program.programId;
33+
console.log("SVM-Spoke Program ID:", programId.toString());
34+
35+
// Parse arguments
36+
const argv = yargs(hideBin(process.argv))
37+
.option("recipient", { type: "string", demandOption: true, describe: "Recipient public key" })
38+
.option("outputToken", { type: "string", demandOption: true, describe: "Output token public key" })
39+
.option("inputAmount", { type: "number", demandOption: true, describe: "Input amount" })
40+
.option("outputAmount", { type: "number", demandOption: true, describe: "Output amount" })
41+
.option("destinationChainId", { type: "string", demandOption: true, describe: "Destination chain ID" })
42+
.option("integratorId", { type: "string", demandOption: false, describe: "integrator ID" }).argv;
43+
44+
async function nativeDeposit(): Promise<void> {
45+
const resolvedArgv = await argv;
46+
const seed = SOLANA_SPOKE_STATE_SEED;
47+
const recipient = new PublicKey(resolvedArgv.recipient);
48+
const inputToken = NATIVE_MINT;
49+
const outputToken = new PublicKey(resolvedArgv.outputToken);
50+
const inputAmount = new BN(resolvedArgv.inputAmount);
51+
const outputAmount = new BN(resolvedArgv.outputAmount);
52+
const destinationChainId = new BN(resolvedArgv.destinationChainId);
53+
const exclusiveRelayer = PublicKey.default;
54+
const quoteTimestamp = Math.floor(Date.now() / 1000) - 1;
55+
const fillDeadline = quoteTimestamp + 3600; // 1 hour from now
56+
const exclusivityDeadline = 0;
57+
const message = Buffer.from([]); // Convert to Buffer
58+
const integratorId = resolvedArgv.integratorId || "";
59+
// Define the state account PDA
60+
const [statePda, _] = PublicKey.findProgramAddressSync(
61+
[Buffer.from("state"), seed.toArrayLike(Buffer, "le", 8)],
62+
programId
63+
);
64+
65+
// Define the signer (replace with your actual signer)
66+
const signer = (provider.wallet as anchor.Wallet).payer;
67+
68+
// Find ATA for the input token to be stored by state (vault). This was created when the route was enabled.
69+
const vault = getAssociatedTokenAddressSync(
70+
inputToken,
71+
statePda,
72+
true,
73+
TOKEN_PROGRAM_ID,
74+
ASSOCIATED_TOKEN_PROGRAM_ID
75+
);
76+
77+
const userTokenAccount = getAssociatedTokenAddressSync(inputToken, signer.publicKey);
78+
const userTokenAccountInfo = await provider.connection.getAccountInfo(userTokenAccount);
79+
const existingTokenAccount = userTokenAccountInfo !== null && userTokenAccountInfo.owner.equals(TOKEN_PROGRAM_ID);
80+
81+
console.log("Depositing V3...");
82+
console.table([
83+
{ property: "seed", value: seed.toString() },
84+
{ property: "recipient", value: recipient.toString() },
85+
{ property: "inputToken", value: inputToken.toString() },
86+
{ property: "outputToken", value: outputToken.toString() },
87+
{ property: "inputAmount", value: inputAmount.toString() },
88+
{ property: "outputAmount", value: outputAmount.toString() },
89+
{ property: "destinationChainId", value: destinationChainId.toString() },
90+
{ property: "quoteTimestamp", value: quoteTimestamp.toString() },
91+
{ property: "fillDeadline", value: fillDeadline.toString() },
92+
{ property: "exclusivityDeadline", value: exclusivityDeadline.toString() },
93+
{ property: "message", value: message.toString("hex") },
94+
{ property: "integratorId", value: integratorId },
95+
{ property: "programId", value: programId.toString() },
96+
{ property: "providerPublicKey", value: provider.wallet.publicKey.toString() },
97+
{ property: "statePda", value: statePda.toString() },
98+
{ property: "vault", value: vault.toString() },
99+
{ property: "userTokenAccount", value: userTokenAccount.toString() },
100+
{ property: "existingTokenAccount", value: existingTokenAccount },
101+
]);
102+
103+
const tokenDecimals = (await getMint(provider.connection, inputToken, undefined, TOKEN_PROGRAM_ID)).decimals;
104+
105+
// Will need to add rent exemption to the deposit amount if the user token account does not exist.
106+
const rentExempt = existingTokenAccount ? 0 : await getMinimumBalanceForRentExemptAccount(provider.connection);
107+
const transferIx = SystemProgram.transfer({
108+
fromPubkey: signer.publicKey,
109+
toPubkey: userTokenAccount,
110+
lamports: BigInt(inputAmount.toString()) + BigInt(rentExempt),
111+
});
112+
113+
// Create wSOL user account if it doesn't exist, otherwise sync its native balance.
114+
const syncOrCreateIx = existingTokenAccount
115+
? createSyncNativeInstruction(userTokenAccount)
116+
: createAssociatedTokenAccountIdempotentInstruction(
117+
signer.publicKey,
118+
userTokenAccount,
119+
signer.publicKey,
120+
inputToken
121+
);
122+
123+
// Close the user token account if it did not exist before.
124+
const lastIxs = existingTokenAccount
125+
? []
126+
: [createCloseAccountInstruction(userTokenAccount, signer.publicKey, signer.publicKey)];
127+
128+
// Delegate state PDA to pull depositor tokens.
129+
const approveIx = await createApproveCheckedInstruction(
130+
userTokenAccount,
131+
inputToken,
132+
statePda,
133+
signer.publicKey,
134+
BigInt(inputAmount.toString()),
135+
tokenDecimals,
136+
undefined,
137+
TOKEN_PROGRAM_ID
138+
);
139+
140+
const depositIx = await (
141+
program.methods.deposit(
142+
signer.publicKey,
143+
recipient,
144+
inputToken,
145+
outputToken,
146+
inputAmount,
147+
outputAmount,
148+
destinationChainId,
149+
exclusiveRelayer,
150+
quoteTimestamp,
151+
fillDeadline,
152+
exclusivityDeadline,
153+
message
154+
) as any
155+
)
156+
.accounts({
157+
state: statePda,
158+
signer: signer.publicKey,
159+
userTokenAccount,
160+
vault: vault,
161+
tokenProgram: TOKEN_PROGRAM_ID,
162+
mint: inputToken,
163+
})
164+
.instruction();
165+
166+
// Create the deposit transaction
167+
const depositTx = new Transaction().add(transferIx, syncOrCreateIx, approveIx, depositIx, ...lastIxs);
168+
169+
if (integratorId !== "") {
170+
const MemoIx = new TransactionInstruction({
171+
keys: [{ pubkey: signer.publicKey, isSigner: true, isWritable: true }],
172+
data: Buffer.from(integratorId, "utf-8"),
173+
programId: new PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"), // Memo program ID
174+
});
175+
depositTx.add(MemoIx);
176+
}
177+
178+
const tx = await sendAndConfirmTransaction(provider.connection, depositTx, [signer]);
179+
console.log("Transaction signature:", tx);
180+
}
181+
182+
// Run the nativeDeposit function
183+
nativeDeposit();

test/svm/SvmSpoke.Deposit.ts

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,24 @@ import {
1212
pipe,
1313
} from "@solana/kit";
1414
import {
15-
ASSOCIATED_TOKEN_PROGRAM_ID,
1615
ExtensionType,
16+
NATIVE_MINT,
1717
TOKEN_2022_PROGRAM_ID,
1818
TOKEN_PROGRAM_ID,
1919
createApproveCheckedInstruction,
20+
createAssociatedTokenAccountIdempotentInstruction,
21+
createCloseAccountInstruction,
2022
createEnableCpiGuardInstruction,
2123
createMint,
2224
createReallocateInstruction,
25+
createSyncNativeInstruction,
2326
getAccount,
27+
getAssociatedTokenAddressSync,
28+
getMinimumBalanceForRentExemptAccount,
2429
getOrCreateAssociatedTokenAccount,
2530
mintTo,
2631
} from "@solana/spl-token";
27-
import { Keypair, PublicKey, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
32+
import { Keypair, PublicKey, SystemProgram, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
2833
import { BigNumber, ethers } from "ethers";
2934
import { SvmSpokeClient } from "../../src/svm";
3035
import { DepositInput } from "../../src/svm/clients/SvmSpoke";
@@ -578,6 +583,118 @@ describe("svm_spoke.deposit", () => {
578583
}
579584
});
580585

586+
it("Deposit native token, new token account", async () => {
587+
// Fund depositor account with SOL.
588+
const nativeAmount = 1_000_000_000; // 1 SOL
589+
await connection.requestAirdrop(depositor.publicKey, nativeAmount * 2); // Add buffer for transaction fees.
590+
591+
// Setup wSOL as the input token.
592+
inputToken = NATIVE_MINT;
593+
const nativeDecimals = 9;
594+
depositorTA = getAssociatedTokenAddressSync(inputToken, depositor.publicKey);
595+
await createVault();
596+
597+
// Will need to add rent exemption to the deposit amount, will recover it at the end of the transaction.
598+
const rentExempt = await getMinimumBalanceForRentExemptAccount(connection);
599+
const transferIx = SystemProgram.transfer({
600+
fromPubkey: depositor.publicKey,
601+
toPubkey: depositorTA,
602+
lamports: BigInt(nativeAmount) + BigInt(rentExempt),
603+
});
604+
605+
// Create wSOL user account.
606+
const createIx = createAssociatedTokenAccountIdempotentInstruction(
607+
depositor.publicKey,
608+
depositorTA,
609+
depositor.publicKey,
610+
inputToken
611+
);
612+
613+
const approveIx = await createApproveCheckedInstruction(
614+
depositAccounts.depositorTokenAccount,
615+
depositAccounts.mint,
616+
depositAccounts.state,
617+
depositor.publicKey,
618+
BigInt(nativeAmount),
619+
nativeDecimals,
620+
undefined,
621+
tokenProgram
622+
);
623+
624+
const nativeDepositData = { ...depositData, inputAmount: new BN(nativeAmount), outputAmount: new BN(nativeAmount) };
625+
const depositDataValues = Object.values(nativeDepositData) as DepositDataValues;
626+
const depositIx = await program.methods
627+
.deposit(...depositDataValues)
628+
.accounts(depositAccounts)
629+
.instruction();
630+
631+
const closeIx = createCloseAccountInstruction(depositorTA, depositor.publicKey, depositor.publicKey);
632+
633+
const iVaultAmount = (await getAccount(connection, vault, undefined, tokenProgram)).amount;
634+
635+
const depositTx = new Transaction().add(transferIx, createIx, approveIx, depositIx, closeIx);
636+
const tx = await sendAndConfirmTransaction(connection, depositTx, [depositor]);
637+
638+
const fVaultAmount = (await getAccount(connection, vault, undefined, tokenProgram)).amount;
639+
assertSE(
640+
fVaultAmount,
641+
iVaultAmount + BigInt(nativeAmount),
642+
"Vault balance should be increased by the deposited amount"
643+
);
644+
});
645+
646+
it("Deposit native token, existing token account", async () => {
647+
// Fund depositor account with SOL.
648+
const nativeAmount = 1_000_000_000; // 1 SOL
649+
await connection.requestAirdrop(depositor.publicKey, nativeAmount * 2); // Add buffer for transaction fees.
650+
651+
// Setup wSOL as the input token, creating the associated token account for the user.
652+
inputToken = NATIVE_MINT;
653+
const nativeDecimals = 9;
654+
depositorTA = (await getOrCreateAssociatedTokenAccount(connection, payer, inputToken, depositor.publicKey)).address;
655+
await createVault();
656+
657+
// Transfer SOL to the user token account.
658+
const transferIx = SystemProgram.transfer({
659+
fromPubkey: depositor.publicKey,
660+
toPubkey: depositorTA,
661+
lamports: nativeAmount,
662+
});
663+
664+
// Sync the user token account with the native balance.
665+
const syncIx = createSyncNativeInstruction(depositorTA);
666+
667+
const approveIx = await createApproveCheckedInstruction(
668+
depositAccounts.depositorTokenAccount,
669+
depositAccounts.mint,
670+
depositAccounts.state,
671+
depositor.publicKey,
672+
BigInt(nativeAmount),
673+
nativeDecimals,
674+
undefined,
675+
tokenProgram
676+
);
677+
678+
const nativeDepositData = { ...depositData, inputAmount: new BN(nativeAmount), outputAmount: new BN(nativeAmount) };
679+
const depositDataValues = Object.values(nativeDepositData) as DepositDataValues;
680+
const depositIx = await program.methods
681+
.deposit(...depositDataValues)
682+
.accounts(depositAccounts)
683+
.instruction();
684+
685+
const iVaultAmount = (await getAccount(connection, vault, undefined, tokenProgram)).amount;
686+
687+
const depositTx = new Transaction().add(transferIx, syncIx, approveIx, depositIx);
688+
const tx = await sendAndConfirmTransaction(connection, depositTx, [depositor]);
689+
690+
const fVaultAmount = (await getAccount(connection, vault, undefined, tokenProgram)).amount;
691+
assertSE(
692+
fVaultAmount,
693+
iVaultAmount + BigInt(nativeAmount),
694+
"Vault balance should be increased by the deposited amount"
695+
);
696+
});
697+
581698
describe("codama client and solana kit", () => {
582699
it("Deposit with with solana kit and codama client", async () => {
583700
// typescript is not happy with the depositData object

0 commit comments

Comments
 (0)