Skip to content

Commit 4774c05

Browse files
committed
feat: add custom instruction tx builder for sol
- added custom instruction for sol transactions TICKET: TMS-1232
1 parent 41d9246 commit 4774c05

File tree

7 files changed

+383
-2
lines changed

7 files changed

+383
-2
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export enum InstructionBuilderTypes {
4949
SetPriorityFee = 'SetPriorityFee',
5050
MintTo = 'MintTo',
5151
Burn = 'Burn',
52+
CustomInstruction = 'CustomInstruction',
5253
}
5354

5455
export const VALID_SYSTEM_INSTRUCTION_TYPES: ValidInstructionTypes[] = [
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
2+
import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core';
3+
import { TransactionInstruction } from '@solana/web3.js';
4+
import { Transaction } from './transaction';
5+
import { TransactionBuilder } from './transactionBuilder';
6+
import { InstructionBuilderTypes } from './constants';
7+
import { CustomInstruction } from './iface';
8+
import assert from 'assert';
9+
10+
/**
11+
* Transaction builder for custom Solana instructions.
12+
* Allows building transactions with any set of raw Solana instructions.
13+
*/
14+
export class CustomInstructionBuilder extends TransactionBuilder {
15+
private _customInstructions: CustomInstruction[] = [];
16+
17+
constructor(_coinConfig: Readonly<CoinConfig>) {
18+
super(_coinConfig);
19+
}
20+
21+
protected get transactionType(): TransactionType {
22+
return TransactionType.Send;
23+
}
24+
25+
/**
26+
* Initialize the builder from an existing transaction
27+
*/
28+
initBuilder(tx: Transaction): void {
29+
super.initBuilder(tx);
30+
31+
for (const instruction of this._instructionsData) {
32+
if (instruction.type === InstructionBuilderTypes.CustomInstruction) {
33+
const customInstruction = instruction as CustomInstruction;
34+
this.addCustomInstruction(customInstruction.params.instruction);
35+
}
36+
}
37+
}
38+
39+
/**
40+
* Add a custom Solana instruction to the transaction
41+
*
42+
* @param instruction - The raw Solana TransactionInstruction
43+
* @returns This transaction builder
44+
*/
45+
addCustomInstruction(instruction: TransactionInstruction): this {
46+
if (!instruction) {
47+
throw new BuildTransactionError('Instruction cannot be null or undefined');
48+
}
49+
50+
if (!instruction.programId) {
51+
throw new BuildTransactionError('Instruction must have a valid programId');
52+
}
53+
54+
if (!instruction.keys || !Array.isArray(instruction.keys)) {
55+
throw new BuildTransactionError('Instruction must have valid keys array');
56+
}
57+
58+
if (!instruction.data || !Buffer.isBuffer(instruction.data)) {
59+
throw new BuildTransactionError('Instruction must have valid data buffer');
60+
}
61+
62+
const customInstruction: CustomInstruction = {
63+
type: InstructionBuilderTypes.CustomInstruction,
64+
params: {
65+
instruction,
66+
},
67+
};
68+
69+
this._customInstructions.push(customInstruction);
70+
return this;
71+
}
72+
73+
/**
74+
* Add multiple custom Solana instructions to the transaction
75+
*
76+
* @param instructions - Array of raw Solana TransactionInstructions
77+
* @returns This transaction builder
78+
*/
79+
addCustomInstructions(instructions: TransactionInstruction[]): this {
80+
if (!Array.isArray(instructions)) {
81+
throw new BuildTransactionError('Instructions must be an array');
82+
}
83+
84+
for (const instruction of instructions) {
85+
this.addCustomInstruction(instruction);
86+
}
87+
88+
return this;
89+
}
90+
91+
/**
92+
* Clear all custom instructions
93+
*
94+
* @returns This transaction builder
95+
*/
96+
clearInstructions(): this {
97+
this._customInstructions = [];
98+
return this;
99+
}
100+
101+
/**
102+
* Get the current custom instructions
103+
*
104+
* @returns Array of custom instructions
105+
*/
106+
getInstructions(): CustomInstruction[] {
107+
return [...this._customInstructions];
108+
}
109+
110+
/** @inheritdoc */
111+
protected async buildImplementation(): Promise<Transaction> {
112+
assert(this._customInstructions.length > 0, 'At least one custom instruction must be specified');
113+
114+
// Set the instructions data to our custom instructions
115+
this._instructionsData = [...this._customInstructions];
116+
117+
return await super.buildImplementation();
118+
}
119+
}

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { TransactionExplanation as BaseTransactionExplanation, Recipient } from '@bitgo/sdk-core';
22
import { DecodedCloseAccountInstruction } from '@solana/spl-token';
3-
import { Blockhash, StakeInstructionType, SystemInstructionType, TransactionSignature } from '@solana/web3.js';
3+
import {
4+
Blockhash,
5+
StakeInstructionType,
6+
SystemInstructionType,
7+
TransactionInstruction,
8+
TransactionSignature,
9+
} from '@solana/web3.js';
410
import { InstructionBuilderTypes } from './constants';
511

612
// TODO(STLX-9890): Add the interfaces for validityWindow and SequenceId
@@ -40,7 +46,8 @@ export type InstructionParams =
4046
| StakingAuthorize
4147
| StakingDelegate
4248
| MintTo
43-
| Burn;
49+
| Burn
50+
| CustomInstruction;
4451

4552
export interface Memo {
4653
type: InstructionBuilderTypes.Memo;
@@ -201,6 +208,13 @@ export type StakingDelegateParams = {
201208
validator: string;
202209
};
203210

211+
export interface CustomInstruction {
212+
type: InstructionBuilderTypes.CustomInstruction;
213+
params: {
214+
instruction: TransactionInstruction;
215+
};
216+
}
217+
204218
export interface TransactionExplanation extends BaseTransactionExplanation {
205219
type: string;
206220
blockhash: Blockhash;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as Utils from './utils';
33

44
export { AtaInitializationBuilder } from './ataInitializationBuilder';
55
export { CloseAtaBuilder } from './closeAtaBuilder';
6+
export { CustomInstructionBuilder } from './customInstructionBuilder';
67
export { KeyPair } from './keyPair';
78
export { StakingActivateBuilder } from './stakingActivateBuilder';
89
export { StakingAuthorizeBuilder } from './stakingAuthorizeBuilder';

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
Transfer,
3939
WalletInit,
4040
SetPriorityFee,
41+
CustomInstruction,
4142
} from './iface';
4243
import { getSolTokenFromTokenName } from './utils';
4344

@@ -79,6 +80,8 @@ export function solInstructionFactory(instructionToBuild: InstructionParams): Tr
7980
return mintToInstruction(instructionToBuild);
8081
case InstructionBuilderTypes.Burn:
8182
return burnInstruction(instructionToBuild);
83+
case InstructionBuilderTypes.CustomInstruction:
84+
return customInstruction(instructionToBuild);
8285
default:
8386
throw new Error(`Invalid instruction type or not supported`);
8487
}
@@ -546,3 +549,17 @@ function burnInstruction(data: Burn): TransactionInstruction[] {
546549

547550
return [burnInstr];
548551
}
552+
553+
/**
554+
* Process custom instruction - simply returns the raw instruction
555+
*
556+
* @param {CustomInstruction} data - the data containing the custom instruction
557+
* @returns {TransactionInstruction[]} An array containing the custom instruction
558+
*/
559+
function customInstruction(data: InstructionParams): TransactionInstruction[] {
560+
const {
561+
params: { instruction },
562+
} = data as CustomInstruction;
563+
assert(instruction, 'Missing instruction param');
564+
return [instruction];
565+
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { BaseTransactionBuilderFactory, InvalidTransactionError, TransactionType
22
import { BaseCoin as CoinConfig } from '@bitgo/statics';
33
import { AtaInitializationBuilder } from './ataInitializationBuilder';
44
import { CloseAtaBuilder } from './closeAtaBuilder';
5+
import { CustomInstructionBuilder } from './customInstructionBuilder';
56
import { StakingActivateBuilder } from './stakingActivateBuilder';
67
import { StakingAuthorizeBuilder } from './stakingAuthorizeBuilder';
78
import { StakingDeactivateBuilder } from './stakingDeactivateBuilder';
@@ -175,6 +176,13 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
175176
return this.initializeBuilder(tx, new CloseAtaBuilder(this._coinConfig));
176177
}
177178

179+
/**
180+
* Returns the builder to create transactions with custom Solana instructions.
181+
*/
182+
getCustomInstructionBuilder(tx?: Transaction): CustomInstructionBuilder {
183+
return this.initializeBuilder(tx, new CustomInstructionBuilder(this._coinConfig));
184+
}
185+
178186
/**
179187
* Initialize the builder with the given transaction
180188
*

0 commit comments

Comments
 (0)