Skip to content

Commit

Permalink
check for double deposits on mini launchpad
Browse files Browse the repository at this point in the history
  • Loading branch information
pk910 committed Nov 4, 2024
1 parent d1f5966 commit 8c8cc42
Show file tree
Hide file tree
Showing 12 changed files with 379 additions and 31 deletions.
2 changes: 1 addition & 1 deletion cmd/dora-explorer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func startFrontend(webserver *http.Server) {
router.HandleFunc("/validators", handlers.Validators).Methods("GET")
router.HandleFunc("/validators/activity", handlers.ValidatorsActivity).Methods("GET")
router.HandleFunc("/validators/deposits", handlers.Deposits).Methods("GET")
router.HandleFunc("/validators/deposits/submit", handlers.SubmitDeposit).Methods("GET")
router.HandleFunc("/validators/deposits/submit", handlers.SubmitDeposit).Methods("GET", "POST")
router.HandleFunc("/validators/initiated_deposits", handlers.InitiatedDeposits).Methods("GET")
router.HandleFunc("/validators/included_deposits", handlers.IncludedDeposits).Methods("GET")
router.HandleFunc("/validators/voluntary_exits", handlers.VoluntaryExits).Methods("GET")
Expand Down
12 changes: 12 additions & 0 deletions db/deposits.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,18 @@ func GetDepositTxsFiltered(offset uint64, limit uint32, finalizedBlock uint64, f
fmt.Fprintf(&sql, " %v publickey = $%v", filterOp, len(args))
filterOp = "AND"
}
if len(filter.PublicKeys) > 0 {
fmt.Fprintf(&sql, " %v publickey IN (", filterOp)
for i, pubKey := range filter.PublicKeys {
if i > 0 {
fmt.Fprintf(&sql, ", ")
}
args = append(args, pubKey)
fmt.Fprintf(&sql, "$%v", len(args))
}
fmt.Fprintf(&sql, ")")
filterOp = "AND"
}
if filter.MinAmount > 0 {
args = append(args, filter.MinAmount*utils.GWEI.Uint64())
fmt.Fprintf(&sql, " %v amount >= $%v", filterOp, len(args))
Expand Down
1 change: 1 addition & 0 deletions dbtypes/other.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type DepositTxFilter struct {
Address []byte
TargetAddress []byte
PublicKey []byte
PublicKeys [][]byte
ValidatorName string
MinAmount uint64
MaxAmount uint64
Expand Down
89 changes: 89 additions & 0 deletions handlers/submit_deposit.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package handlers

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/sirupsen/logrus"

"github.com/ethpandaops/dora/db"
"github.com/ethpandaops/dora/dbtypes"
"github.com/ethpandaops/dora/services"
"github.com/ethpandaops/dora/templates"
"github.com/ethpandaops/dora/types/models"
Expand All @@ -25,6 +30,20 @@ func SubmitDeposit(w http.ResponseWriter, r *http.Request) {
return
}

query := r.URL.Query()
if query.Has("ajax") {
err := handleSubmitDepositPageDataAjax(w, r)
if err != nil {
handlePageError(w, r, err)
}
return
}

if r.Method != http.MethodGet {
handlePageError(w, r, errors.New("invalid method"))
return
}

pageData, pageError := getSubmitDepositPageData()
if pageError != nil {
handlePageError(w, r, pageError)
Expand Down Expand Up @@ -82,3 +101,73 @@ func buildSubmitDepositPageData() (*models.SubmitDepositPageData, time.Duration)

return pageData, 1 * time.Hour
}

func handleSubmitDepositPageDataAjax(w http.ResponseWriter, r *http.Request) error {
query := r.URL.Query()
var pageData interface{}

switch query.Get("ajax") {
case "load_deposits":
if r.Method != http.MethodPost {
return fmt.Errorf("invalid method")
}

var hexPubkeys []string
err := json.NewDecoder(r.Body).Decode(&hexPubkeys)
if err != nil {
return fmt.Errorf("failed to decode request body: %v", err)
}

pubkeys := make([][]byte, 0, len(hexPubkeys))
for i, hexPubkey := range hexPubkeys {
pubkey := common.FromHex(hexPubkey)
if len(pubkey) != 48 {
return fmt.Errorf("invalid pubkey length (%d) for pubkey %v", len(pubkey), i)
}

pubkeys = append(pubkeys, pubkey)
}

depositSyncState := dbtypes.DepositIndexerState{}
db.GetExplorerState("indexer.depositstate", &depositSyncState)

deposits, depositCount, err := db.GetDepositTxsFiltered(0, 1000, depositSyncState.FinalBlock, &dbtypes.DepositTxFilter{
PublicKeys: pubkeys,
WithOrphaned: 0,
})
if err != nil {
return fmt.Errorf("failed to get deposits: %v", err)
}

result := models.SubmitDepositPageDataDeposits{
Deposits: make([]models.SubmitDepositPageDataDeposit, 0, len(deposits)),
Count: depositCount,
HaveMore: depositCount > 1000,
}

for _, deposit := range deposits {
result.Deposits = append(result.Deposits, models.SubmitDepositPageDataDeposit{
Pubkey: fmt.Sprintf("0x%x", deposit.PublicKey),
Amount: deposit.Amount,
BlockNumber: deposit.BlockNumber,
BlockHash: fmt.Sprintf("0x%x", deposit.BlockRoot),
BlockTime: deposit.BlockTime,
TxOrigin: common.BytesToAddress(deposit.TxSender).String(),
TxTarget: common.BytesToAddress(deposit.TxTarget).String(),
TxHash: fmt.Sprintf("0x%x", deposit.TxHash),
})
}

pageData = result
default:
return errors.New("invalid ajax request")
}

w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(pageData)
if err != nil {
logrus.WithError(err).Error("error encoding index data")
http.Error(w, "Internal server error", http.StatusServiceUnavailable)
}
return nil
}
16 changes: 16 additions & 0 deletions templates/submit_deposit/submit_deposit.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,22 @@ <h1 class="h4 mb-1 mb-md-0">
submitDepositConfig: {
genesisForkVersion: "0x{{ printf "%x" .GenesisForkVersion }}",
depositContract: "0x{{ printf "%x" .DepositContract }}",
loadDepositTxs: function(pubkeys) {
return fetch("?ajax=load_deposits", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(pubkeys)
}).then((response) => {
if (!response.ok) {
throw new Error("Failed to load deposits: " + response.statusText);
}
return response;
}).then((response) => {
return response.json();
});
}
}
}
);
Expand Down
17 changes: 17 additions & 0 deletions types/models/submit_deposit.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,20 @@ type SubmitDepositPageData struct {
GenesisForkVersion []byte `json:"genesisforkversion"`
DepositContract []byte `json:"depositcontract"`
}

type SubmitDepositPageDataDeposits struct {
Deposits []SubmitDepositPageDataDeposit `json:"deposits"`
Count uint64 `json:"count"`
HaveMore bool `json:"havemore"`
}

type SubmitDepositPageDataDeposit struct {
Pubkey string `json:"pubkey"`
Amount uint64 `json:"amount"`
BlockNumber uint64 `json:"block"`
BlockHash string `json:"block_hash"`
BlockTime uint64 `json:"block_time"`
TxOrigin string `json:"tx_origin"`
TxTarget string `json:"tx_target"`
TxHash string `json:"tx_hash"`
}
115 changes: 113 additions & 2 deletions ui-package/src/components/SubmitDepositsForm/DepositEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ import { useState } from 'react';
import { Modal } from 'react-bootstrap';

import { IDeposit } from './DepositsTable';
import { toReadableAmount } from '../../utils/ReadableAmount';

interface IDepositEntryProps {
deposit: IDeposit;
depositContract: string;
explorerUrl?: string;
}

const DepositContractAbi = [{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes","name":"pubkey","type":"bytes"},{"indexed":false,"internalType":"bytes","name":"withdrawal_credentials","type":"bytes"},{"indexed":false,"internalType":"bytes","name":"amount","type":"bytes"},{"indexed":false,"internalType":"bytes","name":"signature","type":"bytes"},{"indexed":false,"internalType":"bytes","name":"index","type":"bytes"}],"name":"DepositEvent","type":"event"},{"inputs":[{"internalType":"bytes","name":"pubkey","type":"bytes"},{"internalType":"bytes","name":"withdrawal_credentials","type":"bytes"},{"internalType":"bytes","name":"signature","type":"bytes"},{"internalType":"bytes32","name":"deposit_data_root","type":"bytes32"}],"name":"deposit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"get_deposit_count","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"get_deposit_root","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"pure","type":"function"}];

const DepositEntry = (props: IDepositEntryProps): React.ReactElement => {
const { address: walletAddress, chain } = useAccount();
const { address: walletAddress, chain, isConnected } = useAccount();
const [errorModal, setErrorModal] = useState<string | null>(null);
const [showTxDetails, setShowTxDetails] = useState<boolean>(false);

const depositRequest = useWriteContract();
window.setTimeout(() => {
Expand Down Expand Up @@ -50,9 +53,14 @@ const DepositEntry = (props: IDepositEntryProps): React.ReactElement => {
<span className="text-danger"></span>
}
</span>
{props.deposit.depositTxs.length > 0 ?
<a className="text-warning ms-2" href="#" data-bs-toggle="tooltip" data-bs-placement="top" title="This pubkey has already been submitted to the deposit contract. Click to see more." onClick={() => setShowTxDetails(true)}>
<i className="fa fa-exclamation-triangle"></i>
</a>
: null}
</td>
<td className="p-0">
<button className="btn btn-primary" disabled={!props.deposit.validity || depositRequest.isPending || depositRequest.isSuccess} onClick={() => submitDeposit()}>
<button className="btn btn-primary" disabled={!isConnected || !props.deposit.validity || depositRequest.isPending || depositRequest.isSuccess} onClick={() => submitDeposit()}>
{depositRequest.isSuccess ?
<span>Submitted</span> :
depositRequest.isPending ? (
Expand All @@ -79,6 +87,109 @@ const DepositEntry = (props: IDepositEntryProps): React.ReactElement => {
</Modal.Footer>
</Modal>
)}
{showTxDetails && (
<Modal show={true} onHide={() => setShowTxDetails(false)} size="lg" className="deposit-txs-modal">
<Modal.Header closeButton>
<Modal.Title>Initiated Deposits</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>Some deposits have already been submitted for this validator:</p>
{props.deposit.depositTxs.map((tx, index) => (
<div key={index + "-" + tx.tx_hash} className="mt-2">
{index > 0 && <hr />}
<div className="d-flex">
<div className="tx-details-label">
Tx Hash:
</div>
<div className="tx-details-value">
<div className="d-flex">
{props.explorerUrl ?
<a className="flex-grow-1 text-truncate" href={props.explorerUrl + "tx/" + tx.tx_hash} target="_blank" rel="noreferrer">{tx.tx_hash}</a>
: <span className="flex-grow-1 text-truncate">{tx.tx_hash}</span>
}
<div className="ms-2">
<i className="fa fa-copy text-muted p-1" role="button" data-bs-toggle="tooltip" data-clipboard-text={tx.tx_hash} title="Copy to clipboard"></i>
</div>
</div>
</div>
</div>
<div className="d-flex">
<div className="tx-details-label">
Block:
</div>
<div className="tx-details-value">
<div className="d-flex">
<span className="flex-grow-1 text-truncate">{tx.block}</span>
<div className="ms-2">
<i className="fa fa-copy text-muted p-1" role="button" data-bs-toggle="tooltip" data-clipboard-text={tx.block} title="Copy to clipboard"></i>
</div>
</div>
</div>
</div>
<div className="d-flex">
<div className="tx-details-label">
Block Time:
</div>
<div className="tx-details-value">
<div className="d-flex">
<span className="flex-grow-1 text-truncate">{(window as any).explorer.renderRecentTime(tx.block_time)}</span>
<div className="ms-2">
<i className="fa fa-copy text-muted p-1" role="button" data-bs-toggle="tooltip" data-clipboard-text={new Date(tx.block_time * 1000).toISOString()} title="Copy to clipboard"></i>
</div>
</div>
</div>
</div>
<div className="d-flex">
<div className="tx-details-label">
TX Origin:
</div>
<div className="tx-details-value">
<div className="d-flex">
{props.explorerUrl ?
<a className="flex-grow-1 text-truncate" href={props.explorerUrl + "address/" + tx.tx_origin} target="_blank" rel="noreferrer">{tx.tx_origin}</a>
: <span className="flex-grow-1 text-truncate">{tx.tx_origin}</span>
}
<div className="ms-2">
<i className="fa fa-copy text-muted p-1" role="button" data-bs-toggle="tooltip" data-clipboard-text={tx.tx_origin} title="Copy to clipboard"></i>
</div>
</div>
</div>
</div>
<div className="d-flex">
<div className="tx-details-label">
TX Target:
</div>
<div className="tx-details-value">
<div className="d-flex">
{props.explorerUrl ?
<a className="flex-grow-1 text-truncate" href={props.explorerUrl + "address/" + tx.tx_target} target="_blank" rel="noreferrer">{tx.tx_target}</a>
: <span className="flex-grow-1 text-truncate">{tx.tx_target}</span>
}
<div className="ms-2">
<i className="fa fa-copy text-muted p-1" role="button" data-bs-toggle="tooltip" data-clipboard-text={tx.tx_target} title="Copy to clipboard"></i>
</div>
</div>
</div>
</div>
<div className="d-flex">
<div className="tx-details-label">
Amount:
</div>
<div className="tx-details-value">
<div className="d-flex">
<span className="flex-grow-1 text-truncate">{toReadableAmount(tx.amount, 9, "ETH", 0)}</span>
</div>
</div>
</div>
</div>
))}
</Modal.Body>
<Modal.Footer>
<button className="btn btn-primary" onClick={() => setErrorModal(null)}>Close</button>
</Modal.Footer>
</Modal>
)}

</td>
</tr>
);
Expand Down
Loading

0 comments on commit 8c8cc42

Please sign in to comment.