@solsdk/xswap_sdk
Version:
Universal cross-chain swaps SDK
302 lines (252 loc) • 9.26 kB
text/typescript
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,
},
};
}
}