diff --git a/bridgeservice/bridge_test.go b/bridgeservice/bridge_test.go index 25befb7a4..ded10b414 100644 --- a/bridgeservice/bridge_test.go +++ b/bridgeservice/bridge_test.go @@ -535,6 +535,8 @@ func TestGetBridgesHandler(t *testing.T) { Amount: common.Big0, DepositCount: 0, Metadata: []byte("metadata"), + TxnSender: common.HexToAddress("0x5555555555555555555555555555555555555555"), + ToAddress: common.HexToAddress("0xF9D64d54D32EE2BDceAAbFA60C4C438E224427d0"), }, } @@ -567,6 +569,11 @@ func TestGetBridgesHandler(t *testing.T) { require.Equal(t, bridgeResponses, response.Bridges) require.Equal(t, len(expectedBridges), response.Count) + + // Verify to_address is present in the response + require.NotNil(t, response.Bridges) + require.Len(t, response.Bridges, 1) + require.Equal(t, bridgetypes.Address("0xF9D64d54D32EE2BDceAAbFA60C4C438E224427d0"), response.Bridges[0].ToAddress) }) t.Run("GetBridges for L1 network error", func(t *testing.T) { diff --git a/bridgeservice/docs/docs.go b/bridgeservice/docs/docs.go index 4df5d449f..cb04d48ac 100644 --- a/bridgeservice/docs/docs.go +++ b/bridgeservice/docs/docs.go @@ -757,6 +757,11 @@ const docTemplate = `{ "type": "integer", "example": 10 }, + "to_address": { + "description": "Address of the contract that was the recipient of the transaction. This may differ from the bridge contract address.", + "type": "string", + "example": "0xF9D64d54D32EE2BDceAAbFA60C4C438E224427d0" + }, "tx_hash": { "description": "Hash of the transaction that included the bridge event", "type": "string", diff --git a/bridgeservice/docs/swagger.json b/bridgeservice/docs/swagger.json index deab5e467..3fb902749 100644 --- a/bridgeservice/docs/swagger.json +++ b/bridgeservice/docs/swagger.json @@ -750,6 +750,11 @@ "type": "integer", "example": 10 }, + "to_address": { + "description": "Address of the contract that was the recipient of the transaction. This may differ from the bridge contract address.", + "type": "string", + "example": "0xF9D64d54D32EE2BDceAAbFA60C4C438E224427d0" + }, "tx_hash": { "description": "Hash of the transaction that included the bridge event", "type": "string", diff --git a/bridgeservice/docs/swagger.yaml b/bridgeservice/docs/swagger.yaml index 6cf02b645..1587de33f 100644 --- a/bridgeservice/docs/swagger.yaml +++ b/bridgeservice/docs/swagger.yaml @@ -80,6 +80,11 @@ definitions: description: ID of the network where the bridge transaction originated example: 10 type: integer + to_address: + description: Address of the contract that was the recipient of the transaction. + This may differ from the bridge contract address. + example: 0xF9D64d54D32EE2BDceAAbFA60C4C438E224427d0 + type: string tx_hash: description: Hash of the transaction that included the bridge event example: 0xdef4567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef diff --git a/bridgeservice/types/types.go b/bridgeservice/types/types.go index 6c51e31c2..f5dc18707 100644 --- a/bridgeservice/types/types.go +++ b/bridgeservice/types/types.go @@ -140,6 +140,9 @@ type BridgeResponse struct { // Address of the transaction sender who initiated the bridge transaction TxnSender Address `json:"txn_sender" example:"0xabc1234567890abcdef1234567890abcdef12345"` + + // Address of the contract that was the recipient of the transaction. This may differ from the bridge contract address. + ToAddress Address `json:"to_address" example:"0xF9D64d54D32EE2BDceAAbFA60C4C438E224427d0"` } // ClaimsResult contains the list of claim records and the total count diff --git a/bridgeservice/utils.go b/bridgeservice/utils.go index 1a2d66a87..574451c7f 100644 --- a/bridgeservice/utils.go +++ b/bridgeservice/utils.go @@ -127,6 +127,7 @@ func NewBridgeResponse(bridge *bridgesync.Bridge, networkID uint32, DepositCount: bridge.DepositCount, BridgeHash: bridgetypes.Hash(bridge.Hash().Hex()), TxnSender: bridgetypes.Address(bridge.TxnSender.Hex()), + ToAddress: bridgetypes.Address(bridge.ToAddress.Hex()), } } diff --git a/bridgesync/backfill_tx_sender.go b/bridgesync/backfill_tx_sender.go index 2ee099306..f3ef9f85b 100644 --- a/bridgesync/backfill_tx_sender.go +++ b/bridgesync/backfill_tx_sender.go @@ -337,7 +337,8 @@ func (b *BackfillTxnSender) extractData(ctx context.Context, return common.Address{}, common.Address{}, ctx.Err() default: } - return ExtractTxnSenderAndFrom(ctx, b.client, b.bridgeAddr, txHash, logEvent, b.log) + txnSender, fromAddr, _, err = ExtractTxnAddresses(ctx, b.client, b.bridgeAddr, txHash, logEvent, b.log) + return txnSender, fromAddr, err } // bulkUpdate performs a bulk update of multiple records @@ -370,7 +371,7 @@ func (b *BackfillTxnSender) bulkUpdate( stmt, err := tx.PrepareContext(dbCtx, fmt.Sprintf(` UPDATE %s - SET + SET txn_sender = COALESCE(NULLIF(txn_sender, ''), ?), from_address = COALESCE(NULLIF(from_address, ''), ?) WHERE block_num = ? AND block_pos = ?; diff --git a/bridgesync/downloader.go b/bridgesync/downloader.go index 52df230d5..e457b3d48 100644 --- a/bridgesync/downloader.go +++ b/bridgesync/downloader.go @@ -139,6 +139,13 @@ func (t *Transaction) From() common.Address { return common.HexToAddress(t.FromRaw) } +func (t *Transaction) ToAddress() common.Address { + if t.To == "" { + return common.Address{} + } + return common.HexToAddress(t.To) +} + func RPCTransactionByHash(client aggkittypes.EthClienter, txHash common.Hash) (*Transaction, error) { // Use client.Call to fetch transaction details using eth_getTransactionByHash @@ -150,33 +157,25 @@ func RPCTransactionByHash(client aggkittypes.EthClienter, return &tx, nil } -func extractTxnSender( - client aggkittypes.EthClienter, - txHash common.Hash) (common.Address, error) { - tx, err := RPCTransactionByHash(client, txHash) - if err != nil { - return common.Address{}, fmt.Errorf("failed to get transaction by hash for %s: %w", txHash.Hex(), err) - } - return tx.From(), nil -} - -// ExtractTxnSenderAndFrom extracts the txn_sender and from address from the transaction trace. -// Return txnSender (same for all events in the same transaction) and fromAddr (specific for the event) -func ExtractTxnSenderAndFrom(ctx context.Context, +// ExtractTxnAddresses extracts the txn_sender, from address, and to address from the transaction trace. +func ExtractTxnAddresses(ctx context.Context, client aggkittypes.EthClienter, bridgeAddr common.Address, txHash common.Hash, logEvent *agglayerbridge.AgglayerbridgeBridgeEvent, - logger *logger.Logger) (txnSender common.Address, fromAddr common.Address, err error) { + logger *logger.Logger) (txnSender common.Address, fromAddr common.Address, toAddr common.Address, err error) { // If event is a message, fromAddr is log.origin_address // so we only need the txn_sender that can be obtained from hash_receipt + // and toAddr from the transaction receipt (same source as txn_sender) if logEvent.LeafType == bridgeLeafTypeMessage { - txnSender, err = extractTxnSender(client, txHash) + tx, err := RPCTransactionByHash(client, txHash) if err != nil { - return common.Address{}, common.Address{}, - fmt.Errorf("extractTxnSenderAndFrom: failed to extract txn sender from tx_hash:%s: %w", txHash.Hex(), err) + return common.Address{}, common.Address{}, common.Address{}, + fmt.Errorf("extractTxnAddresses: failed to extract txn sender from tx_hash:%s: %w", txHash.Hex(), err) } - return txnSender, logEvent.OriginAddress, nil + txnSender = tx.From() + toAddr = tx.ToAddress() + return txnSender, logEvent.OriginAddress, toAddr, nil } foundCalls, rootCall, err := extractCallData(client, bridgeAddr, txHash, logger, func(c Call) (bool, error) { if logEvent.LeafType == bridgeLeafTypeAsset { @@ -185,18 +184,19 @@ func ExtractTxnSenderAndFrom(ctx context.Context, return false, nil }) if err != nil { - return common.Address{}, common.Address{}, - fmt.Errorf("extractTxnSenderAndFrom:failed to extract bridge event data (tx hash: %s): %w", txHash, err) + return common.Address{}, common.Address{}, common.Address{}, + fmt.Errorf("extractTxnAddresses:failed to extract bridge event data (tx hash: %s): %w", txHash, err) } txnSender = rootCall.From + toAddr = rootCall.To fromAddr, err = ExtractFromAddrFromCalls(foundCalls, logEvent) if err != nil { - return common.Address{}, common.Address{}, - fmt.Errorf("extractTxnSenderAndFrom: failed to extract fromAddr from tx_hash:%s calls: %w", + return common.Address{}, common.Address{}, common.Address{}, + fmt.Errorf("extractTxnAddresses: failed to extract fromAddr from tx_hash:%s calls: %w", txHash.Hex(), err) } - return txnSender, fromAddr, nil + return txnSender, fromAddr, toAddr, nil } type bridgeCallParams struct { @@ -355,7 +355,7 @@ func buildBridgeEventHandler( "DestinationNetwork: %d, DestinationAddress: %s, DepositCount: %d, Amount: %s, ", bridgeEvent.LeafType, bridgeEvent.OriginNetwork, bridgeEvent.OriginAddress.Hex(), bridgeEvent.DestinationNetwork, bridgeEvent.DestinationAddress.Hex(), bridgeEvent.DepositCount, bridgeEvent.Amount.String()) - txnSender, fromAddress, err := ExtractTxnSenderAndFrom(ctx, client, bridgeAddr, l.TxHash, + txnSender, fromAddress, toAddress, err := ExtractTxnAddresses(ctx, client, bridgeAddr, l.TxHash, bridgeEvent, logger) if err != nil { return fmt.Errorf("failed to extract bridge event data (tx hash: %s): %w", l.TxHash, err) @@ -376,6 +376,7 @@ func buildBridgeEventHandler( Metadata: bridgeEvent.Metadata, DepositCount: bridgeEvent.DepositCount, TxnSender: txnSender, + ToAddress: toAddress, }}) return nil } diff --git a/bridgesync/downloader_test.go b/bridgesync/downloader_test.go index 83da73568..d2d2ac5c2 100644 --- a/bridgesync/downloader_test.go +++ b/bridgesync/downloader_test.go @@ -28,7 +28,7 @@ import ( // mainnet: // case https://etherscan.io/tx/0x8db8e288d25102b64d8a37ad05769817d1b43f0384dd05da075d24d2cee9cb65 (bn: 19566985) -> fix // case: https://etherscan.io/tx/0x0b276867aa22d1c162c2700d35c500a124a6a953c7b24931a1d3efc63f7cd4ab (bn: 22770713) -func TestExtractTxnSenderAndFromExploratory(t *testing.T) { +func TestExtractTxnAddressesExploratory(t *testing.T) { t.Skip("Skipping exploratory test") ctx := t.Context() l1url := os.Getenv("L1URL") @@ -1016,7 +1016,7 @@ func TestTxnSenderField(t *testing.T) { } } -func TestExtractTxnSenderAndFrom(t *testing.T) { +func TestExtractTxnAddresses(t *testing.T) { bridgeAddr := common.HexToAddress("0x10") txHash := common.HexToHash("0xabcde12345abcde12345abcde12345abcde12345abcde12345abcde12345abcd") @@ -1029,6 +1029,7 @@ func TestExtractTxnSenderAndFrom(t *testing.T) { responseTransactionHashError error expectedTxnSender common.Address expectedFrom common.Address + expectedTo common.Address expectErr string }{ { @@ -1042,6 +1043,23 @@ func TestExtractTxnSenderAndFrom(t *testing.T) { responseTransactionHashError: fmt.Errorf("RPC error"), expectErr: "RPC error", }, + { + name: "messageLeaf: successful extraction with to address", + logEvent: &agglayerbridge.AgglayerbridgeBridgeEvent{ + LeafType: bridgeLeafTypeMessage, + OriginAddress: common.HexToAddress("0x40"), + DestinationNetwork: 1, + DestinationAddress: common.HexToAddress("0x30"), + Amount: big.NewInt(100), + }, + responseTransactionHash: &Transaction{ + FromRaw: "0x1111111111111111111111111111111111111111", + To: "0x2222222222222222222222222222222222222222", + }, + expectedTxnSender: common.HexToAddress("0x1111111111111111111111111111111111111111"), + expectedFrom: common.HexToAddress("0x40"), + expectedTo: common.HexToAddress("0x2222222222222222222222222222222222222222"), + }, { name: "assetLeaf: error can't find From from calls", logEvent: &agglayerbridge.AgglayerbridgeBridgeEvent{ @@ -1066,6 +1084,30 @@ func TestExtractTxnSenderAndFrom(t *testing.T) { }, expectErr: "failed to extract", }, + { + name: "assetLeaf: successful extraction with to address from rootCall", + logEvent: &agglayerbridge.AgglayerbridgeBridgeEvent{ + LeafType: bridgeLeafTypeAsset, + OriginAddress: common.HexToAddress("0x50"), + DestinationNetwork: 1, + DestinationAddress: common.HexToAddress("0x30"), + Amount: big.NewInt(100), + }, + responseDebugTrace: &Call{ + From: common.HexToAddress("0x3333333333333333333333333333333333333333"), + To: common.HexToAddress("0x4444444444444444444444444444444444444444"), + Calls: []Call{ + { + To: bridgeAddr, + From: common.HexToAddress("0x50"), + Input: append(BridgeAssetMethodID, make([]byte, 100)...), + }, + }, + }, + expectedTxnSender: common.HexToAddress("0x3333333333333333333333333333333333333333"), + expectedFrom: common.HexToAddress("0x50"), + expectedTo: common.HexToAddress("0x4444444444444444444444444444444444444444"), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1091,7 +1133,7 @@ func TestExtractTxnSenderAndFrom(t *testing.T) { }).Return(nil). Maybe() - txnSender, from, err := ExtractTxnSenderAndFrom(ctx, ethClient, + txnSender, from, to, err := ExtractTxnAddresses(ctx, ethClient, bridgeAddr, txHash, tt.logEvent, logger) if tt.expectErr != "" { require.ErrorContains(t, err, tt.expectErr) @@ -1099,6 +1141,7 @@ func TestExtractTxnSenderAndFrom(t *testing.T) { require.NoError(t, err) require.Equal(t, tt.expectedTxnSender, txnSender) require.Equal(t, tt.expectedFrom, from) + require.Equal(t, tt.expectedTo, to) } }) } diff --git a/bridgesync/e2e_test.go b/bridgesync/e2e_test.go index 688ed4c65..7746d90dd 100644 --- a/bridgesync/e2e_test.go +++ b/bridgesync/e2e_test.go @@ -78,6 +78,12 @@ func TestBridgeEventE2E(t *testing.T) { bridge.BlockTimestamp = block.Time() require.NoError(t, err) require.Equal(t, receipt.Status, types.ReceiptStatusSuccessful) + // Extract ToAddress from the transaction + txDetails, _, err := simulatedClient.TransactionByHash(ctx, tx.Hash()) + require.NoError(t, err) + if txDetails.To() != nil { + bridge.ToAddress = *txDetails.To() + } expectedBridges = append(expectedBridges, bridge) expectedRoot, err := l1Setup.BridgeContract.GetRoot(nil) require.NoError(t, err) diff --git a/bridgesync/migrations/bridgesync0012.sql b/bridgesync/migrations/bridgesync0012.sql new file mode 100644 index 000000000..aab4673de --- /dev/null +++ b/bridgesync/migrations/bridgesync0012.sql @@ -0,0 +1,6 @@ +-- +migrate Down +ALTER TABLE bridge DROP COLUMN to_address; + +-- +migrate Up +ALTER TABLE bridge ADD COLUMN to_address VARCHAR; + diff --git a/bridgesync/migrations/migrations.go b/bridgesync/migrations/migrations.go index f4ebe8d02..cedf6c412 100644 --- a/bridgesync/migrations/migrations.go +++ b/bridgesync/migrations/migrations.go @@ -41,6 +41,9 @@ var mig0010 string //go:embed bridgesync0011.sql var mig0011 string +//go:embed bridgesync0012.sql +var mig0012 string + func RunMigrations(dbPath string) error { migrations := []types.Migration{ { @@ -87,6 +90,10 @@ func RunMigrations(dbPath string) error { ID: "bridgesync0011", SQL: mig0011, }, + { + ID: "bridgesync0012", + SQL: mig0012, + }, } migrations = append(migrations, treeMigrations.Migrations...) return db.RunMigrations(dbPath, migrations) diff --git a/bridgesync/processor.go b/bridgesync/processor.go index fc7c7f61b..c934dcfcf 100644 --- a/bridgesync/processor.go +++ b/bridgesync/processor.go @@ -135,6 +135,7 @@ type Bridge struct { Metadata []byte `meddler:"metadata"` DepositCount uint32 `meddler:"deposit_count"` TxnSender common.Address `meddler:"txn_sender,address"` + ToAddress common.Address `meddler:"to_address,address"` } func (b *Bridge) String() string { @@ -145,11 +146,11 @@ func (b *Bridge) String() string { return fmt.Sprintf("Bridge{BlockNum: %d, BlockPos: %d, FromAddress: %s, TxHash: %s, "+ "BlockTimestamp: %d, LeafType: %d, OriginNetwork: %d, OriginAddress: %s, "+ "DestinationNetwork: %d, DestinationAddress: %s, Amount: %s, Metadata: %x, "+ - "DepositCount: %d, TxnSender: %s}", + "DepositCount: %d, TxnSender: %s, ToAddress: %s}", b.BlockNum, b.BlockPos, b.FromAddress.String(), b.TxHash.String(), b.BlockTimestamp, b.LeafType, b.OriginNetwork, b.OriginAddress.String(), b.DestinationNetwork, b.DestinationAddress.String(), amountStr, b.Metadata, - b.DepositCount, b.TxnSender.String()) + b.DepositCount, b.TxnSender.String(), b.ToAddress.String()) } // Hash returns the hash of the bridge event as expected by the exit tree diff --git a/docs/assets/swagger/bridge_service/swagger.json b/docs/assets/swagger/bridge_service/swagger.json index deab5e467..3fb902749 100644 --- a/docs/assets/swagger/bridge_service/swagger.json +++ b/docs/assets/swagger/bridge_service/swagger.json @@ -750,6 +750,11 @@ "type": "integer", "example": 10 }, + "to_address": { + "description": "Address of the contract that was the recipient of the transaction. This may differ from the bridge contract address.", + "type": "string", + "example": "0xF9D64d54D32EE2BDceAAbFA60C4C438E224427d0" + }, "tx_hash": { "description": "Hash of the transaction that included the bridge event", "type": "string",