@robertprp/intents-sdk
Version:
Shogun Network Intent-based cross-chain swaps SDK
272 lines (219 loc) • 8.87 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';
/**
* 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,
},
};
}
}