feat(runes): add Get Transaction by hash api (#39)

* feat: implement pagination on get balance, get holders

* feat: paginate get transactions

* fix: remove debug

* feat: implement pagination in get utxos

* feat: sort response in get holders

* feat: cap batch query

* feat: add default limits to all endpoints

* chore: rename endpoint funcs

* fix: parse rune name spacers

* feat(runes): get tx by hash api

* fix: error

* refactor: use map to collect rune ids

---------

Co-authored-by: Gaze <gazenw@users.noreply.github.com>
This commit is contained in:
gazenw
2024-10-06 23:50:13 +07:00
committed by GitHub
parent 32c3c5c1d4
commit f1d4651042
5 changed files with 192 additions and 9 deletions

View File

@@ -0,0 +1,171 @@
package httphandler
import (
"encoding/hex"
"fmt"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/modules/runes/runes"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
)
type getTransactionByHashRequest struct {
Hash string `params:"hash"`
}
func (r getTransactionByHashRequest) Validate() error {
var errList []error
if len(r.Hash) == 0 {
errList = append(errList, errs.NewPublicError("hash is required"))
}
if len(r.Hash) > chainhash.MaxHashStringSize {
errList = append(errList, errs.NewPublicError(fmt.Sprintf("hash length must be less than or equal to %d bytes", chainhash.MaxHashStringSize)))
}
if len(errList) == 0 {
return nil
}
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
}
type getTransactionByHashResponse = HttpResponse[transaction]
func (h *HttpHandler) GetTransactionByHash(ctx *fiber.Ctx) (err error) {
var req getTransactionByHashRequest
if err := ctx.ParamsParser(&req); err != nil {
return errors.WithStack(err)
}
if err := req.Validate(); err != nil {
return errors.WithStack(err)
}
hash, err := chainhash.NewHashFromStr(req.Hash)
if err != nil {
return errs.NewPublicError("invalid transaction hash")
}
tx, err := h.usecase.GetRuneTransaction(ctx.UserContext(), *hash)
if err != nil {
if errors.Is(err, errs.NotFound) {
return fiber.NewError(fiber.StatusNotFound, "transaction not found")
}
return errors.Wrap(err, "error during GetRuneTransaction")
}
allRuneIds := make(map[runes.RuneId]struct{})
for id := range tx.Mints {
allRuneIds[id] = struct{}{}
}
for id := range tx.Burns {
allRuneIds[id] = struct{}{}
}
for _, input := range tx.Inputs {
allRuneIds[input.RuneId] = struct{}{}
}
for _, output := range tx.Outputs {
allRuneIds[output.RuneId] = struct{}{}
}
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), lo.Keys(allRuneIds))
if err != nil {
return errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch")
}
respTx := &transaction{
TxHash: tx.Hash,
BlockHeight: tx.BlockHeight,
Index: tx.Index,
Timestamp: tx.Timestamp.Unix(),
Inputs: make([]txInputOutput, 0, len(tx.Inputs)),
Outputs: make([]txInputOutput, 0, len(tx.Outputs)),
Mints: make(map[string]amountWithDecimal, len(tx.Mints)),
Burns: make(map[string]amountWithDecimal, len(tx.Burns)),
Extend: runeTransactionExtend{
RuneEtched: tx.RuneEtched,
Runestone: nil,
},
}
for _, input := range tx.Inputs {
address := addressFromPkScript(input.PkScript, h.network)
respTx.Inputs = append(respTx.Inputs, txInputOutput{
PkScript: hex.EncodeToString(input.PkScript),
Address: address,
Id: input.RuneId,
Amount: input.Amount,
Decimals: runeEntries[input.RuneId].Divisibility,
Index: input.Index,
})
}
for _, output := range tx.Outputs {
address := addressFromPkScript(output.PkScript, h.network)
respTx.Outputs = append(respTx.Outputs, txInputOutput{
PkScript: hex.EncodeToString(output.PkScript),
Address: address,
Id: output.RuneId,
Amount: output.Amount,
Decimals: runeEntries[output.RuneId].Divisibility,
Index: output.Index,
})
}
for id, amount := range tx.Mints {
respTx.Mints[id.String()] = amountWithDecimal{
Amount: amount,
Decimals: runeEntries[id].Divisibility,
}
}
for id, amount := range tx.Burns {
respTx.Burns[id.String()] = amountWithDecimal{
Amount: amount,
Decimals: runeEntries[id].Divisibility,
}
}
if tx.Runestone != nil {
var e *etching
if tx.Runestone.Etching != nil {
var symbol *string
if tx.Runestone.Etching.Symbol != nil {
symbol = lo.ToPtr(string(*tx.Runestone.Etching.Symbol))
}
var t *terms
if tx.Runestone.Etching.Terms != nil {
t = &terms{
Amount: tx.Runestone.Etching.Terms.Amount,
Cap: tx.Runestone.Etching.Terms.Cap,
HeightStart: tx.Runestone.Etching.Terms.HeightStart,
HeightEnd: tx.Runestone.Etching.Terms.HeightEnd,
OffsetStart: tx.Runestone.Etching.Terms.OffsetStart,
OffsetEnd: tx.Runestone.Etching.Terms.OffsetEnd,
}
}
e = &etching{
Divisibility: tx.Runestone.Etching.Divisibility,
Premine: tx.Runestone.Etching.Premine,
Rune: tx.Runestone.Etching.Rune,
Spacers: tx.Runestone.Etching.Spacers,
Symbol: symbol,
Terms: t,
Turbo: tx.Runestone.Etching.Turbo,
}
}
respTx.Extend.Runestone = &runestone{
Cenotaph: tx.Runestone.Cenotaph,
Flaws: lo.Ternary(tx.Runestone.Cenotaph, tx.Runestone.Flaws.CollectAsString(), nil),
Etching: e,
Edicts: lo.Map(tx.Runestone.Edicts, func(ed runes.Edict, _ int) edict {
return edict{
Id: ed.Id,
Amount: ed.Amount,
Output: ed.Output,
}
}),
Mint: tx.Runestone.Mint,
Pointer: tx.Runestone.Pointer,
}
}
return errors.WithStack(ctx.JSON(getTransactionByHashResponse{
Result: respTx,
}))
}

View File

@@ -191,23 +191,22 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
return errors.Wrap(err, "error during GetRuneTransactions")
}
var allRuneIds []runes.RuneId
allRuneIds := make(map[runes.RuneId]struct{})
for _, tx := range txs {
for id := range tx.Mints {
allRuneIds = append(allRuneIds, id)
allRuneIds[id] = struct{}{}
}
for id := range tx.Burns {
allRuneIds = append(allRuneIds, id)
allRuneIds[id] = struct{}{}
}
for _, input := range tx.Inputs {
allRuneIds = append(allRuneIds, input.RuneId)
allRuneIds[input.RuneId] = struct{}{}
}
for _, output := range tx.Outputs {
allRuneIds = append(allRuneIds, output.RuneId)
allRuneIds[output.RuneId] = struct{}{}
}
}
allRuneIds = lo.Uniq(allRuneIds)
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), allRuneIds)
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), lo.Keys(allRuneIds))
if err != nil {
if errors.Is(err, errs.NotFound) {
return errs.NewPublicError("rune entries not found")

View File

@@ -10,6 +10,7 @@ func (h *HttpHandler) Mount(router fiber.Router) error {
r.Post("/balances/wallet/batch", h.GetBalancesBatch)
r.Get("/balances/wallet/:wallet", h.GetBalances)
r.Get("/transactions", h.GetTransactions)
r.Get("/transactions/hash/:hash", h.GetTransactionByHash)
r.Get("/holders/:id", h.GetHolders)
r.Get("/info/:id", h.GetTokenInfo)
r.Get("/utxos/wallet/:wallet", h.GetUTXOs)

View File

@@ -122,8 +122,11 @@ func (r *Repository) GetRuneTransactions(ctx context.Context, pkScript []byte, r
func (r *Repository) GetRuneTransaction(ctx context.Context, txHash chainhash.Hash) (*entity.RuneTransaction, error) {
row, err := r.queries.GetRuneTransaction(ctx, txHash.String())
if errors.Is(err, pgx.ErrNoRows) {
return nil, errors.WithStack(errs.NotFound)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, errors.WithStack(errs.NotFound)
}
return nil, errors.Wrap(err, "error during query")
}
runeTxModel, runestoneModel, err := extractModelRuneTxAndRunestone(gen.GetRuneTransactionsRow(row))

View File

@@ -3,6 +3,7 @@ package usecase
import (
"context"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
"github.com/gaze-network/indexer-network/modules/runes/runes"
@@ -16,3 +17,11 @@ func (u *Usecase) GetRuneTransactions(ctx context.Context, pkScript []byte, rune
}
return txs, nil
}
func (u *Usecase) GetRuneTransaction(ctx context.Context, hash chainhash.Hash) (*entity.RuneTransaction, error) {
tx, err := u.runesDg.GetRuneTransaction(ctx, hash)
if err != nil {
return nil, errors.Wrap(err, "error during GetRuneTransaction")
}
return tx, nil
}