-
Notifications
You must be signed in to change notification settings - Fork 28
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
fadeev
wants to merge
33
commits into
main
Choose a base branch
from
solana-deposit
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
b53146d
feat(solana): deposit command
fadeev 7c96815
refactor
fadeev 4c27cc1
both localnet and devnet work
fadeev 428b221
--network flag
fadeev 84f6eee
lint
fadeev 2b4478a
wip
fadeev e769aca
wip
fadeev 8acea6e
fix build
fadeev 24e7a4a
fix build
fadeev ab88103
remove from
fadeev 89b2fc1
derive to and from
fadeev 0f3d1f8
lint
fadeev fc1b6e8
default token address
fadeev 1aec56c
merge main
fadeev f928096
fix
fadeev dc2e6c5
lint
fadeev fd2e2f8
accounts import solana should correctly accept base58 private ket
fadeev 0739ade
lint
fadeev 01d5f06
fix build
fadeev db5ed3f
Changed encoding to bs58
hernan-clich 56536e9
Chaining commands
hernan-clich 96dd080
Merge branch 'main' into solana-deposit
hernan-clich b1e9f24
fix solana task import
fadeev 6638427
zod
fadeev 0e3207f
lint
fadeev c6d8cca
remove empty object inside new anchor.AnchorProvider
fadeev a4f049f
zod validation
fadeev 458783c
import solana token program
fadeev 439d823
decimals wip
fadeev a8da703
handle tx error
fadeev 255aec5
lint
fadeev 5ad56ae
check balances
fadeev 58b43da
fix build
fadeev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.