diff --git a/modules/sdk-coin-apt/src/lib/constants.ts b/modules/sdk-coin-apt/src/lib/constants.ts index ba6abd2887..c58a4b5af1 100644 --- a/modules/sdk-coin-apt/src/lib/constants.ts +++ b/modules/sdk-coin-apt/src/lib/constants.ts @@ -7,4 +7,10 @@ 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'; +export const FUNGIBLE_ASSET_MODULE = 'primary_fungible_store'; + +export const FUNGIBLE_ASSET_TRANSFER_FUNCTION = '0x1::primary_fungible_store::transfer'; +export const COIN_TRANSFER_FUNCTION = '0x1::aptos_account::transfer_coins'; + +export const FUNGIBLE_ASSET_TYPE_ARGUMENT = '0x1::fungible_asset::Metadata'; +export const APTOS_COIN = '0x1::aptos_coin::AptosCoin'; diff --git a/modules/sdk-coin-apt/src/lib/iface.ts b/modules/sdk-coin-apt/src/lib/iface.ts index 0e055a9bb4..78ad1305c8 100644 --- a/modules/sdk-coin-apt/src/lib/iface.ts +++ b/modules/sdk-coin-apt/src/lib/iface.ts @@ -12,7 +12,7 @@ export interface AptTransactionExplanation extends BaseTransactionExplanation { /** * The transaction data returned from the toJson() function of a transaction */ -export interface TransferTxData { +export interface TxData { id: string; sender: string; recipient: TransactionRecipient; @@ -22,4 +22,5 @@ export interface TransferTxData { gasUsed: number; expirationTime: number; feePayer: string; + assetId: string; } diff --git a/modules/sdk-coin-apt/src/lib/index.ts b/modules/sdk-coin-apt/src/lib/index.ts index e0d02046f3..9b1722fdae 100644 --- a/modules/sdk-coin-apt/src/lib/index.ts +++ b/modules/sdk-coin-apt/src/lib/index.ts @@ -4,7 +4,7 @@ import * as Interface from './iface'; export { KeyPair } from './keyPair'; export { Transaction } from './transaction/transaction'; export { TransferTransaction } from './transaction/transferTransaction'; -export { TransactionBuilder } from './transactionBuilder'; -export { TransferBuilder } from './transferBuilder'; +export { TransactionBuilder } from './transactionBuilder/transactionBuilder'; +export { TransferBuilder } from './transactionBuilder/transferBuilder'; export { TransactionBuilderFactory } from './transactionBuilderFactory'; export { Interface, Utils }; diff --git a/modules/sdk-coin-apt/src/lib/transaction/fungibleAssetTransaction.ts b/modules/sdk-coin-apt/src/lib/transaction/fungibleAssetTransaction.ts new file mode 100644 index 0000000000..3013dd618b --- /dev/null +++ b/modules/sdk-coin-apt/src/lib/transaction/fungibleAssetTransaction.ts @@ -0,0 +1,71 @@ +import { Transaction } from './transaction'; +import { + AccountAddress, + Aptos, + AptosConfig, + EntryFunctionABI, + Network, + parseTypeTag, + TransactionPayload, + TransactionPayloadEntryFunction, + TypeTagAddress, + TypeTagU64, +} from '@aptos-labs/ts-sdk'; +import { InvalidTransactionError, TransactionRecipient, TransactionType } from '@bitgo/sdk-core'; +import { BaseCoin as CoinConfig, NetworkType } from '@bitgo/statics'; +import { FUNGIBLE_ASSET_TYPE_ARGUMENT, FUNGIBLE_ASSET_TRANSFER_FUNCTION } from '../constants'; + +export class FungibleAssetTransaction extends Transaction { + constructor(coinConfig: Readonly) { + super(coinConfig); + this._type = TransactionType.SendToken; + } + + protected parseTransactionPayload(payload: TransactionPayload): void { + if ( + !(payload instanceof TransactionPayloadEntryFunction) || + payload.entryFunction.args.length !== 3 || + FUNGIBLE_ASSET_TYPE_ARGUMENT !== payload.entryFunction.type_args[0].toString() + ) { + throw new InvalidTransactionError('Invalid transaction payload'); + } + const entryFunction = payload.entryFunction; + if (!this._recipient) { + this._recipient = {} as TransactionRecipient; + } + this._assetId = entryFunction.args[0].toString(); + this._recipient.address = entryFunction.args[1].toString(); + const amountBuffer = Buffer.from(entryFunction.args[2].bcsToBytes()); + this._recipient.amount = amountBuffer.readBigUint64LE().toString(); + } + + 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 fungibleTokenAddress = this._assetId; + + const faTransferAbi: EntryFunctionABI = { + typeParameters: [{ constraints: [] }], + parameters: [parseTypeTag('0x1::object::Object'), new TypeTagAddress(), new TypeTagU64()], + }; + + const simpleTxn = await aptos.transaction.build.simple({ + sender: senderAddress, + data: { + function: FUNGIBLE_ASSET_TRANSFER_FUNCTION, + typeArguments: [FUNGIBLE_ASSET_TYPE_ARGUMENT], + functionArguments: [fungibleTokenAddress, recipientAddress, this.recipient.amount], + abi: faTransferAbi, + }, + 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/transaction/transaction.ts b/modules/sdk-coin-apt/src/lib/transaction/transaction.ts index 6ca4515c99..8e1b37809f 100644 --- a/modules/sdk-coin-apt/src/lib/transaction/transaction.ts +++ b/modules/sdk-coin-apt/src/lib/transaction/transaction.ts @@ -24,11 +24,12 @@ import { SignedTransaction, SimpleTransaction, TransactionAuthenticatorFeePayer, + TransactionPayload, } from '@aptos-labs/ts-sdk'; 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'; +import { AptTransactionExplanation, TxData } from '../iface'; export abstract class Transaction extends BaseTransaction { protected _rawTransaction: RawTransaction; @@ -42,6 +43,7 @@ export abstract class Transaction extends BaseTransaction { protected _gasUsed: number; protected _expirationTime: number; protected _feePayerAddress: string; + protected _assetId: string; static EMPTY_PUBLIC_KEY = Buffer.alloc(32); static EMPTY_SIGNATURE = Buffer.alloc(64); @@ -54,6 +56,7 @@ export abstract class Transaction extends BaseTransaction { this._expirationTime = Math.floor(Date.now() / 1e3) + SECONDS_PER_WEEK; this._sequenceNumber = 0; this._sender = AccountAddress.ZERO.toString(); + this._assetId = AccountAddress.ZERO.toString(); this._senderSignature = { publicKey: { pub: Hex.fromHexInput(Transaction.EMPTY_PUBLIC_KEY).toString(), @@ -139,6 +142,45 @@ export abstract class Transaction extends BaseTransaction { this._type = transactionType; } + get assetId(): string { + return this._assetId; + } + + set assetId(value: string) { + this._assetId = value; + } + + protected abstract buildRawTransaction(): void; + + protected abstract parseTransactionPayload(payload: TransactionPayload): void; + + fromDeserializedSignedTransaction(signedTxn: SignedTransaction): void { + try { + const rawTxn = signedTxn.raw_txn; + this.parseTransactionPayload(rawTxn.payload); + this._sender = rawTxn.sender.toString(); + 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'); + } + } + canSign(_key: BaseKey): boolean { return false; } @@ -212,8 +254,16 @@ export abstract class Transaction extends BaseTransaction { ]; } - abstract fromRawTransaction(rawTransaction: string): void; - + 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); + } /** * Deserializes a signed transaction hex string * @param {string} signedRawTransaction @@ -228,7 +278,20 @@ export abstract class Transaction extends BaseTransaction { } } - protected abstract buildRawTransaction(): void; + toJson(): TxData { + return { + id: this.id, + sender: this.sender, + recipient: this.recipient, + sequenceNumber: this.sequenceNumber, + maxGasAmount: this.maxGasAmount, + gasUnitPrice: this.gasUnitPrice, + gasUsed: this.gasUsed, + expirationTime: this.expirationTime, + feePayer: this.feePayerAddress, + assetId: this.assetId, + }; + } public getFee(): string { return new BigNumber(this.gasUsed).multipliedBy(this.gasUnitPrice).toString(); diff --git a/modules/sdk-coin-apt/src/lib/transaction/transferTransaction.ts b/modules/sdk-coin-apt/src/lib/transaction/transferTransaction.ts index fac1b0dbd2..710514120f 100644 --- a/modules/sdk-coin-apt/src/lib/transaction/transferTransaction.ts +++ b/modules/sdk-coin-apt/src/lib/transaction/transferTransaction.ts @@ -1,74 +1,40 @@ import { Transaction } from './transaction'; -import { TransferTxData } from '../iface'; -import { TransactionType } from '@bitgo/sdk-core'; +import { InvalidTransactionError, TransactionRecipient, TransactionType } from '@bitgo/sdk-core'; import { AccountAddress, - AccountAuthenticatorEd25519, Aptos, AptosConfig, Network, - SignedTransaction, - TransactionAuthenticatorFeePayer, + TransactionPayload, + TransactionPayloadEntryFunction, } from '@aptos-labs/ts-sdk'; -import utils from '../utils'; -import { NetworkType } from '@bitgo/statics'; + +import { BaseCoin as CoinConfig, NetworkType } from '@bitgo/statics'; +import { APTOS_COIN, COIN_TRANSFER_FUNCTION } from '../constants'; export class TransferTransaction extends Transaction { - constructor(coinConfig) { + constructor(coinConfig: Readonly) { super(coinConfig); this._type = TransactionType.Send; + this._assetId = APTOS_COIN; } - toJson(): TransferTxData { - return { - id: this.id, - sender: this.sender, - recipient: this.recipient, - sequenceNumber: this.sequenceNumber, - maxGasAmount: this.maxGasAmount, - gasUnitPrice: this.gasUnitPrice, - gasUsed: this.gasUsed, - expirationTime: this.expirationTime, - 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'); + protected parseTransactionPayload(payload: TransactionPayload): void { + if ( + !(payload instanceof TransactionPayloadEntryFunction) || + !payload.entryFunction.type_args[0] || + payload.entryFunction.type_args[0].toString().length === 0 + ) { + throw new InvalidTransactionError('Invalid transaction payload'); } - 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'); + const entryFunction = payload.entryFunction; + if (!this._recipient) { + this._recipient = {} as TransactionRecipient; } + this._assetId = entryFunction.type_args[0].toString(); + this._recipient.address = entryFunction.args[0].toString(); + const amountBuffer = Buffer.from(entryFunction.args[1].bcsToBytes()); + this._recipient.amount = amountBuffer.readBigUint64LE().toString(); } protected async buildRawTransaction(): Promise { @@ -76,11 +42,11 @@ export class TransferTransaction extends Transaction { 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', + function: COIN_TRANSFER_FUNCTION, + typeArguments: [this.assetId], functionArguments: [recipientAddress, this.recipient.amount], }, options: { diff --git a/modules/sdk-coin-apt/src/lib/transactionBuilder/fungibleAssetTransactionBuilder.ts b/modules/sdk-coin-apt/src/lib/transactionBuilder/fungibleAssetTransactionBuilder.ts new file mode 100644 index 0000000000..77d7ba415e --- /dev/null +++ b/modules/sdk-coin-apt/src/lib/transactionBuilder/fungibleAssetTransactionBuilder.ts @@ -0,0 +1,62 @@ +import { TransactionBuilder } from './transactionBuilder'; +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { FungibleAssetTransaction } from '../transaction/fungibleAssetTransaction'; +import { TransactionType } from '@bitgo/sdk-core'; +import BigNumber from 'bignumber.js'; +import utils from '../utils'; +import { TransactionPayload, TransactionPayloadEntryFunction } from '@aptos-labs/ts-sdk'; +import { FUNGIBLE_ASSET_TYPE_ARGUMENT } from '../constants'; + +export class FungibleAssetTransactionBuilder extends TransactionBuilder { + constructor(_coinConfig: Readonly) { + super(_coinConfig); + this._transaction = new FungibleAssetTransaction(_coinConfig); + } + + protected get transactionType(): TransactionType { + return TransactionType.SendToken; + } + + assetId(assetId: string): TransactionBuilder { + this.validateAddress({ address: assetId }); + this.transaction.assetId = assetId; + return this; + } + + /** @inheritdoc */ + validateTransaction(transaction?: FungibleAssetTransaction): void { + if (!transaction) { + throw new Error('transaction not defined'); + } + this.validateAddress({ address: transaction.sender }); + this.validateAddress({ address: transaction.recipient.address }); + this.validateValue(new BigNumber(transaction.recipient.amount)); + this.validateAddress({ address: transaction.assetId }); + } + + protected isValidTransactionPayload(payload: TransactionPayload) { + try { + if ( + !(payload instanceof TransactionPayloadEntryFunction) || + payload.entryFunction.args.length !== 3 || + FUNGIBLE_ASSET_TYPE_ARGUMENT !== payload.entryFunction.type_args[0].toString() + ) { + console.error('invalid transaction payload'); + return false; + } + const entryFunction = payload.entryFunction; + const fungibleTokenAddress = entryFunction.args[0].toString(); + const recipientAddress = entryFunction.args[1].toString(); + const amountBuffer = Buffer.from(entryFunction.args[2].bcsToBytes()); + const recipientAmount = new BigNumber(amountBuffer.readBigUint64LE().toString()); + return ( + utils.isValidAddress(recipientAddress) && + utils.isValidAddress(fungibleTokenAddress) && + !recipientAmount.isLessThan(0) + ); + } catch (e) { + console.error('invalid transaction payload', e); + return false; + } + } +} diff --git a/modules/sdk-coin-apt/src/lib/transactionBuilder.ts b/modules/sdk-coin-apt/src/lib/transactionBuilder/transactionBuilder.ts similarity index 74% rename from modules/sdk-coin-apt/src/lib/transactionBuilder.ts rename to modules/sdk-coin-apt/src/lib/transactionBuilder/transactionBuilder.ts index 15afe9e3bf..831ddf3381 100644 --- a/modules/sdk-coin-apt/src/lib/transactionBuilder.ts +++ b/modules/sdk-coin-apt/src/lib/transactionBuilder/transactionBuilder.ts @@ -8,11 +8,12 @@ import { Recipient, TransactionType, } from '@bitgo/sdk-core'; -import { Transaction } from './transaction/transaction'; -import utils from './utils'; +import { Transaction } from '../transaction/transaction'; +import utils from '../utils'; import BigNumber from 'bignumber.js'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import { GasData } from './types'; +import { GasData } from '../types'; +import { TransactionPayload } from '@aptos-labs/ts-sdk'; export abstract class TransactionBuilder extends BaseTransactionBuilder { protected _transaction: Transaction; @@ -27,6 +28,15 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { */ protected abstract get transactionType(): TransactionType; + /** + * Initialize the transaction builder fields using the decoded transaction data + * + * @param {Transaction} tx the transaction data + */ + initBuilder(tx: Transaction): void { + this._transaction = tx; + } + /** @inheritdoc */ protected get transaction(): Transaction { return this._transaction; @@ -74,6 +84,8 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { return this; } + abstract assetId(assetId: string): TransactionBuilder; + /** @inheritdoc */ protected signImplementation(key: BaseKey): Transaction { throw new Error('Method not implemented.'); @@ -88,12 +100,19 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { this.transaction.addFeePayerSignature(publicKey, signature); } - /** - * Initialize the transaction builder fields using the decoded transaction data - * - * @param {Transaction} tx the transaction data - */ - abstract initBuilder(tx: Transaction): void; + /** @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; + } // region Validators /** @inheritdoc */ @@ -108,26 +127,30 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { throw new Error('Method not implemented.'); } + protected abstract isValidTransactionPayload(payload: TransactionPayload); + + isValidRawTransaction(rawTransaction: string): boolean { + try { + const signedTxn = utils.deserializeSignedTransaction(rawTransaction); + const rawTxn = signedTxn.raw_txn; + const senderAddress = rawTxn.sender.toString(); + return utils.isValidAddress(senderAddress) && this.isValidTransactionPayload(rawTxn.payload); + } catch (e) { + console.error('invalid raw transaction', e); + return false; + } + } + /** @inheritdoc */ validateRawTransaction(rawTransaction: string): void { if (!rawTransaction) { throw new ParseTransactionError('Invalid raw transaction: Undefined'); } - if (!utils.isValidRawTransaction(rawTransaction)) { + if (!this.isValidRawTransaction(rawTransaction)) { throw new ParseTransactionError('Invalid raw transaction'); } } - /** @inheritdoc */ - validateTransaction(transaction?: Transaction): void { - if (!transaction) { - throw new Error('transaction not defined'); - } - this.validateAddress({ address: transaction.sender }); - this.validateAddress({ address: transaction.recipient.address }); - this.validateValue(new BigNumber(transaction.recipient.amount)); - } - /** @inheritdoc */ validateValue(value: BigNumber): void { if (value.isNaN()) { diff --git a/modules/sdk-coin-apt/src/lib/transactionBuilder/transferBuilder.ts b/modules/sdk-coin-apt/src/lib/transactionBuilder/transferBuilder.ts new file mode 100644 index 0000000000..f95204ed90 --- /dev/null +++ b/modules/sdk-coin-apt/src/lib/transactionBuilder/transferBuilder.ts @@ -0,0 +1,50 @@ +import { TransactionBuilder } from './transactionBuilder'; +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { TransactionType } from '@bitgo/sdk-core'; +import { TransferTransaction } from '../transaction/transferTransaction'; +import BigNumber from 'bignumber.js'; +import utils from '../utils'; +import { TransactionPayload, TransactionPayloadEntryFunction } from '@aptos-labs/ts-sdk'; + +export class TransferBuilder extends TransactionBuilder { + constructor(_coinConfig: Readonly) { + super(_coinConfig); + this.transaction = new TransferTransaction(_coinConfig); + } + + protected get transactionType(): TransactionType { + return TransactionType.Send; + } + + assetId(assetId: string): TransactionBuilder { + this.transaction.assetId = assetId; + return this; + } + + /** @inheritdoc */ + validateTransaction(transaction?: TransferTransaction): void { + if (!transaction) { + throw new Error('transaction not defined'); + } + this.validateAddress({ address: transaction.sender }); + this.validateAddress({ address: transaction.recipient.address }); + this.validateValue(new BigNumber(transaction.recipient.amount)); + } + + protected isValidTransactionPayload(payload: TransactionPayload) { + try { + if (!(payload instanceof TransactionPayloadEntryFunction)) { + console.error('invalid transaction payload'); + return false; + } + const entryFunction = payload.entryFunction; + const recipientAddress = entryFunction.args[0].toString(); + const amountBuffer = Buffer.from(entryFunction.args[1].bcsToBytes()); + const recipientAmount = new BigNumber(amountBuffer.readBigUint64LE().toString()); + return utils.isValidAddress(recipientAddress) && !recipientAmount.isLessThan(0); + } catch (e) { + console.error('invalid transaction payload', e); + return false; + } + } +} diff --git a/modules/sdk-coin-apt/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-apt/src/lib/transactionBuilderFactory.ts index 0dcfdfa76c..e9de0af880 100644 --- a/modules/sdk-coin-apt/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-apt/src/lib/transactionBuilderFactory.ts @@ -1,11 +1,13 @@ import { BaseTransactionBuilderFactory, InvalidTransactionError, TransactionType } from '@bitgo/sdk-core'; -import { TransactionBuilder } from './transactionBuilder'; -import { TransferBuilder } from './transferBuilder'; +import { TransactionBuilder } from './transactionBuilder/transactionBuilder'; +import { TransferBuilder } from './transactionBuilder/transferBuilder'; import utils from './utils'; import { Transaction } from './transaction/transaction'; import { SignedTransaction } from '@aptos-labs/ts-sdk'; import { TransferTransaction } from './transaction/transferTransaction'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { FungibleAssetTransaction } from './transaction/fungibleAssetTransaction'; +import { FungibleAssetTransactionBuilder } from './transactionBuilder/fungibleAssetTransactionBuilder'; export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { constructor(_coinConfig: Readonly) { @@ -14,7 +16,6 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { /** @inheritdoc */ from(signedRawTxn: string): TransactionBuilder { - utils.validateRawTransaction(signedRawTxn); try { const signedTxn = this.parseTransaction(signedRawTxn); const txnType = this.getTransactionTypeFromSignedTxn(signedTxn); @@ -23,6 +24,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { const transferTx = new TransferTransaction(this._coinConfig); transferTx.fromDeserializedSignedTransaction(signedTxn); return this.getTransferBuilder(transferTx); + case TransactionType.SendToken: + const fungibleTransferTokenTx = new FungibleAssetTransaction(this._coinConfig); + fungibleTransferTokenTx.fromDeserializedSignedTransaction(signedTxn); + return this.getFungibleAssetTransactionBuilder(fungibleTransferTokenTx); default: throw new InvalidTransactionError('Invalid transaction'); } @@ -41,6 +46,11 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return this.initializeBuilder(tx, new TransferBuilder(this._coinConfig)); } + /** @inheritdoc */ + getFungibleAssetTransactionBuilder(tx?: Transaction): FungibleAssetTransactionBuilder { + return this.initializeBuilder(tx, new FungibleAssetTransactionBuilder(this._coinConfig)); + } + /** @inheritdoc */ getWalletInitializationBuilder(): void { throw new Error('Method not implemented.'); diff --git a/modules/sdk-coin-apt/src/lib/transferBuilder.ts b/modules/sdk-coin-apt/src/lib/transferBuilder.ts deleted file mode 100644 index 4cfb8f3b0a..0000000000 --- a/modules/sdk-coin-apt/src/lib/transferBuilder.ts +++ /dev/null @@ -1,39 +0,0 @@ -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) { - super(_coinConfig); - this.transaction = new TransferTransaction(_coinConfig); - } - - protected get transactionType(): TransactionType { - return TransactionType.Send; - } - - /** - * Initialize the transaction builder fields using the decoded transaction data - * - * @param {Transaction} tx the transaction data - */ - 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 ca23d92d35..6a4a111347 100644 --- a/modules/sdk-coin-apt/src/lib/utils.ts +++ b/modules/sdk-coin-apt/src/lib/utils.ts @@ -12,8 +12,6 @@ import { InvalidTransactionError, isValidEd25519PublicKey, isValidEd25519SecretKey, - ParseTransactionError, - TransactionRecipient, TransactionType, } from '@bitgo/sdk-core'; import { @@ -69,24 +67,12 @@ export class Utils implements BaseUtils { return accountAddress.toString(); } - getRecipientFromTransactionPayload(payload: TransactionPayload): TransactionRecipient { - let address = 'INVALID'; - let amount = '0'; - if (payload instanceof TransactionPayloadEntryFunction) { - const entryFunction = payload.entryFunction; - address = entryFunction.args[0].toString(); - const amountBuffer = Buffer.from(entryFunction.args[1].bcsToBytes()); - amount = amountBuffer.readBigUint64LE().toString(); - } - 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(); + const moduleIdentifier = entryFunction.module_name.name.identifier; switch (moduleIdentifier) { case APTOS_ACCOUNT_MODULE: return TransactionType.Send; @@ -97,32 +83,6 @@ export class Utils implements BaseUtils { } } - isValidRawTransaction(rawTransaction: string): boolean { - try { - const signedTxn = this.deserializeSignedTransaction(rawTransaction); - const rawTxn = signedTxn.raw_txn; - const senderAddress = rawTxn.sender.toString(); - const recipient = utils.getRecipientFromTransactionPayload(rawTxn.payload); - const recipientAddress = recipient.address; - const recipientAmount = new BigNumber(recipient.amount); - return ( - this.isValidAddress(senderAddress) && this.isValidAddress(recipientAddress) && !recipientAmount.isLessThan(0) - ); - } catch (e) { - console.error('invalid raw transaction', e); - return false; - } - } - - validateRawTransaction(rawTransaction: string): void { - if (!rawTransaction) { - throw new ParseTransactionError('Invalid raw transaction: Undefined'); - } - if (!this.isValidRawTransaction(rawTransaction)) { - throw new ParseTransactionError('Invalid raw transaction'); - } - } - deserializeSignedTransaction(rawTransaction: string): SignedTransaction { const txnBytes = Hex.fromHexString(rawTransaction).toUint8Array(); const deserializer = new Deserializer(txnBytes); diff --git a/modules/sdk-coin-apt/test/resources/apt.ts b/modules/sdk-coin-apt/test/resources/apt.ts index cf11b72d35..97a601887e 100644 --- a/modules/sdk-coin-apt/test/resources/apt.ts +++ b/modules/sdk-coin-apt/test/resources/apt.ts @@ -1,6 +1,7 @@ import { Recipient } from '@bitgo/sdk-core'; export const AMOUNT = 1000; +export const FUNGIBLE_TOKEN_AMOUNT = 1; export const addresses = { validAddresses: [ @@ -38,6 +39,13 @@ export const recipients: Recipient[] = [ }, ]; +export const fungibleTokenRecipients: Recipient[] = [ + { + address: addresses.validAddresses[0], + amount: FUNGIBLE_TOKEN_AMOUNT.toString(), + }, +]; + export const invalidRecipients: Recipient[] = [ { address: addresses.invalidAddresses[0], @@ -56,5 +64,17 @@ export const invalidRecipients: Recipient[] = [ export const TRANSFER = '0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a37244992000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e74087472616e73666572000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d0300000000006400000000000000979390670000000002030020f73836f42257240e43d439552471fc9dbcc3f1af5bd0b4ed83f44b5f6614644240caeb90efd4b7ecd922c97bb3163e6a9de1fbb2ee0fc0d56af484f4af9b0015c5831341550af29b3686713b6657c821d894635fe13c7933f06ee043728f040b090000dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f200205223396c531f13e031a9f0cb26d459d799a52e51be9a1cb9e871afb4c31f91ff4013e7e8a1325ee5f656c93baa3d0206a1d9bd6da5abdc6f5d9b8bbbb0926ddac68f3e57a915dd217d2d43e776a6cc01af72f895ea712acc836d30349f29a3c606'; +export const TRANSACTION_USING_TRANSFER_COINS = + '0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a37244992000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e740e7472616e736665725f636f696e73010700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d0300000000006400000000000000979390670000000002030020f73836f42257240e43d439552471fc9dbcc3f1af5bd0b4ed83f44b5f6614644240caeb90efd4b7ecd922c97bb3163e6a9de1fbb2ee0fc0d56af484f4af9b0015c5831341550af29b3686713b6657c821d894635fe13c7933f06ee043728f040b090000dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f200205223396c531f13e031a9f0cb26d459d799a52e51be9a1cb9e871afb4c31f91ff4013e7e8a1325ee5f656c93baa3d0206a1d9bd6da5abdc6f5d9b8bbbb0926ddac68f3e57a915dd217d2d43e776a6cc01af72f895ea712acc836d30349f29a3c606'; + export const INVALID_TRANSFER = 'AAAAAAAAAAAAA6e7361637469bc4a58e500b9e64cb6547ee9b403000000000000002064ba1fb2f2fbd2938a350015d601f4db89cd7e8e2370d0dd9ae3ac4f635c1581111b8a49f67370bc4a58e500b9e64cb6462e39b802000000000000002064ba1fb2f2fbd2938a350015d601f4db89cd7e8e2370d0dd9ae3ac47aa1ff81f01c4173a804406a365e69dfb297d4eaaf002546ebd016400000000000000cba4a48bb0f8b586c167e5dcefaa1c5e96ab3f0836d6ca08f2081732944d1e5b6b406a4a462e39b8030000000000000020b9490ede63215262c434e03f606d9799f3ba704523ceda184b386d47aa1ff81f01000000000000006400000000000000'; + +export const fungibleTokenAddress = { + usdt: '0xd5d0d561493ea2b9410f67da804653ae44e793c2423707d4f11edb2e38192050', +}; + +export const FUNGIBLE_TOKEN_TRANSFER = + '0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a372449a700000000000000020000000000000000000000000000000000000000000000000000000000000001167072696d6172795f66756e6769626c655f73746f7265087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010e66756e6769626c655f6173736574084d65746164617461000320d5d0d561493ea2b9410f67da804653ae44e793c2423707d4f11edb2e3819205020f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad9080100000000000000400d0300000000006400000000000000e42696670000000002030020f73836f42257240e43d439552471fc9dbcc3f1af5bd0b4ed83f44b5f661464424029665cd4c94658a0d83907bbed7e761794b25bccc03fc87e6dd63a543accdddfd7a6f1e7a15e8681547ca437ff99b58c92f816e35a0f333d7f1fd1330c21ad0a0000dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f200205223396c531f13e031a9f0cb26d459d799a52e51be9a1cb9e871afb4c31f91ff40de7b0bb86ca346031942e9cf21ff9604c7c08c2c662e38a0afe3dd640512c0441396c0697cd8bbbcf39694d6f88e35f6bed9fb34bd209b0479b5e8bd0cf3eb0b'; + +export const LEGACY_COIN = '0x4fb379c10c763a13e724064ecfb7d946690bea519ba982c81b518d1c11dd23fe::fa_test::Coinz'; diff --git a/modules/sdk-coin-apt/test/unit/getBuilderFactory.ts b/modules/sdk-coin-apt/test/unit/getBuilderFactory.ts new file mode 100644 index 0000000000..082badb593 --- /dev/null +++ b/modules/sdk-coin-apt/test/unit/getBuilderFactory.ts @@ -0,0 +1,6 @@ +import { TransactionBuilderFactory } from '../../src'; +import { coins } from '@bitgo/statics'; + +export const getBuilderFactory = (coin: string): TransactionBuilderFactory => { + return new TransactionBuilderFactory(coins.get(coin)); +}; diff --git a/modules/sdk-coin-apt/test/unit/transactionBuilder/fungibleTokenTransferBuilder.ts b/modules/sdk-coin-apt/test/unit/transactionBuilder/fungibleTokenTransferBuilder.ts new file mode 100644 index 0000000000..2b40cb9c3b --- /dev/null +++ b/modules/sdk-coin-apt/test/unit/transactionBuilder/fungibleTokenTransferBuilder.ts @@ -0,0 +1,176 @@ +import { getBuilderFactory } from '../getBuilderFactory'; +import { coins } from '@bitgo/statics'; +import { TransferTransaction } from '../../../src'; +import * as testData from '../../resources/apt'; +import { TransactionType } from '@bitgo/sdk-core'; +import should from 'should'; +import { FungibleAssetTransaction } from '../../../src/lib/transaction/fungibleAssetTransaction'; + +describe('Apt Token Transfer Builder', () => { + const factory = getBuilderFactory('tapt:usdt'); + + describe('Succeed', () => { + it('should build a token transfer', async function () { + const fungibleTokenTransfer = new FungibleAssetTransaction(coins.get('tapt:usdt')); + const txBuilder = factory.getFungibleAssetTransactionBuilder(fungibleTokenTransfer); + txBuilder.sender(testData.sender2.address); + txBuilder.recipient(testData.fungibleTokenRecipients[0]); + txBuilder.gasData({ + maxGasAmount: 200000, + gasUnitPrice: 100, + }); + txBuilder.assetId(testData.fungibleTokenAddress.usdt); + txBuilder.sequenceNumber(14); + txBuilder.expirationTime(1736246155); + txBuilder.addFeePayerAddress(testData.feePayer.address); + const tx = (await txBuilder.build()) as FungibleAssetTransaction; + should.equal(tx.sender, testData.sender2.address); + should.equal(tx.recipient.address, testData.fungibleTokenRecipients[0].address); + should.equal(tx.recipient.amount, testData.fungibleTokenRecipients[0].amount); + should.equal(tx.assetId, testData.fungibleTokenAddress.usdt); + should.equal(tx.maxGasAmount, 200000); + should.equal(tx.gasUnitPrice, 100); + should.equal(tx.sequenceNumber, 14); + should.equal(tx.expirationTime, 1736246155); + should.equal(tx.type, TransactionType.SendToken); + tx.inputs.length.should.equal(1); + tx.inputs[0].should.deepEqual({ + address: testData.sender2.address, + value: testData.fungibleTokenRecipients[0].amount, + coin: 'tapt:usdt', + }); + tx.outputs.length.should.equal(1); + tx.outputs[0].should.deepEqual({ + address: testData.fungibleTokenRecipients[0].address, + value: testData.fungibleTokenRecipients[0].amount, + coin: 'tapt:usdt', + }); + const rawTx = tx.toBroadcastFormat(); + should.equal(txBuilder.isValidRawTransaction(rawTx), true); + rawTx.should.equal( + '0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e00000000000000020000000000000000000000000000000000000000000000000000000000000001167072696d6172795f66756e6769626c655f73746f7265087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010e66756e6769626c655f6173736574084d65746164617461000320d5d0d561493ea2b9410f67da804653ae44e793c2423707d4f11edb2e3819205020f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad9080100000000000000400d03000000000064000000000000008b037d670000000002030020000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2002000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + ); + }); + + it('should build and send a signed tx', async function () { + const txBuilder = factory.from(testData.FUNGIBLE_TOKEN_TRANSFER); + const tx = (await txBuilder.build()) as FungibleAssetTransaction; + tx.inputs.length.should.equal(1); + tx.inputs[0].should.deepEqual({ + address: testData.sender2.address, + value: testData.fungibleTokenRecipients[0].amount, + coin: 'tapt:usdt', + }); + tx.outputs.length.should.equal(1); + tx.outputs[0].should.deepEqual({ + address: testData.fungibleTokenRecipients[0].address, + value: testData.fungibleTokenRecipients[0].amount, + coin: 'tapt:usdt', + }); + should.equal(tx.id, '0x2dae2ecd096a212d6f565bd161c92f506cebb700aaf16c88bd3456b88d4c392a'); + should.equal(tx.maxGasAmount, 200000); + should.equal(tx.gasUnitPrice, 100); + should.equal(tx.sequenceNumber, 167); + should.equal(tx.expirationTime, 1737893604); + should.equal(tx.type, TransactionType.SendToken); + const rawTx = tx.toBroadcastFormat(); + should.equal(txBuilder.isValidRawTransaction(rawTx), true); + should.equal(rawTx, testData.FUNGIBLE_TOKEN_TRANSFER); + }); + + it('should succeed to validate a valid signablePayload', async function () { + const transaction = new FungibleAssetTransaction(coins.get('tapt')); + const txBuilder = factory.getFungibleAssetTransactionBuilder(transaction); + txBuilder.sender(testData.sender2.address); + txBuilder.recipient(testData.fungibleTokenRecipients[0]); + txBuilder.gasData({ + maxGasAmount: 200000, + gasUnitPrice: 100, + }); + txBuilder.sequenceNumber(14); + txBuilder.expirationTime(1736246155); + txBuilder.assetId(testData.fungibleTokenAddress.usdt); + txBuilder.addFeePayerAddress(testData.feePayer.address); + const tx = (await txBuilder.build()) as FungibleAssetTransaction; + const signablePayload = tx.signablePayload; + should.equal( + signablePayload.toString('hex'), + '5efa3c4f02f83a0f4b2d69fc95c607cc02825cc4e7be536ef0992df050d9e67c011aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e00000000000000020000000000000000000000000000000000000000000000000000000000000001167072696d6172795f66756e6769626c655f73746f7265087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010e66756e6769626c655f6173736574084d65746164617461000320d5d0d561493ea2b9410f67da804653ae44e793c2423707d4f11edb2e3819205020f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad9080100000000000000400d03000000000064000000000000008b037d67000000000200dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2' + ); + }); + + it('should build a unsigned tx and validate its toJson', async function () { + const transaction = new FungibleAssetTransaction(coins.get('tapt')); + const txBuilder = factory.getFungibleAssetTransactionBuilder(transaction); + txBuilder.sender(testData.sender2.address); + txBuilder.recipient(testData.fungibleTokenRecipients[0]); + txBuilder.gasData({ + maxGasAmount: 200000, + gasUnitPrice: 100, + }); + txBuilder.sequenceNumber(14); + txBuilder.expirationTime(1736246155); + txBuilder.assetId(testData.fungibleTokenAddress.usdt); + txBuilder.addFeePayerAddress(testData.feePayer.address); + const tx = (await txBuilder.build()) as FungibleAssetTransaction; + const toJson = tx.toJson(); + should.equal(toJson.sender, testData.sender2.address); + should.deepEqual(toJson.recipient, { + address: testData.fungibleTokenRecipients[0].address, + amount: testData.fungibleTokenRecipients[0].amount, + }); + should.equal(toJson.sequenceNumber, 14); + should.equal(tx.assetId, testData.fungibleTokenAddress.usdt); + should.equal(toJson.maxGasAmount, 200000); + should.equal(toJson.gasUnitPrice, 100); + should.equal(toJson.expirationTime, 1736246155); + should.equal(toJson.feePayer, testData.feePayer.address); + }); + + it('should build a signed tx and validate its toJson', async function () { + const txBuilder = factory.from(testData.FUNGIBLE_TOKEN_TRANSFER); + const tx = (await txBuilder.build()) as FungibleAssetTransaction; + const toJson = tx.toJson(); + should.equal(toJson.id, '0x2dae2ecd096a212d6f565bd161c92f506cebb700aaf16c88bd3456b88d4c392a'); + should.equal(toJson.sender, testData.sender2.address); + should.deepEqual(toJson.recipient, { + address: testData.fungibleTokenRecipients[0].address, + amount: testData.fungibleTokenRecipients[0].amount.toString(), + }); + should.equal(tx.assetId, testData.fungibleTokenAddress.usdt); + should.equal(toJson.sequenceNumber, 167); + should.equal(toJson.maxGasAmount, 200000); + should.equal(toJson.gasUnitPrice, 100); + should.equal(toJson.expirationTime, 1737893604); + }); + }); + + describe('Fail', () => { + it('should fail for invalid sender', async function () { + const transaction = new TransferTransaction(coins.get('tapt')); + const builder = factory.getTransferBuilder(transaction); + should(() => builder.sender('randomString')).throwError('Invalid address randomString'); + }); + + it('should fail for invalid recipient', async function () { + const builder = factory.getTransferBuilder(); + should(() => builder.recipient(testData.invalidRecipients[0])).throwError('Invalid address randomString'); + should(() => builder.recipient(testData.invalidRecipients[1])).throwError('Value cannot be less than zero'); + should(() => builder.recipient(testData.invalidRecipients[2])).throwError('Invalid amount format'); + }); + it('should fail for invalid gas amount', async function () { + const builder = factory.getTransferBuilder(); + should(() => builder.gasData({ maxGasAmount: -1, gasUnitPrice: 100 })).throwError( + 'Value cannot be less than zero' + ); + should(() => builder.gasData({ maxGasAmount: 200000, gasUnitPrice: -1 })).throwError( + 'Value cannot be less than zero' + ); + }); + it('should fail for invalid fungible token address', async function () { + const transaction = new FungibleAssetTransaction(coins.get('tapt')); + const txBuilder = factory.getFungibleAssetTransactionBuilder(transaction); + should(() => txBuilder.assetId('randomString')).throwError('Invalid address randomString'); + }); + }); +}); diff --git a/modules/sdk-coin-apt/test/unit/transactionBuilder/transferBuilder.ts b/modules/sdk-coin-apt/test/unit/transactionBuilder/transferBuilder.ts new file mode 100644 index 0000000000..cb12758d38 --- /dev/null +++ b/modules/sdk-coin-apt/test/unit/transactionBuilder/transferBuilder.ts @@ -0,0 +1,257 @@ +import { coins } from '@bitgo/statics'; +import { TransactionBuilderFactory, TransferTransaction } from '../../../src'; +import * as testData from '../../resources/apt'; +import { TransactionType } from '@bitgo/sdk-core'; +import should from 'should'; + +describe('Apt Transfer Transaction', () => { + const factory = new TransactionBuilderFactory(coins.get('tapt')); + + describe('Aptos Coin Transfer Transaction', () => { + describe('Succeed', () => { + it('should build a transfer tx', async function () { + const transaction = new TransferTransaction(coins.get('tapt')); + const txBuilder = factory.getTransferBuilder(transaction); + txBuilder.sender(testData.sender2.address); + txBuilder.recipient(testData.recipients[0]); + txBuilder.gasData({ + maxGasAmount: 200000, + gasUnitPrice: 100, + }); + txBuilder.sequenceNumber(14); + txBuilder.expirationTime(1736246155); + txBuilder.addFeePayerAddress(testData.feePayer.address); + const tx = (await txBuilder.build()) as TransferTransaction; + should.equal(tx.sender, testData.sender2.address); + should.equal(tx.recipient.address, testData.recipients[0].address); + should.equal(tx.recipient.amount, testData.recipients[0].amount); + should.equal(tx.maxGasAmount, 200000); + should.equal(tx.gasUnitPrice, 100); + should.equal(tx.sequenceNumber, 14); + should.equal(tx.expirationTime, 1736246155); + should.equal(tx.type, TransactionType.Send); + tx.inputs.length.should.equal(1); + tx.inputs[0].should.deepEqual({ + address: testData.sender2.address, + value: testData.recipients[0].amount, + coin: 'tapt', + }); + tx.outputs.length.should.equal(1); + tx.outputs[0].should.deepEqual({ + address: testData.recipients[0].address, + value: testData.recipients[0].amount, + coin: 'tapt', + }); + const rawTx = tx.toBroadcastFormat(); + should.equal(txBuilder.isValidRawTransaction(rawTx), true); + rawTx.should.equal( + '0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e740e7472616e736665725f636f696e73010700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d670000000002030020000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2002000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + ); + }); + + it('should build and send a signed tx', async function () { + const txBuilder = factory.from(testData.TRANSACTION_USING_TRANSFER_COINS); + const tx = (await txBuilder.build()) as TransferTransaction; + should.equal(tx.type, TransactionType.Send); + tx.inputs.length.should.equal(1); + tx.inputs[0].should.deepEqual({ + address: testData.sender2.address, + value: testData.recipients[0].amount, + coin: 'tapt', + }); + tx.outputs.length.should.equal(1); + tx.outputs[0].should.deepEqual({ + address: testData.recipients[0].address, + value: testData.recipients[0].amount, + coin: 'tapt', + }); + should.equal(tx.id, '0x80a52dd5d4f712a80b77ad7b4a12a8e61b76243a546099b0ab9acfef4e9a4e31'); + should.equal(tx.maxGasAmount, 200000); + should.equal(tx.gasUnitPrice, 100); + should.equal(tx.sequenceNumber, 146); + should.equal(tx.expirationTime, 1737528215); + should.equal(tx.type, TransactionType.Send); + const rawTx = tx.toBroadcastFormat(); + should.equal(txBuilder.isValidRawTransaction(rawTx), true); + should.equal(rawTx, testData.TRANSACTION_USING_TRANSFER_COINS); + }); + + it('should succeed to validate a valid signablePayload', async function () { + const transaction = new TransferTransaction(coins.get('tapt')); + const txBuilder = factory.getTransferBuilder(transaction); + txBuilder.sender(testData.sender2.address); + txBuilder.recipient(testData.recipients[0]); + txBuilder.gasData({ + maxGasAmount: 200000, + gasUnitPrice: 100, + }); + txBuilder.sequenceNumber(14); + txBuilder.expirationTime(1736246155); + txBuilder.addFeePayerAddress(testData.feePayer.address); + const tx = (await txBuilder.build()) as TransferTransaction; + const signablePayload = tx.signablePayload; + should.equal( + signablePayload.toString('hex'), + '5efa3c4f02f83a0f4b2d69fc95c607cc02825cc4e7be536ef0992df050d9e67c011aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e740e7472616e736665725f636f696e73010700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d67000000000200dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2' + ); + }); + + it('should build a unsigned tx and validate its toJson', async function () { + const transaction = new TransferTransaction(coins.get('tapt')); + const txBuilder = factory.getTransferBuilder(transaction); + txBuilder.sender(testData.sender2.address); + txBuilder.recipient(testData.recipients[0]); + txBuilder.gasData({ + maxGasAmount: 200000, + gasUnitPrice: 100, + }); + txBuilder.sequenceNumber(14); + txBuilder.expirationTime(1736246155); + txBuilder.addFeePayerAddress(testData.feePayer.address); + const tx = (await txBuilder.build()) as TransferTransaction; + const toJson = tx.toJson(); + should.equal(toJson.sender, testData.sender2.address); + should.deepEqual(toJson.recipient, { + address: testData.recipients[0].address, + amount: testData.recipients[0].amount, + }); + should.equal(toJson.sequenceNumber, 14); + should.equal(toJson.maxGasAmount, 200000); + should.equal(toJson.gasUnitPrice, 100); + should.equal(toJson.expirationTime, 1736246155); + should.equal(toJson.feePayer, testData.feePayer.address); + }); + + it('should build a signed tx and validate its toJson', async function () { + const txBuilder = factory.from(testData.TRANSACTION_USING_TRANSFER_COINS); + const tx = (await txBuilder.build()) as TransferTransaction; + const toJson = tx.toJson(); + should.equal(toJson.id, '0x80a52dd5d4f712a80b77ad7b4a12a8e61b76243a546099b0ab9acfef4e9a4e31'); + should.equal(toJson.sender, '0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a372449'); + should.deepEqual(toJson.recipient, { + address: '0xf7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad9', + amount: '1000', + }); + should.equal(toJson.sequenceNumber, 146); + should.equal(toJson.maxGasAmount, 200000); + should.equal(toJson.gasUnitPrice, 100); + should.equal(toJson.expirationTime, 1737528215); + }); + }); + + describe('Fail', () => { + it('should fail for invalid sender', async function () { + const transaction = new TransferTransaction(coins.get('tapt')); + const builder = factory.getTransferBuilder(transaction); + should(() => builder.sender('randomString')).throwError('Invalid address randomString'); + }); + + it('should fail for invalid recipient', async function () { + const builder = factory.getTransferBuilder(); + should(() => builder.recipient(testData.invalidRecipients[0])).throwError('Invalid address randomString'); + should(() => builder.recipient(testData.invalidRecipients[1])).throwError('Value cannot be less than zero'); + should(() => builder.recipient(testData.invalidRecipients[2])).throwError('Invalid amount format'); + }); + it('should fail for invalid gas amount', async function () { + const builder = factory.getTransferBuilder(); + should(() => builder.gasData({ maxGasAmount: -1, gasUnitPrice: 100 })).throwError( + 'Value cannot be less than zero' + ); + should(() => builder.gasData({ maxGasAmount: 200000, gasUnitPrice: -1 })).throwError( + 'Value cannot be less than zero' + ); + }); + }); + }); + + describe('Legacy Coin Transfer Transaction', () => { + it('should build a coinz (legacy coin) transfer tx', async function () { + const transaction = new TransferTransaction(coins.get('tapt')); + const txBuilder = factory.getTransferBuilder(transaction); + txBuilder.sender(testData.sender2.address); + txBuilder.recipient(testData.recipients[0]); + txBuilder.gasData({ + maxGasAmount: 200000, + gasUnitPrice: 100, + }); + txBuilder.sequenceNumber(14); + txBuilder.expirationTime(1736246155); + txBuilder.assetId(testData.LEGACY_COIN); + txBuilder.addFeePayerAddress(testData.feePayer.address); + const tx = (await txBuilder.build()) as TransferTransaction; + should.equal(tx.sender, testData.sender2.address); + should.equal(tx.recipient.address, testData.recipients[0].address); + should.equal(tx.recipient.amount, testData.recipients[0].amount); + should.equal(tx.maxGasAmount, 200000); + should.equal(tx.gasUnitPrice, 100); + should.equal(tx.sequenceNumber, 14); + should.equal(tx.expirationTime, 1736246155); + should.equal(tx.type, TransactionType.Send); + tx.inputs.length.should.equal(1); + tx.inputs[0].should.deepEqual({ + address: testData.sender2.address, + value: testData.recipients[0].amount, + coin: 'tapt', + }); + tx.outputs.length.should.equal(1); + tx.outputs[0].should.deepEqual({ + address: testData.recipients[0].address, + value: testData.recipients[0].amount, + coin: 'tapt', + }); + const rawTx = tx.toBroadcastFormat(); + should.equal(txBuilder.isValidRawTransaction(rawTx), true); + rawTx.should.equal( + '0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e740e7472616e736665725f636f696e7301074fb379c10c763a13e724064ecfb7d946690bea519ba982c81b518d1c11dd23fe0766615f7465737405436f696e7a000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d670000000002030020000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2002000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + ); + }); + + it('should succeed to validate a valid signablePayload for coinz (legacy coin)', async function () { + const transaction = new TransferTransaction(coins.get('tapt')); + const txBuilder = factory.getTransferBuilder(transaction); + txBuilder.sender(testData.sender2.address); + txBuilder.recipient(testData.recipients[0]); + txBuilder.gasData({ + maxGasAmount: 200000, + gasUnitPrice: 100, + }); + txBuilder.sequenceNumber(14); + txBuilder.expirationTime(1736246155); + txBuilder.assetId(testData.LEGACY_COIN); + txBuilder.addFeePayerAddress(testData.feePayer.address); + const tx = (await txBuilder.build()) as TransferTransaction; + const signablePayload = tx.signablePayload; + should.equal( + signablePayload.toString('hex'), + '5efa3c4f02f83a0f4b2d69fc95c607cc02825cc4e7be536ef0992df050d9e67c011aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e740e7472616e736665725f636f696e7301074fb379c10c763a13e724064ecfb7d946690bea519ba982c81b518d1c11dd23fe0766615f7465737405436f696e7a000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d67000000000200dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2' + ); + }); + + it('should build a coinz (legacy coin) unsigned tx and validate its toJson', async function () { + const transaction = new TransferTransaction(coins.get('tapt')); + const txBuilder = factory.getTransferBuilder(transaction); + txBuilder.sender(testData.sender2.address); + txBuilder.recipient(testData.recipients[0]); + txBuilder.gasData({ + maxGasAmount: 200000, + gasUnitPrice: 100, + }); + txBuilder.sequenceNumber(14); + txBuilder.expirationTime(1736246155); + txBuilder.assetId(testData.LEGACY_COIN); + txBuilder.addFeePayerAddress(testData.feePayer.address); + const tx = (await txBuilder.build()) as TransferTransaction; + const toJson = tx.toJson(); + should.equal(toJson.sender, testData.sender2.address); + should.deepEqual(toJson.recipient, { + address: testData.recipients[0].address, + amount: testData.recipients[0].amount, + }); + should.equal(toJson.sequenceNumber, 14); + should.equal(toJson.maxGasAmount, 200000); + should.equal(toJson.gasUnitPrice, 100); + should.equal(toJson.expirationTime, 1736246155); + should.equal(toJson.feePayer, testData.feePayer.address); + }); + }); +}); diff --git a/modules/sdk-coin-apt/test/unit/transferBuilder.ts b/modules/sdk-coin-apt/test/unit/transferBuilder.ts deleted file mode 100644 index 241c100469..0000000000 --- a/modules/sdk-coin-apt/test/unit/transferBuilder.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { coins } from '@bitgo/statics'; -import { TransactionBuilderFactory, TransferTransaction } from '../../src'; -import * as testData from '../resources/apt'; -import utils from '../../src/lib/utils'; -import { TransactionType } from '@bitgo/sdk-core'; -import should from 'should'; - -describe('Apt Transfer Transaction', () => { - const factory = new TransactionBuilderFactory(coins.get('tapt')); - - describe('Succeed', () => { - it('should build a transfer tx', async function () { - const transaction = new TransferTransaction(coins.get('tapt')); - const txBuilder = factory.getTransferBuilder(transaction); - txBuilder.sender(testData.sender2.address); - txBuilder.recipient(testData.recipients[0]); - txBuilder.gasData({ - maxGasAmount: 200000, - gasUnitPrice: 100, - }); - txBuilder.sequenceNumber(14); - txBuilder.expirationTime(1736246155); - txBuilder.addFeePayerAddress(testData.feePayer.address); - const tx = (await txBuilder.build()) as TransferTransaction; - should.equal(tx.sender, testData.sender2.address); - should.equal(tx.recipient.address, testData.recipients[0].address); - should.equal(tx.recipient.amount, testData.recipients[0].amount); - should.equal(tx.maxGasAmount, 200000); - should.equal(tx.gasUnitPrice, 100); - should.equal(tx.sequenceNumber, 14); - should.equal(tx.expirationTime, 1736246155); - should.equal(tx.type, TransactionType.Send); - tx.inputs.length.should.equal(1); - tx.inputs[0].should.deepEqual({ - address: testData.sender2.address, - value: testData.recipients[0].amount, - coin: 'tapt', - }); - tx.outputs.length.should.equal(1); - tx.outputs[0].should.deepEqual({ - address: testData.recipients[0].address, - value: testData.recipients[0].amount, - coin: 'tapt', - }); - const rawTx = tx.toBroadcastFormat(); - should.equal(utils.isValidRawTransaction(rawTx), true); - rawTx.should.equal( - '0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e74087472616e73666572000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d670000000002030020000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2002000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' - ); - }); - - it('should build and send a signed tx', async function () { - const txBuilder = factory.from(testData.TRANSFER); - const tx = (await txBuilder.build()) as TransferTransaction; - should.equal(tx.type, TransactionType.Send); - tx.inputs.length.should.equal(1); - tx.inputs[0].should.deepEqual({ - address: testData.sender2.address, - value: testData.recipients[0].amount, - coin: 'tapt', - }); - tx.outputs.length.should.equal(1); - tx.outputs[0].should.deepEqual({ - address: testData.recipients[0].address, - value: testData.recipients[0].amount, - coin: 'tapt', - }); - should.equal(tx.id, '0x9ec764992194c4b4095289a61073e91cf5404d5bedb5a42ab8bf16d07353332b'); - should.equal(tx.maxGasAmount, 200000); - should.equal(tx.gasUnitPrice, 100); - should.equal(tx.sequenceNumber, 146); - should.equal(tx.expirationTime, 1737528215); - should.equal(tx.type, TransactionType.Send); - const rawTx = tx.toBroadcastFormat(); - should.equal(utils.isValidRawTransaction(rawTx), true); - should.equal(rawTx, testData.TRANSFER); - }); - - it('should succeed to validate a valid signablePayload', async function () { - const transaction = new TransferTransaction(coins.get('tapt')); - const txBuilder = factory.getTransferBuilder(transaction); - txBuilder.sender(testData.sender2.address); - txBuilder.recipient(testData.recipients[0]); - txBuilder.gasData({ - maxGasAmount: 200000, - gasUnitPrice: 100, - }); - txBuilder.sequenceNumber(14); - txBuilder.expirationTime(1736246155); - txBuilder.addFeePayerAddress(testData.feePayer.address); - const tx = (await txBuilder.build()) as TransferTransaction; - const signablePayload = tx.signablePayload; - should.equal( - signablePayload.toString('hex'), - '5efa3c4f02f83a0f4b2d69fc95c607cc02825cc4e7be536ef0992df050d9e67c011aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e74087472616e73666572000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d67000000000200dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2' - ); - }); - - it('should build a unsigned tx and validate its toJson', async function () { - const transaction = new TransferTransaction(coins.get('tapt')); - const txBuilder = factory.getTransferBuilder(transaction); - txBuilder.sender(testData.sender2.address); - txBuilder.recipient(testData.recipients[0]); - txBuilder.gasData({ - maxGasAmount: 200000, - gasUnitPrice: 100, - }); - txBuilder.sequenceNumber(14); - txBuilder.expirationTime(1736246155); - txBuilder.addFeePayerAddress(testData.feePayer.address); - const tx = (await txBuilder.build()) as TransferTransaction; - const toJson = tx.toJson(); - should.equal(toJson.sender, testData.sender2.address); - should.deepEqual(toJson.recipient, { - address: testData.recipients[0].address, - amount: testData.recipients[0].amount, - }); - should.equal(toJson.sequenceNumber, 14); - should.equal(toJson.maxGasAmount, 200000); - should.equal(toJson.gasUnitPrice, 100); - should.equal(toJson.expirationTime, 1736246155); - should.equal(toJson.feePayer, testData.feePayer.address); - }); - - it('should build a signed tx and validate its toJson', async function () { - const txBuilder = factory.from(testData.TRANSFER); - const tx = (await txBuilder.build()) as TransferTransaction; - const toJson = tx.toJson(); - should.equal(toJson.id, '0x9ec764992194c4b4095289a61073e91cf5404d5bedb5a42ab8bf16d07353332b'); - should.equal(toJson.sender, '0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a372449'); - should.deepEqual(toJson.recipient, { - address: '0xf7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad9', - amount: '1000', - }); - should.equal(toJson.sequenceNumber, 146); - should.equal(toJson.maxGasAmount, 200000); - should.equal(toJson.gasUnitPrice, 100); - should.equal(toJson.expirationTime, 1737528215); - }); - }); - - describe('Fail', () => { - it('should fail for invalid sender', async function () { - const transaction = new TransferTransaction(coins.get('tapt')); - const builder = factory.getTransferBuilder(transaction); - should(() => builder.sender('randomString')).throwError('Invalid address randomString'); - }); - - it('should fail for invalid recipient', async function () { - const builder = factory.getTransferBuilder(); - should(() => builder.recipient(testData.invalidRecipients[0])).throwError('Invalid address randomString'); - should(() => builder.recipient(testData.invalidRecipients[1])).throwError('Value cannot be less than zero'); - should(() => builder.recipient(testData.invalidRecipients[2])).throwError('Invalid amount format'); - }); - it('should fail for invalid gas amount', async function () { - const builder = factory.getTransferBuilder(); - should(() => builder.gasData({ maxGasAmount: -1, gasUnitPrice: 100 })).throwError( - 'Value cannot be less than zero' - ); - should(() => builder.gasData({ maxGasAmount: 200000, gasUnitPrice: -1 })).throwError( - 'Value cannot be less than zero' - ); - }); - }); -}); diff --git a/modules/sdk-coin-apt/test/unit/utils.ts b/modules/sdk-coin-apt/test/unit/utils.ts index df571f9489..b7a3a64bc6 100644 --- a/modules/sdk-coin-apt/test/unit/utils.ts +++ b/modules/sdk-coin-apt/test/unit/utils.ts @@ -23,16 +23,6 @@ describe('Aptos util library', function () { }); }); - describe('isValidRawTransaction', function () { - it('should succeed to validate a valid raw transaction', function () { - should.equal(utils.isValidRawTransaction(testData.TRANSFER), true); - }); - it('should fail to validate an invalid raw transaction', function () { - should.doesNotThrow(() => utils.isValidRawTransaction(testData.INVALID_TRANSFER)); - should.equal(utils.isValidRawTransaction(testData.INVALID_TRANSFER), false); - }); - }); - describe('isValidDeserialize', function () { it('should succeed to correctly deserialize serialized transaction', function () { const signedTxn: SignedTransaction = utils.deserializeSignedTransaction(testData.TRANSFER); @@ -41,15 +31,6 @@ describe('Aptos util library', function () { authenticator.fee_payer.address.toString(), '0xdbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2' ); - const rawTx = signedTxn.raw_txn; - const recipient = utils.getRecipientFromTransactionPayload(rawTx.payload); - should.equal(rawTx.sender.toString(), testData.sender2.address); - should.equal(rawTx.max_gas_amount, 200000); - should.equal(rawTx.gas_unit_price, 100); - should.equal(rawTx.sequence_number, 146); - should.equal(rawTx.expiration_timestamp_secs, 1737528215); - should.equal(recipient.address, testData.recipients[0].address); - should.equal(recipient.amount, testData.recipients[0].amount); }); it('should fail to deserialize an invalid serialized transaction', function () { should.throws(() => utils.deserializeSignedTransaction(testData.INVALID_TRANSFER));