Skip to content

Commit

Permalink
Add Solana support to ZetaChain client and browser deposits (#199)
Browse files Browse the repository at this point in the history
Co-authored-by: Denis Fadeev <[email protected]>
  • Loading branch information
lukema95 and fadeev authored Nov 29, 2024
1 parent b38d277 commit 856177b
Show file tree
Hide file tree
Showing 5 changed files with 488 additions and 63 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,11 @@
"@nomiclabs/hardhat-ethers": "^2.2.3",
"@openzeppelin/contracts": "^5.0.2",
"@openzeppelin/contracts-upgradeable": "^5.0.2",
"@solana/wallet-adapter-react": "^0.15.35",
"@solana/web3.js": "^1.95.3",
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@zetachain/faucet-cli": "^4.1.1",
"@zetachain/networks": "10.0.0",
"@zetachain/networks": "v10.0.0-rc1",
"@zetachain/protocol-contracts": "11.0.0-rc3",
"axios": "^1.4.0",
"bech32": "^2.0.0",
Expand Down
52 changes: 49 additions & 3 deletions packages/client/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import type { Wallet as SolanaWallet } from "@coral-xyz/anchor";
import type { WalletContextState } from "@solana/wallet-adapter-react";
import { PublicKey } from "@solana/web3.js";
import { networks } from "@zetachain/networks";
import type { Signer, Wallet } from "ethers";
import merge from "lodash/merge";
Expand Down Expand Up @@ -33,16 +36,45 @@ export interface ZetaChainClientParamsBase {

export type ZetaChainClientParams = ZetaChainClientParamsBase &
(
| { signer: Signer; wallet?: never }
| { signer?: never; wallet: Wallet }
| { signer?: undefined; wallet?: undefined }
| {
signer: Signer;
solanaAdapter?: never;
solanaWallet?: never;
wallet?: never;
}
| {
signer?: never;
solanaAdapter: WalletContextState;
solanaWallet?: never;
wallet?: never;
}
| {
signer?: never;
solanaAdapter?: never;
solanaWallet: SolanaWallet;
wallet?: never;
}
| {
signer?: never;
solanaAdapter?: never;
solanaWallet?: never;
wallet: Wallet;
}
| {
signer?: undefined;
solanaAdapter?: undefined;
solanaWallet?: undefined;
wallet?: undefined;
}
);

export class ZetaChainClient {
public chains: { [key: string]: any };
public network: string;
public wallet: Wallet | undefined;
public signer: any | undefined;
public solanaWallet: SolanaWallet | undefined;
public solanaAdapter: WalletContextState | undefined;

/**
* Initializes ZetaChainClient instance.
Expand Down Expand Up @@ -96,6 +128,10 @@ export class ZetaChainClient {
this.wallet = params.wallet;
} else if (params.signer) {
this.signer = params.signer;
} else if (params.solanaWallet) {
this.solanaWallet = params.solanaWallet;
} else if (params.solanaAdapter) {
this.solanaAdapter = params.solanaAdapter;
}
this.chains = { ...networks };
this.network = params.network || "";
Expand All @@ -115,6 +151,16 @@ export class ZetaChainClient {
return this.chains;
}

public isSolanaWalletConnected(): boolean {
return this.solanaAdapter?.connected || this.solanaWallet !== undefined;
}

public getSolanaPublicKey(): PublicKey | null {
return (
this.solanaAdapter?.publicKey || this.solanaWallet?.publicKey || null
);
}

getEndpoint = getEndpoint;
getBalances = getBalances;
getForeignCoins = getForeignCoins;
Expand Down
123 changes: 74 additions & 49 deletions packages/client/src/solanaDeposit.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as anchor from "@coral-xyz/anchor";
import { Keypair } from "@solana/web3.js";
import { TransactionMessage, VersionedTransaction } from "@solana/web3.js";
import { Transaction } from "@solana/web3.js";
import { getEndpoints } from "@zetachain/networks";
import { ethers } from "ethers";

import { ZetaChainClient } from "./client";
Expand All @@ -11,21 +13,53 @@ export const solanaDeposit = async function (
this: ZetaChainClient,
args: {
amount: number;
api: string;
idPath: string;
params: any[];
recipient: string;
}
) {
const keypair = await getKeypairFromFile(args.idPath);
const wallet = new anchor.Wallet(keypair);

const connection = new anchor.web3.Connection(args.api);
const provider = new anchor.AnchorProvider(
connection,
wallet,
anchor.AnchorProvider.defaultOptions()
);
if (!this.isSolanaWalletConnected()) {
throw new Error("Solana wallet not connected");
}

const network = "solana_" + this.network;
const api = getEndpoints("solana" as any, network);

const connection = new anchor.web3.Connection(api[0].url);

let provider;
if (this.solanaAdapter) {
const walletAdapter = {
publicKey: this.solanaAdapter.publicKey!,
signAllTransactions: async (txs: Transaction[]) => {
if (!this.solanaAdapter?.signAllTransactions) {
throw new Error(
"Wallet does not support signing multiple transactions"
);
}
return await this.solanaAdapter.signAllTransactions(txs);
},
signTransaction: async (tx: Transaction) => {
if (!this.solanaAdapter?.signTransaction) {
throw new Error("Wallet does not support transaction signing");
}
return await this.solanaAdapter.signTransaction(tx);
},
};

provider = new anchor.AnchorProvider(
connection,
walletAdapter as any,
anchor.AnchorProvider.defaultOptions()
);
} else if (this.solanaWallet) {
provider = new anchor.AnchorProvider(
connection,
this.solanaWallet,
anchor.AnchorProvider.defaultOptions()
);
} else {
throw new Error("No valid Solana wallet found");
}
anchor.setProvider(provider);

const programId = new anchor.web3.PublicKey(Gateway_IDL.address);
Expand Down Expand Up @@ -58,53 +92,44 @@ export const solanaDeposit = async function (
.deposit(depositAmount, m)
.accounts({
pda: pdaAccount,
signer: wallet.publicKey,
signer: this.solanaAdapter
? this.solanaAdapter.publicKey!
: this.solanaWallet!.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.instruction();

tx.add(depositInstruction);

// Send the transaction
const txSignature = await anchor.web3.sendAndConfirmTransaction(
connection,
tx,
[keypair]
);
let txSignature;
if (this.solanaAdapter) {
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash();
const messageLegacy = new TransactionMessage({
instructions: tx.instructions,
payerKey: this.solanaAdapter.publicKey!,
recentBlockhash: blockhash,
}).compileToV0Message();

console.log("Transaction signature:", txSignature);
} catch (error) {
console.error("Transaction failed:", error);
}
};
const versionedTransaction = new VersionedTransaction(messageLegacy);

const getKeypairFromFile = async (filepath: string) => {
const path = await import("path");
if (filepath[0] === "~") {
const home = process.env.HOME || null;
if (home) {
filepath = path.join(home, filepath.slice(1));
txSignature = await this.solanaAdapter.sendTransaction(
versionedTransaction,
connection
);
} else {
txSignature = await anchor.web3.sendAndConfirmTransaction(
connection,
tx,
[this.solanaWallet!.payer]
);
}
}
// Get contents of file
let fileContents;
try {
const { readFile } = await import("fs/promises");
const fileContentsBuffer = await readFile(filepath);
fileContents = fileContentsBuffer.toString();

console.log("Transaction signature:", txSignature);

return txSignature;
} catch (error) {
throw new Error(`Could not read keypair from file at '${filepath}'`);
}
// Parse contents of file
let parsedFileContents;
try {
parsedFileContents = Uint8Array.from(JSON.parse(fileContents));
} catch (thrownObject) {
const error: any = thrownObject;
if (!error.message.includes("Unexpected token")) {
throw error;
}
throw new Error(`Invalid secret key file at '${filepath}'!`);
console.error("Transaction failed:", error);
}
return Keypair.fromSecretKey(parsedFileContents);
};
47 changes: 43 additions & 4 deletions packages/tasks/src/solanaDeposit.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Wallet } from "@coral-xyz/anchor";
import { Keypair } from "@solana/web3.js";
import bech32 from "bech32";
import { utils } from "ethers";
import { task } from "hardhat/config";
Expand All @@ -9,7 +11,13 @@ export const solanaDeposit = async (
args: any,
hre: HardhatRuntimeEnvironment
) => {
const client = new ZetaChainClient({ network: "testnet" });
const keypair = await getKeypairFromFile(args.idPath);
const wallet = new Wallet(keypair);

const client = new ZetaChainClient({
network: args.solanaNetwork,
solanaWallet: wallet,
});
let recipient;
try {
if ((bech32 as any).decode(args.recipient)) {
Expand All @@ -21,15 +29,46 @@ export const solanaDeposit = async (
} catch (e) {
recipient = args.recipient;
}
const { amount, api, idPath } = args;
const { amount, idPath } = args;
const params = [JSON.parse(args.types), args.values];
await client.solanaDeposit({ amount, api, idPath, params, recipient });
await client.solanaDeposit({ amount, params, recipient });
};

task("solana-deposit", "Solana deposit", solanaDeposit)
.addParam("amount", "Amount of SOL to deposit")
.addParam("recipient", "Universal contract address")
.addOptionalParam("api", "Solana API", "https://api.devnet.solana.com")
.addOptionalParam("solanaNetwork", "Solana Network", "devnet")
.addOptionalParam("idPath", "Path to id.json", "~/.config/solana/id.json")
.addParam("types", "The types of the parameters (example: ['string'])")
.addVariadicPositionalParam("values", "The values of the parameters");

export const getKeypairFromFile = async (filepath: string) => {
const path = await import("path");
if (filepath[0] === "~") {
const home = process.env.HOME || null;
if (home) {
filepath = path.join(home, filepath.slice(1));
}
}
// Get contents of file
let fileContents;
try {
const { readFile } = await import("fs/promises");
const fileContentsBuffer = await readFile(filepath);
fileContents = fileContentsBuffer.toString();
} catch (error) {
throw new Error(`Could not read keypair from file at '${filepath}'`);
}
// Parse contents of file
let parsedFileContents;
try {
parsedFileContents = Uint8Array.from(JSON.parse(fileContents));
} catch (thrownObject) {
const error: any = thrownObject;
if (!error.message.includes("Unexpected token")) {
throw error;
}
throw new Error(`Invalid secret key file at '${filepath}'!`);
}
return Keypair.fromSecretKey(parsedFileContents);
};
Loading

0 comments on commit 856177b

Please sign in to comment.