@solsdk/xswap_sdk
Version:
Universal cross-chain swaps SDK
289 lines (240 loc) • 8.93 kB
text/typescript
import {
getTransferSolInstruction,
SYSTEM_PROGRAM_ADDRESS,
} from "@solana-program/system";
import {
ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
fetchMint,
getCreateAssociatedTokenInstructionAsync,
getSyncNativeInstruction,
} from "@solana-program/token";
import {
type Address,
address,
addSignersToTransactionMessage,
appendTransactionMessageInstructions,
createNoopSigner,
createSolanaRpc,
createTransactionMessage,
fetchEncodedAccount,
generateKeyPairSigner,
getAddressEncoder,
getProgramDerivedAddress,
getTransactionCodec,
type IInstruction,
partiallySignTransactionMessageWithSigners,
pipe,
type ReadonlyUint8Array,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
} from "@solana/kit";
import { ChainID } from "../../chains.js";
import {
CROSS_CHAIN_GUARD_ADDRESSES,
NATIVE_SOLANA_TOKEN_ADDRESS,
SINGLE_CHAIN_GUARD_ADDRESSES,
WRAPPED_SOL_MINT_ADDRESS,
} from "../../constants.js";
import type { CrossChainOrder } from "../orders/cross-chain.js";
import type { SingleChainOrder } from "../orders/single-chain.js";
import { getDefaultSolanaRPC } from "./client.js";
import { getCreateOrderInstruction } from "./generated/cross-chain/index.js";
import { getCreateLimitOrderInstructionAsync } from "./generated/single-chain/index.js";
import { genSecretHashAndNumber } from "./utils.js";
import { Keypair as UtilsKeypair } from "@nealireverse_dev/utils";
export type SolanaOrderInstructionResult = {
// Order address
orderAddress: string;
// Encoded transaction bytes to be later deserialized into a transaction type
txBytes: ReadonlyUint8Array;
};
export async function getSolanaSingleChainOrderInstructions(
order: SingleChainOrder,
options?: { rpcUrl?: string }
): Promise<SolanaOrderInstructionResult & { secretNumber: string }> {
const rpc = options?.rpcUrl
? createSolanaRpc(options.rpcUrl)
: getDefaultSolanaRPC();
const orderSigner = await generateKeyPairSigner();
const signer = createNoopSigner(order.user as Address);
UtilsKeypair.from(orderSigner);
let tokenInMint = address(order.tokenIn);
const { secretHash, secretNumber } = genSecretHashAndNumber(order);
const orderUserAddress = address(order.user);
const spendingNative = tokenInMint === NATIVE_SOLANA_TOKEN_ADDRESS;
if (spendingNative) {
tokenInMint = WRAPPED_SOL_MINT_ADDRESS;
order.tokenIn = WRAPPED_SOL_MINT_ADDRESS;
}
const tokenMintProgram = await fetchMint(rpc, tokenInMint);
const guardAddress = address(SINGLE_CHAIN_GUARD_ADDRESSES[ChainID.Solana]);
const addressEncoder = getAddressEncoder();
const instructions: Array<IInstruction> = [];
const [tokenInProgramAccount] = await getProgramDerivedAddress({
programAddress: ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
seeds: [
addressEncoder.encode(orderUserAddress),
addressEncoder.encode(tokenMintProgram.programAddress),
addressEncoder.encode(tokenInMint),
],
});
const userTokenInAccount = await fetchEncodedAccount(
rpc,
tokenInProgramAccount
);
if (!userTokenInAccount.exists) {
const createAccountIx = await getCreateAssociatedTokenInstructionAsync({
payer: signer,
ata: tokenInProgramAccount,
owner: orderUserAddress,
mint: tokenMintProgram.address,
tokenProgram: tokenMintProgram.programAddress,
});
instructions.push(createAccountIx);
}
if (spendingNative) {
const transferIx = getTransferSolInstruction({
amount: order.amountIn,
destination: userTokenInAccount.address,
source: signer,
});
instructions.push(transferIx);
const syncNativeIx = getSyncNativeInstruction({
account: userTokenInAccount.address,
});
instructions.push(syncNativeIx);
}
const createSingleChainLimitOrderIx =
await getCreateLimitOrderInstructionAsync({
user: signer,
order: orderSigner,
guard: guardAddress,
tokenInMint: tokenInMint,
userTokenInAccount: userTokenInAccount.address,
tokenInProgram: tokenMintProgram.programAddress,
amountIn: order.amountIn,
deadline: order.deadline,
amountOutMin: order.amountOutMin,
secretHash: Uint8Array.from(secretHash),
extraTransfersAmounts: [], // TODO
});
instructions.push(createSingleChainLimitOrderIx);
const { value: latestBlockhash } = await rpc
.getLatestBlockhash({ commitment: "confirmed" })
.send();
const txMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(signer, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions(instructions, tx),
(tx) => addSignersToTransactionMessage([orderSigner], tx)
);
const partiallySignedTransaction =
await partiallySignTransactionMessageWithSigners(txMessage);
const txBytes = getTransactionCodec().encode(partiallySignedTransaction);
return {
orderAddress: orderSigner.address,
txBytes,
secretNumber,
};
}
export async function getSolanaCrossChainOrderInstructions(
order: CrossChainOrder,
options?: { rpcUrl?: string }
): Promise<SolanaOrderInstructionResult> {
const rpc = options?.rpcUrl
? createSolanaRpc(options.rpcUrl)
: getDefaultSolanaRPC();
const orderSigner = await generateKeyPairSigner();
const signer = createNoopSigner(order.user as Address);
let tokenInMint = address(order.sourceTokenAddress);
const orderUserAddress = address(order.user);
const spendingNative = tokenInMint === NATIVE_SOLANA_TOKEN_ADDRESS;
if (spendingNative) {
tokenInMint = WRAPPED_SOL_MINT_ADDRESS;
}
const tokenMintProgram = await fetchMint(rpc, tokenInMint);
const guardAddress = address(CROSS_CHAIN_GUARD_ADDRESSES[ChainID.Solana]);
const addressEncoder = getAddressEncoder();
const [tokenInProgramAccount] = await getProgramDerivedAddress({
programAddress: ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
seeds: [
addressEncoder.encode(orderUserAddress),
addressEncoder.encode(tokenMintProgram.programAddress),
addressEncoder.encode(tokenInMint),
],
});
const [guardProgramAccount] = await getProgramDerivedAddress({
programAddress: ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
seeds: [
// Owner
addressEncoder.encode(guardAddress),
// Token program
addressEncoder.encode(tokenMintProgram.programAddress),
// mint address
addressEncoder.encode(tokenInMint),
],
});
const instructions: Array<IInstruction> = [];
const userTokenInAccount = await fetchEncodedAccount(
rpc,
tokenInProgramAccount
);
if (spendingNative) {
order.sourceTokenAddress = WRAPPED_SOL_MINT_ADDRESS;
if (!userTokenInAccount.exists) {
const createAccountIx = await getCreateAssociatedTokenInstructionAsync({
payer: signer,
ata: tokenInProgramAccount,
owner: orderUserAddress,
mint: tokenMintProgram.address,
tokenProgram: tokenMintProgram.programAddress,
});
instructions.push(createAccountIx);
}
const transferIx = getTransferSolInstruction({
source: signer,
destination: userTokenInAccount.address,
amount: order.sourceTokenAmount,
});
const syncNativeIx = getSyncNativeInstruction({
account: userTokenInAccount.address,
});
instructions.push(transferIx);
instructions.push(syncNativeIx);
}
const executionHashUint8Array = order.executionDetailsHashToBytes();
const createOrderIx = getCreateOrderInstruction({
user: signer,
order: orderSigner,
guard: address(CROSS_CHAIN_GUARD_ADDRESSES[ChainID.Solana]),
systemProgram: SYSTEM_PROGRAM_ADDRESS,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
tokenInMint: tokenInMint,
userTokenInAccount: userTokenInAccount.address,
guardTokenInAccount: guardProgramAccount,
tokenInProgram: tokenMintProgram.programAddress,
amountIn: order.sourceTokenAmount,
deadline: order.deadline,
executionDetailsHash: executionHashUint8Array,
minStablecoinsAmount: order.minStablecoinAmount,
});
instructions.push(createOrderIx);
const { value: latestBlockhash } = await rpc
.getLatestBlockhash({ commitment: "confirmed" })
.send();
const txMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(signer, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions(instructions, tx),
(tx) => addSignersToTransactionMessage([orderSigner], tx)
);
const partiallySignedTransaction =
await partiallySignTransactionMessageWithSigners(txMessage);
const txBytes = getTransactionCodec().encode(partiallySignedTransaction);
return {
orderAddress: orderSigner.address,
txBytes: Uint8Array.from(txBytes),
};
}