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