Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check for double deposits on mini launchpad #167

Merged
merged 2 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
skylenet marked this conversation as resolved.
Show resolved Hide resolved
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>
skylenet marked this conversation as resolved.
Show resolved Hide resolved
<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
Loading