Skip to content

Commit 6933bd9

Browse files
feat(sdk-coin-sol): add unstake flow for marinade
Ticket: SC-1660
1 parent 4f5d026 commit 6933bd9

File tree

14 files changed

+217
-17
lines changed

14 files changed

+217
-17
lines changed

modules/sdk-coin-sol/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@bitgo/sdk-core": "^32.2.0",
4444
"@bitgo/sdk-lib-mpc": "^10.2.0",
4545
"@bitgo/statics": "^51.8.0",
46+
"@bitgo/public-types": "4.28.1",
4647
"@solana/spl-token": "0.3.1",
4748
"@solana/web3.js": "1.92.1",
4849
"bignumber.js": "^9.0.0",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export const MEMO_PROGRAM_PK = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr';
44

55
export const SEED_LENGTH = 32;
66

7-
export const MAX_MEMO_LENGTH = 100;
7+
export const MAX_MEMO_LENGTH = 130;
88
export const STAKE_ACCOUNT_RENT_EXEMPT_AMOUNT = 2282880;
99

1010
export const UNAVAILABLE_TEXT = 'UNAVAILABLE';

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { TransactionExplanation as BaseTransactionExplanation } from '@bitgo/sdk
22
import { DecodedCloseAccountInstruction } from '@solana/spl-token';
33
import { Blockhash, StakeInstructionType, SystemInstructionType, TransactionSignature } from '@solana/web3.js';
44
import { InstructionBuilderTypes } from './constants';
5+
import { RecipientEntry } from '@bitgo/public-types';
56

67
// TODO(STLX-9890): Add the interfaces for validityWindow and SequenceId
78
export interface SolanaKeys {
@@ -98,6 +99,8 @@ export interface StakingDeactivate {
9899
stakingAddress: string;
99100
amount?: string;
100101
unstakingAddress?: string;
102+
isMarinade?: boolean;
103+
recipients?: RecipientEntry[];
101104
};
102105
}
103106

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

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
ComputeBudgetInstruction,
1717
} from '@solana/web3.js';
1818

19+
import { RecipientEntry } from '@bitgo/public-types';
20+
1921
import { NotSupported, TransactionType } from '@bitgo/sdk-core';
2022
import { coins, SolCoin } from '@bitgo/statics';
2123
import assert from 'assert';
@@ -47,7 +49,8 @@ import { getInstructionType } from './utils';
4749
*/
4850
export function instructionParamsFactory(
4951
type: TransactionType,
50-
instructions: TransactionInstruction[]
52+
instructions: TransactionInstruction[],
53+
coinName?: string
5154
): InstructionParams[] {
5255
switch (type) {
5356
case TransactionType.WalletInitialization:
@@ -57,7 +60,7 @@ export function instructionParamsFactory(
5760
case TransactionType.StakingActivate:
5861
return parseStakingActivateInstructions(instructions);
5962
case TransactionType.StakingDeactivate:
60-
return parseStakingDeactivateInstructions(instructions);
63+
return parseStakingDeactivateInstructions(instructions, coinName);
6164
case TransactionType.StakingWithdraw:
6265
return parseStakingWithdrawInstructions(instructions);
6366
case TransactionType.AssociatedTokenAccountInitialization:
@@ -358,7 +361,8 @@ function validateStakingInstructions(stakingInstructions: StakingInstructions) {
358361
* @returns {InstructionParams[]} An array containing instruction params for staking deactivate tx
359362
*/
360363
function parseStakingDeactivateInstructions(
361-
instructions: TransactionInstruction[]
364+
instructions: TransactionInstruction[],
365+
coinName?: string
362366
): Array<Nonce | StakingDeactivate | Memo> {
363367
const instructionData: Array<Nonce | StakingDeactivate | Memo> = [];
364368
const unstakingInstructions: UnstakingInstructions[] = [];
@@ -467,6 +471,21 @@ function parseStakingDeactivateInstructions(
467471
'',
468472
amount: unstakingInstruction.split?.lamports.toString(),
469473
unstakingAddress: unstakingInstruction.split?.splitStakePubkey.toString(),
474+
isMarinade: unstakingInstruction.deactivate === undefined,
475+
recipients:
476+
unstakingInstruction.deactivate === undefined
477+
? ([
478+
{
479+
address: {
480+
address: unstakingInstruction.transfer?.toPubkey.toString(),
481+
},
482+
amount: {
483+
value: unstakingInstruction.transfer?.lamports.toString(),
484+
symbol: coinName,
485+
},
486+
},
487+
] as RecipientEntry[])
488+
: undefined,
470489
},
471490
};
472491
instructionData.push(stakingDeactivate);
@@ -485,6 +504,14 @@ interface UnstakingInstructions {
485504

486505
function validateUnstakingInstructions(unstakingInstructions: UnstakingInstructions) {
487506
if (!unstakingInstructions.deactivate) {
507+
if (
508+
unstakingInstructions.transfer &&
509+
!unstakingInstructions.allocate &&
510+
!unstakingInstructions.assign &&
511+
!unstakingInstructions.split
512+
) {
513+
return;
514+
}
488515
throw new NotSupported('Invalid deactivate stake transaction, missing deactivate stake account instruction');
489516
} else if (
490517
unstakingInstructions.allocate ||

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,10 +243,26 @@ function stakingInitializeInstruction(data: StakingActivate): TransactionInstruc
243243
*/
244244
function stakingDeactivateInstruction(data: StakingDeactivate): TransactionInstruction[] {
245245
const {
246-
params: { fromAddress, stakingAddress },
246+
params: { fromAddress, stakingAddress, isMarinade, recipients },
247247
} = data;
248248
assert(fromAddress, 'Missing fromAddress param');
249-
assert(stakingAddress, 'Missing stakingAddress param');
249+
if (!isMarinade) {
250+
assert(stakingAddress, 'Missing stakingAddress param');
251+
}
252+
253+
if (isMarinade) {
254+
const tx = new Transaction();
255+
assert(recipients, 'Missing recipients param');
256+
const toPubkeyAddress = new PublicKey(recipients[0].address.address || '');
257+
const transferInstruction = SystemProgram.transfer({
258+
fromPubkey: new PublicKey(fromAddress),
259+
toPubkey: toPubkeyAddress,
260+
lamports: parseInt(recipients[0].amount.value, 10),
261+
});
262+
263+
tx.add(transferInstruction);
264+
return tx.instructions;
265+
}
250266

251267
if (data.params.amount && data.params.unstakingAddress) {
252268
const tx = new Transaction();

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

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ import { StakingDeactivate, Transfer } from './iface';
77
import { Transaction } from './transaction';
88
import { TransactionBuilder } from './transactionBuilder';
99
import { isValidStakingAmount, validateAddress } from './utils';
10+
import { RecipientEntry } from '@bitgo/public-types';
1011

1112
export class StakingDeactivateBuilder extends TransactionBuilder {
1213
protected _stakingAddress: string;
1314
protected _stakingAddresses: string[];
1415
protected _amount?: string;
1516
protected _unstakingAddress: string;
17+
protected _isMarinade = false;
18+
protected _recipients: RecipientEntry[];
1619

1720
constructor(_coinConfig: Readonly<CoinConfig>) {
1821
super(_coinConfig);
@@ -29,7 +32,13 @@ export class StakingDeactivateBuilder extends TransactionBuilder {
2932
for (const instruction of this._instructionsData) {
3033
if (instruction.type === InstructionBuilderTypes.StakingDeactivate) {
3134
const deactivateInstruction: StakingDeactivate = instruction;
32-
this.sender(deactivateInstruction.params.fromAddress);
35+
this.isMarinade(deactivateInstruction.params.isMarinade ?? false);
36+
if (!deactivateInstruction.params.isMarinade) {
37+
this.sender(deactivateInstruction.params.fromAddress);
38+
}
39+
if (deactivateInstruction.params.isMarinade) {
40+
this.recipients(deactivateInstruction.params.recipients ?? []);
41+
}
3342
stakingAddresses.push(deactivateInstruction.params.stakingAddress);
3443
if (deactivateInstruction.params.amount && deactivateInstruction.params.unstakingAddress) {
3544
this.amount(deactivateInstruction.params.amount);
@@ -40,7 +49,9 @@ export class StakingDeactivateBuilder extends TransactionBuilder {
4049
if (stakingAddresses.length > 1) {
4150
this.stakingAddresses(stakingAddresses);
4251
} else {
43-
this.stakingAddress(stakingAddresses[0]);
52+
if (!this._isMarinade) {
53+
this.stakingAddress(stakingAddresses[0]);
54+
}
4455
}
4556
}
4657

@@ -91,6 +102,17 @@ export class StakingDeactivateBuilder extends TransactionBuilder {
91102
return this;
92103
}
93104

105+
/**
106+
* Setter to set the recipients object
107+
*
108+
* @param recipients RecipientEntry[] - The recipients object
109+
* @returns {StakingDeactivateBuilder} This staking builder.
110+
*/
111+
recipients(recipients: RecipientEntry[]): this {
112+
this._recipients = recipients;
113+
return this;
114+
}
115+
94116
/**
95117
* When partially unstaking move the amount to unstake to this account and initiate the
96118
* unstake process. The original stake account will continue staking.
@@ -106,9 +128,20 @@ export class StakingDeactivateBuilder extends TransactionBuilder {
106128
return this;
107129
}
108130

131+
/**
132+
* Set isMarinade flag
133+
* @param {boolean} flag - true if the transaction is for Marinade, false by default if not set
134+
* @returns {StakingActivateBuilder} This staking builder
135+
*/
136+
isMarinade(flag: boolean): this {
137+
this._isMarinade = flag;
138+
return this;
139+
}
140+
109141
/** @inheritdoc */
110142
protected async buildImplementation(): Promise<Transaction> {
111143
assert(this._sender, 'Sender must be set before building the transaction');
144+
assert(this._isMarinade !== undefined, 'isMarinade must be set before building the transaction');
112145

113146
if (this._stakingAddresses && this._stakingAddresses.length > 0) {
114147
this._instructionsData = [];
@@ -123,7 +156,10 @@ export class StakingDeactivateBuilder extends TransactionBuilder {
123156
this._instructionsData.push(stakingDeactivateData);
124157
}
125158
} else {
126-
assert(this._stakingAddress, 'Staking address must be set before building the transaction');
159+
if (!this._isMarinade) {
160+
// we don't need stakingAddress in marinade staking deactivate txn
161+
assert(this._stakingAddress, 'Staking address must be set before building the transaction');
162+
}
127163

128164
if (this._sender === this._stakingAddress) {
129165
throw new BuildTransactionError('Sender address cannot be the same as the Staking address');
@@ -136,7 +172,7 @@ export class StakingDeactivateBuilder extends TransactionBuilder {
136172
);
137173
}
138174
this._instructionsData = [];
139-
if (this._unstakingAddress) {
175+
if (this._unstakingAddress && !this._isMarinade) {
140176
assert(
141177
this._amount,
142178
'If an unstaking address is given then a partial amount to unstake must also be set before building the transaction'
@@ -159,6 +195,8 @@ export class StakingDeactivateBuilder extends TransactionBuilder {
159195
stakingAddress: this._stakingAddress,
160196
amount: this._amount,
161197
unstakingAddress: this._unstakingAddress,
198+
isMarinade: this._isMarinade,
199+
recipients: this._recipients,
162200
},
163201
};
164202
this._instructionsData.push(stakingDeactivateData);

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,11 @@ export class Transaction extends BaseTransaction {
239239
authWalletAddress: nonceInstruction.authorizedPubkey.toString(),
240240
};
241241
}
242-
const instructionData = instructionParamsFactory(this._type, this._solTransaction.instructions);
242+
const instructionData = instructionParamsFactory(
243+
this._type,
244+
this._solTransaction.instructions,
245+
this._coinConfig.name
246+
);
243247
if (this._type) {
244248
if (
245249
!durableNonce &&
@@ -284,7 +288,11 @@ export class Transaction extends BaseTransaction {
284288
}
285289
const outputs: Entry[] = [];
286290
const inputs: Entry[] = [];
287-
const instructionParams = instructionParamsFactory(this.type, this._solTransaction.instructions);
291+
const instructionParams = instructionParamsFactory(
292+
this.type,
293+
this._solTransaction.instructions,
294+
this._coinConfig.name
295+
);
288296

289297
for (const instruction of instructionParams) {
290298
switch (instruction.type) {
@@ -378,7 +386,11 @@ export class Transaction extends BaseTransaction {
378386
if (validateRawMsgInstruction(this._solTransaction.instructions)) {
379387
return this.explainRawMsgAuthorizeTransaction();
380388
}
381-
const decodedInstructions = instructionParamsFactory(this._type, this._solTransaction.instructions);
389+
const decodedInstructions = instructionParamsFactory(
390+
this._type,
391+
this._solTransaction.instructions,
392+
this._coinConfig.name
393+
);
382394

383395
let memo: string | undefined = undefined;
384396
let durableNonce: DurableNonceParams | undefined = undefined;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
7575
this.sender(sender);
7676
this.feePayer(txData.feePayer as string);
7777
this.nonce(txData.nonce, txData.durableNonce);
78-
this._instructionsData = instructionParamsFactory(tx.type, tx.solTransaction.instructions);
78+
this._instructionsData = instructionParamsFactory(tx.type, tx.solTransaction.instructions, this._coinConfig.name);
7979
// Parse priority fee instruction data
8080
const filteredPriorityFeeInstructionsData = txData.instructionsData.filter(
8181
(data) => data.type === InstructionBuilderTypes.SetPriorityFee

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,11 +276,14 @@ export function getTransactionType(transaction: SolTransaction): TransactionType
276276
}
277277
validateIntructionTypes(instructions);
278278
// check if deactivate instruction does not exist because deactivate can be include a transfer instruction
279+
const memoInstruction = instructions.find((instruction) => getInstructionType(instruction) === 'Memo');
280+
const memoData = memoInstruction?.data.toString('utf-8');
279281
if (instructions.filter((instruction) => getInstructionType(instruction) === 'Deactivate').length == 0) {
280282
for (const instruction of instructions) {
281283
const instructionType = getInstructionType(instruction);
284+
// Check if memo instruction is there and if it contains 'PrepareForRevoke' because Marinade staking deactivate transaction will have this
282285
if (
283-
instructionType === ValidInstructionTypesEnum.Transfer ||
286+
(instructionType === ValidInstructionTypesEnum.Transfer && !memoData?.includes('PrepareForRevoke')) ||
284287
instructionType === ValidInstructionTypesEnum.TokenTransfer
285288
) {
286289
return TransactionType.Send;

modules/sdk-coin-sol/test/resources/sol.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,9 @@ export const MARINADE_STAKING_ACTIVATE_UNSIGNED_TX_WITH_MEMO =
173173
export const STAKING_DEACTIVATE_SIGNED_TX =
174174
'AUfyWtl4IUxhH21qX/H03hJZer1XxQaxL2r/uDTM/u1GzBIyePCHu78O2SkWGEYP6eDdiY3OLfJmUM1jiy8NCAoBAAIEReV5vPklPPaLR9/x+zo6XCwhusWyPAmuEqbgVWvwi0Fiey+6ASdh+bkZvPlMu0ydyAUdnwkymTFNOUkjMmi96Qah2BeRN1QqmDQ3vf4qerJVf1NcinhyK2ikncAAAAAABqfVFxjHdMkoVmOYaR1etoteuKObS21cc1VbIQAAAADjMtr5L6vs6LY/96RABeX9/Zr6FYdWthxalfkEs7jQgQECAwEDAAQFAAAA';
175175

176+
export const MARINADE_STAKING_DEACTIVATE_SIGNED_TX =
177+
'AaiHUOzzeJaUzpdkm2BmLJI3AVOhFHTD5BnVMwUH3lRv8hKpH1fnXiXNm6ghZgNwhggXBAqhL3t4XEl+H7T95gIBAAIEReV5vPklPPaLR9/x+zo6XCwhusWyPAmuEqbgVWvwi0EL/kczKamI94jNtLT/BR9nkfa/PR2IU6d7qaEV8VVC3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABUpTWpkpIQZNJOhxYNo4fHw1td28kruB5B+oQEEFRI3jMtr5L6vs6LY/96RABeX9/Zr6FYdWthxalfkEs7jQgQICAgABDAIAAABgGCMAAAAAAAMAbntcIlByZXBhcmVGb3JSZXZva2VcIjp7XCJ1c2VyXCI6XCI1aHI1ZmlzUGk2RFhOdXVScG01WFVienBpRW5tZHl4WHVCRFR3endaajVQZX1cIixcImFtb3VudFwiOlwiNTAwMDAwMDAwMDAwXCJ9';
178+
176179
export const STAKING_DEACTIVATE_SIGNED_TX_single =
177180
'AUfyWtl4IUxhH21qX/H03hJZer1XxQaxL2r/uDTM/u1GzBIyePCHu78O2SkWGEYP6eDdiY3OLfJmUM1jiy8NCAoBAAIEReV5vPklPPaLR9/x+zo6XCwhusWyPAmuEqbgVWvwi0Fiey+6ASdh+bkZvPlMu0ydyAUdnwkymTFNOUkjMmi96Qah2BeRN1QqmDQ3vf4qerJVf1NcinhyK2ikncAAAAAABqfVFxjHdMkoVmOYaR1etoteuKObS21cc1VbIQAAAADjMtr5L6vs6LY/96RABeX9/Zr6FYdWthxalfkEs7jQgQECAwEDAAQFAAAA';
178181

0 commit comments

Comments
 (0)