Skip to content
Merged
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-sui/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export interface SuiTransaction<T = SuiProgrammableTransaction> {
sender: string;
tx: T;
gasData: GasData;
inputObjects?: SuiObjectRef[];
}

export interface RequestAddStake {
Expand Down
15 changes: 15 additions & 0 deletions modules/sdk-coin-sui/src/lib/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export abstract class TransactionBuilder<T = SuiProgrammableTransaction> extends
protected _type: SuiTransactionType;
protected _sender: string;
protected _gasData: GasData;
protected _inputObjects: SuiObjectRef[];

protected constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
Expand Down Expand Up @@ -93,6 +94,12 @@ export abstract class TransactionBuilder<T = SuiProgrammableTransaction> extends
return this;
}

inputObjects(inputObjects: SuiObjectRef[]): this {
this.validateInputObjectsBase(inputObjects);
this._inputObjects = inputObjects;
return this;
}

/**
* Initialize the transaction builder fields using the decoded transaction data
*
Expand Down Expand Up @@ -148,6 +155,14 @@ export abstract class TransactionBuilder<T = SuiProgrammableTransaction> extends
});
}

protected validateInputObjectsBase(inputObjects: SuiObjectRef[]): void {
if (inputObjects && inputObjects.length > 0) {
inputObjects.forEach((inputObject) => {
this.validateSuiObjectRef(inputObject, 'input object');
});
}
}

validateSuiObjectRef(suiObjectRef: SuiObjectRef, field: string): void {
if (!suiObjectRef.hasOwnProperty('objectId')) {
throw new BuildTransactionError(`Invalid ${field}, missing objectId`);
Expand Down
116 changes: 76 additions & 40 deletions modules/sdk-coin-sui/src/lib/transferBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Inputs,
Transactions as TransactionsConstructor,
TransactionBlock as ProgrammingTransactionBlockBuilder,
TransactionArgument,
} from './mystenlab/builder';
import utils from './utils';
import { MAX_COMMAND_ARGS, MAX_GAS_OBJECTS } from './constants';
Expand Down Expand Up @@ -88,6 +89,10 @@ export class TransferBuilder extends TransactionBuilder<TransferProgrammableTran
this.sender(txData.sender);
this.gasData(txData.gasData);

if (txData.inputObjects) {
this.inputObjects(txData.inputObjects);
}

const recipients = utils.getRecipients(tx.suiTransaction);
this.send(recipients);
}
Expand All @@ -104,6 +109,11 @@ export class TransferBuilder extends TransactionBuilder<TransferProgrammableTran
);
assert(this._gasData, new BuildTransactionError('gasData is required before building'));
this.validateGasData(this._gasData);

// If inputObjects are provided, validate them
if (this._inputObjects && this._inputObjects.length > 0) {
this.validateInputObjectsBase(this._inputObjects);
}
}

/**
Expand All @@ -115,47 +125,73 @@ export class TransferBuilder extends TransactionBuilder<TransferProgrammableTran
this.validateTransactionFields();
const programmableTxBuilder = new ProgrammingTransactionBlockBuilder();

// number of objects passed as gas payment should be strictly less than `MAX_GAS_OBJECTS`. When the transaction
// requires a larger number of inputs we use the merge command to merge the rest of the objects into the gasCoin
if (this._gasData.payment.length >= MAX_GAS_OBJECTS) {
const gasPaymentObjects = this._gasData.payment
.slice(MAX_GAS_OBJECTS - 1)
.map((object) => Inputs.ObjectRef(object));

// limit for total number of `args: CallArg[]` for a single command is MAX_COMMAND_ARGS so the max length of
// `sources[]` for a `mergeCoins(destination, sources[])` command is MAX_COMMAND_ARGS - 1 (1 used up for
// `destination`). We need to create a total of `gasPaymentObjects/(MAX_COMMAND_ARGS - 1)` merge commands to
// merge all the objects
while (gasPaymentObjects.length > 0) {
programmableTxBuilder.mergeCoins(
programmableTxBuilder.gas,
gasPaymentObjects.splice(0, MAX_COMMAND_ARGS - 1).map((object) => programmableTxBuilder.object(object))
);
if (this._sender !== this._gasData.owner && this._inputObjects && this._inputObjects.length > 0) {
const inputObjects = this._inputObjects.map((object) => programmableTxBuilder.object(Inputs.ObjectRef(object)));
const mergedObject = inputObjects.shift() as TransactionArgument;
if (inputObjects.length > 0) {
programmableTxBuilder.mergeCoins(mergedObject, inputObjects);
}
}

this._recipients.forEach((recipient) => {
const coin = programmableTxBuilder.add(
TransactionsConstructor.SplitCoins(programmableTxBuilder.gas, [
this._recipients.forEach((recipient) => {
const splitObject = programmableTxBuilder.splitCoins(mergedObject, [
programmableTxBuilder.pure(Number(recipient.amount)),
])
);
programmableTxBuilder.add(
TransactionsConstructor.TransferObjects([coin], programmableTxBuilder.object(recipient.address))
);
});
const txData = programmableTxBuilder.blockData;
return {
type: this._type,
sender: this._sender,
tx: {
inputs: [...txData.inputs],
transactions: [...txData.transactions],
},
gasData: {
...this._gasData,
payment: this._gasData.payment.slice(0, MAX_GAS_OBJECTS - 1),
},
};
]);
programmableTxBuilder.transferObjects([splitObject], programmableTxBuilder.object(recipient.address));
});
const txData = programmableTxBuilder.blockData;
return {
type: this._type,
sender: this._sender,
tx: {
inputs: [...txData.inputs],
transactions: [...txData.transactions],
},
gasData: {
...this._gasData,
},
};
} else {
// number of objects passed as gas payment should be strictly less than `MAX_GAS_OBJECTS`. When the transaction
// requires a larger number of inputs we use the merge command to merge the rest of the objects into the gasCoin
if (this._gasData.payment.length >= MAX_GAS_OBJECTS) {
const gasPaymentObjects = this._gasData.payment
.slice(MAX_GAS_OBJECTS - 1)
.map((object) => Inputs.ObjectRef(object));

// limit for total number of `args: CallArg[]` for a single command is MAX_COMMAND_ARGS so the max length of
// `sources[]` for a `mergeCoins(destination, sources[])` command is MAX_COMMAND_ARGS - 1 (1 used up for
// `destination`). We need to create a total of `gasPaymentObjects/(MAX_COMMAND_ARGS - 1)` merge commands to
// merge all the objects
while (gasPaymentObjects.length > 0) {
programmableTxBuilder.mergeCoins(
programmableTxBuilder.gas,
gasPaymentObjects.splice(0, MAX_COMMAND_ARGS - 1).map((object) => programmableTxBuilder.object(object))
);
}
}

this._recipients.forEach((recipient) => {
const coin = programmableTxBuilder.add(
TransactionsConstructor.SplitCoins(programmableTxBuilder.gas, [
programmableTxBuilder.pure(Number(recipient.amount)),
])
);
programmableTxBuilder.add(
TransactionsConstructor.TransferObjects([coin], programmableTxBuilder.object(recipient.address))
);
});
const txData = programmableTxBuilder.blockData;
return {
type: this._type,
sender: this._sender,
tx: {
inputs: [...txData.inputs],
transactions: [...txData.transactions],
},
gasData: {
...this._gasData,
payment: this._gasData.payment.slice(0, MAX_GAS_OBJECTS - 1),
},
};
}
}
}
64 changes: 62 additions & 2 deletions modules/sdk-coin-sui/src/lib/transferTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@ import { MAX_GAS_OBJECTS, SUI_ADDRESS_LENGTH, UNAVAILABLE_TEXT } from './constan
import { Buffer } from 'buffer';
import { Transaction } from './transaction';
import { CallArg, SuiObjectRef, normalizeSuiAddress } from './mystenlab/types';
import utils from './utils';
import { builder, Inputs, TransactionBlockInput } from './mystenlab/builder';
import utils, { isImmOrOwnedObj } from './utils';
import {
builder,
Inputs,
TransactionArgument,
TransactionBlockInput,
TransactionType as SuiTransactionBlockType,
} from './mystenlab/builder';
import { BCS } from '@mysten/bcs';
import BigNumber from 'bignumber.js';

Expand Down Expand Up @@ -70,6 +76,17 @@ export class TransferTransaction extends Transaction<TransferProgrammableTransac
}

const tx = this._suiTransaction;
if (tx.gasData.owner !== tx.sender) {
// Sponsored transaction
return {
id: this._id,
sender: tx.sender,
kind: { ProgrammableTransaction: tx.tx },
gasData: tx.gasData,
expiration: { None: null },
inputObjects: this.getInputObjectsFromTx(tx.tx),
};
}
return {
id: this._id,
sender: tx.sender,
Expand Down Expand Up @@ -194,6 +211,16 @@ export class TransferTransaction extends Transaction<TransferProgrammableTransac
transactions: this._suiTransaction.tx.transactions,
} as TransferProgrammableTransaction;

if (this._suiTransaction.gasData.owner !== this._suiTransaction.sender) {
return {
sender: this._suiTransaction.sender,
expiration: { None: null },
gasData: this._suiTransaction.gasData,
kind: {
ProgrammableTransaction: programmableTx,
},
};
}
return {
sender: this._suiTransaction.sender,
expiration: { None: null },
Expand Down Expand Up @@ -229,4 +256,37 @@ export class TransferTransaction extends Transaction<TransferProgrammableTransac
outputs,
};
}

/**
* Extracts the objects that were provided as inputs while building the transaction
* @param tx
* @returns {SuiObjectRef[]} Objects that are inputs for the transaction
*/
private getInputObjectsFromTx(tx: TransferProgrammableTransaction): SuiObjectRef[] {
const inputs = tx.inputs;
const transaction = tx.transactions[0] as SuiTransactionBlockType;

let args: TransactionArgument[] = [];
if (transaction.kind === 'MergeCoins') {
const { destination, sources } = transaction;
args = [destination, ...sources];
} else if (transaction.kind === 'SplitCoins') {
args = [transaction.coin];
}

const inputObjects: SuiObjectRef[] = [];
args.forEach((arg) => {
if (arg.kind === 'Input') {
let input = inputs[arg.index];
if ('value' in input) {
input = input.value;
}
if ('Object' in input && isImmOrOwnedObj(input.Object)) {
inputObjects.push(input.Object.ImmOrOwned);
}
}
});

return inputObjects;
}
}
Loading