@@ -15,6 +15,9 @@ import {
1515 encodeBatchTransferMulti ,
1616 encodeERC20Transfer ,
1717 encodeBatchTransfer ,
18+ encodeBridgeETHTo ,
19+ encodeLzSend ,
20+ encodeApproveAndCall ,
1821 TxStatus ,
1922 TX_CREATED_EVENT ,
2023 TX_VOTED_EVENT ,
@@ -30,6 +33,11 @@ import {
3033 encodeRemoveSigners ,
3134 SignerData ,
3235 ZERO_ADDRESS ,
36+ getTokenByAddress ,
37+ getBridgeMechanism ,
38+ getBridgeContract ,
39+ OP_BRIDGE_ADDRESSES ,
40+ LZ_ENDPOINT_IDS ,
3341} from '@polypay/shared' ;
3442import { RelayerService } from '@/relayer-wallet/relayer-wallet.service' ;
3543import { BatchItemService } from '@/batch-item/batch-item.service' ;
@@ -164,6 +172,8 @@ export class TransactionService {
164172 contactId : dto . contactId ,
165173 signerData : dto . signers ? JSON . stringify ( dto . signers ) : null ,
166174 newThreshold : dto . newThreshold ,
175+ destChainId : dto . destChainId ,
176+ bridgeFee : dto . bridgeFee ,
167177 createdBy : userCommitment ,
168178 status : 'PENDING' ,
169179 batchData,
@@ -654,7 +664,7 @@ export class TransactionService {
654664 }
655665
656666 // Build execute params based on tx type
657- const { to, value, data } = this . buildExecuteParams ( transaction ) ;
667+ const { to, value, data } = await this . buildExecuteParams ( transaction ) ;
658668
659669 // Format proofs for smart contract
660670 const zkProofs = approveVotes . map ( ( vote ) => ( {
@@ -1106,17 +1116,27 @@ export class TransactionService {
11061116 ) ;
11071117 }
11081118
1109- private buildExecuteParams ( transaction : Transaction ) : {
1119+ private async buildExecuteParams ( transaction : Transaction ) : Promise < {
11101120 to : string ;
11111121 value : string ;
11121122 data : string ;
1113- } {
1123+ } > {
11141124 switch ( transaction . type ) {
1115- case TxType . TRANSFER :
1125+ case TxType . TRANSFER : {
1126+ // Check for cross-chain bridge
1127+ if ( transaction . destChainId ) {
1128+ const account = await this . prisma . account . findUnique ( {
1129+ where : { address : transaction . accountAddress } ,
1130+ } ) ;
1131+ if ( account && transaction . destChainId !== account . chainId ) {
1132+ return this . buildBridgeExecuteParams ( transaction , account . chainId ) ;
1133+ }
1134+ }
1135+
11161136 // ERC20 transfer
11171137 if ( transaction ?. tokenAddress ) {
11181138 return {
1119- to : transaction . tokenAddress , // Token contract address
1139+ to : transaction . tokenAddress ,
11201140 value : '0' ,
11211141 data : encodeERC20Transfer (
11221142 transaction . to ,
@@ -1130,6 +1150,7 @@ export class TransactionService {
11301150 value : transaction . value ,
11311151 data : '0x' ,
11321152 } ;
1153+ }
11331154
11341155 case TxType . ADD_SIGNER : {
11351156 const signers : SignerData [ ] = transaction . signerData
@@ -1200,6 +1221,110 @@ export class TransactionService {
12001221 }
12011222 }
12021223
1224+ private buildBridgeExecuteParams (
1225+ transaction : Transaction ,
1226+ srcChainId : number ,
1227+ ) : { to : string ; value : string ; data : string } {
1228+ const destChainId = transaction . destChainId ;
1229+ const tokenSymbol = transaction . tokenAddress
1230+ ? transaction . tokenAddress === ZERO_ADDRESS
1231+ ? 'ETH'
1232+ : null
1233+ : 'ETH' ;
1234+
1235+ let resolvedSymbol = tokenSymbol ;
1236+ if ( ! resolvedSymbol && transaction . tokenAddress ) {
1237+ resolvedSymbol = this . resolveTokenSymbol (
1238+ transaction . tokenAddress ,
1239+ srcChainId ,
1240+ ) ;
1241+ }
1242+
1243+ const mechanism = getBridgeMechanism (
1244+ srcChainId ,
1245+ destChainId ,
1246+ resolvedSymbol ,
1247+ ) ;
1248+
1249+ if ( ! mechanism ) {
1250+ throw new BadRequestException (
1251+ `No bridge route for ${ resolvedSymbol } from chain ${ srcChainId } to ${ destChainId } ` ,
1252+ ) ;
1253+ }
1254+
1255+ const recipient = transaction . to ;
1256+ const amount = BigInt ( transaction . value ) ;
1257+ const bridgeFee = transaction . bridgeFee
1258+ ? BigInt ( transaction . bridgeFee )
1259+ : 0n ;
1260+
1261+ if ( mechanism === 'OP_STACK' ) {
1262+ const bridgeAddress = OP_BRIDGE_ADDRESSES [ srcChainId ] ;
1263+ if ( ! bridgeAddress ) {
1264+ throw new BadRequestException ( `No OP bridge on chain ${ srcChainId } ` ) ;
1265+ }
1266+ return {
1267+ to : bridgeAddress ,
1268+ value : amount . toString ( ) ,
1269+ data : encodeBridgeETHTo ( recipient ) ,
1270+ } ;
1271+ }
1272+
1273+ // LAYERZERO
1274+ const dstEid = LZ_ENDPOINT_IDS [ destChainId ] ;
1275+ if ( ! dstEid ) {
1276+ throw new BadRequestException ( `No LZ endpoint for chain ${ destChainId } ` ) ;
1277+ }
1278+
1279+ const oftEntry = getBridgeContract ( resolvedSymbol , srcChainId ) ;
1280+ if ( ! oftEntry ) {
1281+ throw new BadRequestException (
1282+ `No OFT contract for ${ resolvedSymbol } on chain ${ srcChainId } ` ,
1283+ ) ;
1284+ }
1285+
1286+ const lzSendData = encodeLzSend (
1287+ dstEid ,
1288+ recipient ,
1289+ amount ,
1290+ bridgeFee ,
1291+ transaction . accountAddress ,
1292+ ) ;
1293+
1294+ if ( oftEntry . type === 'adapter' ) {
1295+ // Base side: approve token + call adapter. Self-call with value=0.
1296+ return {
1297+ to : transaction . accountAddress ,
1298+ value : '0' ,
1299+ data : encodeApproveAndCall (
1300+ transaction . tokenAddress ,
1301+ oftEntry . address ,
1302+ amount ,
1303+ oftEntry . address ,
1304+ bridgeFee ,
1305+ lzSendData ,
1306+ ) ,
1307+ } ;
1308+ }
1309+
1310+ // Horizen side: direct OFT send with bridgeFee as value
1311+ return {
1312+ to : oftEntry . address ,
1313+ value : bridgeFee . toString ( ) ,
1314+ data : lzSendData ,
1315+ } ;
1316+ }
1317+
1318+ private resolveTokenSymbol ( tokenAddress : string , chainId : number ) : string {
1319+ const token = getTokenByAddress ( tokenAddress , chainId ) ;
1320+ if ( token . address === ZERO_ADDRESS ) {
1321+ throw new BadRequestException (
1322+ `Cannot resolve token symbol for address ${ tokenAddress } on chain ${ chainId } ` ,
1323+ ) ;
1324+ }
1325+ return token . symbol ;
1326+ }
1327+
12031328 /**
12041329 * Check if transaction should be marked as FAILED
12051330 * Query totalSigners realtime from account.signers
0 commit comments