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

Add indices endpoint #90

Merged
merged 4 commits into from
Sep 6, 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
7 changes: 7 additions & 0 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ func (c *Client) Transactions(ids []types.TransactionID) (resp []explorer.Transa
return
}

// TransactionChainIndices returns chain indices a transaction was
// included in.
func (c *Client) TransactionChainIndices(id types.TransactionID, offset, limit uint64) (resp []types.ChainIndex, err error) {
err = c.c.GET(fmt.Sprintf("/transactions/%s/indices?offset=%d&limit=%d", id, offset, limit), &resp)
return
}

// AddressSiacoinUTXOs returns the specified address' unspent outputs.
func (c *Client) AddressSiacoinUTXOs(address types.Address, offset, limit uint64) (resp []explorer.SiacoinOutput, err error) {
err = c.c.GET(fmt.Sprintf("/addresses/%s/utxos/siacoin?offset=%d&limit=%d", address, offset, limit), &resp)
Expand Down
29 changes: 27 additions & 2 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type (
Metrics(id types.BlockID) (explorer.Metrics, error)
HostMetrics() (explorer.HostMetrics, error)
Transactions(ids []types.TransactionID) ([]explorer.Transaction, error)
TransactionChainIndices(id types.TransactionID, offset, limit uint64) ([]types.ChainIndex, error)
Balance(address types.Address) (sc types.Currency, immatureSC types.Currency, sf uint64, err error)
SiacoinElements(ids []types.SiacoinOutputID) (result []explorer.SiacoinOutput, err error)
SiafundElements(ids []types.SiafundOutputID) (result []explorer.SiafundOutput, err error)
Expand Down Expand Up @@ -262,6 +263,29 @@ func (s *server) transactionsIDHandler(jc jape.Context) {
jc.Encode(txns[0])
}

func (s *server) transactionsIDIndicesHandler(jc jape.Context) {
var id types.TransactionID
if jc.DecodeParam("id", &id) != nil {
return
}

limit := uint64(100)
offset := uint64(0)
if jc.DecodeForm("limit", &limit) != nil || jc.DecodeForm("offset", &offset) != nil {
return
}

if limit > 500 {
limit = 500
}

indices, err := s.e.TransactionChainIndices(id, offset, limit)
if jc.Check("failed to get transaction indices", err) != nil {
return
}
jc.Encode(indices)
}

func (s *server) transactionsBatchHandler(jc jape.Context) {
var ids []types.TransactionID
if jc.Decode(&ids) != nil {
Expand Down Expand Up @@ -507,8 +531,9 @@ func NewServer(e Explorer, cm ChainManager, s Syncer) http.Handler {

"GET /blocks/:id": srv.blocksIDHandler,

"GET /transactions/:id": srv.transactionsIDHandler,
"POST /transactions": srv.transactionsBatchHandler,
"GET /transactions/:id": srv.transactionsIDHandler,
"POST /transactions": srv.transactionsBatchHandler,
"GET /transactions/:id/indices": srv.transactionsIDIndicesHandler,

"GET /addresses/:address/utxos/siacoin": srv.addressessAddressUtxosSiacoinHandler,
"GET /addresses/:address/utxos/siafund": srv.addressessAddressUtxosSiafundHandler,
Expand Down
8 changes: 8 additions & 0 deletions explorer/explorer.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type Store interface {
Metrics(id types.BlockID) (Metrics, error)
HostMetrics() (HostMetrics, error)
Transactions(ids []types.TransactionID) ([]Transaction, error)
TransactionChainIndices(txid types.TransactionID, offset, limit uint64) ([]types.ChainIndex, error)
UnspentSiacoinOutputs(address types.Address, offset, limit uint64) ([]SiacoinOutput, error)
UnspentSiafundOutputs(address types.Address, offset, limit uint64) ([]SiafundOutput, error)
AddressEvents(address types.Address, offset, limit uint64) (events []Event, err error)
Expand Down Expand Up @@ -196,6 +197,13 @@ func (e *Explorer) Transactions(ids []types.TransactionID) ([]Transaction, error
return e.s.Transactions(ids)
}

// TransactionChainIndices returns the chain indices of the blocks the transaction
// was included in. If the transaction has not been included in any blocks, the
// result will be nil,nil.
func (e *Explorer) TransactionChainIndices(id types.TransactionID, offset, limit uint64) ([]types.ChainIndex, error) {
return e.s.TransactionChainIndices(id, offset, limit)
}

// UnspentSiacoinOutputs returns the unspent siacoin outputs owned by the
// specified address.
func (e *Explorer) UnspentSiacoinOutputs(address types.Address, offset, limit uint64) ([]SiacoinOutput, error) {
Expand Down
1 change: 0 additions & 1 deletion explorer/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ type FileContractRevision struct {
// A Transaction is a transaction that uses the wrapped types above.
type Transaction struct {
ID types.TransactionID `json:"id"`
ChainIndices []types.ChainIndex `json:"chainIndices,omitempty"`
SiacoinInputs []SiacoinInput `json:"siacoinInputs,omitempty"`
SiacoinOutputs []SiacoinOutput `json:"siacoinOutputs,omitempty"`
SiafundInputs []SiafundInput `json:"siafundInputs,omitempty"`
Expand Down
30 changes: 28 additions & 2 deletions persist/sqlite/consensus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,19 @@ func TestSendTransactions(t *testing.T) {
check(t, "siafunds", expectSF, sf)
}

checkChainIndices := func(t *testing.T, txnID types.TransactionID, expected []types.ChainIndex) {
indices, err := db.TransactionChainIndices(txnID, 0, 100)
switch {
case err != nil:
t.Fatal(err)
case len(indices) != len(expected):
t.Fatalf("expected %d indices, got %d", len(expected), len(indices))
}
for i := range indices {
check(t, "index", expected[i], indices[i])
}
}

checkTransaction := func(expectTxn types.Transaction, gotTxn explorer.Transaction) {
check(t, "siacoin inputs", len(expectTxn.SiacoinInputs), len(gotTxn.SiacoinInputs))
check(t, "siacoin outputs", len(expectTxn.SiacoinOutputs), len(gotTxn.SiacoinOutputs))
Expand Down Expand Up @@ -578,7 +591,7 @@ func TestSendTransactions(t *testing.T) {
// with the actual transactions
for i := range b.Transactions {
checkTransaction(b.Transactions[i], block.Transactions[i])
check(t, "chain indices", []types.ChainIndex{cm.Tip()}, block.Transactions[i].ChainIndices)
checkChainIndices(t, b.Transactions[i].ID(), []types.ChainIndex{cm.Tip()})

txns, err := db.Transactions([]types.TransactionID{b.Transactions[i].ID()})
if err != nil {
Expand Down Expand Up @@ -1625,6 +1638,19 @@ func TestRevertSendTransactions(t *testing.T) {
check(t, "siafunds", expectSF, sf)
}

checkChainIndices := func(t *testing.T, txnID types.TransactionID, expected []types.ChainIndex) {
indices, err := db.TransactionChainIndices(txnID, 0, 100)
switch {
case err != nil:
t.Fatal(err)
case len(indices) != len(expected):
t.Fatalf("expected %d indices, got %d", len(expected), len(indices))
}
for i := range indices {
check(t, "index", expected[i], indices[i])
}
}

checkTransaction := func(expectTxn types.Transaction, gotTxn explorer.Transaction) {
check(t, "siacoin inputs", len(expectTxn.SiacoinInputs), len(gotTxn.SiacoinInputs))
check(t, "siacoin outputs", len(expectTxn.SiacoinOutputs), len(gotTxn.SiacoinOutputs))
Expand Down Expand Up @@ -1782,7 +1808,7 @@ func TestRevertSendTransactions(t *testing.T) {
// with the actual transactions
for i := range b.Transactions {
checkTransaction(b.Transactions[i], block.Transactions[i])
check(t, "chain indices", []types.ChainIndex{cm.Tip()}, block.Transactions[i].ChainIndices)
checkChainIndices(t, b.Transactions[i].ID(), []types.ChainIndex{cm.Tip()})

txns, err := db.Transactions([]types.TransactionID{b.Transactions[i].ID()})
if err != nil {
Expand Down
54 changes: 25 additions & 29 deletions persist/sqlite/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,32 @@ import (
"go.sia.tech/explored/explorer"
)

// transactionChainIndices returns the chain indices of the blocks the transaction
// was in.
func transactionChainIndices(tx *txn, txnIDs []int64) (map[int64][]types.ChainIndex, error) {
query := `SELECT bt.transaction_id, bt.block_id, b.height
FROM block_transactions bt
JOIN blocks b ON bt.block_id = b.id
WHERE bt.transaction_id IN (` + queryPlaceHolders(len(txnIDs)) + `)
ORDER BY bt.block_order ASC`
rows, err := tx.Query(query, queryArgs(txnIDs)...)
if err != nil {
return nil, err
}
defer rows.Close()

result := make(map[int64][]types.ChainIndex)
for rows.Next() {
var txnID int64
var index types.ChainIndex
if err := rows.Scan(&txnID, decode(&index.ID), decode(&index.Height)); err != nil {
return nil, fmt.Errorf("failed to scan chain index: %w", err)
// TransactionChainIndices returns the chain indices of the blocks the transaction
// was included in. If the transaction has not been included in any blocks, the
// result will be nil,nil.
func (s *Store) TransactionChainIndices(txnID types.TransactionID, offset, limit uint64) (indices []types.ChainIndex, err error) {
err = s.transaction(func(tx *txn) error {
rows, err := tx.Query(`SELECT DISTINCT b.id, b.height FROM blocks b
INNER JOIN block_transactions bt ON (bt.block_id = b.id)
INNER JOIN transactions t ON (t.id = bt.transaction_id)
WHERE t.transaction_id = ?
ORDER BY b.height DESC
LIMIT ? OFFSET ?`, encode(txnID), limit, offset)
if err != nil {
return err
}
result[txnID] = append(result[txnID], index)
}
return result, nil
defer rows.Close()

for rows.Next() {
var index types.ChainIndex
if err := rows.Scan(decode(&index.ID), decode(&index.Height)); err != nil {
return fmt.Errorf("failed to scan chain index: %w", err)
}
indices = append(indices, index)
}
return rows.Err()
})
return
}

// transactionMinerFee returns the miner fees for each transaction.
Expand Down Expand Up @@ -484,11 +486,6 @@ func getTransactions(tx *txn, idMap map[int64]transactionID) ([]explorer.Transac
dbIDs[id.order] = dbID
}

txnChainIndices, err := transactionChainIndices(tx, dbIDs)
if err != nil {
return nil, fmt.Errorf("getTransactions: failed to get chain indices: %w", err)
}

txnArbitraryData, err := transactionArbitraryData(tx, dbIDs)
if err != nil {
return nil, fmt.Errorf("getTransactions: failed to get arbitrary data: %w", err)
Expand Down Expand Up @@ -543,7 +540,6 @@ func getTransactions(tx *txn, idMap map[int64]transactionID) ([]explorer.Transac
for _, dbID := range dbIDs {
txn := explorer.Transaction{
ID: idMap[dbID].id,
ChainIndices: txnChainIndices[dbID],
SiacoinInputs: txnSiacoinInputs[dbID],
SiacoinOutputs: txnSiacoinOutputs[dbID],
SiafundInputs: txnSiafundInputs[dbID],
Expand Down
Loading