Skip to content

Commit b8c3df5

Browse files
authored
Merge pull request #6564 from BitGo/TMS-977-sol-mint-burn-instructions
feat: add sol mint and burn instruction functions
2 parents b0c9335 + 28dcc38 commit b8c3df5

File tree

7 files changed

+600
-12
lines changed

7 files changed

+600
-12
lines changed

modules/sdk-coin-sol/src/lib/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export enum ValidInstructionTypesEnum {
2828
Split = 'Split',
2929
Authorize = 'Authorize',
3030
SetPriorityFee = 'SetPriorityFee',
31+
MintTo = 'MintTo',
32+
Burn = 'Burn',
3133
}
3234

3335
// Internal instructions types
@@ -45,6 +47,8 @@ export enum InstructionBuilderTypes {
4547
StakingAuthorize = 'Authorize',
4648
StakingDelegate = 'Delegate',
4749
SetPriorityFee = 'SetPriorityFee',
50+
MintTo = 'MintTo',
51+
Burn = 'Burn',
4852
}
4953

5054
export const VALID_SYSTEM_INSTRUCTION_TYPES: ValidInstructionTypes[] = [
@@ -65,6 +69,8 @@ export const VALID_SYSTEM_INSTRUCTION_TYPES: ValidInstructionTypes[] = [
6569
ValidInstructionTypesEnum.Split,
6670
ValidInstructionTypesEnum.Authorize,
6771
ValidInstructionTypesEnum.SetPriorityFee,
72+
ValidInstructionTypesEnum.MintTo,
73+
ValidInstructionTypesEnum.Burn,
6874
];
6975

7076
/** Const to check the order of the Wallet Init instructions when decode */

modules/sdk-coin-sol/src/lib/iface.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ export type InstructionParams =
3838
| AtaClose
3939
| TokenTransfer
4040
| StakingAuthorize
41-
| StakingDelegate;
41+
| StakingDelegate
42+
| MintTo
43+
| Burn;
4244

4345
export interface Memo {
4446
type: InstructionBuilderTypes.Memo;
@@ -78,6 +80,32 @@ export interface TokenTransfer {
7880
};
7981
}
8082

83+
export interface MintTo {
84+
type: InstructionBuilderTypes.MintTo;
85+
params: {
86+
mintAddress: string;
87+
destinationAddress: string;
88+
authorityAddress: string;
89+
amount: string;
90+
tokenName: string;
91+
decimalPlaces?: number;
92+
programId?: string;
93+
};
94+
}
95+
96+
export interface Burn {
97+
type: InstructionBuilderTypes.Burn;
98+
params: {
99+
mintAddress: string;
100+
accountAddress: string;
101+
authorityAddress: string;
102+
amount: string;
103+
tokenName: string;
104+
decimalPlaces?: number;
105+
programId?: string;
106+
};
107+
}
108+
81109
export interface StakingActivate {
82110
type: InstructionBuilderTypes.StakingActivate;
83111
params: {
@@ -154,7 +182,9 @@ export type ValidInstructionTypes =
154182
| 'CloseAssociatedTokenAccount'
155183
| DecodedCloseAccountInstruction
156184
| 'TokenTransfer'
157-
| 'SetPriorityFee';
185+
| 'SetPriorityFee'
186+
| 'MintTo'
187+
| 'Burn';
158188

159189
export type StakingAuthorizeParams = {
160190
stakingAddress: string;

modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import {
22
DecodedTransferCheckedInstruction,
33
decodeTransferCheckedInstruction,
4+
DecodedBurnInstruction,
5+
decodeBurnInstruction,
6+
DecodedMintToInstruction,
7+
decodeMintToInstruction,
48
TOKEN_2022_PROGRAM_ID,
59
} from '@solana/spl-token';
610
import {
@@ -27,8 +31,10 @@ import { InstructionBuilderTypes, ValidInstructionTypesEnum, walletInitInstructi
2731
import {
2832
AtaClose,
2933
AtaInit,
34+
Burn,
3035
InstructionParams,
3136
Memo,
37+
MintTo,
3238
Nonce,
3339
StakingActivate,
3440
StakingAuthorize,
@@ -125,8 +131,10 @@ function parseSendInstructions(
125131
instructions: TransactionInstruction[],
126132
instructionMetadata?: InstructionParams[],
127133
_useTokenAddressTokenName?: boolean
128-
): Array<Nonce | Memo | Transfer | TokenTransfer | AtaInit | AtaClose | SetPriorityFee> {
129-
const instructionData: Array<Nonce | Memo | Transfer | TokenTransfer | AtaInit | AtaClose | SetPriorityFee> = [];
134+
): Array<Nonce | Memo | Transfer | TokenTransfer | AtaInit | AtaClose | SetPriorityFee | MintTo | Burn> {
135+
const instructionData: Array<
136+
Nonce | Memo | Transfer | TokenTransfer | AtaInit | AtaClose | SetPriorityFee | MintTo | Burn
137+
> = [];
130138
for (const instruction of instructions) {
131139
const type = getInstructionType(instruction);
132140
switch (type) {
@@ -232,6 +240,60 @@ function parseSendInstructions(
232240
};
233241
instructionData.push(setPriorityFee);
234242
break;
243+
case ValidInstructionTypesEnum.MintTo:
244+
let mintToInstruction: DecodedMintToInstruction;
245+
if (instruction.programId.toString() !== TOKEN_2022_PROGRAM_ID.toString()) {
246+
mintToInstruction = decodeMintToInstruction(instruction);
247+
} else {
248+
mintToInstruction = decodeMintToInstruction(instruction, TOKEN_2022_PROGRAM_ID);
249+
}
250+
const mintAddressForMint = mintToInstruction.keys.mint.pubkey.toString();
251+
const tokenNameForMint = findTokenName(mintAddressForMint, instructionMetadata, _useTokenAddressTokenName);
252+
let programIDForMint: string | undefined;
253+
if (instruction.programId) {
254+
programIDForMint = instruction.programId.toString();
255+
}
256+
const mintTo: MintTo = {
257+
type: InstructionBuilderTypes.MintTo,
258+
params: {
259+
mintAddress: mintAddressForMint,
260+
destinationAddress: mintToInstruction.keys.destination.pubkey.toString(),
261+
authorityAddress: mintToInstruction.keys.authority.pubkey.toString(),
262+
amount: mintToInstruction.data.amount.toString(),
263+
tokenName: tokenNameForMint,
264+
decimalPlaces: undefined,
265+
programId: programIDForMint,
266+
},
267+
};
268+
instructionData.push(mintTo);
269+
break;
270+
case ValidInstructionTypesEnum.Burn:
271+
let burnInstruction: DecodedBurnInstruction;
272+
if (instruction.programId.toString() !== TOKEN_2022_PROGRAM_ID.toString()) {
273+
burnInstruction = decodeBurnInstruction(instruction);
274+
} else {
275+
burnInstruction = decodeBurnInstruction(instruction, TOKEN_2022_PROGRAM_ID);
276+
}
277+
const mintAddressForBurn = burnInstruction.keys.mint.pubkey.toString();
278+
const tokenNameForBurn = findTokenName(mintAddressForBurn, instructionMetadata, _useTokenAddressTokenName);
279+
let programIDForBurn: string | undefined;
280+
if (instruction.programId) {
281+
programIDForBurn = instruction.programId.toString();
282+
}
283+
const burn: Burn = {
284+
type: InstructionBuilderTypes.Burn,
285+
params: {
286+
mintAddress: mintAddressForBurn,
287+
accountAddress: burnInstruction.keys.account.pubkey.toString(),
288+
authorityAddress: burnInstruction.keys.owner.pubkey.toString(),
289+
amount: burnInstruction.data.amount.toString(),
290+
tokenName: tokenNameForBurn,
291+
decimalPlaces: undefined,
292+
programId: programIDForBurn,
293+
},
294+
};
295+
instructionData.push(burn);
296+
break;
235297
default:
236298
throw new NotSupported(
237299
'Invalid transaction, instruction type not supported: ' + getInstructionType(instruction)

modules/sdk-coin-sol/src/lib/solInstructionFactory.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { SolCoin } from '@bitgo/statics';
22
import {
33
createAssociatedTokenAccountInstruction,
44
createCloseAccountInstruction,
5+
createMintToInstruction,
6+
createBurnInstruction,
57
createTransferCheckedInstruction,
68
TOKEN_2022_PROGRAM_ID,
79
} from '@solana/spl-token';
@@ -24,6 +26,8 @@ import {
2426
AtaInit,
2527
InstructionParams,
2628
Memo,
29+
MintTo,
30+
Burn,
2731
Nonce,
2832
StakingActivate,
2933
StakingAuthorize,
@@ -71,6 +75,10 @@ export function solInstructionFactory(instructionToBuild: InstructionParams): Tr
7175
return stakingDelegateInstruction(instructionToBuild);
7276
case InstructionBuilderTypes.SetPriorityFee:
7377
return fetchPriorityFeeInstruction(instructionToBuild);
78+
case InstructionBuilderTypes.MintTo:
79+
return mintToInstruction(instructionToBuild);
80+
case InstructionBuilderTypes.Burn:
81+
return burnInstruction(instructionToBuild);
7482
default:
7583
throw new Error(`Invalid instruction type or not supported`);
7684
}
@@ -480,3 +488,61 @@ function stakingDelegateInstruction(data: StakingDelegate): TransactionInstructi
480488

481489
return tx.instructions;
482490
}
491+
492+
/**
493+
* Construct MintTo Solana instructions
494+
*
495+
* @param {MintTo} data - the data to build the instruction
496+
* @returns {TransactionInstruction[]} An array containing MintTo Solana instructions
497+
*/
498+
function mintToInstruction(data: MintTo): TransactionInstruction[] {
499+
const {
500+
params: { mintAddress, destinationAddress, authorityAddress, amount, programId },
501+
} = data;
502+
assert(mintAddress, 'Missing mintAddress param');
503+
assert(destinationAddress, 'Missing destinationAddress param');
504+
assert(authorityAddress, 'Missing authorityAddress param');
505+
assert(amount, 'Missing amount param');
506+
507+
const mint = new PublicKey(mintAddress);
508+
const destination = new PublicKey(destinationAddress);
509+
const authority = new PublicKey(authorityAddress);
510+
511+
let mintToInstr: TransactionInstruction;
512+
if (programId && programId === TOKEN_2022_PROGRAM_ID.toString()) {
513+
mintToInstr = createMintToInstruction(mint, destination, authority, BigInt(amount), [], TOKEN_2022_PROGRAM_ID);
514+
} else {
515+
mintToInstr = createMintToInstruction(mint, destination, authority, BigInt(amount));
516+
}
517+
518+
return [mintToInstr];
519+
}
520+
521+
/**
522+
* Construct Burn Solana instructions
523+
*
524+
* @param {Burn} data - the data to build the instruction
525+
* @returns {TransactionInstruction[]} An array containing Burn Solana instructions
526+
*/
527+
function burnInstruction(data: Burn): TransactionInstruction[] {
528+
const {
529+
params: { mintAddress, accountAddress, authorityAddress, amount, programId },
530+
} = data;
531+
assert(mintAddress, 'Missing mintAddress param');
532+
assert(accountAddress, 'Missing accountAddress param');
533+
assert(authorityAddress, 'Missing authorityAddress param');
534+
assert(amount, 'Missing amount param');
535+
536+
const mint = new PublicKey(mintAddress);
537+
const account = new PublicKey(accountAddress);
538+
const authority = new PublicKey(authorityAddress);
539+
540+
let burnInstr: TransactionInstruction;
541+
if (programId && programId === TOKEN_2022_PROGRAM_ID.toString()) {
542+
burnInstr = createBurnInstruction(account, mint, authority, BigInt(amount), [], TOKEN_2022_PROGRAM_ID);
543+
} else {
544+
burnInstr = createBurnInstruction(account, mint, authority, BigInt(amount));
545+
}
546+
547+
return [burnInstr];
548+
}

modules/sdk-coin-sol/src/lib/utils.ts

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ import { BaseCoin, BaseNetwork, CoinNotDefinedError, coins, SolCoin } from '@bit
1010
import {
1111
ASSOCIATED_TOKEN_PROGRAM_ID,
1212
decodeCloseAccountInstruction,
13+
decodeBurnInstruction,
14+
decodeMintToInstruction,
1315
getAssociatedTokenAddress,
1416
TOKEN_PROGRAM_ID,
1517
TOKEN_2022_PROGRAM_ID,
18+
DecodedBurnInstruction,
19+
DecodedMintToInstruction,
1620
} from '@solana/spl-token';
1721
import {
1822
Keypair,
@@ -279,7 +283,7 @@ export function getTransactionType(transaction: SolTransaction): TransactionType
279283
// check if deactivate instruction does not exist because deactivate can be include a transfer instruction
280284
const memoInstruction = instructions.find((instruction) => getInstructionType(instruction) === 'Memo');
281285
const memoData = memoInstruction?.data.toString('utf-8');
282-
if (instructions.filter((instruction) => getInstructionType(instruction) === 'Deactivate').length == 0) {
286+
if (instructions.filter((instruction) => getInstructionType(instruction) === 'Deactivate').length === 0) {
283287
for (const instruction of instructions) {
284288
const instructionType = getInstructionType(instruction);
285289
// Check if memo instruction is there and if it contains 'PrepareForRevoke' because Marinade staking deactivate transaction will have this
@@ -345,9 +349,40 @@ export function getInstructionType(instruction: TransactionInstruction): ValidIn
345349
return 'CloseAssociatedTokenAccount';
346350
}
347351
} catch (e) {
348-
// ignore error and default to TokenTransfer
349-
return 'TokenTransfer';
352+
// ignore error and continue to check for other instruction types
350353
}
354+
355+
// Check for burn instructions (instruction code 8)
356+
try {
357+
let burnInstruction: DecodedBurnInstruction;
358+
if (instruction.programId.toString() !== TOKEN_2022_PROGRAM_ID.toString()) {
359+
burnInstruction = decodeBurnInstruction(instruction);
360+
} else {
361+
burnInstruction = decodeBurnInstruction(instruction, TOKEN_2022_PROGRAM_ID);
362+
}
363+
if (burnInstruction && burnInstruction.data.instruction === 8) {
364+
return 'Burn';
365+
}
366+
} catch (e) {
367+
// ignore error and continue to check for other instruction types
368+
}
369+
370+
// Check for mint instructions (instruction code 7)
371+
try {
372+
let mintInstruction: DecodedMintToInstruction;
373+
if (instruction.programId.toString() !== TOKEN_2022_PROGRAM_ID.toString()) {
374+
mintInstruction = decodeMintToInstruction(instruction);
375+
} else {
376+
mintInstruction = decodeMintToInstruction(instruction, TOKEN_2022_PROGRAM_ID);
377+
}
378+
if (mintInstruction && mintInstruction.data.instruction === 7) {
379+
return 'MintTo';
380+
}
381+
} catch (e) {
382+
// ignore error and continue to check for other instruction types
383+
}
384+
385+
// Default to TokenTransfer for other token instructions
351386
return 'TokenTransfer';
352387
case StakeProgram.programId.toString():
353388
return StakeInstruction.decodeInstructionType(instruction);
@@ -546,8 +581,8 @@ export async function getAssociatedTokenAccountAddress(
546581

547582
if (!programId) {
548583
const coin = getSolTokenFromAddressOnly(tokenMintAddress);
549-
if (coin && coin instanceof SolCoin && (coin as any).programId) {
550-
programId = (coin as any).programId.toString();
584+
if (coin && coin instanceof SolCoin && 'programId' in coin && coin.programId) {
585+
programId = coin.programId.toString();
551586
} else {
552587
programId = TOKEN_PROGRAM_ID.toString();
553588
}
@@ -562,13 +597,13 @@ export async function getAssociatedTokenAccountAddress(
562597
return ataAddress.toString();
563598
}
564599

565-
export function validateMintAddress(mintAddress: string) {
600+
export function validateMintAddress(mintAddress: string): void {
566601
if (!mintAddress || !isValidAddress(mintAddress)) {
567602
throw new BuildTransactionError('Invalid or missing mintAddress, got: ' + mintAddress);
568603
}
569604
}
570605

571-
export function validateOwnerAddress(ownerAddress: string) {
606+
export function validateOwnerAddress(ownerAddress: string): void {
572607
if (!ownerAddress || !isValidAddress(ownerAddress)) {
573608
throw new BuildTransactionError('Invalid or missing ownerAddress, got: ' + ownerAddress);
574609
}

0 commit comments

Comments
 (0)