11import { Transaction } from './transaction' ;
22import { BaseCoin as CoinConfig } from '@bitgo/statics' ;
3- import { TransactionType } from '@bitgo/sdk-core' ;
3+ import { InvalidTransactionError , TransactionType } from '@bitgo/sdk-core' ;
44import {
55 EntryFunctionABI ,
66 EntryFunctionArgumentTypes ,
7- InputGenerateTransactionPayloadData ,
87 SimpleEntryFunctionArgumentTypes ,
8+ InputGenerateTransactionPayloadData ,
99 TransactionPayload ,
1010 TransactionPayloadEntryFunction ,
11+ AccountAddress ,
12+ TypeTagAddress ,
13+ TypeTagBool ,
14+ TypeTagU8 ,
15+ TypeTagU16 ,
16+ TypeTagU32 ,
17+ TypeTagU64 ,
18+ TypeTagU128 ,
19+ TypeTagU256 ,
1120} from '@aptos-labs/ts-sdk' ;
1221import { CustomTransactionParams } from '../iface' ;
1322import { validateModuleName , validateFunctionName } from '../utils/validation' ;
1423
1524/**
16- * Transaction class for custom Aptos transactions with entry function payloads .
25+ * Transaction class for custom Aptos transactions.
1726 */
1827export class CustomTransaction extends Transaction {
1928 private _moduleName : string ;
2029 private _functionName : string ;
2130 private _typeArguments : string [ ] = [ ] ;
2231 private _functionArguments : Array < EntryFunctionArgumentTypes | SimpleEntryFunctionArgumentTypes > = [ ] ;
23- private _entryFunctionAbi : EntryFunctionABI ;
32+ private _entryFunctionAbi ? : EntryFunctionABI ;
2433
2534 constructor ( coinConfig : Readonly < CoinConfig > ) {
2635 super ( coinConfig ) ;
@@ -29,13 +38,10 @@ export class CustomTransaction extends Transaction {
2938
3039 /**
3140 * Set the custom transaction parameters
32- *
33- * @param {CustomTransactionParams } params - Custom transaction parameters
3441 */
3542 setCustomTransactionParams ( params : CustomTransactionParams ) : void {
3643 validateModuleName ( params . moduleName ) ;
3744 validateFunctionName ( params . functionName ) ;
38- this . validateAbi ( params . abi ) ;
3945
4046 this . _moduleName = params . moduleName ;
4147 this . _functionName = params . functionName ;
@@ -46,17 +52,13 @@ export class CustomTransaction extends Transaction {
4652
4753 /**
4854 * Set the entry function ABI
49- *
50- * @param {EntryFunctionABI } abi - The ABI definition for the entry function
5155 */
5256 setEntryFunctionAbi ( abi : EntryFunctionABI ) : void {
5357 this . _entryFunctionAbi = abi ;
5458 }
5559
5660 /**
57- * Get the full function name in the format moduleName::functionName
58- *
59- * @returns {string } The full function name
61+ * Get the full function name
6062 */
6163 get fullFunctionName ( ) : string {
6264 if ( ! this . _moduleName || ! this . _functionName ) {
@@ -72,7 +74,7 @@ export class CustomTransaction extends Transaction {
7274 */
7375 protected parseTransactionPayload ( payload : TransactionPayload ) : void {
7476 if ( ! ( payload instanceof TransactionPayloadEntryFunction ) ) {
75- throw new Error ( 'Expected entry function payload for custom transaction' ) ;
77+ throw new InvalidTransactionError ( 'Expected entry function payload for custom transaction' ) ;
7678 }
7779
7880 const entryFunction = payload . entryFunction ;
@@ -84,40 +86,151 @@ export class CustomTransaction extends Transaction {
8486
8587 const moduleName = `${ moduleAddress } ::${ moduleIdentifier } ` ;
8688
87- // Validate the extracted names using our existing validation
89+ // Validate
8890 validateModuleName ( moduleName ) ;
8991 validateFunctionName ( functionIdentifier ) ;
9092
9193 this . _moduleName = moduleName ;
9294 this . _functionName = functionIdentifier ;
93-
94- // Extract type arguments and function arguments
9595 this . _typeArguments = entryFunction . type_args . map ( ( typeArg ) => typeArg . toString ( ) ) ;
96- this . _functionArguments = entryFunction . args as Array <
97- EntryFunctionArgumentTypes | SimpleEntryFunctionArgumentTypes
98- > ;
96+
97+ this . _functionArguments = entryFunction . args . map ( ( arg : any ) => {
98+ if ( typeof arg === 'string' || typeof arg === 'number' || typeof arg === 'boolean' ) {
99+ return arg ;
100+ }
101+ if ( arg && typeof arg === 'object' && 'data' in arg && arg . data ) {
102+ const bytes = Array . from ( arg . data ) as number [ ] ;
103+ return '0x' + bytes . map ( ( b : number ) => b . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ;
104+ }
105+ return arg ;
106+ } ) ;
99107 }
100108
101109 /**
102- * Generate the transaction payload data for the custom transaction
103- *
104- * @returns {InputGenerateTransactionPayloadData } The transaction payload data
110+ * Generate transaction payload data
105111 */
106112 protected getTransactionPayloadData ( ) : InputGenerateTransactionPayloadData {
107113 const functionName = this . getValidatedFullFunctionName ( ) ;
108114
115+ // Convert arguments based on ABI information if available
116+ const processedArguments = this . _functionArguments . map ( ( arg : any , index : number ) => {
117+ // Use ABI to identify the expected type for this argument
118+ const paramType = this . _entryFunctionAbi ?. parameters ?. [ index ] ;
119+ if ( paramType ) {
120+ return this . convertArgumentByABI ( arg , paramType ) ;
121+ }
122+
123+ // Fallback: basic conversion for common cases
124+ if ( typeof arg === 'string' && arg . startsWith ( '0x' ) && arg . length === 66 ) {
125+ try {
126+ return AccountAddress . fromString ( arg ) ;
127+ } catch {
128+ return arg ;
129+ }
130+ }
131+ return arg ;
132+ } ) ;
133+
109134 return {
110135 function : functionName ,
111136 typeArguments : this . _typeArguments ,
112- functionArguments : this . _functionArguments ,
137+ functionArguments : processedArguments ,
113138 abi : this . _entryFunctionAbi ,
139+ } as InputGenerateTransactionPayloadData ;
140+ }
141+
142+ /**
143+ * Convert argument based on ABI type information
144+ */
145+ private convertArgumentByABI ( arg : any , paramType : any ) : any {
146+ // Helper function to convert bytes to hex string
147+ const bytesToHex = ( bytes : number [ ] ) : string => {
148+ return '0x' + bytes . map ( ( b ) => b . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ;
114149 } ;
150+
151+ // Helper function to try converting a hex string to an AccountAddress
152+ const tryToAddress = ( hexStr : string ) : any => {
153+ try {
154+ return AccountAddress . fromString ( hexStr ) ;
155+ } catch {
156+ return hexStr ;
157+ }
158+ } ;
159+
160+ // Handle primitive values (string, number, boolean)
161+ if ( typeof arg === 'string' || typeof arg === 'number' || typeof arg === 'boolean' ) {
162+ // Address conversion for hex strings
163+ if ( paramType instanceof TypeTagAddress && typeof arg === 'string' && arg . startsWith ( '0x' ) ) {
164+ return tryToAddress ( arg ) ;
165+ }
166+
167+ // Type conversions based on parameter type
168+ if ( paramType instanceof TypeTagBool ) return Boolean ( arg ) ;
169+ if ( paramType instanceof TypeTagU8 || paramType instanceof TypeTagU16 || paramType instanceof TypeTagU32 )
170+ return Number ( arg ) ;
171+ if ( paramType instanceof TypeTagU64 || paramType instanceof TypeTagU128 || paramType instanceof TypeTagU256 )
172+ return String ( arg ) ;
173+
174+ return arg ;
175+ }
176+
177+ // Handle BCS-encoded data with 'data' property
178+ if ( arg && typeof arg === 'object' && 'data' in arg && arg . data ) {
179+ const bytes = Array . from ( arg . data ) as number [ ] ;
180+ const hexString = bytesToHex ( bytes ) ;
181+
182+ return paramType instanceof TypeTagAddress ? tryToAddress ( hexString ) : hexString ;
183+ }
184+
185+ // Handle nested BCS structures with 'value' property
186+ if ( arg && typeof arg === 'object' && 'value' in arg && arg . value ) {
187+ // Simple value wrapper
188+ if ( ! ( 'value' in arg . value ) || typeof arg . value . value !== 'object' ) {
189+ return this . convertArgumentByABI ( arg . value , paramType ) ;
190+ }
191+
192+ // Double nested structure with numeric keys
193+ const bytesObj = arg . value . value ;
194+ const keys = Object . keys ( bytesObj )
195+ . filter ( ( k ) => ! isNaN ( parseInt ( k , 10 ) ) )
196+ . sort ( ( a , b ) => parseInt ( a , 10 ) - parseInt ( b , 10 ) ) ;
197+
198+ if ( keys . length === 0 ) return arg ;
199+
200+ const bytes = keys . map ( ( k ) => bytesObj [ k ] ) ;
201+ let extractedValue : any ;
202+
203+ // Convert bytes based on parameter type
204+ if (
205+ paramType instanceof TypeTagAddress ||
206+ paramType instanceof TypeTagU64 ||
207+ paramType instanceof TypeTagU128 ||
208+ paramType instanceof TypeTagU256
209+ ) {
210+ extractedValue = bytesToHex ( bytes ) ;
211+ } else if ( paramType instanceof TypeTagBool ) {
212+ extractedValue = bytes [ 0 ] === 1 ;
213+ } else if ( paramType instanceof TypeTagU8 || paramType instanceof TypeTagU16 || paramType instanceof TypeTagU32 ) {
214+ // Convert little-endian bytes to number using the original algorithm
215+ // to ensure consistent behavior with large numbers
216+ let result = 0 ;
217+ for ( let i = bytes . length - 1 ; i >= 0 ; i -- ) {
218+ result = result * 256 + bytes [ i ] ;
219+ }
220+ extractedValue = result ;
221+ } else {
222+ extractedValue = bytesToHex ( bytes ) ;
223+ }
224+
225+ return this . convertArgumentByABI ( extractedValue , paramType ) ;
226+ }
227+
228+ // For anything else, return as-is
229+ return arg ;
115230 }
116231
117232 /**
118- * Get the custom transaction parameters
119- *
120- * @returns {CustomTransactionParams } The custom transaction parameters
233+ * Get custom transaction parameters
121234 */
122235 getCustomTransactionParams ( ) : CustomTransactionParams {
123236 return {
@@ -130,27 +243,20 @@ export class CustomTransaction extends Transaction {
130243 }
131244
132245 /**
133- * Override the deprecated recipient getter to handle custom transactions gracefully
134- * Custom transactions may not have traditional recipients
135- *
136- * @deprecated - use `recipients()`
246+ * Override recipient getter for custom transactions
137247 */
138248 get recipient ( ) : any {
139- // For custom transactions, return a placeholder recipient if no recipients exist
140249 if ( this . _recipients . length === 0 ) {
141250 return {
142- address : '' , // Empty address for custom transactions
251+ address : '' ,
143252 amount : '0' ,
144253 } ;
145254 }
146255 return this . _recipients [ 0 ] ;
147256 }
148257
149258 /**
150- * Get validated full function name with runtime format checking
151- *
152- * @returns {string } The validated full function name
153- * @throws {Error } If function name format is invalid
259+ * Get validated full function name
154260 */
155261 private getValidatedFullFunctionName ( ) : `${string } ::${string } ::${string } ` {
156262 if ( ! this . _moduleName || ! this . _functionName ) {
@@ -159,8 +265,7 @@ export class CustomTransaction extends Transaction {
159265
160266 const fullName = `${ this . _moduleName } ::${ this . _functionName } ` ;
161267
162- // Runtime validation of the expected format
163- // Supports both hex addresses (SHORT/LONG) and named addresses
268+ // Basic validation
164269 const fullFunctionPattern =
165270 / ^ ( 0 x [ a - f A - F 0 - 9 ] { 1 , 64 } | [ a - z A - Z _ ] [ a - z A - Z 0 - 9 _ ] * ) : : [ a - z A - Z _ ] [ a - z A - Z 0 - 9 _ ] * : : [ a - z A - Z _ ] [ a - z A - Z 0 - 9 _ ] * $ / ;
166271 if ( ! fullFunctionPattern . test ( fullName ) ) {
@@ -169,24 +274,4 @@ export class CustomTransaction extends Transaction {
169274
170275 return fullName as `${string } ::${string } ::${string } `;
171276 }
172-
173- /**
174- * Validate ABI structure and provide helpful error messages
175- *
176- * @param {EntryFunctionABI } abi - The ABI to validate
177- * @throws {Error } If ABI format is invalid
178- */
179- private validateAbi ( abi : EntryFunctionABI ) : void {
180- if ( ! abi || typeof abi !== 'object' ) {
181- throw new Error ( 'ABI must be a valid EntryFunctionABI object' ) ;
182- }
183-
184- if ( ! Array . isArray ( abi . typeParameters ) ) {
185- throw new Error ( 'ABI must have a typeParameters array. Use [] if the function has no type parameters' ) ;
186- }
187-
188- if ( ! Array . isArray ( abi . parameters ) ) {
189- throw new Error ( 'ABI must have a parameters array containing TypeTag objects for each function parameter' ) ;
190- }
191- }
192277}
0 commit comments