Skip to content

Commit

Permalink
Merge pull request #3656 from BitGo/BG-78857.sui-unstaking-again
Browse files Browse the repository at this point in the history
fix(sdk-coin-sui): fix unstaking again
  • Loading branch information
OttoAllmendinger authored Jun 14, 2023
2 parents 9c3b276 + ea0345f commit 95f66ec
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 63 deletions.
2 changes: 1 addition & 1 deletion modules/sdk-coin-sui/src/lib/mystenlab/builder/Inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const Inputs = {
},
};

export function getIdFromCallArg(arg: ObjectId | ObjectCallArg) {
export function getIdFromCallArg(arg: ObjectId | ObjectCallArg): string {
if (typeof arg === 'string') {
return normalizeSuiAddress(arg);
}
Expand Down
27 changes: 12 additions & 15 deletions modules/sdk-coin-sui/src/lib/unstakingBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,7 @@ import { TransactionBuilder } from './transactionBuilder';
import { Transaction } from './transaction';
import assert from 'assert';
import { TransferTransaction } from './transferTransaction';
import {
TransactionBlock as ProgrammingTransactionBlockBuilder,
TransactionBlockInput,
MoveCallTransaction,
Inputs,
} from './mystenlab/builder';
import { TransactionBlock as ProgrammingTransactionBlockBuilder, Inputs } from './mystenlab/builder';
import {
SUI_STAKING_POOL_MODULE_NAME,
SUI_STAKING_POOL_SPLIT_FUN_NAME,
Expand All @@ -26,7 +21,7 @@ import {
} from './mystenlab/framework';
import { UnstakingTransaction } from './unstakingTransaction';
import utils from './utils';
import { SuiObjectRef } from './mystenlab/types';
import { normalizeSuiObjectId, SuiObjectRef } from './mystenlab/types';
import { SerializedTransactionDataBuilder } from './mystenlab/builder/TransactionDataBlock';

export class UnstakingBuilder extends TransactionBuilder<UnstakingProgrammableTransaction> {
Expand Down Expand Up @@ -136,14 +131,16 @@ export class UnstakingBuilder extends TransactionBuilder<UnstakingProgrammableTr
this.type(SuiTransactionType.WithdrawStake);
this.sender(txData.sender);
this.gasData(txData.gasData);

const stakedSuiInputIdx = (
(txData.kind.ProgrammableTransaction.transactions[0] as MoveCallTransaction).arguments[1] as TransactionBlockInput
).index;
const stakedSuiInput = txData.kind.ProgrammableTransaction.inputs[stakedSuiInputIdx] as TransactionBlockInput;
const stakedSui = 'value' in stakedSuiInput ? stakedSuiInput.value : stakedSuiInput;

this.unstake({ stakedSui: stakedSui.Object.ImmOrOwned });
const parsed = UnstakingTransaction.parseTransaction(tx.suiTransaction.tx);
this.unstake({
stakedSui: {
// it is a bit unclear why we have to normalize this way
...parsed.stakedObjectRef,
objectId: normalizeSuiObjectId(parsed.stakedObjectRef.objectId),
version: Number(parsed.stakedObjectRef.version),
},
amount: parsed.amount === undefined ? undefined : Number(parsed.amount),
});
}

/**
Expand Down
129 changes: 94 additions & 35 deletions modules/sdk-coin-sui/src/lib/unstakingTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ import {
} from '@bitgo/sdk-core';
import { UnstakingProgrammableTransaction, SuiTransaction, TransactionExplanation, TxData } from './iface';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import utils from './utils';
import utils, { isImmOrOwnedObj } from './utils';
import { Buffer } from 'buffer';
import { Transaction } from './transaction';
import {
builder,
getIdFromCallArg,
Inputs,
MoveCallTransaction,
ObjectCallArg,
PureCallArg,
TransactionBlockInput,
} from './mystenlab/builder';
import { CallArg, normalizeSuiAddress, SuiObjectRef } from './mystenlab/types';
import { bcs, CallArg, normalizeSuiAddress, SuiObjectRef } from './mystenlab/types';
import { BCS } from '@mysten/bcs';
import { AMOUNT_UNKNOWN_TEXT, SUI_ADDRESS_LENGTH } from './constants';
import { UnstakingBuilder } from './unstakingBuilder';
Expand Down Expand Up @@ -124,15 +124,11 @@ export class UnstakingTransaction extends Transaction<UnstakingProgrammableTrans
this._type = transactionType;
}

getEntriesForStakedSuiInput(
stakedSuiInput: TransactionBlockInput,
amount?: bigint
): { inputs: Entry[]; outputs: Entry[] } {
const stakedSui = 'value' in stakedSuiInput ? stakedSuiInput.value : stakedSuiInput;
getEntriesForStakedSuiInput(stakedSuiInput: SuiObjectRef, amount?: bigint): { inputs: Entry[]; outputs: Entry[] } {
return {
inputs: [
{
address: normalizeSuiAddress(getIdFromCallArg(stakedSui)),
address: normalizeSuiAddress(stakedSuiInput.objectId),
value: amount === undefined ? AMOUNT_UNKNOWN_TEXT : amount.toString(),
coin: this._coinConfig.name,
},
Expand All @@ -147,12 +143,48 @@ export class UnstakingTransaction extends Transaction<UnstakingProgrammableTrans
};
}

getEntriesForTransactionPair(
/**
* @param inputs
* @param transactions
*/
static getEntriesForTransactionPairReserialized(
inputs: [unknown, unknown, unknown],
transactions: [unknown, unknown]
): {
stakedObjectRef: SuiObjectRef;
amount: bigint;
} {
const [inputStakedSui, inputAmount, inputSharedObj] = inputs;

if (!ObjectCallArg.is(inputStakedSui)) {
throw new Error('Invalid input staked sui');
}

if (!PureCallArg.is(inputAmount)) {
throw new Error('Invalid input amount');
}

if (!ObjectCallArg.is(inputSharedObj)) {
throw new Error('Invalid input shared object');
}

const amount = BigInt(bcs.de(BCS.U64, Uint8Array.from(inputAmount.Pure)));
if (!isImmOrOwnedObj(inputStakedSui.Object)) {
throw new Error('Invalid input shared object');
}

return {
stakedObjectRef: inputStakedSui.Object.ImmOrOwned,
amount,
};
}

static parseTransactionPair(
inputs: SuiTransaction['tx']['inputs'],
transactions: unknown[]
): {
inputs: Entry[];
outputs: Entry[];
stakedObjectRef: SuiObjectRef;
amount: bigint;
} {
if (transactions.length !== 2) {
throw new Error('Invalid transaction pair');
Expand All @@ -166,15 +198,22 @@ export class UnstakingTransaction extends Transaction<UnstakingProgrammableTrans
throw new Error('Invalid inputs');
}

function isImmOrOwnedObj(obj: ObjectCallArg['Object']): obj is { ImmOrOwned: SuiObjectRef } {
return 'ImmOrOwned' in obj;
}

const [inputStakedSui, inputAmount, inputSharedObj] = inputs;
if (
!TransactionBlockInput.is(inputStakedSui) ||
!TransactionBlockInput.is(inputAmount) ||
!TransactionBlockInput.is(inputSharedObj) ||
!TransactionBlockInput.is(inputSharedObj)
) {
// for unclear reasons there seem to be two different serialization formats that we are dealing with
// try the other one here
return this.getEntriesForTransactionPairReserialized(
// we have length checked these earlier
inputs as [unknown, unknown, unknown],
transactions as [unknown, unknown]
);
}

if (
inputStakedSui.type !== 'object' ||
inputAmount.type !== 'pure' ||
typeof inputAmount.value !== 'string' ||
Expand All @@ -193,28 +232,56 @@ export class UnstakingTransaction extends Transaction<UnstakingProgrammableTrans
UnstakingBuilder.getTransactionBlockData(inputStakedSui.value.Object.ImmOrOwned, amount)
);

return this.getEntriesForStakedSuiInput(inputStakedSui, amount);
return {
stakedObjectRef: inputStakedSui.value.Object.ImmOrOwned,
amount,
};
}

getEntriesForSingleTransaction(
static parseTransactionSingle(
inputs: SuiTransaction['tx']['inputs'],
tx: unknown
): {
inputs: Entry[];
outputs: Entry[];
stakedObjectRef: SuiObjectRef;
} {
if (!MoveCallTransaction.is(tx) || !TransactionBlockInput.is(tx.arguments[1])) {
throw new Error('Invalid transaction');
}
const stakedSuiInputIdx = tx.arguments[1].index;
const stakedSuiInput = inputs[stakedSuiInputIdx];
let stakedSuiInput: unknown | SuiObjectRef = inputs[stakedSuiInputIdx];
if (!TransactionBlockInput.is(stakedSuiInput)) {
// for unclear reasons, in tests the stakedSuiInput is not a TransactionBlockInput sometimes
if (!ObjectCallArg.is(stakedSuiInput)) {
throw new Error('Invalid transaction');
}
}
return this.getEntriesForStakedSuiInput(stakedSuiInput as TransactionBlockInput);
if ('Object' in stakedSuiInput && isImmOrOwnedObj(stakedSuiInput.Object)) {
stakedSuiInput = stakedSuiInput.Object.ImmOrOwned as SuiObjectRef;
} else if ('value' in stakedSuiInput && isImmOrOwnedObj(stakedSuiInput.value.Object)) {
stakedSuiInput = stakedSuiInput.value.Object.ImmOrOwned as SuiObjectRef;
} else {
throw new Error('Invalid transaction');
}
if (!SuiObjectRef.is(stakedSuiInput)) {
throw new Error('Invalid transaction');
}
return {
stakedObjectRef: stakedSuiInput,
};
}

static parseTransaction(tx: UnstakingProgrammableTransaction): {
stakedObjectRef: SuiObjectRef;
amount?: bigint;
} {
const { inputs, transactions } = tx;
if (transactions.length === 1) {
return UnstakingTransaction.parseTransactionSingle(inputs, transactions[0]);
} else if (transactions.length === 2) {
return UnstakingTransaction.parseTransactionPair(inputs, transactions);
} else {
throw new InvalidTransactionError('Invalid transaction');
}
}

/**
Expand All @@ -225,18 +292,10 @@ export class UnstakingTransaction extends Transaction<UnstakingProgrammableTrans
return;
}

let parsed;
const { inputs, transactions } = this.suiTransaction.tx;
if (transactions.length === 1) {
parsed = this.getEntriesForSingleTransaction(inputs, transactions[0]);
} else if (transactions.length === 2) {
parsed = this.getEntriesForTransactionPair(inputs, transactions);
} else {
throw new InvalidTransactionError('Invalid transaction');
}

this._inputs = parsed.inputs;
this._outputs = parsed.outputs;
const parsed = UnstakingTransaction.parseTransaction(this.suiTransaction.tx);
const { inputs, outputs } = this.getEntriesForStakedSuiInput(parsed.stakedObjectRef, parsed.amount);
this._inputs = inputs;
this._outputs = outputs;
}

/**
Expand Down
11 changes: 10 additions & 1 deletion modules/sdk-coin-sui/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,19 @@ import {
SuiJsonValue,
SuiObjectRef,
} from './mystenlab/types';
import { builder, TransactionBlockInput, TransactionType as TransactionCommandType } from './mystenlab/builder';
import {
builder,
ObjectCallArg,
TransactionBlockInput,
TransactionType as TransactionCommandType,
} from './mystenlab/builder';
import { SIGNATURE_SCHEME_TO_FLAG } from './keyPair';
import blake2b from '@bitgo/blake2b';

export function isImmOrOwnedObj(obj: ObjectCallArg['Object']): obj is { ImmOrOwned: SuiObjectRef } {
return 'ImmOrOwned' in obj;
}

export class Utils implements BaseUtils {
/** @inheritdoc */
isValidBlockId(hash: string): boolean {
Expand Down
2 changes: 1 addition & 1 deletion modules/sdk-coin-sui/test/local_fullnode/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ async function getStakes(
if (result.length) {
return result;
}
const { attempts = 10, sleepMs = 1000 } = params;
const { attempts = 60, sleepMs = 1000 } = params;
if (0 < attempts) {
await new Promise((resolve) => setTimeout(resolve, sleepMs));
return await getStakes(conn, owner, { ...params, attempts: attempts - 1, sleepMs });
Expand Down
8 changes: 4 additions & 4 deletions modules/sdk-coin-sui/test/resources/sui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,8 @@ export const txInputWithdrawStaked = [
value: {
Object: {
ImmOrOwned: {
objectId: 'ee6dfc3da32e21541a2aeadfcd250f8a0a23bb7abda9c8988407fc32068c3746',
version: '1121',
objectId: '0xee6dfc3da32e21541a2aeadfcd250f8a0a23bb7abda9c8988407fc32068c3746',
version: 1121,
digest: 'EZ5yqap5XJJy9KhnW3dsbE73UmC5bd1KBEx7eQ5k4HNT',
},
},
Expand Down Expand Up @@ -295,8 +295,8 @@ export const txTransactionsWithdrawStaked = [
value: {
Object: {
ImmOrOwned: {
objectId: 'ee6dfc3da32e21541a2aeadfcd250f8a0a23bb7abda9c8988407fc32068c3746',
version: '1121',
objectId: '0xee6dfc3da32e21541a2aeadfcd250f8a0a23bb7abda9c8988407fc32068c3746',
version: 1121,
digest: 'EZ5yqap5XJJy9KhnW3dsbE73UmC5bd1KBEx7eQ5k4HNT',
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,14 @@ import should from 'should';
import utils from '../../../src/lib/utils';
import { Transaction as SuiTransaction } from '../../../src/lib/transaction';
import { SuiTransactionType, UnstakingProgrammableTransaction } from '../../../src/lib/iface';
import { UnstakingBuilder } from '../../../src';

describe('Sui unstaking Builder', () => {
const factory = getBuilderFactory('tsui');

function testUnstakingBuilder(amount: number | undefined) {
describe(`Success (amount=${amount})`, () => {
it(`should build a unstaking tx`, async function () {
const txBuilder = factory.getUnstakingBuilder();
txBuilder.type(SuiTransactionType.WithdrawStake);
txBuilder.sender(testData.sender.address);
txBuilder.unstake({ ...testData.requestWithdrawStakedSui, amount });
txBuilder.gasData(testData.gasData);
async function assertMatchesFixture(txBuilder: UnstakingBuilder, rebuild = true) {
const tx = (await txBuilder.build()) as SuiTransaction<UnstakingProgrammableTransaction>;

tx.suiTransaction.tx.should.eql(
Expand All @@ -28,6 +24,21 @@ describe('Sui unstaking Builder', () => {
rawTx,
amount === undefined ? testData.WITHDRAW_STAKED_SUI : testData.WITHDRAW_STAKED_SUI_WITH_AMOUNT
);

if (rebuild) {
const txBuilder = factory.getUnstakingBuilder();
txBuilder.from(rawTx);
await assertMatchesFixture(txBuilder, false);
}
}

it(`should build a unstaking tx`, async function () {
const txBuilder = factory.getUnstakingBuilder();
txBuilder.type(SuiTransactionType.WithdrawStake);
txBuilder.sender(testData.sender.address);
txBuilder.unstake({ ...testData.requestWithdrawStakedSui, amount });
txBuilder.gasData(testData.gasData);
await assertMatchesFixture(txBuilder);
});
});
}
Expand Down

0 comments on commit 95f66ec

Please sign in to comment.