Skip to content

feat(solana): deposit command #317

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "18"
node-version: "21"
registry-url: "https://registry.npmjs.org"

- name: Install Dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "18"
node-version: "21"
registry-url: "https://registry.npmjs.org"

- name: Install Dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "18"
node-version: "21"
Copy link
Member

Choose a reason for hiding this comment

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

Ideally, we should address these types of changes in their own separate PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree, but the test were failing because of the node version. So either we update node in this PR or merge with failing tests.

registry-url: "https://registry.npmjs.org"

- name: Install Dependencies
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
"@zetachain/faucet-cli": "^4.1.1",
"@zetachain/networks": "14.0.0-rc1",
"@zetachain/protocol-contracts": "^12.0.0",
"@zetachain/protocol-contracts-solana": "2.0.0-rc1",
"@zetachain/protocol-contracts-solana": "3.0.2-rc2",
"axios": "^1.4.0",
"bech32": "^2.0.0",
"bip39": "^3.1.0",
Expand Down
5 changes: 3 additions & 2 deletions packages/client/src/solanaDeposit.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import * as anchor from "@coral-xyz/anchor";
import {
Connection,
Transaction,
TransactionMessage,
VersionedTransaction,
} from "@solana/web3.js";
import { getEndpoints } from "@zetachain/networks";
import Gateway_IDL from "@zetachain/protocol-contracts-solana/idl/gateway.json";
import Gateway_IDL from "@zetachain/protocol-contracts-solana/dev/idl/gateway.json";
import { ethers } from "ethers";

import { handleError } from "../../../utils/handleError";
Expand Down Expand Up @@ -111,7 +112,7 @@ export const solanaDeposit = async function (

txSignature = await this.solanaAdapter.sendTransaction(
versionedTransaction,
connection
connection as unknown as Connection
);
} else {
txSignature = await anchor.web3.sendAndConfirmTransaction(
Expand Down
5 changes: 3 additions & 2 deletions packages/client/src/solanaDepositAndCall.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import * as anchor from "@coral-xyz/anchor";
import {
Connection,
Transaction,
TransactionMessage,
VersionedTransaction,
} from "@solana/web3.js";
import { getEndpoints } from "@zetachain/networks";
import Gateway_IDL from "@zetachain/protocol-contracts-solana/idl/gateway.json";
import Gateway_IDL from "@zetachain/protocol-contracts-solana/dev/idl/gateway.json";
import { AbiCoder, ethers } from "ethers";

import { ParseAbiValuesReturnType } from "../../../types/parseAbiValues.types";
Expand Down Expand Up @@ -126,7 +127,7 @@ export const solanaDepositAndCall = async function (

txSignature = await this.solanaAdapter.sendTransaction(
versionedTransaction,
connection
connection as unknown as Connection
);
} else {
txSignature = await anchor.web3.sendAndConfirmTransaction(
Expand Down
210 changes: 210 additions & 0 deletions packages/commands/src/solana/deposit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import * as anchor from "@coral-xyz/anchor";
import { Wallet } from "@coral-xyz/anchor";
import {
AccountLayout,
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { clusterApiUrl, PublicKey } from "@solana/web3.js";
import GATEWAY_DEV_IDL from "@zetachain/protocol-contracts-solana/dev/idl/gateway.json";
import GATEWAY_PROD_IDL from "@zetachain/protocol-contracts-solana/prod/idl/gateway.json";
import * as bip39 from "bip39";
import bs58 from "bs58";
import { Command, Option } from "commander";
import { ethers } from "ethers";
import { z } from "zod";

import {
SOLANA_NETWORKS,
SOLANA_TOKEN_PROGRAM,
} from "../../../../types/shared.constants";
import { handleError, validateAndParseSchema } from "../../../../utils";
import { solanaDepositOptionsSchema } from "../../../../utils/solana.commands.helpers";

type DepositOptions = z.infer<typeof solanaDepositOptionsSchema>;

export const keypairFromMnemonic = async (
mnemonic: string
): Promise<anchor.web3.Keypair> => {
const seed = await bip39.mnemonicToSeed(mnemonic);
const seedSlice = new Uint8Array(seed).slice(0, 32);
return anchor.web3.Keypair.fromSeed(seedSlice);
};

export const keypairFromPrivateKey = (
privateKey: string
): anchor.web3.Keypair => {
try {
const decodedKey = bs58.decode(privateKey);
return anchor.web3.Keypair.fromSecretKey(decodedKey);
} catch (error) {
throw new Error(
"Invalid private key format. Expected base58-encoded private key."
);
}
};

const main = async (options: DepositOptions) => {
// Mainnet and devnet use the same IDL
const gatewayIDL =
options.network === "localnet" ? GATEWAY_DEV_IDL : GATEWAY_PROD_IDL;

let keypair: anchor.web3.Keypair;
if (options.privateKey) {
keypair = keypairFromPrivateKey(options.privateKey);
} else if (options.mnemonic) {
keypair = await keypairFromMnemonic(options.mnemonic);
}

let API = "http://localhost:8899";
if (options.network === "devnet") {
API = clusterApiUrl("devnet");
} else if (options.network === "mainnet") {
API = clusterApiUrl("mainnet-beta");
}

const connection = new anchor.web3.Connection(API);

const provider = new anchor.AnchorProvider(connection, new Wallet(keypair!));

const gatewayProgram = new anchor.Program(gatewayIDL as anchor.Idl, provider);

const receiverBytes = ethers.getBytes(options.recipient);

const tokenAccounts = await connection.getTokenAccountsByOwner(
provider.wallet.publicKey,
{
programId: TOKEN_PROGRAM_ID,
}
);

try {
if (options.mint) {
const mintInfo = await connection.getTokenSupply(
new PublicKey(options.mint)
);
const decimals = mintInfo.value.decimals;

// Find the token account that matches the mint
const matchingTokenAccount = tokenAccounts.value.find(({ account }) => {
const data = AccountLayout.decode(account.data);
return new PublicKey(data.mint).toBase58() === options.mint;
});

if (!matchingTokenAccount) {
throw new Error(`No token account found for mint ${options.mint}`);
}

// Check token balance
const accountInfo = await connection.getTokenAccountBalance(
matchingTokenAccount.pubkey
);
const balance = accountInfo.value.uiAmount;
const amountToSend = parseFloat(options.amount);
if (!balance || balance < amountToSend) {
throw new Error(
`Insufficient token balance. Available: ${
balance ?? 0
}, Required: ${amountToSend}`
);
}

const from = matchingTokenAccount.pubkey;

// Find the TSS PDA (meta)
const [tssPda] = PublicKey.findProgramAddressSync(
[Buffer.from("meta", "utf-8")],
gatewayProgram.programId
);

// Find the TSS's ATA for the mint
const tssAta = await PublicKey.findProgramAddress(
[
tssPda.toBuffer(),
TOKEN_PROGRAM_ID.toBuffer(),
new PublicKey(options.mint).toBuffer(),
],
ASSOCIATED_TOKEN_PROGRAM_ID
);

const to = tssAta[0].toBase58();

const tx = await gatewayProgram.methods
.depositSplToken(
new anchor.BN(ethers.parseUnits(options.amount, decimals).toString()),
receiverBytes,
null
)
.accounts({
from,
mintAccount: options.mint,
signer: keypair!.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
to,
tokenProgram: options.tokenProgram,
})
.rpc();
console.log("Transaction hash:", tx);
} else {
// Check SOL balance
const balance = await connection.getBalance(keypair!.publicKey);
const lamportsNeeded = ethers.parseUnits(options.amount, 9).toString();
if (balance < parseInt(lamportsNeeded)) {
throw new Error(
`Insufficient SOL balance. Available: ${balance / 1e9}, Required: ${
options.amount
}`
);
}
const tx = await gatewayProgram.methods
.deposit(
new anchor.BN(ethers.parseUnits(options.amount, 9).toString()),
receiverBytes,
null
)
.accounts({})
.rpc();
console.log("Transaction hash:", tx);
}
} catch (error) {
handleError({
context: "Error during deposit",
error,
shouldThrow: false,
});
process.exit(1);
}
};

export const depositCommand = new Command("deposit")
.description("Deposit tokens from Solana")
.requiredOption("--amount <amount>", "Amount of tokens to deposit")
.requiredOption("--recipient <recipient>", "Recipient address")
.addOption(
new Option("--mnemonic <mnemonic>", "Mnemonic").conflicts(["private-key"])
)
.addOption(
new Option(
"--private-key <privateKey>",
"Private key in base58 format"
).conflicts(["mnemonic"])
)
.option(
"--token-program <tokenProgram>",
"Token program",
SOLANA_TOKEN_PROGRAM
)
.option("--mint <mint>", "SPL token mint address")
.addOption(
new Option("--network <network>", "Solana network").choices(SOLANA_NETWORKS)
)
.action(async (options) => {
const validatedOptions = validateAndParseSchema(
options,
solanaDepositOptionsSchema,
{
exitOnError: true,
}
);
await main(validatedOptions);
});
11 changes: 6 additions & 5 deletions packages/commands/src/solana/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Command } from "commander";

import { depositCommand } from "./deposit";
import { encodeCommand } from "./encode";

export const solanaCommand = new Command("solana").description(
"Solana commands"
);

solanaCommand.addCommand(encodeCommand).helpCommand(false);
export const solanaCommand = new Command("solana")
.description("Solana commands")
.addCommand(depositCommand)
.addCommand(encodeCommand)
.helpCommand(false);
5 changes: 2 additions & 3 deletions packages/tasks/src/solanaDeposit.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Wallet } from "@coral-xyz/anchor";
import { Keypair } from "@solana/web3.js";
import { Wallet, web3 } from "@coral-xyz/anchor";
import { bech32 } from "bech32";
import { ethers } from "ethers";
import { task } from "hardhat/config";
Expand Down Expand Up @@ -81,7 +80,7 @@ export const getKeypairFromFile = async (filepath: string) => {
throw new Error(`Invalid secret key file at '${filepath}'!`);
}

return Keypair.fromSecretKey(parsedFileContents);
return web3.Keypair.fromSecretKey(parsedFileContents);
};

task("solana-deposit", "Solana deposit", solanaDeposit)
Expand Down
5 changes: 2 additions & 3 deletions packages/tasks/src/solanaDepositAndCall.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Wallet } from "@coral-xyz/anchor";
import { Keypair } from "@solana/web3.js";
import { Wallet, web3 } from "@coral-xyz/anchor";
import { bech32 } from "bech32";
import { ethers } from "ethers";
import { task } from "hardhat/config";
Expand Down Expand Up @@ -105,7 +104,7 @@ export const getKeypairFromFile = async (filepath: string) => {
throw new Error(`Invalid secret key file at '${filepath}'!`);
}

return Keypair.fromSecretKey(parsedFileContents);
return web3.Keypair.fromSecretKey(parsedFileContents);
};

task("solana-deposit-and-call", "Solana deposit and call", solanaDepositAndCall)
Expand Down
Loading