@coinbase/cdp-sdk
Version:
SDK for interacting with the Coinbase Developer Platform Wallet API
232 lines (207 loc) • 6.58 kB
text/typescript
import {
pipe,
createTransactionMessage,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
Instruction,
createSolanaRpc,
address,
compileTransaction,
setTransactionMessageFeePayer,
createNoopSigner,
getBase64EncodedWireTransaction,
} from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
import {
findAssociatedTokenPda,
getCreateAssociatedTokenInstructionAsync,
getTransferCheckedInstruction,
fetchToken,
fetchMint,
TOKEN_PROGRAM_ADDRESS,
} from "@solana-program/token";
import { createRpcClient } from "./rpc.js";
import { sendTransaction } from "./sendTransaction.js";
import {
getConnectedNetwork,
getOrCreateConnection,
getUsdcMintAddress,
type Network,
type SolanaRpcClient,
} from "./utils.js";
import type { SignatureResult } from "../../client/solana/solana.types.js";
import type { CdpOpenApiClientType } from "../../openapi-client/index.js";
export interface TransferOptions {
/**
* The base58 encoded Solana address of the source account.
*/
from: string;
/**
* The base58 encoded Solana address of the destination account.
*/
to: string;
/**
* The amount to transfer, represented as an atomic unit of the token.
*/
amount: bigint;
/**
* The token to transfer, or mint address of the SPL token to transfer.
*/
token: "sol" | "usdc" | string;
/**
* The network to use which will be used to create an RPC client, otherwise an existing RPC client can be provided.
*/
network: Network | SolanaRpcClient;
}
/**
* Transfers SOL or SPL tokens between accounts
*
* @param apiClient - The API client to use
* @param options - The transfer options
*
* @returns The transfer result
*/
export async function transfer(
apiClient: CdpOpenApiClientType,
options: TransferOptions,
): Promise<SignatureResult> {
const connection = getOrCreateConnection({ networkOrConnection: options.network });
const connectedNetwork = await getConnectedNetwork(connection);
const rpc = createRpcClient(connectedNetwork);
const base64Transaction =
options.token === "sol"
? await getNativeTransferBase64Transaction({
rpc,
from: options.from,
to: options.to,
amount: options.amount,
})
: await getSplTransferBase64Transaction({
rpc,
from: options.from,
to: options.to,
mintAddress:
options.token === "usdc" ? getUsdcMintAddress(connectedNetwork) : options.token,
amount: options.amount,
});
const signature = await sendTransaction(apiClient, {
network: connectedNetwork === "mainnet" ? "solana" : "solana-devnet",
transaction: base64Transaction,
});
return signature;
}
type GetNativeTransferOptions = Omit<TransferOptions, "token" | "network"> & {
rpc: ReturnType<typeof createSolanaRpc>;
};
/**
* Gets the transaction for a SOL transfer
*
* @param options - The options for the SOL transfer
*
* @param options.rpc - The Solana RPC client
* @param options.from - The source address
* @param options.to - The destination address
* @param options.amount - The amount in lamports to transfer
*
* @returns The SOL transfer transaction
*/
export async function getNativeTransferBase64Transaction({
rpc,
from,
to,
amount,
}: GetNativeTransferOptions): Promise<string> {
const fromAddr = address(from);
const toAddr = address(to);
const instructions: Instruction[] = [
getTransferSolInstruction({
source: createNoopSigner(fromAddr),
destination: toAddr,
amount,
}),
];
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const txMsg = pipe(
createTransactionMessage({ version: 0 }),
tx => setTransactionMessageFeePayer(fromAddr, tx),
tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
tx => appendTransactionMessageInstructions(instructions, tx),
);
const compiledTransaction = compileTransaction(txMsg);
return getBase64EncodedWireTransaction(compiledTransaction);
}
type GetSplTokenTransferOptions = Omit<TransferOptions, "network" | "token"> & {
mintAddress: string;
rpc: ReturnType<typeof createSolanaRpc>;
};
/**
* Gets the transaction for a SPL token transfer
*
* @param options - The options for the SPL token transfer
*
* @param options.rpc - The Solana RPC client
* @param options.from - The source address
* @param options.to - The destination address
* @param options.mintAddress - The mint address of the token
* @param options.amount - The amount in units of the token to transfer
*
* @returns The SPL token transfer transaction
*/
export async function getSplTransferBase64Transaction({
rpc,
from,
to,
mintAddress,
amount,
}: GetSplTokenTransferOptions): Promise<string> {
const fromAddr = address(from);
const toAddr = address(to);
const mintAddr = address(mintAddress);
const mintInfo = await fetchMint(rpc, mintAddr);
const [sourceAta] = await findAssociatedTokenPda({
mint: mintAddr,
owner: fromAddr,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
});
const [destAta] = await findAssociatedTokenPda({
mint: mintAddr,
owner: toAddr,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
});
const sourceAcct = await fetchToken(rpc, sourceAta);
if (sourceAcct.data.amount < amount) {
throw new Error(`Insufficient token balance: have ${sourceAcct.data.amount}, need ${amount}`);
}
const instructions: Instruction[] = [];
// If destination ATA does not exist, add create instruction
try {
await fetchToken(rpc, destAta);
} catch {
const createDestIx = await getCreateAssociatedTokenInstructionAsync({
payer: createNoopSigner(fromAddr),
owner: toAddr,
ata: destAta,
mint: mintAddr,
});
instructions.push(createDestIx);
}
instructions.push(
getTransferCheckedInstruction({
source: sourceAta,
mint: mintAddr,
destination: destAta,
authority: fromAddr,
amount,
decimals: mintInfo.data.decimals,
}),
);
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const txMsg = pipe(
createTransactionMessage({ version: 0 }),
tx => setTransactionMessageFeePayer(fromAddr, tx),
tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
tx => appendTransactionMessageInstructions(instructions, tx),
);
const compiledTransaction = compileTransaction(txMsg);
return getBase64EncodedWireTransaction(compiledTransaction);
}