diff --git a/modules/sdk-coin-sol/src/lib/iface.ts b/modules/sdk-coin-sol/src/lib/iface.ts index c4126809de..12ed2cb3d3 100644 --- a/modules/sdk-coin-sol/src/lib/iface.ts +++ b/modules/sdk-coin-sol/src/lib/iface.ts @@ -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 { diff --git a/modules/sdk-coin-sol/src/lib/index.ts b/modules/sdk-coin-sol/src/lib/index.ts index 57d7ff8d05..6112ce26bd 100644 --- a/modules/sdk-coin-sol/src/lib/index.ts +++ b/modules/sdk-coin-sol/src/lib/index.ts @@ -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'; @@ -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'; diff --git a/modules/sdk-coin-sol/src/lib/splTokenOpsBuilder.ts b/modules/sdk-coin-sol/src/lib/splTokenOpsBuilder.ts new file mode 100644 index 0000000000..fec70f9d0a --- /dev/null +++ b/modules/sdk-coin-sol/src/lib/splTokenOpsBuilder.ts @@ -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) { + 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 { + 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, + }; + } +} diff --git a/modules/sdk-coin-sol/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-sol/src/lib/transactionBuilderFactory.ts index 45e348d0bc..eafb58e212 100644 --- a/modules/sdk-coin-sol/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-sol/src/lib/transactionBuilderFactory.ts @@ -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'; @@ -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 * diff --git a/modules/sdk-coin-sol/src/sol.ts b/modules/sdk-coin-sol/src/sol.ts index 0955ec2167..6f114f3d53 100644 --- a/modules/sdk-coin-sol/src/sol.ts +++ b/modules/sdk-coin-sol/src/sol.ts @@ -30,6 +30,7 @@ import { OvcInput, OvcOutput, ParsedTransaction, + PrebuildTransactionOptions, PresignTransactionOptions, PublicKey, RecoveryTxRequest, @@ -43,12 +44,14 @@ import { MultisigType, multisigTypes, AuditDecryptedKeyParams, + PopulatedIntent, + PrebuildTransactionWithIntentOptions, } from '@bitgo/sdk-core'; import { auditEddsaPrivateKey, getDerivationPath } from '@bitgo/sdk-lib-mpc'; import { BaseNetwork, CoinFamily, coins, BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; import * as _ from 'lodash'; import * as request from 'superagent'; -import { KeyPair as SolKeyPair, Transaction, TransactionBuilder, TransactionBuilderFactory } from './lib'; +import { KeyPair as SolKeyPair, Transaction, TransactionBuilder, TransactionBuilderFactory, MintTo, Burn } from './lib'; import { getAssociatedTokenAccountAddress, getSolTokenFromAddress, @@ -74,6 +77,14 @@ export interface ExplainTransactionOptions { tokenAccountRentExemptAmount?: string; } +export interface SolPrebuildTransactionOptions extends PrebuildTransactionOptions { + splTokenOps?: (MintTo | Burn)[]; +} + +export interface SolPopulatedIntent extends PopulatedIntent { + splTokenOps?: SolPrebuildTransactionOptions['splTokenOps']; +} + export interface TxInfo { recipients: TransactionRecipient[]; from: string; @@ -1413,4 +1424,16 @@ export class Sol extends BaseCoin { } auditEddsaPrivateKey(prv, publicKey ?? ''); } + + /** inherited doc */ + setCoinSpecificFieldsInIntent(intent: PopulatedIntent, params: PrebuildTransactionWithIntentOptions): void { + // Handle Solana-specific intent fields + if (params.intentType === 'splTokenOps') { + const solParams = params as unknown as SolPrebuildTransactionOptions; + if (solParams.splTokenOps) { + // Cast intent to our extended interface and add the splTokenOps operations + (intent as SolPopulatedIntent).splTokenOps = solParams.splTokenOps; + } + } + } } diff --git a/modules/sdk-coin-sol/test/unit/transactionBuilder/splTokenOpsBuilder.ts b/modules/sdk-coin-sol/test/unit/transactionBuilder/splTokenOpsBuilder.ts new file mode 100644 index 0000000000..60414943cc --- /dev/null +++ b/modules/sdk-coin-sol/test/unit/transactionBuilder/splTokenOpsBuilder.ts @@ -0,0 +1,339 @@ +import { getBuilderFactory } from '../getBuilderFactory'; +import { KeyPair, Utils, InstructionBuilderTypes } from '../../../src'; +import should from 'should'; +import * as testData from '../../resources/sol'; +import { TransactionType } from '@bitgo/sdk-core'; + +describe('Sol SPL Token Ops Builder', () => { + const factory = getBuilderFactory('tsol'); + const authAccount = new KeyPair(testData.authAccount).getKeys(); + const otherAccount = new KeyPair({ prv: testData.prvKeys.prvKey1.base58 }).getKeys(); + const recentBlockHash = 'GHtXQBsoZHVnNFa9YevAzFr17DJjgHXk3ycTKD5xD3Zi'; + const amount = '1000000'; + const nameUSDC = testData.tokenTransfers.nameUSDC; + const mintUSDC = testData.tokenTransfers.mintUSDC; + + describe('Succeed', () => { + it('should build a mint operation transaction', async () => { + const txBuilder = factory.getSplTokenOpsBuilder(); + txBuilder.nonce(recentBlockHash); + txBuilder.sender(authAccount.pub); + txBuilder.mint({ + mintAddress: mintUSDC, + destinationAddress: otherAccount.pub, + authorityAddress: authAccount.pub, + amount: amount, + tokenName: nameUSDC, + }); + txBuilder.setPriorityFee({ amount: 5000 }); + + const tx = await txBuilder.build(); + tx.type.should.equal(TransactionType.Send); + + const txJson = tx.toJson(); + txJson.instructionsData.length.should.equal(2); + txJson.instructionsData[0].type.should.equal('SetPriorityFee'); + txJson.instructionsData[1].type.should.equal('MintTo'); + txJson.instructionsData[1].params.should.deepEqual({ + mintAddress: mintUSDC, + destinationAddress: otherAccount.pub, + authorityAddress: authAccount.pub, + amount: amount, + tokenName: nameUSDC, + programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', + decimalPlaces: undefined, + }); + + const rawTx = tx.toBroadcastFormat(); + should.equal(Utils.isValidRawTransaction(rawTx), true); + }); + + it('should build a burn operation transaction', async () => { + const txBuilder = factory.getSplTokenOpsBuilder(); + txBuilder.nonce(recentBlockHash); + txBuilder.sender(authAccount.pub); + txBuilder.burn({ + mintAddress: mintUSDC, + accountAddress: otherAccount.pub, + authorityAddress: authAccount.pub, + amount: amount, + tokenName: nameUSDC, + }); + txBuilder.setPriorityFee({ amount: 5000 }); + + const tx = await txBuilder.build(); + tx.type.should.equal(TransactionType.Send); + + const txJson = tx.toJson(); + txJson.instructionsData.length.should.equal(2); + txJson.instructionsData[0].type.should.equal('SetPriorityFee'); + txJson.instructionsData[1].type.should.equal('Burn'); + txJson.instructionsData[1].params.should.deepEqual({ + mintAddress: mintUSDC, + accountAddress: otherAccount.pub, + authorityAddress: authAccount.pub, + amount: amount, + tokenName: nameUSDC, + programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', + decimalPlaces: undefined, + }); + + const rawTx = tx.toBroadcastFormat(); + should.equal(Utils.isValidRawTransaction(rawTx), true); + }); + + it('should build a mixed mint and burn operations transaction', async () => { + const txBuilder = factory.getSplTokenOpsBuilder(); + txBuilder.nonce(recentBlockHash); + txBuilder.sender(authAccount.pub); + + // Add mint operation + txBuilder.mint({ + mintAddress: mintUSDC, + destinationAddress: otherAccount.pub, + authorityAddress: authAccount.pub, + amount: amount, + tokenName: nameUSDC, + }); + + // Add burn operation + txBuilder.burn({ + mintAddress: mintUSDC, + accountAddress: authAccount.pub, + authorityAddress: authAccount.pub, + amount: '500000', + tokenName: nameUSDC, + }); + + txBuilder.setPriorityFee({ amount: 5000 }); + + const tx = await txBuilder.build(); + tx.type.should.equal(TransactionType.Send); + + const txJson = tx.toJson(); + txJson.instructionsData.length.should.equal(3); + txJson.instructionsData[0].type.should.equal('SetPriorityFee'); + txJson.instructionsData[1].type.should.equal('MintTo'); + txJson.instructionsData[1].params.should.deepEqual({ + mintAddress: mintUSDC, + destinationAddress: otherAccount.pub, + authorityAddress: authAccount.pub, + amount: amount, + tokenName: nameUSDC, + programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', + decimalPlaces: undefined, + }); + txJson.instructionsData[2].type.should.equal('Burn'); + txJson.instructionsData[2].params.should.deepEqual({ + mintAddress: mintUSDC, + accountAddress: authAccount.pub, + authorityAddress: authAccount.pub, + amount: '500000', + tokenName: nameUSDC, + programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', + decimalPlaces: undefined, + }); + + const rawTx = tx.toBroadcastFormat(); + should.equal(Utils.isValidRawTransaction(rawTx), true); + }); + + it('should build operations using generic addOperation method', async () => { + const txBuilder = factory.getSplTokenOpsBuilder(); + txBuilder.nonce(recentBlockHash); + txBuilder.sender(authAccount.pub); + + txBuilder.addOperation({ + type: InstructionBuilderTypes.MintTo, + params: { + mintAddress: mintUSDC, + destinationAddress: otherAccount.pub, + authorityAddress: authAccount.pub, + amount: amount, + tokenName: nameUSDC, + }, + }); + + txBuilder.addOperation({ + type: InstructionBuilderTypes.Burn, + params: { + mintAddress: mintUSDC, + accountAddress: authAccount.pub, + authorityAddress: authAccount.pub, + amount: '250000', + tokenName: nameUSDC, + }, + }); + + const tx = await txBuilder.build(); + tx.type.should.equal(TransactionType.Send); + + const txJson = tx.toJson(); + txJson.instructionsData.length.should.equal(2); + txJson.instructionsData[0].type.should.equal('MintTo'); + txJson.instructionsData[1].type.should.equal('Burn'); + + const rawTx = tx.toBroadcastFormat(); + should.equal(Utils.isValidRawTransaction(rawTx), true); + }); + + it('should work with token name only (without explicit mint address)', async () => { + const txBuilder = factory.getSplTokenOpsBuilder(); + txBuilder.nonce(recentBlockHash); + txBuilder.sender(authAccount.pub); + + txBuilder.mint({ + tokenName: nameUSDC, + destinationAddress: otherAccount.pub, + authorityAddress: authAccount.pub, + amount: amount, + }); + + const tx = await txBuilder.build(); + tx.type.should.equal(TransactionType.Send); + + const txJson = tx.toJson(); + txJson.instructionsData.length.should.equal(1); + txJson.instructionsData[0].type.should.equal('MintTo'); + txJson.instructionsData[0].params.mintAddress.should.equal(mintUSDC); + + const rawTx = tx.toBroadcastFormat(); + should.equal(Utils.isValidRawTransaction(rawTx), true); + }); + }); + + describe('Build and sign', () => { + it('should build and sign a mint operation transaction', async () => { + const txBuilder = factory.getSplTokenOpsBuilder(); + txBuilder.nonce(recentBlockHash); + txBuilder.sender(authAccount.pub); + txBuilder.mint({ + mintAddress: mintUSDC, + destinationAddress: otherAccount.pub, + authorityAddress: authAccount.pub, + amount: amount, + tokenName: nameUSDC, + }); + txBuilder.sign({ key: authAccount.prv }); + + const tx = await txBuilder.build(); + tx.type.should.equal(TransactionType.Send); + + const rawTx = tx.toBroadcastFormat(); + should.equal(Utils.isValidRawTransaction(rawTx, true, true), true); + }); + + it('should build and sign a mixed operations transaction', async () => { + const txBuilder = factory.getSplTokenOpsBuilder(); + txBuilder.nonce(recentBlockHash); + txBuilder.sender(authAccount.pub); + + txBuilder.mint({ + mintAddress: mintUSDC, + destinationAddress: otherAccount.pub, + authorityAddress: authAccount.pub, + amount: amount, + tokenName: nameUSDC, + }); + + txBuilder.burn({ + mintAddress: mintUSDC, + accountAddress: authAccount.pub, + authorityAddress: authAccount.pub, + amount: '500000', + tokenName: nameUSDC, + }); + + txBuilder.sign({ key: authAccount.prv }); + + const tx = await txBuilder.build(); + + // Should be a valid signed transaction + const rawTx = tx.toBroadcastFormat(); + should.equal(Utils.isValidRawTransaction(rawTx, true, true), true); + + // Verify transaction structure + const txJson = tx.toJson(); + txJson.instructionsData.length.should.equal(2); + txJson.instructionsData[0].type.should.equal('MintTo'); + txJson.instructionsData[1].type.should.equal('Burn'); + }); + }); + + describe('Fail', () => { + it('should fail when no operations are provided', async () => { + const txBuilder = factory.getSplTokenOpsBuilder(); + txBuilder.nonce(recentBlockHash); + txBuilder.sender(authAccount.pub); + + await txBuilder.build().should.be.rejectedWith('At least one SPL token operation must be specified'); + }); + + it('should fail with invalid operation type', () => { + const txBuilder = factory.getSplTokenOpsBuilder(); + + should(() => + txBuilder.addOperation({ + type: 'invalid' as unknown as InstructionBuilderTypes, + params: { + mintAddress: mintUSDC, + destinationAddress: otherAccount.pub, + authorityAddress: authAccount.pub, + amount: amount, + tokenName: nameUSDC, + }, + } as unknown as any) + ).throwError('Operation type must be one of: MintTo, Burn'); + }); + + it('should fail mint operation without destination address', () => { + const txBuilder = factory.getSplTokenOpsBuilder(); + + should(() => + txBuilder.mint({ + mintAddress: mintUSDC, + authorityAddress: authAccount.pub, + amount: amount, + } as any) + ).throwError('Mint operation requires destinationAddress'); + }); + + it('should fail burn operation without account address', () => { + const txBuilder = factory.getSplTokenOpsBuilder(); + + should(() => + txBuilder.burn({ + mintAddress: mintUSDC, + authorityAddress: authAccount.pub, + amount: amount, + } as any) + ).throwError('Burn operation requires accountAddress'); + }); + + it('should fail with invalid amount', () => { + const txBuilder = factory.getSplTokenOpsBuilder(); + + should(() => + txBuilder.mint({ + mintAddress: mintUSDC, + destinationAddress: otherAccount.pub, + authorityAddress: authAccount.pub, + amount: 'invalid', + }) + ).throwError('Invalid amount: invalid'); + }); + + it('should fail with invalid token name and no mint address', () => { + const txBuilder = factory.getSplTokenOpsBuilder(); + + should(() => + txBuilder.mint({ + tokenName: 'invalid-token', + destinationAddress: otherAccount.pub, + authorityAddress: authAccount.pub, + amount: amount, + }) + ).throwError('Invalid token name or missing mintAddress: invalid-token'); + }); + }); +});