From 21410fc7f5e75aedc1a895980ca97fb7e590b431 Mon Sep 17 00:00:00 2001 From: Alok Baltiyal Date: Fri, 24 Jan 2025 20:54:55 +0530 Subject: [PATCH] refactor(sdk-coin-apt): transaction builder TICKET: COIN-2896 --- modules/sdk-coin-apt/src/lib/constants.ts | 3 + .../src/lib/transaction/transaction.ts | 95 ++++++---------- .../lib/transaction/transferTransaction.ts | 104 ++++++++++++------ .../src/lib/transactionBuilder.ts | 14 --- .../src/lib/transactionBuilderFactory.ts | 21 +++- .../sdk-coin-apt/src/lib/transferBuilder.ts | 15 +++ modules/sdk-coin-apt/src/lib/utils.ts | 27 ++++- 7 files changed, 165 insertions(+), 114 deletions(-) diff --git a/modules/sdk-coin-apt/src/lib/constants.ts b/modules/sdk-coin-apt/src/lib/constants.ts index 836a1b11b6..ba6abd2887 100644 --- a/modules/sdk-coin-apt/src/lib/constants.ts +++ b/modules/sdk-coin-apt/src/lib/constants.ts @@ -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'; diff --git a/modules/sdk-coin-apt/src/lib/transaction/transaction.ts b/modules/sdk-coin-apt/src/lib/transaction/transaction.ts index 76f8599aea..6ca4515c99 100644 --- a/modules/sdk-coin-apt/src/lib/transaction/transaction.ts +++ b/modules/sdk-coin-apt/src/lib/transaction/transaction.ts @@ -7,12 +7,10 @@ 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, @@ -20,7 +18,6 @@ import { generateSigningMessage, generateUserTransactionHash, Hex, - Network, RAW_TRANSACTION_SALT, RAW_TRANSACTION_WITH_DATA_SALT, RawTransaction, @@ -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; @@ -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 @@ -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(); @@ -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, diff --git a/modules/sdk-coin-apt/src/lib/transaction/transferTransaction.ts b/modules/sdk-coin-apt/src/lib/transaction/transferTransaction.ts index 237f1836dc..fac1b0dbd2 100644 --- a/modules/sdk-coin-apt/src/lib/transaction/transferTransaction.ts +++ b/modules/sdk-coin-apt/src/lib/transaction/transferTransaction.ts @@ -1,6 +1,17 @@ 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) { @@ -8,35 +19,6 @@ export class TransferTransaction extends Transaction { 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, @@ -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 { + 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; + } } diff --git a/modules/sdk-coin-apt/src/lib/transactionBuilder.ts b/modules/sdk-coin-apt/src/lib/transactionBuilder.ts index eb15602574..15afe9e3bf 100644 --- a/modules/sdk-coin-apt/src/lib/transactionBuilder.ts +++ b/modules/sdk-coin-apt/src/lib/transactionBuilder.ts @@ -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 { - this.transaction.transactionType = this.transactionType; - await this.transaction.build(); - return this.transaction; - } - /** * Initialize the transaction builder fields using the decoded transaction data * diff --git a/modules/sdk-coin-apt/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-apt/src/lib/transactionBuilderFactory.ts index 9fbfb0a466..0dcfdfa76c 100644 --- a/modules/sdk-coin-apt/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-apt/src/lib/transactionBuilderFactory.ts @@ -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'; @@ -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)); diff --git a/modules/sdk-coin-apt/src/lib/transferBuilder.ts b/modules/sdk-coin-apt/src/lib/transferBuilder.ts index b1cd739500..4cfb8f3b0a 100644 --- a/modules/sdk-coin-apt/src/lib/transferBuilder.ts +++ b/modules/sdk-coin-apt/src/lib/transferBuilder.ts @@ -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) { @@ -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 { + this.transaction.transactionType = this.transactionType; + await this.transaction.build(); + return this.transaction; + } } diff --git a/modules/sdk-coin-apt/src/lib/utils.ts b/modules/sdk-coin-apt/src/lib/utils.ts index 2c62ba80c3..ca23d92d35 100644 --- a/modules/sdk-coin-apt/src/lib/utils.ts +++ b/modules/sdk-coin-apt/src/lib/utils.ts @@ -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 { @@ -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);