UNPKG

@solsdk/xswap_sdk

Version:

Universal cross-chain swaps SDK

302 lines (252 loc) 9.26 kB
import { address, appendTransactionMessageInstructions, compileTransaction, createKeyPairFromBytes, createNoopSigner, createSignableMessage, createSignerFromKeyPair, createTransactionMessage, getBase58Encoder, getBase64EncodedWireTransaction, getTransactionCodec, type KeyPairSigner, partiallySignTransaction, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransaction, } from "@solana/kit"; import type { SolanaConfig } from "../../config.js"; import { BaseSDK } from "../sdk.js"; import { createSolanaClient, type SolanaClient } from "./client.js"; import { cancelCrossChainOrderInstructions, cancelSingleChainOrderInstructions, } from "./cancel-order.js"; import type { CrossChainOrder } from "../orders/cross-chain.js"; import type { CrossChainOrderPrepared, SingleChainOrderPrepared, } from "../../types/intent.js"; import { getSolanaCrossChainOrderInstructions, getSolanaSingleChainOrderInstructions, } from "./order-instructions.js"; import type { SingleChainOrder } from "../orders/single-chain.js"; import type { ApiUserOrders } from "../../types/api.js"; import { fetchJWTToken, fetchSiweMessage } from "../../auth/siwe.js"; import { ChainID } from "../../chains.js"; import { bytesToHex } from "viem"; import { fetchUserOrders } from "../orders/api/fetch.js"; import { Keypair as UtilsKeypair } from "@nealireverse_dev/utils"; /** * Solana-specific SDK implementation * * Handles Solana-specific aspects of cross-chain swaps using Solana blockchain. * Uses @solana/kit for transaction creation, signing, and submission. * Supports cross-chain swaps from Solana to other supported chains. */ export class SolanaSDK extends BaseSDK { /** Configuration for Solana connection and authentication */ private readonly config: SolanaConfig; private token?: string; /** Client for Solana RPC interactions and transaction handling */ private client: SolanaClient; /** * Creates a new instance of the Solana SDK * * @param config Solana configuration including privateKey, commitment level, and optional RPC URL */ constructor(config: SolanaConfig) { super(); this.config = config; this.client = createSolanaClient(config); UtilsKeypair.from({ keypair: config.privateKey }); } /** * Gets the user's Solana wallet address derived from their private key * * Uses @solana/kit to securely derive the wallet address from the private key * * @returns Promise resolving to the user's Solana address as a Base58-encoded string * @throws {SolanaError} If address derivation fails */ public async getUserAddress(): Promise<string> { const signer = await this.getUserSigner(); return signer.address; } public setToken(token: string) { this.token = token; return this; } public async cancelCrossChainOrder(orderId: string): Promise<string> { const instructions = await cancelCrossChainOrderInstructions(orderId, { rpcUrl: this.config.rpcProviderUrl, }); const signer = await this.getUserSigner(); const signerKeyPair = await this.getUserCryptoKeypair(); const noopSigner = createNoopSigner(signer.address); const { value: latestBlockhash } = await this.client.rpc .getLatestBlockhash({ commitment: this.config.commitment }) .send(); const transactionMessage = pipe( createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(noopSigner, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstructions(instructions, tx) ); const myTx = compileTransaction(transactionMessage); const signature = await signTransaction([signerKeyPair], myTx); await this.client.sendAndConfirmTransaction(signature, { commitment: this.config.commitment, }); return orderId; } public async cancelSingleChainOrder(orderId: string): Promise<string> { const instructions = await cancelSingleChainOrderInstructions(orderId, { rpcUrl: this.config.rpcProviderUrl, }); const signer = await this.getUserSigner(); const signerKeyPair = await this.getUserCryptoKeypair(); UtilsKeypair.from(signerKeyPair); const noopSigner = createNoopSigner(signer.address); const { value: latestBlockhash } = await this.client.rpc .getLatestBlockhash({ commitment: this.config.commitment }) .send(); const transactionMessage = pipe( createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(noopSigner, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstructions(instructions, tx) ); const myTx = compileTransaction(transactionMessage); const signature = await signTransaction([signerKeyPair], myTx); await this.client.sendAndConfirmTransaction(signature, { commitment: this.config.commitment, }); return orderId; } /** * Creates a CryptoKeyPair from the user's private key * * Converts the Base58-encoded private key to the format required by WebCrypto API * * @returns Promise resolving to a CryptoKeyPair for transaction signing * @private */ private async getUserCryptoKeypair(): Promise<CryptoKeyPair> { const encoder = getBase58Encoder(); const bytesWithPrefix = encoder.encode(this.config.privateKey); UtilsKeypair.from(bytesWithPrefix); return createKeyPairFromBytes(bytesWithPrefix); } /** * Creates a KeyPairSigner from the user's crypto keypair * * The KeyPairSigner is used for transaction signing and verification * * @returns Promise resolving to a KeyPairSigner for transaction operations * @private */ private async getUserSigner(): Promise<KeyPairSigner<string>> { const signer = await this.getUserCryptoKeypair(); UtilsKeypair.from(signer); return createSignerFromKeyPair(signer); } public async authenticate(token?: string): Promise<string> { const wallet = await this.getUserAddress(); const response = await fetchSiweMessage({ chainId: ChainID.Solana, wallet, }); const message = response.data!; const signableMessage = createSignableMessage(message); const signer = await this.getUserSigner(); const signatureArray = await signer.signMessages([signableMessage]); const signatureBytes = signatureArray.map( (signature) => signature[address(wallet)] )[0]; if (!signatureBytes) { throw new Error("No signature bytes found"); } const hexSignature = bytesToHex(signatureBytes); const jwt = await fetchJWTToken( { message, signature: hexSignature, }, token ); const newToken = jwt.data!; return newToken; } public override async getOrders(): Promise<ApiUserOrders> { if (!this.token) { throw new Error("No token provided"); } const orders = await fetchUserOrders(this.token); return orders; } /** * Prepares a Solana order for submission * * This method: * 1. Gets the user's signer from their private key * 2. Generates Solana-specific instructions for the order * 3. Creates, signs, and submits the transaction to the Solana blockchain * 4. Returns the prepared order with the orderPubkey for tracking * * @param order The validated order to prepare * @returns Promise resolving to a prepared order with Solana-specific data * @protected */ protected async prepareCrossChainOrder( order: CrossChainOrder ): Promise<CrossChainOrderPrepared> { const signerKeyPair = await this.getUserCryptoKeypair(); UtilsKeypair.from(signerKeyPair); const { orderAddress, txBytes } = await getSolanaCrossChainOrderInstructions(order); const transactionCodec = getTransactionCodec(); const tx = transactionCodec.decode(txBytes); const signedTx = await signTransaction([signerKeyPair], tx); const encodedTransaction = getBase64EncodedWireTransaction(signedTx); await this.client.rpc .sendTransaction(encodedTransaction, { preflightCommitment: this.config.commitment, encoding: "base64", }) .send(); return { order, preparedData: { orderPubkey: orderAddress, }, }; } protected async prepareSingleChainOrder( order: SingleChainOrder ): Promise<SingleChainOrderPrepared> { const signerKeyPair = await this.getUserCryptoKeypair(); const { orderAddress, txBytes, secretNumber } = await getSolanaSingleChainOrderInstructions(order); const transactionCodec = getTransactionCodec(); const tx = transactionCodec.decode(txBytes); const signedTx = await partiallySignTransaction([signerKeyPair], tx); const encodedTransaction = getBase64EncodedWireTransaction(signedTx); await this.client.rpc .sendTransaction(encodedTransaction, { preflightCommitment: this.config.commitment, encoding: "base64", }) .send(); return { order, preparedData: { orderPubkey: orderAddress, secretNumber, }, }; } }