Skip to content

feat: add spl token operations builder for sol #6583

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
40 changes: 22 additions & 18 deletions modules/sdk-coin-sol/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,30 +80,34 @@ export interface TokenTransfer {
};
}

export interface MintToParams {
mintAddress?: string;
destinationAddress: string;
authorityAddress: string;
amount: string;
tokenName?: string;
decimalPlaces?: number;
programId?: string;
}

export interface BurnParams {
mintAddress?: string;
accountAddress: string;
authorityAddress: string;
amount: string;
tokenName?: string;
decimalPlaces?: number;
programId?: string;
}

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

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

export interface StakingActivate {
Expand Down
3 changes: 3 additions & 0 deletions modules/sdk-coin-sol/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as Utils from './utils';
export { AtaInitializationBuilder } from './ataInitializationBuilder';
export { CloseAtaBuilder } from './closeAtaBuilder';
export { KeyPair } from './keyPair';
export { SplTokenOpsBuilder } from './splTokenOpsBuilder';
export { StakingActivateBuilder } from './stakingActivateBuilder';
export { StakingAuthorizeBuilder } from './stakingAuthorizeBuilder';
export { StakingDeactivateBuilder } from './stakingDeactivateBuilder';
Expand All @@ -17,5 +18,7 @@ export { TransactionBuilderFactory } from './transactionBuilderFactory';
export { TransferBuilder } from './transferBuilder';
export { TransferBuilderV2 } from './transferBuilderV2';
export { WalletInitializationBuilder } from './walletInitializationBuilder';
export { MintTo, Burn, MintToParams, BurnParams } from './iface';
export { InstructionBuilderTypes } from './constants';
export { Interface, Utils };
export { MessageBuilderFactory } from './messages';
275 changes: 275 additions & 0 deletions modules/sdk-coin-sol/src/lib/splTokenOpsBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core';
import { Transaction } from './transaction';
import { getSolTokenFromTokenName, isValidAmount, validateAddress, validateMintAddress } from './utils';
import { InstructionBuilderTypes } from './constants';
import { MintTo, Burn, SetPriorityFee, MintToParams, BurnParams } from './iface';
import assert from 'assert';
import { TransactionBuilder } from './transactionBuilder';

/**
* Transaction builder for SPL token mint and burn operations.
* Supports mixed operations in a single transaction.
*/
export class SplTokenOpsBuilder extends TransactionBuilder {
private _operations: (MintTo | Burn)[] = [];

constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
}

protected get transactionType(): TransactionType {
return TransactionType.Send;
}

/**
* Add a mint operation to the transaction
*
* @param params - The mint operation parameters
* @returns This transaction builder
*/
mint(params: MintToParams): this {
const operation: MintTo = {
type: InstructionBuilderTypes.MintTo,
params,
};
this.addOperation(operation);
return this;
}

/**
* Add a burn operation to the transaction
*
* @param params - The burn operation parameters
* @returns This transaction builder
*/
burn(params: BurnParams): this {
const operation: Burn = {
type: InstructionBuilderTypes.Burn,
params,
};
this.addOperation(operation);
return this;
}

/**
* Add a generic SPL token operation (mint or burn)
*
* @param operation - The operation parameters
* @returns This transaction builder
*/
addOperation(operation: MintTo | Burn): this {
this.validateOperation(operation);
this._operations.push(operation);
return this;
}

/**
* Validates an SPL token operation
* @param operation - The operation to validate
*/
private validateOperation(operation: MintTo | Burn): void {
this.validateOperationType(operation.type);
this.validateCommonFields(operation);
this.validateOperationSpecificFields(operation);
this.validateTokenInformation(operation);
}

/**
* Validates the operation type
*/
private validateOperationType(type: InstructionBuilderTypes): void {
const validTypes = [InstructionBuilderTypes.MintTo, InstructionBuilderTypes.Burn];
if (!type || !validTypes.includes(type)) {
throw new BuildTransactionError(`Operation type must be one of: ${validTypes.join(', ')}`);
}
}

/**
* Validates fields common to all operations
*/
private validateCommonFields(operation: MintTo | Burn): void {
const params = operation.params;
if (!params.amount || !isValidAmount(params.amount)) {
throw new BuildTransactionError('Invalid amount: ' + params.amount);
}

if (!params.authorityAddress) {
throw new BuildTransactionError('Operation requires authorityAddress');
}
validateAddress(params.authorityAddress, 'authorityAddress');
}

/**
* Validates operation-specific fields based on type
*/
private validateOperationSpecificFields(operation: MintTo | Burn): void {
if (operation.type === InstructionBuilderTypes.MintTo) {
this.validateMintOperation(operation);
} else if (operation.type === InstructionBuilderTypes.Burn) {
this.validateBurnOperation(operation);
} else {
throw new BuildTransactionError(`Unsupported operation type: ${String((operation as { type: string }).type)}`);
}
}

/**
* Validates mint-specific fields
*/
private validateMintOperation(operation: MintTo): void {
if (!operation.params.destinationAddress) {
throw new BuildTransactionError('Mint operation requires destinationAddress');
}
validateAddress(operation.params.destinationAddress, 'destinationAddress');
}

/**
* Validates burn-specific fields
*/
private validateBurnOperation(operation: Burn): void {
if (!operation.params.accountAddress) {
throw new BuildTransactionError('Burn operation requires accountAddress');
}
validateAddress(operation.params.accountAddress, 'accountAddress');
}

/**
* Validates token information (name or mint address)
*/
private validateTokenInformation(operation: MintTo | Burn): void {
const params = operation.params;
if (!params.tokenName && !params.mintAddress) {
throw new BuildTransactionError('Either tokenName or mintAddress must be provided');
}

if (params.tokenName) {
const token = getSolTokenFromTokenName(params.tokenName);
if (!token && !params.mintAddress) {
throw new BuildTransactionError('Invalid token name or missing mintAddress: ' + params.tokenName);
}
}

if (params.mintAddress) {
validateMintAddress(params.mintAddress);
}
}

initBuilder(tx: Transaction): void {
super.initBuilder(tx);

for (const instruction of this._instructionsData) {
if (instruction.type === InstructionBuilderTypes.MintTo) {
this.addOperation(instruction as MintTo);
} else if (instruction.type === InstructionBuilderTypes.Burn) {
this.addOperation(instruction as Burn);
}
}
}

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
assert(this._operations.length > 0, 'At least one SPL token operation must be specified');

const instructions = this._operations.map((operation) => this.processOperation(operation));

// Add priority fee instruction if needed
if (this._priorityFee && this._priorityFee > 0) {
const priorityFeeInstruction: SetPriorityFee = {
type: InstructionBuilderTypes.SetPriorityFee,
params: { fee: this._priorityFee },
};
this._instructionsData = [priorityFeeInstruction, ...instructions];
} else {
this._instructionsData = instructions;
}

return await super.buildImplementation();
}

/**
* Processes an operation to ensure it has complete token information
*/
private processOperation(operation: MintTo | Burn): MintTo | Burn {
const tokenInfo = this.resolveTokenInfo(operation);
const operationType = operation.type;
switch (operationType) {
case InstructionBuilderTypes.MintTo:
return this.enrichMintInstruction(operation, tokenInfo);
case InstructionBuilderTypes.Burn:
return this.enrichBurnInstruction(operation, tokenInfo);
default:
throw new BuildTransactionError(`Unsupported operation type: ${operationType}`);
}
}

/**
* Resolves token information from operation
*/
private resolveTokenInfo(operation: MintTo | Burn): {
mintAddress: string;
tokenName: string;
programId?: string;
} {
const params = operation.params;
if (params.mintAddress) {
return {
mintAddress: params.mintAddress,
tokenName: params.tokenName || params.mintAddress,
programId: params.programId,
};
} else if (params.tokenName) {
const token = getSolTokenFromTokenName(params.tokenName);
if (token) {
return {
mintAddress: token.tokenAddress,
tokenName: token.name,
programId: token.programId,
};
} else {
throw new BuildTransactionError('Invalid token name: ' + params.tokenName);
}
} else {
throw new BuildTransactionError('Either tokenName or mintAddress must be provided');
}
}

/**
* Enriches a mint instruction with complete token information
*/
private enrichMintInstruction(
operation: MintTo,
tokenInfo: { mintAddress: string; tokenName: string; programId?: string }
): MintTo {
const params = {
...operation.params,
mintAddress: tokenInfo.mintAddress,
tokenName: tokenInfo.tokenName,
programId: tokenInfo.programId || operation.params.programId,
};

return {
type: InstructionBuilderTypes.MintTo,
params,
};
}

/**
* Enriches a burn instruction with complete token information
*/
private enrichBurnInstruction(
operation: Burn,
tokenInfo: { mintAddress: string; tokenName: string; programId?: string }
): Burn {
const params = {
...operation.params,
mintAddress: tokenInfo.mintAddress,
tokenName: tokenInfo.tokenName,
programId: tokenInfo.programId || operation.params.programId,
};

return {
type: InstructionBuilderTypes.Burn,
params,
};
}
}
8 changes: 8 additions & 0 deletions modules/sdk-coin-sol/src/lib/transactionBuilderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { BaseTransactionBuilderFactory, InvalidTransactionError, TransactionType
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { AtaInitializationBuilder } from './ataInitializationBuilder';
import { CloseAtaBuilder } from './closeAtaBuilder';
import { SplTokenOpsBuilder } from './splTokenOpsBuilder';
import { StakingActivateBuilder } from './stakingActivateBuilder';
import { StakingAuthorizeBuilder } from './stakingAuthorizeBuilder';
import { StakingDeactivateBuilder } from './stakingDeactivateBuilder';
Expand Down Expand Up @@ -175,6 +176,13 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
return this.initializeBuilder(tx, new CloseAtaBuilder(this._coinConfig));
}

/**
* Returns the builder to create SPL token mint and burn operations.
*/
getSplTokenOpsBuilder(tx?: Transaction): SplTokenOpsBuilder {
return this.initializeBuilder(tx, new SplTokenOpsBuilder(this._coinConfig));
}

/**
* Initialize the builder with the given transaction
*
Expand Down
Loading