Skip to content
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
1 change: 1 addition & 0 deletions modules/sdk-coin-sol/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@bitgo/sdk-core": "^36.30.0",
"@bitgo/sdk-lib-mpc": "^10.9.0",
"@bitgo/statics": "^58.24.0",
"@bitgo/wasm-solana": "^1.4.1",
"@solana/spl-stake-pool": "1.1.8",
"@solana/spl-token": "0.3.1",
"@solana/web3.js": "1.92.1",
Expand Down
19 changes: 17 additions & 2 deletions modules/sdk-coin-sol/src/lib/ataInitializationBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TransactionBuilder } from './transactionBuilder';
import { BuildTransactionError, DuplicateMethodError, TransactionType } from '@bitgo/sdk-core';
import { BaseTransaction, BuildTransactionError, DuplicateMethodError, TransactionType } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { Transaction } from './transaction';
import { AtaInit, TokenAssociateRecipient } from './iface';
Expand All @@ -10,6 +10,7 @@ import {
validateMintAddress,
validateOwnerAddress,
} from './utils';
import { WasmTransaction } from './wasm';
import assert from 'assert';
import * as _ from 'lodash';

Expand All @@ -35,6 +36,20 @@ export class AtaInitializationBuilder extends TransactionBuilder {
/** @inheritDoc */
initBuilder(tx: Transaction): void {
super.initBuilder(tx);
this.initFromInstructionsData();
}

/** @inheritDoc */
initBuilderFromWasm(wasmTx: WasmTransaction): void {
super.initBuilderFromWasm(wasmTx);
this.initFromInstructionsData();
}

/**
* Extract ATA initialization parameters from instructionsData.
* Called by both initBuilder and initBuilderFromWasm.
*/
private initFromInstructionsData(): void {
this._tokenAssociateRecipients = [];
for (const instruction of this._instructionsData) {
if (instruction.type === InstructionBuilderTypes.CreateAssociatedTokenAccount) {
Expand Down Expand Up @@ -132,7 +147,7 @@ export class AtaInitializationBuilder extends TransactionBuilder {
}

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
protected async buildImplementation(): Promise<BaseTransaction> {
assert(this._sender, 'Sender must be set before building the transaction');
if (this._tokenAssociateRecipients.length === 0) {
assert(this._mint && this._tokenName, 'Mint must be set before building the transaction');
Expand Down
19 changes: 17 additions & 2 deletions modules/sdk-coin-sol/src/lib/closeAtaBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core';
import { BaseTransaction, BuildTransactionError, TransactionType } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import assert from 'assert';
import { InstructionBuilderTypes } from './constants';
import { AtaClose } from './iface';
import { Transaction } from './transaction';
import { TransactionBuilder } from './transactionBuilder';
import { validateAddress } from './utils';
import { WasmTransaction } from './wasm';

export class CloseAtaBuilder extends TransactionBuilder {
protected _accountAddress: string;
Expand Down Expand Up @@ -42,6 +43,20 @@ export class CloseAtaBuilder extends TransactionBuilder {
/** @inheritDoc */
initBuilder(tx: Transaction): void {
super.initBuilder(tx);
this.initFromInstructionsData();
}

/** @inheritDoc */
initBuilderFromWasm(wasmTx: WasmTransaction): void {
super.initBuilderFromWasm(wasmTx);
this.initFromInstructionsData();
}

/**
* Extract close ATA parameters from instructionsData.
* Called by both initBuilder and initBuilderFromWasm.
*/
private initFromInstructionsData(): void {
for (const instruction of this._instructionsData) {
if (instruction.type === InstructionBuilderTypes.CloseAssociatedTokenAccount) {
const ataCloseInstruction: AtaClose = instruction;
Expand All @@ -53,7 +68,7 @@ export class CloseAtaBuilder extends TransactionBuilder {
}

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
protected async buildImplementation(): Promise<BaseTransaction> {
assert(this._accountAddress, 'Account Address must be set before building the transaction');
assert(this._destinationAddress, 'Destination Address must be set before building the transaction');
assert(this._authorityAddress, 'Authority Address must be set before building the transaction');
Expand Down
65 changes: 44 additions & 21 deletions modules/sdk-coin-sol/src/lib/customInstructionBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { BuildTransactionError, SolInstruction, SolVersionedInstruction, TransactionType } from '@bitgo/sdk-core';
import {
BaseTransaction,
BuildTransactionError,
SolInstruction,
SolVersionedInstruction,
TransactionType,
} from '@bitgo/sdk-core';
import { PublicKey, SystemProgram, SYSVAR_RECENT_BLOCKHASHES_PUBKEY } from '@solana/web3.js';
import { Transaction } from './transaction';
import { TransactionBuilder } from './transactionBuilder';
import { InstructionBuilderTypes } from './constants';
import { CustomInstruction, VersionedCustomInstruction, VersionedTransactionData } from './iface';
import { isSolLegacyInstruction } from './utils';
import { WasmTransaction } from './wasm';
import assert from 'assert';

/**
Expand All @@ -28,7 +35,20 @@ export class CustomInstructionBuilder extends TransactionBuilder {
*/
initBuilder(tx: Transaction): void {
super.initBuilder(tx);
this.initFromInstructionsData();
}

/** @inheritDoc */
initBuilderFromWasm(wasmTx: WasmTransaction): void {
super.initBuilderFromWasm(wasmTx);
this.initFromInstructionsData();
}

/**
* Extract custom instruction parameters from instructionsData.
* Called by both initBuilder and initBuilderFromWasm.
*/
private initFromInstructionsData(): void {
for (const instruction of this._instructionsData) {
if (instruction.type === InstructionBuilderTypes.CustomInstruction) {
const customInstruction = instruction as CustomInstruction;
Expand Down Expand Up @@ -125,22 +145,28 @@ export class CustomInstructionBuilder extends TransactionBuilder {
throw new BuildTransactionError('messageHeader.numReadonlyUnsignedAccounts must be a non-negative number');
}

let processedData = data;
if (this._nonceInfo && this._nonceInfo.params) {
processedData = this.injectNonceAdvanceInstruction(data);
const isTestnet = this._coinConfig.name === 'tsol';

if (isTestnet) {
// Testnet: store on builder for WASM path, nonce injection in wasm/builder.ts
this._versionedTransactionData = data;
this.addCustomInstructions(data.versionedInstructions);
} else {
// Mainnet: original behavior unchanged - store on Transaction, inject nonce here
let processedData = data;
if (this._nonceInfo && this._nonceInfo.params) {
processedData = this.injectNonceAdvanceInstruction(data);
}
this.addCustomInstructions(processedData.versionedInstructions);
if (!this._transaction) {
this._transaction = new Transaction(this._coinConfig);
}
this._transaction.setVersionedTransactionData(processedData);
this._transaction.setTransactionType(TransactionType.CustomTx);
}

this.addCustomInstructions(processedData.versionedInstructions);

if (!this._transaction) {
this._transaction = new Transaction(this._coinConfig);
}
this._transaction.setVersionedTransactionData(processedData);

this._transaction.setTransactionType(TransactionType.CustomTx);

if (!this._sender && processedData.staticAccountKeys.length > 0) {
this._sender = processedData.staticAccountKeys[0];
if (!this._sender && data.staticAccountKeys.length > 0) {
this._sender = data.staticAccountKeys[0];
}

return this;
Expand All @@ -153,11 +179,8 @@ export class CustomInstructionBuilder extends TransactionBuilder {
}

/**
* Inject nonce advance instruction into versioned transaction data for durable nonce support.
* Reorders accounts so signers appear first (required by Solana MessageV0 format).
* @param data - Original versioned transaction data
* @returns Modified versioned transaction data with nonce advance instruction
* @private
* Inject nonce advance instruction into versioned transaction data.
* Used by mainnet legacy path for durable nonce support.
*/
private injectNonceAdvanceInstruction(data: VersionedTransactionData): VersionedTransactionData {
const { walletNonceAddress, authWalletAddress } = this._nonceInfo!.params;
Expand Down Expand Up @@ -317,7 +340,7 @@ export class CustomInstructionBuilder extends TransactionBuilder {
}

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
protected async buildImplementation(): Promise<BaseTransaction> {
assert(this._customInstructions.length > 0, 'At least one custom instruction must be specified');

// Set the instructions data to our custom instructions
Expand Down
4 changes: 4 additions & 0 deletions modules/sdk-coin-sol/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ export { StakingWithdrawBuilder } from './stakingWithdrawBuilder';
export { TokenTransferBuilder } from './tokenTransferBuilder';
export { Transaction } from './transaction';
export { TransactionBuilder } from './transactionBuilder';
export { WasmTransaction } from './wasm';
export { TransactionBuilderFactory } from './transactionBuilderFactory';
export { TransferBuilder } from './transferBuilder';
export { TransferBuilderV2 } from './transferBuilderV2';
export { WalletInitializationBuilder } from './walletInitializationBuilder';
export { Interface, Utils };
export { MessageBuilderFactory } from './messages';
export { InstructionBuilderTypes } from './constants';
export { mapToTransactionIntent, IntentMapperParams } from './wasmIntentMapper';
export { buildUnsignedTransaction } from './wasmTransactionBuilder';
49 changes: 34 additions & 15 deletions modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ type StakingInstructions = {
initialize?: InitializeStakeParams;
delegate?: DelegateStakeParams;
hasAtaInit?: boolean;
ataInitInstruction?: AtaInit;
};

type JitoStakingInstructions = StakingInstructions & {
Expand Down Expand Up @@ -454,7 +455,9 @@ function parseStakingActivateInstructions(

case ValidInstructionTypesEnum.InitializeAssociatedTokenAccount:
stakingInstructions.hasAtaInit = true;
instructionData.push({
// Store the ATA init instruction - we'll decide later whether to add it to instructionData
// based on staking type (Jito staking uses a flag instead of a separate instruction)
stakingInstructions.ataInitInstruction = {
type: InstructionBuilderTypes.CreateAssociatedTokenAccount,
params: {
mintAddress: instruction.keys[ataInitInstructionKeysIndexes.MintAddress].pubkey.toString(),
Expand All @@ -463,7 +466,7 @@ function parseStakingActivateInstructions(
payerAddress: instruction.keys[ataInitInstructionKeysIndexes.PayerAddress].pubkey.toString(),
tokenName: findTokenName(instruction.keys[ataInitInstructionKeysIndexes.MintAddress].pubkey.toString()),
},
});
};
break;
}
}
Expand Down Expand Up @@ -536,6 +539,12 @@ function parseStakingActivateInstructions(
}
}

// For non-Jito staking, add the ATA instruction as a separate instruction
// (Jito staking uses the createAssociatedTokenAccount flag in extraParams instead)
if (stakingType !== SolStakingTypeEnum.JITO && stakingInstructions.ataInitInstruction) {
instructionData.push(stakingInstructions.ataInitInstruction);
}

instructionData.push(stakingActivate);

return instructionData;
Expand Down Expand Up @@ -1171,7 +1180,10 @@ function parseStakingAuthorizeInstructions(
*/
function parseStakingAuthorizeRawInstructions(instructions: TransactionInstruction[]): Array<Nonce | StakingAuthorize> {
const instructionData: Array<Nonce | StakingAuthorize> = [];
assert(instructions.length === 2, 'Invalid number of instructions');
// StakingAuthorizeRaw transactions have:
// - 2 instructions: NonceAdvance + 1 Authorize (changing either staking OR withdraw authority)
// - 3 instructions: NonceAdvance + 2 Authorizes (changing BOTH staking AND withdraw authority)
assert(instructions.length >= 2 && instructions.length <= 3, 'Invalid number of instructions');
const advanceNonceInstruction = SystemInstruction.decodeNonceAdvance(instructions[0]);
const nonce: Nonce = {
type: InstructionBuilderTypes.NonceAdvance,
Expand All @@ -1181,17 +1193,24 @@ function parseStakingAuthorizeRawInstructions(instructions: TransactionInstructi
},
};
instructionData.push(nonce);
const authorize = instructions[1];
assert(authorize.keys.length === 5, 'Invalid number of keys in authorize instruction');
instructionData.push({
type: InstructionBuilderTypes.StakingAuthorize,
params: {
stakingAddress: authorize.keys[0].pubkey.toString(),
oldAuthorizeAddress: authorize.keys[2].pubkey.toString(),
newAuthorizeAddress: authorize.keys[3].pubkey.toString(),
custodianAddress: authorize.keys[4].pubkey.toString(),
},
});

// Process all authorize instructions (1 or 2)
for (let i = 1; i < instructions.length; i++) {
const authorize = instructions[i];
// Authorize instruction keys: [stakePubkey, clockSysvar, oldAuthority, newAuthority, custodian?]
// - 4 keys: no custodian required
// - 5 keys: custodian is present (required when stake is locked)
assert(authorize.keys.length >= 4 && authorize.keys.length <= 5, 'Invalid number of keys in authorize instruction');
instructionData.push({
type: InstructionBuilderTypes.StakingAuthorize,
params: {
stakingAddress: authorize.keys[0].pubkey.toString(),
oldAuthorizeAddress: authorize.keys[2].pubkey.toString(),
newAuthorizeAddress: authorize.keys[3].pubkey.toString(),
custodianAddress: authorize.keys.length === 5 ? authorize.keys[4].pubkey.toString() : '',
},
});
}
return instructionData;
}

Expand Down Expand Up @@ -1239,7 +1258,7 @@ function parseCustomInstructions(
return instructionData;
}

function findTokenName(
export function findTokenName(
mintAddress: string,
instructionMetadata?: InstructionParams[],
_useTokenAddressTokenName?: boolean
Expand Down
22 changes: 19 additions & 3 deletions modules/sdk-coin-sol/src/lib/stakingActivateBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { SolStakingTypeEnum } from '@bitgo/public-types';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core';
import { BaseTransaction, BuildTransactionError, TransactionType } from '@bitgo/sdk-core';
import { Transaction } from './transaction';
import { TransactionBuilder } from './transactionBuilder';
import { InstructionBuilderTypes } from './constants';

import assert from 'assert';
import { StakingActivate, StakingActivateExtraParams } from './iface';
import { isValidStakingAmount, validateAddress } from './utils';
import { WasmTransaction } from './wasm';

export class StakingActivateBuilder extends TransactionBuilder {
protected _amount: string;
Expand All @@ -27,6 +28,20 @@ export class StakingActivateBuilder extends TransactionBuilder {
/** @inheritdoc */
initBuilder(tx: Transaction): void {
super.initBuilder(tx);
this.initFromInstructionsData();
}

/** @inheritdoc */
initBuilderFromWasm(wasmTx: WasmTransaction): void {
super.initBuilderFromWasm(wasmTx);
this.initFromInstructionsData();
}

/**
* Extract staking activate parameters from instructionsData.
* Called by both initBuilder and initBuilderFromWasm.
*/
private initFromInstructionsData(): void {
for (const instruction of this._instructionsData) {
if (instruction.type === InstructionBuilderTypes.StakingActivate) {
const activateInstruction: StakingActivate = instruction;
Expand Down Expand Up @@ -105,7 +120,7 @@ export class StakingActivateBuilder extends TransactionBuilder {
}

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
protected async buildImplementation(): Promise<BaseTransaction> {
assert(this._sender, 'Sender must be set before building the transaction');
assert(this._stakingAddress, 'Staking Address must be set before building the transaction');
assert(this._validator, 'Validator must be set before building the transaction');
Expand All @@ -123,7 +138,8 @@ export class StakingActivateBuilder extends TransactionBuilder {
amount: this._amount,
validator: this._validator,
stakingType: this._stakingType,
extraParams: this._extraParams,
// Only include extraParams if defined (matches legacy behavior where key is omitted when undefined)
...(this._extraParams && { extraParams: this._extraParams }),
},
};
this._instructionsData = [stakingAccountData];
Expand Down
Loading