Skip to content

Commit

Permalink
Merge pull request #5428 from BitGo/COIN-2896-apt-transfer-correction
Browse files Browse the repository at this point in the history
refactor(apt): transaction builder
  • Loading branch information
baltiyal authored Jan 25, 2025
2 parents 745715b + 21410fc commit 3b77c20
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 114 deletions.
3 changes: 3 additions & 0 deletions modules/sdk-coin-apt/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ export const APT_SIGNATURE_LENGTH = 128;
export const UNAVAILABLE_TEXT = 'UNAVAILABLE';
export const DEFAULT_GAS_UNIT_PRICE = 100;
export const SECONDS_PER_WEEK = 7 * 24 * 60 * 60; // Days * Hours * Minutes * Seconds

export const APTOS_ACCOUNT_MODULE = 'aptos_account';
export const FUNGIBLE_ASSET_MODULE = 'fungible_asset';
95 changes: 33 additions & 62 deletions modules/sdk-coin-apt/src/lib/transaction/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,17 @@ import {
TransactionRecipient,
TransactionType,
} from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig, NetworkType } from '@bitgo/statics';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import {
AccountAddress,
AccountAuthenticatorEd25519,
Aptos,
AptosConfig,
DEFAULT_MAX_GAS_AMOUNT,
Ed25519PublicKey,
Ed25519Signature,
FeePayerRawTransaction,
generateSigningMessage,
generateUserTransactionHash,
Hex,
Network,
RAW_TRANSACTION_SALT,
RAW_TRANSACTION_WITH_DATA_SALT,
RawTransaction,
Expand All @@ -31,6 +28,7 @@ import {
import { DEFAULT_GAS_UNIT_PRICE, SECONDS_PER_WEEK, UNAVAILABLE_TEXT } from '../constants';
import utils from '../utils';
import BigNumber from 'bignumber.js';
import { AptTransactionExplanation } from '../iface';

export abstract class Transaction extends BaseTransaction {
protected _rawTransaction: RawTransaction;
Expand Down Expand Up @@ -214,43 +212,7 @@ export abstract class Transaction extends BaseTransaction {
];
}

fromRawTransaction(rawTransaction: string): void {
let signedTxn: SignedTransaction;
try {
signedTxn = utils.deserializeSignedTransaction(rawTransaction);
} catch (e) {
console.error('invalid raw transaction', e);
throw new Error('invalid raw transaction');
}
this.fromDeserializedSignedTransaction(signedTxn);
}

fromDeserializedSignedTransaction(signedTxn: SignedTransaction): void {
try {
const rawTxn = signedTxn.raw_txn;
this._sender = rawTxn.sender.toString();
this._recipient = utils.getRecipientFromTransactionPayload(rawTxn.payload);
this._sequenceNumber = utils.castToNumber(rawTxn.sequence_number);
this._maxGasAmount = utils.castToNumber(rawTxn.max_gas_amount);
this._gasUnitPrice = utils.castToNumber(rawTxn.gas_unit_price);
this._expirationTime = utils.castToNumber(rawTxn.expiration_timestamp_secs);
this._rawTransaction = rawTxn;

this.loadInputsAndOutputs();
const authenticator = signedTxn.authenticator as TransactionAuthenticatorFeePayer;
this._feePayerAddress = authenticator.fee_payer.address.toString();
const senderAuthenticator = authenticator.sender as AccountAuthenticatorEd25519;
const senderSignature = Buffer.from(senderAuthenticator.signature.toUint8Array());
this.addSenderSignature({ pub: senderAuthenticator.public_key.toString() }, senderSignature);

const feePayerAuthenticator = authenticator.fee_payer.authenticator as AccountAuthenticatorEd25519;
const feePayerSignature = Buffer.from(feePayerAuthenticator.signature.toUint8Array());
this.addFeePayerSignature({ pub: feePayerAuthenticator.public_key.toString() }, feePayerSignature);
} catch (e) {
console.error('invalid signed transaction', e);
throw new Error('invalid signed transaction');
}
}
abstract fromRawTransaction(rawTransaction: string): void;

/**
* Deserializes a signed transaction hex string
Expand All @@ -266,27 +228,7 @@ export abstract class Transaction extends BaseTransaction {
}
}

protected async buildRawTransaction() {
const network: Network = this._coinConfig.network.type === NetworkType.MAINNET ? Network.MAINNET : Network.TESTNET;
const aptos = new Aptos(new AptosConfig({ network }));
const senderAddress = AccountAddress.fromString(this._sender);
const recipientAddress = AccountAddress.fromString(this._recipient.address);

const simpleTxn = await aptos.transaction.build.simple({
sender: senderAddress,
data: {
function: '0x1::aptos_account::transfer',
functionArguments: [recipientAddress, this.recipient.amount],
},
options: {
maxGasAmount: this.maxGasAmount,
gasUnitPrice: this.gasUnitPrice,
expireTimestamp: this.expirationTime,
accountSequenceNumber: this.sequenceNumber,
},
});
this._rawTransaction = simpleTxn.rawTransaction;
}
protected abstract buildRawTransaction(): void;

public getFee(): string {
return new BigNumber(this.gasUsed).multipliedBy(this.gasUnitPrice).toString();
Expand All @@ -296,6 +238,35 @@ export abstract class Transaction extends BaseTransaction {
return this.feePayerAddress ? this.getSignablePayloadWithFeePayer() : this.getSignablePayloadWithoutFeePayer();
}

/** @inheritDoc */
explainTransaction(): AptTransactionExplanation {
const displayOrder = [
'id',
'outputs',
'outputAmount',
'changeOutputs',
'changeAmount',
'fee',
'withdrawAmount',
'sender',
'type',
];

const outputs: TransactionRecipient[] = [this.recipient];
const outputAmount = outputs[0].amount;
return {
displayOrder,
id: this.id,
outputs,
outputAmount,
changeOutputs: [],
changeAmount: '0',
fee: { fee: this.getFee() },
sender: this.sender,
type: this.type,
};
}

private getSignablePayloadWithFeePayer(): Buffer {
const feePayerRawTxn = new FeePayerRawTransaction(
this._rawTransaction,
Expand Down
104 changes: 73 additions & 31 deletions modules/sdk-coin-apt/src/lib/transaction/transferTransaction.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,24 @@
import { Transaction } from './transaction';
import { AptTransactionExplanation, TransferTxData } from '../iface';
import { TransactionRecipient, TransactionType } from '@bitgo/sdk-core';
import { TransferTxData } from '../iface';
import { TransactionType } from '@bitgo/sdk-core';
import {
AccountAddress,
AccountAuthenticatorEd25519,
Aptos,
AptosConfig,
Network,
SignedTransaction,
TransactionAuthenticatorFeePayer,
} from '@aptos-labs/ts-sdk';
import utils from '../utils';
import { NetworkType } from '@bitgo/statics';

export class TransferTransaction extends Transaction {
constructor(coinConfig) {
super(coinConfig);
this._type = TransactionType.Send;
}

/** @inheritDoc */
explainTransaction(): AptTransactionExplanation {
const displayOrder = [
'id',
'outputs',
'outputAmount',
'changeOutputs',
'changeAmount',
'fee',
'withdrawAmount',
'sender',
'type',
];

const outputs: TransactionRecipient[] = [this.recipient];
const outputAmount = outputs[0].amount;
return {
displayOrder,
id: this.id,
outputs,
outputAmount,
changeOutputs: [],
changeAmount: '0',
fee: { fee: this.getFee() },
sender: this.sender,
type: this.type,
};
}

toJson(): TransferTxData {
return {
id: this.id,
Expand All @@ -50,4 +32,64 @@ export class TransferTransaction extends Transaction {
feePayer: this.feePayerAddress,
};
}

fromRawTransaction(rawTransaction: string): void {
let signedTxn: SignedTransaction;
try {
signedTxn = utils.deserializeSignedTransaction(rawTransaction);
} catch (e) {
console.error('invalid raw transaction', e);
throw new Error('invalid raw transaction');
}
this.fromDeserializedSignedTransaction(signedTxn);
}

fromDeserializedSignedTransaction(signedTxn: SignedTransaction): void {
try {
const rawTxn = signedTxn.raw_txn;
this._sender = rawTxn.sender.toString();
this._recipient = utils.getRecipientFromTransactionPayload(rawTxn.payload);
this._sequenceNumber = utils.castToNumber(rawTxn.sequence_number);
this._maxGasAmount = utils.castToNumber(rawTxn.max_gas_amount);
this._gasUnitPrice = utils.castToNumber(rawTxn.gas_unit_price);
this._expirationTime = utils.castToNumber(rawTxn.expiration_timestamp_secs);
this._rawTransaction = rawTxn;

this.loadInputsAndOutputs();
const authenticator = signedTxn.authenticator as TransactionAuthenticatorFeePayer;
this._feePayerAddress = authenticator.fee_payer.address.toString();
const senderAuthenticator = authenticator.sender as AccountAuthenticatorEd25519;
const senderSignature = Buffer.from(senderAuthenticator.signature.toUint8Array());
this.addSenderSignature({ pub: senderAuthenticator.public_key.toString() }, senderSignature);

const feePayerAuthenticator = authenticator.fee_payer.authenticator as AccountAuthenticatorEd25519;
const feePayerSignature = Buffer.from(feePayerAuthenticator.signature.toUint8Array());
this.addFeePayerSignature({ pub: feePayerAuthenticator.public_key.toString() }, feePayerSignature);
} catch (e) {
console.error('invalid signed transaction', e);
throw new Error('invalid signed transaction');
}
}

protected async buildRawTransaction(): Promise<void> {
const network: Network = this._coinConfig.network.type === NetworkType.MAINNET ? Network.MAINNET : Network.TESTNET;
const aptos = new Aptos(new AptosConfig({ network }));
const senderAddress = AccountAddress.fromString(this._sender);
const recipientAddress = AccountAddress.fromString(this._recipient.address);

const simpleTxn = await aptos.transaction.build.simple({
sender: senderAddress,
data: {
function: '0x1::aptos_account::transfer',
functionArguments: [recipientAddress, this.recipient.amount],
},
options: {
maxGasAmount: this.maxGasAmount,
gasUnitPrice: this.gasUnitPrice,
expireTimestamp: this.expirationTime,
accountSequenceNumber: this.sequenceNumber,
},
});
this._rawTransaction = simpleTxn.rawTransaction;
}
}
14 changes: 0 additions & 14 deletions modules/sdk-coin-apt/src/lib/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,6 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
this.transaction.addFeePayerSignature(publicKey, signature);
}

/** @inheritdoc */
protected fromImplementation(rawTransaction: string): Transaction {
this.transaction.fromRawTransaction(rawTransaction);
this.transaction.transactionType = this.transactionType;
return this.transaction;
}

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
this.transaction.transactionType = this.transactionType;
await this.transaction.build();
return this.transaction;
}

/**
* Initialize the transaction builder fields using the decoded transaction data
*
Expand Down
21 changes: 15 additions & 6 deletions modules/sdk-coin-apt/src/lib/transactionBuilderFactory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BaseTransactionBuilderFactory } from '@bitgo/sdk-core';
import { BaseTransactionBuilderFactory, InvalidTransactionError, TransactionType } from '@bitgo/sdk-core';
import { TransactionBuilder } from './transactionBuilder';
import { TransferBuilder } from './transferBuilder';
import utils from './utils';
Expand All @@ -17,16 +17,25 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
utils.validateRawTransaction(signedRawTxn);
try {
const signedTxn = this.parseTransaction(signedRawTxn);
// Assumption: only a single transaction type exists
// TODO: add txn type switch case
const transferTx = new TransferTransaction(this._coinConfig);
transferTx.fromDeserializedSignedTransaction(signedTxn);
return this.getTransferBuilder(transferTx);
const txnType = this.getTransactionTypeFromSignedTxn(signedTxn);
switch (txnType) {
case TransactionType.Send:
const transferTx = new TransferTransaction(this._coinConfig);
transferTx.fromDeserializedSignedTransaction(signedTxn);
return this.getTransferBuilder(transferTx);
default:
throw new InvalidTransactionError('Invalid transaction');
}
} catch (e) {
throw e;
}
}

getTransactionTypeFromSignedTxn(signedTxn: SignedTransaction): TransactionType {
const rawTxn = signedTxn.raw_txn;
return utils.getTransactionTypeFromTransactionPayload(rawTxn.payload);
}

/** @inheritdoc */
getTransferBuilder(tx?: Transaction): TransferBuilder {
return this.initializeBuilder(tx, new TransferBuilder(this._coinConfig));
Expand Down
15 changes: 15 additions & 0 deletions modules/sdk-coin-apt/src/lib/transferBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { TransactionBuilder } from './transactionBuilder';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { TransactionType } from '@bitgo/sdk-core';
import { TransferTransaction } from './transaction/transferTransaction';
import { Transaction } from './transaction/transaction';

export class TransferBuilder extends TransactionBuilder {
constructor(_coinConfig: Readonly<CoinConfig>) {
Expand All @@ -21,4 +22,18 @@ export class TransferBuilder extends TransactionBuilder {
initBuilder(tx: TransferTransaction): void {
this._transaction = tx;
}

/** @inheritdoc */
protected fromImplementation(rawTransaction: string): Transaction {
this.transaction.fromRawTransaction(rawTransaction);
this.transaction.transactionType = this.transactionType;
return this.transaction;
}

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
this.transaction.transactionType = this.transactionType;
await this.transaction.build();
return this.transaction;
}
}
27 changes: 26 additions & 1 deletion modules/sdk-coin-apt/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,21 @@ import {
} from '@aptos-labs/ts-sdk';
import {
BaseUtils,
InvalidTransactionError,
isValidEd25519PublicKey,
isValidEd25519SecretKey,
ParseTransactionError,
TransactionRecipient,
TransactionType,
} from '@bitgo/sdk-core';
import { APT_ADDRESS_LENGTH, APT_BLOCK_ID_LENGTH, APT_SIGNATURE_LENGTH, APT_TRANSACTION_ID_LENGTH } from './constants';
import {
APT_ADDRESS_LENGTH,
APT_BLOCK_ID_LENGTH,
APT_SIGNATURE_LENGTH,
APT_TRANSACTION_ID_LENGTH,
APTOS_ACCOUNT_MODULE,
FUNGIBLE_ASSET_MODULE,
} from './constants';
import BigNumber from 'bignumber.js';

export class Utils implements BaseUtils {
Expand Down Expand Up @@ -72,6 +81,22 @@ export class Utils implements BaseUtils {
return { address, amount };
}

getTransactionTypeFromTransactionPayload(payload: TransactionPayload): TransactionType {
if (!(payload instanceof TransactionPayloadEntryFunction)) {
throw new Error('Invalid Payload: Expected TransactionPayloadEntryFunction');
}
const entryFunction = payload.entryFunction;
const moduleIdentifier = entryFunction.module_name.name.identifier.trim();
switch (moduleIdentifier) {
case APTOS_ACCOUNT_MODULE:
return TransactionType.Send;
case FUNGIBLE_ASSET_MODULE:
return TransactionType.SendToken;
default:
throw new InvalidTransactionError(`Invalid transaction: unable to fetch transaction type ${moduleIdentifier}`);
}
}

isValidRawTransaction(rawTransaction: string): boolean {
try {
const signedTxn = this.deserializeSignedTransaction(rawTransaction);
Expand Down

0 comments on commit 3b77c20

Please sign in to comment.