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

Merged
merged 43 commits into from
Jun 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
b53146d
feat(solana): deposit command
fadeev May 2, 2025
7c96815
refactor
fadeev May 2, 2025
4c27cc1
both localnet and devnet work
fadeev May 2, 2025
428b221
--network flag
fadeev May 2, 2025
84f6eee
lint
fadeev May 2, 2025
2b4478a
wip
fadeev May 2, 2025
e769aca
wip
fadeev May 2, 2025
8acea6e
fix build
fadeev May 2, 2025
24e7a4a
fix build
fadeev May 2, 2025
ab88103
remove from
fadeev May 2, 2025
89b2fc1
derive to and from
fadeev May 2, 2025
0f3d1f8
lint
fadeev May 2, 2025
fc1b6e8
default token address
fadeev May 2, 2025
1aec56c
merge main
fadeev May 17, 2025
f928096
fix
fadeev May 17, 2025
dc2e6c5
lint
fadeev May 17, 2025
fd2e2f8
accounts import solana should correctly accept base58 private ket
fadeev May 17, 2025
0739ade
lint
fadeev May 17, 2025
01d5f06
fix build
fadeev May 19, 2025
db5ed3f
Changed encoding to bs58
hernan-clich May 20, 2025
56536e9
Chaining commands
hernan-clich May 20, 2025
96dd080
Merge branch 'main' into solana-deposit
hernan-clich May 20, 2025
b1e9f24
fix solana task import
fadeev May 21, 2025
6638427
zod
fadeev May 21, 2025
0e3207f
lint
fadeev May 21, 2025
c6d8cca
remove empty object inside new anchor.AnchorProvider
fadeev May 21, 2025
a4f049f
zod validation
fadeev May 21, 2025
458783c
import solana token program
fadeev May 21, 2025
439d823
decimals wip
fadeev May 21, 2025
a8da703
handle tx error
fadeev May 21, 2025
255aec5
lint
fadeev May 21, 2025
5ad56ae
check balances
fadeev May 21, 2025
58b43da
fix build
fadeev May 21, 2025
7a6ef82
move solana helpers
fadeev May 28, 2025
1278c16
Merge branch 'main' into solana-deposit
fadeev May 28, 2025
195b7a6
createSolanaCommandWithCommonOptions
fadeev May 28, 2025
ee5e98c
feat: added --name option to common solana options
fadeev May 28, 2025
ffe2311
lint
fadeev May 28, 2025
03faf79
validate mnemonic
fadeev May 28, 2025
b35a0d5
Remove redundant refine rule
hernan-clich Jun 5, 2025
65e592b
Move trim0x function to utils folder
hernan-clich Jun 5, 2025
00348fb
Accept solana hex pks
hernan-clich Jun 5, 2025
720d40d
Fix inconsistent error messages in test expectations.
hernan-clich Jun 5, 2025
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"
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
4 changes: 2 additions & 2 deletions packages/commands/src/bitcoin/inscription/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import {
bitcoinEncode,
EncodingFormat,
OpCode,
trimOx,
} from "../../../../../utils/bitcoinEncode";
import { trim0x } from "../../../../../utils/trim0x";
import { validateAndParseSchema } from "../../../../../utils/validateAndParseSchema";

type CallOptions = z.infer<typeof inscriptionCallOptionsSchema>;
Expand Down Expand Up @@ -56,7 +56,7 @@ const main = async (options: CallOptions) => {
data = Buffer.from(
bitcoinEncode(
options.receiver,
Buffer.from(trimOx(payload), "hex"),
Buffer.from(trim0x(payload), "hex"),
options.revertAddress,
OpCode.Call,
EncodingFormat.EncodingFmtABI
Expand Down
4 changes: 2 additions & 2 deletions packages/commands/src/bitcoin/inscription/depositAndCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import {
bitcoinEncode,
EncodingFormat,
OpCode,
trimOx,
} from "../../../../../utils/bitcoinEncode";
import { trim0x } from "../../../../../utils/trim0x";
import { validateAndParseSchema } from "../../../../../utils/validateAndParseSchema";

type DepositAndCallOptions = z.infer<
Expand Down Expand Up @@ -59,7 +59,7 @@ const main = async (options: DepositAndCallOptions) => {
data = Buffer.from(
bitcoinEncode(
options.receiver,
Buffer.from(trimOx(payload), "hex"),
Buffer.from(trim0x(payload), "hex"),
options.revertAddress,
OpCode.DepositAndCall,
EncodingFormat.EncodingFmtABI
Expand Down
4 changes: 2 additions & 2 deletions packages/commands/src/bitcoin/inscription/encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
bitcoinEncode,
EncodingFormat,
OpCode,
trimOx,
} from "../../../../../utils/bitcoinEncode";
import { trim0x } from "../../../../../utils/trim0x";

const encodeOptionsSchema = z
.object({
Expand Down Expand Up @@ -39,7 +39,7 @@ const main = (options: EncodeOptions) => {
payloadBuffer = Buffer.from([]);
} else {
const encodedPayload = new ethers.AbiCoder().encode(types, values);
payloadBuffer = Buffer.from(trimOx(encodedPayload), "hex");
payloadBuffer = Buffer.from(trim0x(encodedPayload), "hex");
}

// Encode the data
Expand Down
185 changes: 185 additions & 0 deletions packages/commands/src/solana/deposit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
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 { ethers } from "ethers";
import { z } from "zod";

import { SolanaAccountData } from "../../../../types/accounts.types";
import { SOLANA_TOKEN_PROGRAM } from "../../../../types/shared.constants";
import { handleError, validateAndParseSchema } from "../../../../utils";
import { getAccountData } from "../../../../utils/accounts";
import {
createSolanaCommandWithCommonOptions,
keypairFromMnemonic,
keypairFromPrivateKey,
solanaDepositOptionsSchema,
} from "../../../../utils/solana.commands.helpers";

type DepositOptions = z.infer<typeof solanaDepositOptionsSchema>;

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);
} else if (options.name) {
const privateKey = getAccountData<SolanaAccountData>(
"solana",
options.name
)?.privateKey;
keypair = keypairFromPrivateKey(privateKey!);
} else {
throw new Error("No account provided");
}

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 = createSolanaCommandWithCommonOptions("deposit")
.description("Deposit tokens from Solana")
.requiredOption("--amount <amount>", "Amount of tokens to deposit")
.option(
"--token-program <tokenProgram>",
"Token program",
SOLANA_TOKEN_PROGRAM
)
.option("--mint <mint>", "SPL token mint address")
.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
10 changes: 3 additions & 7 deletions test/bitcoinEncode.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { ethers } from "ethers";

import {
Address,
bitcoinEncode,
BtcAddress,
trimOx,
} from "../utils/bitcoinEncode";
import { Address, bitcoinEncode, BtcAddress } from "../utils/bitcoinEncode";
import { trim0x } from "../utils/trim0x";

describe("bitcoinEncode", () => {
const receiverAddress: Address = "0xEA9808f0Ac504d1F521B5BbdfC33e6f1953757a7";
Expand Down Expand Up @@ -40,7 +36,7 @@ describe("bitcoinEncode", () => {
["uint8", "bytes"],
[mockOperation.test, params]
);
const payload = Buffer.from(trimOx(message), "hex");
const payload = Buffer.from(trim0x(message), "hex");
const result = bitcoinEncode(receiverAddress, payload, btcRevertAddress);

expect(result).toMatch(
Expand Down
Loading