UNPKG

@robertprp/intents-sdk

Version:

Shogun Network Intent-based cross-chain swaps SDK

272 lines (219 loc) 8.87 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'; /** * 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); } /** * 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(); 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); 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(); 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(); 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, }, }; } }