yamaswap-sdk
Version:
ETF SDK for Solana and Evm
398 lines (327 loc) • 15.6 kB
text/typescript
import * as anchor from "@coral-xyz/anchor";
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
import idl from "../idl/iswap.json";
import type { Iswap } from "../types/iswap";
import { AnchorWallet, ETFCreateParams, ETFBurnParams, MintETFTokenParams, ETFCreateResult, MintETFResult } from "../types/params";
import { deriveEtfTokenMintAccount } from "../utils/getAddress";
import { ETFQueries } from "../utils/queries";
import {
createAssociatedTokenAccountInstruction,
getAccount,
getAssociatedTokenAddressSync,
TokenAccountNotFoundError,
} from '@solana/spl-token';
import { checkATAExists, checkBalance, checkETFExists, validateETFCreateParams, validateMintETFParams } from '../utils/checks';
const DEFAULT_PROGRAM_ID = "dXgZyuguvD2m5G5385XkdokZBryUoSE6LbNJeWiFiN5";
export class DexClient {
public readonly program: anchor.Program<Iswap>;
public readonly queries: ETFQueries;
private readonly wallet: AnchorWallet;
constructor(
public connection: Connection,
wallet: AnchorWallet,
provider: anchor.AnchorProvider,
) {
this.connection = connection;
this.wallet = wallet;
this.program = new anchor.Program(idl as Iswap, provider)
this.queries = new ETFQueries(this);
}
public async createETF(params: ETFCreateParams): Promise<ETFCreateResult> {
try {
validateETFCreateParams(params);
const { name, symbol, description, url, assets } = params;
const [etfAddress] = deriveEtfTokenMintAccount(this.program as unknown as anchor.Program, ["etf_token_v3", symbol]);
const [etfCoreAddress] = deriveEtfTokenMintAccount(this.program as unknown as anchor.Program, ["etf_token_v3", etfAddress]);
const etfInfo = await this.queries.getETFInfo(etfAddress);
if (typeof etfInfo !== 'string') {
return {
success: false,
error: 'etf already exists',
data: {
etfAddress,
etfCoreAddress: etfInfo.etfCoreAddress,
symbol,
description: etfInfo.description,
creator: etfInfo.creator,
assets: etfInfo.assets
}
};
}
const latestBlockhash = await this.connection.getLatestBlockhash('confirmed');
console.log('get the latest blockhash:', latestBlockhash.blockhash);
let tx = new Transaction();
for (const { token } of assets) {
const tokenKey = new PublicKey(token);
const address = getAssociatedTokenAddressSync(tokenKey, etfCoreAddress, true);
try {
await getAccount(this.connection, address);
} catch (e) {
if (e instanceof TokenAccountNotFoundError) {
tx.add(
createAssociatedTokenAccountInstruction(
this.wallet.publicKey,
address,
etfCoreAddress,
tokenKey,
),
);
}
}
}
const ix = await this.program.methods
.etfCreate({
name,
symbol,
description,
url,
assets: assets.map(c => ({
token: new PublicKey(c.token),
weight: c.weight
}))
})
.transaction();
tx.add(ix);
tx.recentBlockhash = latestBlockhash.blockhash;
tx.feePayer = this.wallet.publicKey;
console.log('transaction created:', {
recentBlockhash: tx.recentBlockhash,
feePayer: tx.feePayer.toBase58(),
instructions: tx.instructions.length
});
try {
const signedTx = await this.wallet.signTransaction(tx);
console.log('transaction signed');
const txid = await this.connection.sendRawTransaction(signedTx.serialize(), {
skipPreflight: false,
preflightCommitment: 'confirmed',
maxRetries: 5
});
console.log('transaction sent, signed:', txid);
const confirmation = await this.connection.confirmTransaction({
signature: txid,
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
}, 'confirmed');
if (confirmation.value.err) {
throw new Error(`transaction confirm failed: ${JSON.stringify(confirmation.value.err)}`);
}
console.log('transaction confirmed');
// get the confirmed etf info
const confirmedEtfInfo = await this.queries.getETFInfo(etfAddress);
if (typeof confirmedEtfInfo === 'string') {
throw new Error('etf create success but not found etf info');
}
return {
success: true,
txid,
data: {
etfAddress,
etfCoreAddress,
symbol,
name,
description,
creator: confirmedEtfInfo.creator,
assets: confirmedEtfInfo.assets
}
};
} catch (error) {
console.error("createETF error:", error);
throw error;
}
} catch (error) {
console.error("Error details:", error);
if (error instanceof Error) {
console.error("Error stack:", error.stack);
}
return {
success: false,
txid: '',
error: error instanceof Error ? error.message : 'meet an unknown error when create ETF'
};
}
}
public async purchaseETF(params: MintETFTokenParams): Promise<MintETFResult> {
try {
const etfAddress = new PublicKey(params.etfAddress);
validateMintETFParams(params);
const etfInfo = await checkETFExists(this, etfAddress);
// check if the user has the required tokens
await checkATAExists(this, etfInfo.assets.map(item => item.token), this.wallet.publicKey);
// check if the contract has the required tokens, if the token is pda, need to pass the true
await checkATAExists(this, etfInfo.assets.map(item => item.token), etfInfo.etfCoreAddress, true);
for (const item of etfInfo.assets) {
const requiredAmount = (params.lamports * item.weight) / 10000;
await checkBalance(this, item.token, this.wallet.publicKey, requiredAmount);
}
const latestBlockhash = await this.connection.getLatestBlockhash('confirmed');
console.log('get the latest blockhash:', latestBlockhash.blockhash);
const remainingAccounts = etfInfo.assets.flatMap((item) => {
const userATA = getAssociatedTokenAddressSync(item.token, this.wallet.publicKey);
const contractATA = getAssociatedTokenAddressSync(item.token, etfInfo.etfCoreAddress, true);
return [userATA, contractATA];
});
const ix = await this.program.methods
.etfMint(new anchor.BN(params.lamports))
.accounts({
etfTokenMintAccount: params.etfAddress,
authority: this.wallet.publicKey,
})
.remainingAccounts(
remainingAccounts.map((item) => ({ pubkey: item, isSigner: false, isWritable: true })),
)
.transaction();
const modifyComputeUnits = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 400_000,
});
let tx = new Transaction().add(ix).add(modifyComputeUnits);
tx.recentBlockhash = latestBlockhash.blockhash;
tx.feePayer = this.wallet.publicKey;
console.log('transaction created:', {
recentBlockhash: tx.recentBlockhash,
feePayer: tx.feePayer.toBase58(),
instructions: tx.instructions.length
});
try {
const signedTx = await this.wallet.signTransaction(tx);
console.log('transaction signed');
// 发送交易
const txid = await this.connection.sendRawTransaction(signedTx.serialize(), {
skipPreflight: false,
preflightCommitment: 'confirmed',
maxRetries: 5
});
console.log('transaction sent, signed:', txid);
const confirmation = await this.connection.confirmTransaction({
signature: txid,
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
}, 'confirmed');
if (confirmation.value.err) {
throw new Error(`transaction confirm failed: ${JSON.stringify(confirmation.value.err)}`);
}
console.log('transaction confirmed');
// get the mint tokenata account
const mintTokenataAccount = getAssociatedTokenAddressSync(etfAddress, this.wallet.publicKey);
const balance = await this.queries.getETFBalance(etfAddress, this.wallet.publicKey);
return {
success: true,
txid,
data: {
mintTokenataAccount,
balance,
etfAddress: params.etfAddress
}
};
} catch (error) {
console.error("purchaseETF error:", error);
throw error;
}
} catch (error) {
console.error("Error in purchaseETF:");
console.error("Error details:", error);
if (error instanceof Error) {
console.error("Error name:", error.name);
console.error("Error message:", error.message);
console.error("Error stack:", error.stack);
}
if (error instanceof anchor.web3.SendTransactionError) {
console.error("Transaction error logs:", error.logs);
}
return {
success: false,
txid: '',
error: error instanceof Error ? error.message : 'Mint ETF Token 时发生未知错误'
};
}
}
public async burnETF(params: ETFBurnParams): Promise<MintETFResult> {
try {
const etfAddress = new PublicKey(params.etfAddress);
const etfInfo = await checkETFExists(this, etfAddress);
const tokensToCheck = [...etfInfo.assets.map(item => item.token), etfAddress];
await checkATAExists(this, tokensToCheck, this.wallet.publicKey);
await checkBalance(this, etfAddress, this.wallet.publicKey, params.lamports as number);
const latestBlockhash = await this.connection.getLatestBlockhash('confirmed');
console.log('get the latest blockhash:', latestBlockhash.blockhash);
const remainingAccounts = etfInfo.assets.flatMap((item) => {
const userATA = getAssociatedTokenAddressSync(item.token, this.wallet.publicKey);
const contractATA = getAssociatedTokenAddressSync(item.token, etfInfo.etfCoreAddress, true);
return [
userATA,
contractATA,
];
});
const ix = await this.program.methods
.etfBurn(new anchor.BN(params.lamports as number))
.accounts({
etfTokenMintAccount: params.etfAddress,
authority: this.wallet.publicKey,
})
.remainingAccounts(
remainingAccounts.map((item) => ({ pubkey: item, isSigner: false, isWritable: true })),
)
.transaction();
const modifyComputeUnits = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 400_000,
});
let tx = new Transaction().add(ix).add(modifyComputeUnits);
tx.recentBlockhash = latestBlockhash.blockhash;
tx.feePayer = this.wallet.publicKey;
console.log('transaction created:', {
recentBlockhash: tx.recentBlockhash,
feePayer: tx.feePayer.toBase58(),
instructions: tx.instructions.length
});
try {
const signedTx = await this.wallet.signTransaction(tx);
console.log('transaction signed');
const txid = await this.connection.sendRawTransaction(signedTx.serialize(), {
skipPreflight: false,
preflightCommitment: 'confirmed',
maxRetries: 5
});
console.log('transaction sent, signed:', txid);
const confirmation = await this.connection.confirmTransaction({
signature: txid,
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
}, 'confirmed');
if (confirmation.value.err) {
throw new Error(`transaction confirm failed: ${JSON.stringify(confirmation.value.err)}`);
}
console.log('transaction confirmed');
const mintTokenataAccount = getAssociatedTokenAddressSync(etfAddress, this.wallet.publicKey);
const balance = await this.queries.getETFBalance(etfAddress, this.wallet.publicKey);
return {
success: true,
txid,
data: {
mintTokenataAccount,
balance,
etfAddress: params.etfAddress
}
};
} catch (error) {
console.error("burnETF error:", error);
throw error;
}
} catch (error) {
console.error("Error in burnETF:");
console.error("Error details:", error);
if (error instanceof Error) {
console.error("Error name:", error.name);
console.error("Error message:", error.message);
console.error("Error stack:", error.stack);
}
if (error instanceof anchor.web3.SendTransactionError) {
console.error("Transaction error logs:", error.logs);
}
return {
success: false,
txid: '',
error: error instanceof Error ? error.message : 'Burn ETF Token 时发生未知错误'
};
}
}
}