@@ -21,13 +21,14 @@ import {
2121} from '@bitgo/sdk-core' ;
2222import { BaseCoin as CoinConfig } from '@bitgo/statics' ;
2323import { ethers } from 'ethers' ;
24- import { Address , Hex , Tip20Operation } from './types' ;
24+ import { Address , Hex , RawContractCall , Tip20Operation } from './types' ;
2525import { Tip20Transaction , Tip20TransactionRequest } from './transaction' ;
2626import {
2727 amountToTip20Units ,
2828 encodeTip20TransferWithMemo ,
2929 isTip20Transaction ,
3030 isValidAddress ,
31+ isValidHexData ,
3132 isValidMemoId ,
3233 isValidTip20Amount ,
3334 tip20UnitsToAmount ,
@@ -41,6 +42,7 @@ import { AA_TRANSACTION_TYPE } from './constants';
4142 */
4243export class Tip20TransactionBuilder extends AbstractTransactionBuilder {
4344 private operations : Tip20Operation [ ] = [ ] ;
45+ private rawCalls : RawContractCall [ ] = [ ] ;
4446 private _feeToken ?: Address ;
4547 private _nonce ?: number ;
4648 private _gas ?: bigint ;
@@ -73,8 +75,8 @@ export class Tip20TransactionBuilder extends AbstractTransactionBuilder {
7375 * @throws BuildTransactionError if validation fails
7476 */
7577 validateTransaction ( ) : void {
76- if ( this . operations . length === 0 ) {
77- throw new BuildTransactionError ( 'At least one operation is required to build a transaction' ) ;
78+ if ( this . operations . length === 0 && this . rawCalls . length === 0 ) {
79+ throw new BuildTransactionError ( 'At least one operation or raw call is required to build a transaction' ) ;
7880 }
7981
8082 if ( this . _nonce === undefined ) {
@@ -148,7 +150,16 @@ export class Tip20TransactionBuilder extends AbstractTransactionBuilder {
148150 data : tuple [ 2 ] as Hex ,
149151 } ) ) ;
150152
151- const operations : Tip20Operation [ ] = calls . map ( ( call ) => this . decodeCallToOperation ( call ) ) ;
153+ const operations : Tip20Operation [ ] = [ ] ;
154+ const decodedRawCalls : RawContractCall [ ] = [ ] ;
155+ for ( const call of calls ) {
156+ const op = this . decodeCallToOperation ( call ) ;
157+ if ( op !== null ) {
158+ operations . push ( op ) ;
159+ } else {
160+ decodedRawCalls . push ( { to : call . to , data : call . data , value : call . value . toString ( ) } ) ;
161+ }
162+ }
152163
153164 let signature : { r : Hex ; s : Hex ; yParity : number } | undefined ;
154165 if ( decoded . length >= 14 && decoded [ 13 ] && ( decoded [ 13 ] as string ) . length > 2 ) {
@@ -182,9 +193,10 @@ export class Tip20TransactionBuilder extends AbstractTransactionBuilder {
182193 this . _maxPriorityFeePerGas = maxPriorityFeePerGas ;
183194 this . _feeToken = feeToken ;
184195 this . operations = operations ;
196+ this . rawCalls = decodedRawCalls ;
185197 this . _restoredSignature = signature ;
186198
187- const tx = new Tip20Transaction ( this . _coinConfig , txRequest , operations ) ;
199+ const tx = new Tip20Transaction ( this . _coinConfig , txRequest , operations , decodedRawCalls ) ;
188200 if ( signature ) {
189201 tx . setSignature ( signature ) ;
190202 }
@@ -197,9 +209,10 @@ export class Tip20TransactionBuilder extends AbstractTransactionBuilder {
197209
198210 /**
199211 * Decode a single AA call's data back into a Tip20Operation.
200- * Expects the call data to encode transferWithMemo(address, uint256, bytes32).
212+ * Returns null if the call is not a transferWithMemo — it will be stored as a RawContractCall instead.
213+ * This preserves calldata fidelity for arbitrary smart contract interactions.
201214 */
202- private decodeCallToOperation ( call : { to : Address ; data : Hex ; value : bigint } ) : Tip20Operation {
215+ private decodeCallToOperation ( call : { to : Address ; data : Hex ; value : bigint } ) : Tip20Operation | null {
203216 const iface = new ethers . utils . Interface ( TIP20_TRANSFER_WITH_MEMO_ABI ) ;
204217 try {
205218 const decoded = iface . decodeFunctionData ( 'transferWithMemo' , call . data ) ;
@@ -214,7 +227,8 @@ export class Tip20TransactionBuilder extends AbstractTransactionBuilder {
214227
215228 return { token : call . to , to : toAddress , amount, memo } ;
216229 } catch {
217- return { token : call . to , to : call . to , amount : tip20UnitsToAmount ( call . value ) } ;
230+ // Not a transferWithMemo call — caller will store as RawContractCall
231+ return null ;
218232 }
219233 }
220234
@@ -243,6 +257,14 @@ export class Tip20TransactionBuilder extends AbstractTransactionBuilder {
243257
244258 const calls = this . operations . map ( ( op ) => this . operationToCall ( op ) ) ;
245259
260+ for ( const rawCall of this . rawCalls ) {
261+ calls . push ( {
262+ to : rawCall . to as Address ,
263+ data : rawCall . data as Hex ,
264+ value : rawCall . value ? BigInt ( rawCall . value ) : 0n ,
265+ } ) ;
266+ }
267+
246268 const txRequest : Tip20TransactionRequest = {
247269 type : AA_TRANSACTION_TYPE ,
248270 chainId : this . _common . chainIdBN ( ) . toNumber ( ) ,
@@ -255,7 +277,7 @@ export class Tip20TransactionBuilder extends AbstractTransactionBuilder {
255277 feeToken : this . _feeToken ,
256278 } ;
257279
258- const tx = new Tip20Transaction ( this . _coinConfig , txRequest , this . operations ) ;
280+ const tx = new Tip20Transaction ( this . _coinConfig , txRequest , this . operations , this . rawCalls ) ;
259281
260282 if ( this . _sourceKeyPair && this . _sourceKeyPair . getKeys ( ) . prv ) {
261283 const prv = this . _sourceKeyPair . getKeys ( ) . prv ! ;
@@ -288,6 +310,32 @@ export class Tip20TransactionBuilder extends AbstractTransactionBuilder {
288310 return this ;
289311 }
290312
313+ /**
314+ * Add a raw smart contract call with pre-encoded calldata
315+ * Use this for arbitrary contract interactions where the UI provides ABI-encoded calldata
316+ *
317+ * @param call - Raw contract call with target address and pre-encoded calldata
318+ * @returns this builder instance for chaining
319+ */
320+ addRawCall ( call : RawContractCall ) : this {
321+ if ( ! isValidAddress ( call . to ) ) {
322+ throw new BuildTransactionError ( `Invalid contract address: ${ call . to } ` ) ;
323+ }
324+ if ( ! isValidHexData ( call . data ) ) {
325+ throw new BuildTransactionError ( `Invalid calldata: must be a non-empty 0x-prefixed hex string` ) ;
326+ }
327+ this . rawCalls . push ( call ) ;
328+ return this ;
329+ }
330+
331+ /**
332+ * Get all raw contract calls in this transaction
333+ * @returns Array of raw contract calls
334+ */
335+ getRawCalls ( ) : RawContractCall [ ] {
336+ return [ ...this . rawCalls ] ;
337+ }
338+
291339 /**
292340 * Set which TIP-20 token will be used to pay transaction fees
293341 * This is a global setting for the entire transaction
0 commit comments