Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions modules/sdk-coin-sol/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export enum ValidInstructionTypesEnum {
Split = 'Split',
Authorize = 'Authorize',
SetPriorityFee = 'SetPriorityFee',
MintTo = 'MintTo',
Burn = 'Burn',
}

// Internal instructions types
Expand All @@ -45,6 +47,8 @@ export enum InstructionBuilderTypes {
StakingAuthorize = 'Authorize',
StakingDelegate = 'Delegate',
SetPriorityFee = 'SetPriorityFee',
MintTo = 'MintTo',
Burn = 'Burn',
}

export const VALID_SYSTEM_INSTRUCTION_TYPES: ValidInstructionTypes[] = [
Expand All @@ -65,6 +69,8 @@ export const VALID_SYSTEM_INSTRUCTION_TYPES: ValidInstructionTypes[] = [
ValidInstructionTypesEnum.Split,
ValidInstructionTypesEnum.Authorize,
ValidInstructionTypesEnum.SetPriorityFee,
ValidInstructionTypesEnum.MintTo,
ValidInstructionTypesEnum.Burn,
];

/** Const to check the order of the Wallet Init instructions when decode */
Expand Down
34 changes: 32 additions & 2 deletions modules/sdk-coin-sol/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ export type InstructionParams =
| AtaClose
| TokenTransfer
| StakingAuthorize
| StakingDelegate;
| StakingDelegate
| MintTo
| Burn;

export interface Memo {
type: InstructionBuilderTypes.Memo;
Expand Down Expand Up @@ -78,6 +80,32 @@ export interface TokenTransfer {
};
}

export interface MintTo {
type: InstructionBuilderTypes.MintTo;
params: {
mintAddress: string;
destinationAddress: string;
authorityAddress: string;
amount: string;
tokenName: string;
decimalPlaces?: number;
programId?: string;
};
}

export interface Burn {
type: InstructionBuilderTypes.Burn;
params: {
mintAddress: string;
accountAddress: string;
authorityAddress: string;
amount: string;
tokenName: string;
decimalPlaces?: number;
programId?: string;
};
}

export interface StakingActivate {
type: InstructionBuilderTypes.StakingActivate;
params: {
Expand Down Expand Up @@ -154,7 +182,9 @@ export type ValidInstructionTypes =
| 'CloseAssociatedTokenAccount'
| DecodedCloseAccountInstruction
| 'TokenTransfer'
| 'SetPriorityFee';
| 'SetPriorityFee'
| 'MintTo'
| 'Burn';

export type StakingAuthorizeParams = {
stakingAddress: string;
Expand Down
66 changes: 64 additions & 2 deletions modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import {
DecodedTransferCheckedInstruction,
decodeTransferCheckedInstruction,
DecodedBurnInstruction,
decodeBurnInstruction,
DecodedMintToInstruction,
decodeMintToInstruction,
TOKEN_2022_PROGRAM_ID,
} from '@solana/spl-token';
import {
Expand All @@ -27,8 +31,10 @@ import { InstructionBuilderTypes, ValidInstructionTypesEnum, walletInitInstructi
import {
AtaClose,
AtaInit,
Burn,
InstructionParams,
Memo,
MintTo,
Nonce,
StakingActivate,
StakingAuthorize,
Expand Down Expand Up @@ -125,8 +131,10 @@ function parseSendInstructions(
instructions: TransactionInstruction[],
instructionMetadata?: InstructionParams[],
_useTokenAddressTokenName?: boolean
): Array<Nonce | Memo | Transfer | TokenTransfer | AtaInit | AtaClose | SetPriorityFee> {
const instructionData: Array<Nonce | Memo | Transfer | TokenTransfer | AtaInit | AtaClose | SetPriorityFee> = [];
): Array<Nonce | Memo | Transfer | TokenTransfer | AtaInit | AtaClose | SetPriorityFee | MintTo | Burn> {
const instructionData: Array<
Nonce | Memo | Transfer | TokenTransfer | AtaInit | AtaClose | SetPriorityFee | MintTo | Burn
> = [];
for (const instruction of instructions) {
const type = getInstructionType(instruction);
switch (type) {
Expand Down Expand Up @@ -232,6 +240,60 @@ function parseSendInstructions(
};
instructionData.push(setPriorityFee);
break;
case ValidInstructionTypesEnum.MintTo:
let mintToInstruction: DecodedMintToInstruction;
if (instruction.programId.toString() !== TOKEN_2022_PROGRAM_ID.toString()) {
mintToInstruction = decodeMintToInstruction(instruction);
} else {
mintToInstruction = decodeMintToInstruction(instruction, TOKEN_2022_PROGRAM_ID);
}
const mintAddressForMint = mintToInstruction.keys.mint.pubkey.toString();
const tokenNameForMint = findTokenName(mintAddressForMint, instructionMetadata, _useTokenAddressTokenName);
let programIDForMint: string | undefined;
if (instruction.programId) {
programIDForMint = instruction.programId.toString();
}
const mintTo: MintTo = {
Copy link
Contributor

@abhishekagrawal080 abhishekagrawal080 Jul 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion can we move this common piece of code in one json generator function (for line 256-268 and 284-294)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MintTo and Burn take in different params, I don't think we need a common piece of code here? Or did you mean something else? If you can elaborate.

type: InstructionBuilderTypes.MintTo,
params: {
mintAddress: mintAddressForMint,
destinationAddress: mintToInstruction.keys.destination.pubkey.toString(),
authorityAddress: mintToInstruction.keys.authority.pubkey.toString(),
amount: mintToInstruction.data.amount.toString(),
tokenName: tokenNameForMint,
decimalPlaces: undefined,
programId: programIDForMint,
},
};
instructionData.push(mintTo);
break;
case ValidInstructionTypesEnum.Burn:
let burnInstruction: DecodedBurnInstruction;
if (instruction.programId.toString() !== TOKEN_2022_PROGRAM_ID.toString()) {
burnInstruction = decodeBurnInstruction(instruction);
} else {
burnInstruction = decodeBurnInstruction(instruction, TOKEN_2022_PROGRAM_ID);
}
const mintAddressForBurn = burnInstruction.keys.mint.pubkey.toString();
const tokenNameForBurn = findTokenName(mintAddressForBurn, instructionMetadata, _useTokenAddressTokenName);
let programIDForBurn: string | undefined;
if (instruction.programId) {
programIDForBurn = instruction.programId.toString();
}
const burn: Burn = {
type: InstructionBuilderTypes.Burn,
params: {
mintAddress: mintAddressForBurn,
accountAddress: burnInstruction.keys.account.pubkey.toString(),
authorityAddress: burnInstruction.keys.owner.pubkey.toString(),
amount: burnInstruction.data.amount.toString(),
tokenName: tokenNameForBurn,
decimalPlaces: undefined,
programId: programIDForBurn,
},
};
instructionData.push(burn);
break;
default:
throw new NotSupported(
'Invalid transaction, instruction type not supported: ' + getInstructionType(instruction)
Expand Down
66 changes: 66 additions & 0 deletions modules/sdk-coin-sol/src/lib/solInstructionFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { SolCoin } from '@bitgo/statics';
import {
createAssociatedTokenAccountInstruction,
createCloseAccountInstruction,
createMintToInstruction,
createBurnInstruction,
createTransferCheckedInstruction,
TOKEN_2022_PROGRAM_ID,
} from '@solana/spl-token';
Expand All @@ -24,6 +26,8 @@ import {
AtaInit,
InstructionParams,
Memo,
MintTo,
Burn,
Nonce,
StakingActivate,
StakingAuthorize,
Expand Down Expand Up @@ -71,6 +75,10 @@ export function solInstructionFactory(instructionToBuild: InstructionParams): Tr
return stakingDelegateInstruction(instructionToBuild);
case InstructionBuilderTypes.SetPriorityFee:
return fetchPriorityFeeInstruction(instructionToBuild);
case InstructionBuilderTypes.MintTo:
return mintToInstruction(instructionToBuild);
case InstructionBuilderTypes.Burn:
return burnInstruction(instructionToBuild);
default:
throw new Error(`Invalid instruction type or not supported`);
}
Expand Down Expand Up @@ -480,3 +488,61 @@ function stakingDelegateInstruction(data: StakingDelegate): TransactionInstructi

return tx.instructions;
}

/**
* Construct MintTo Solana instructions
*
* @param {MintTo} data - the data to build the instruction
* @returns {TransactionInstruction[]} An array containing MintTo Solana instructions
*/
function mintToInstruction(data: MintTo): TransactionInstruction[] {
const {
params: { mintAddress, destinationAddress, authorityAddress, amount, programId },
} = data;
assert(mintAddress, 'Missing mintAddress param');
assert(destinationAddress, 'Missing destinationAddress param');
assert(authorityAddress, 'Missing authorityAddress param');
assert(amount, 'Missing amount param');

const mint = new PublicKey(mintAddress);
const destination = new PublicKey(destinationAddress);
const authority = new PublicKey(authorityAddress);

let mintToInstr: TransactionInstruction;
if (programId && programId === TOKEN_2022_PROGRAM_ID.toString()) {
mintToInstr = createMintToInstruction(mint, destination, authority, BigInt(amount), [], TOKEN_2022_PROGRAM_ID);
} else {
mintToInstr = createMintToInstruction(mint, destination, authority, BigInt(amount));
}

return [mintToInstr];
}

/**
* Construct Burn Solana instructions
*
* @param {Burn} data - the data to build the instruction
* @returns {TransactionInstruction[]} An array containing Burn Solana instructions
*/
function burnInstruction(data: Burn): TransactionInstruction[] {
const {
params: { mintAddress, accountAddress, authorityAddress, amount, programId },
} = data;
assert(mintAddress, 'Missing mintAddress param');
assert(accountAddress, 'Missing accountAddress param');
assert(authorityAddress, 'Missing authorityAddress param');
assert(amount, 'Missing amount param');

const mint = new PublicKey(mintAddress);
const account = new PublicKey(accountAddress);
const authority = new PublicKey(authorityAddress);

let burnInstr: TransactionInstruction;
if (programId && programId === TOKEN_2022_PROGRAM_ID.toString()) {
burnInstr = createBurnInstruction(account, mint, authority, BigInt(amount), [], TOKEN_2022_PROGRAM_ID);
} else {
burnInstr = createBurnInstruction(account, mint, authority, BigInt(amount));
}

return [burnInstr];
}
49 changes: 42 additions & 7 deletions modules/sdk-coin-sol/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ import { BaseCoin, BaseNetwork, CoinNotDefinedError, coins, SolCoin } from '@bit
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
decodeCloseAccountInstruction,
decodeBurnInstruction,
decodeMintToInstruction,
getAssociatedTokenAddress,
TOKEN_PROGRAM_ID,
TOKEN_2022_PROGRAM_ID,
DecodedBurnInstruction,
DecodedMintToInstruction,
} from '@solana/spl-token';
import {
Keypair,
Expand Down Expand Up @@ -279,7 +283,7 @@ export function getTransactionType(transaction: SolTransaction): TransactionType
// check if deactivate instruction does not exist because deactivate can be include a transfer instruction
const memoInstruction = instructions.find((instruction) => getInstructionType(instruction) === 'Memo');
const memoData = memoInstruction?.data.toString('utf-8');
if (instructions.filter((instruction) => getInstructionType(instruction) === 'Deactivate').length == 0) {
if (instructions.filter((instruction) => getInstructionType(instruction) === 'Deactivate').length === 0) {
for (const instruction of instructions) {
const instructionType = getInstructionType(instruction);
// Check if memo instruction is there and if it contains 'PrepareForRevoke' because Marinade staking deactivate transaction will have this
Expand Down Expand Up @@ -345,9 +349,40 @@ export function getInstructionType(instruction: TransactionInstruction): ValidIn
return 'CloseAssociatedTokenAccount';
}
} catch (e) {
// ignore error and default to TokenTransfer
return 'TokenTransfer';
// ignore error and continue to check for other instruction types
}

// Check for burn instructions (instruction code 8)
try {
let burnInstruction: DecodedBurnInstruction;
if (instruction.programId.toString() !== TOKEN_2022_PROGRAM_ID.toString()) {
burnInstruction = decodeBurnInstruction(instruction);
} else {
burnInstruction = decodeBurnInstruction(instruction, TOKEN_2022_PROGRAM_ID);
}
if (burnInstruction && burnInstruction.data.instruction === 8) {
return 'Burn';
}
} catch (e) {
// ignore error and continue to check for other instruction types
}

// Check for mint instructions (instruction code 7)
try {
let mintInstruction: DecodedMintToInstruction;
if (instruction.programId.toString() !== TOKEN_2022_PROGRAM_ID.toString()) {
mintInstruction = decodeMintToInstruction(instruction);
} else {
mintInstruction = decodeMintToInstruction(instruction, TOKEN_2022_PROGRAM_ID);
}
if (mintInstruction && mintInstruction.data.instruction === 7) {
return 'MintTo';
}
} catch (e) {
// ignore error and continue to check for other instruction types
}

// Default to TokenTransfer for other token instructions
return 'TokenTransfer';
case StakeProgram.programId.toString():
return StakeInstruction.decodeInstructionType(instruction);
Expand Down Expand Up @@ -546,8 +581,8 @@ export async function getAssociatedTokenAccountAddress(

if (!programId) {
const coin = getSolTokenFromAddressOnly(tokenMintAddress);
if (coin && coin instanceof SolCoin && (coin as any).programId) {
programId = (coin as any).programId.toString();
if (coin && coin instanceof SolCoin && 'programId' in coin && coin.programId) {
programId = coin.programId.toString();
} else {
programId = TOKEN_PROGRAM_ID.toString();
}
Expand All @@ -562,13 +597,13 @@ export async function getAssociatedTokenAccountAddress(
return ataAddress.toString();
}

export function validateMintAddress(mintAddress: string) {
export function validateMintAddress(mintAddress: string): void {
if (!mintAddress || !isValidAddress(mintAddress)) {
throw new BuildTransactionError('Invalid or missing mintAddress, got: ' + mintAddress);
}
}

export function validateOwnerAddress(ownerAddress: string) {
export function validateOwnerAddress(ownerAddress: string): void {
if (!ownerAddress || !isValidAddress(ownerAddress)) {
throw new BuildTransactionError('Invalid or missing ownerAddress, got: ' + ownerAddress);
}
Expand Down
Loading