Compare commits

..

6 Commits

Author SHA1 Message Date
Gaze
2d51e52b83 feat: support to config 2024-10-08 01:12:49 +07:00
Gaze
618220d0cb feat: support high httpclient conns 2024-10-08 00:44:24 +07:00
gazenw
6004744721 Merge pull request #64 from gaze-network/develop
Release v0.5.1
2024-10-07 21:18:11 +07:00
gazenw
90ed7bc350 fix: update sql (#63) 2024-10-07 21:17:16 +07:00
gazenw
7a0fe84e40 Merge pull request #62 from gaze-network/develop
Release v0.5.0
2024-10-06 23:52:10 +07:00
gazenw
f1d4651042 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>
2024-10-06 23:50:13 +07:00
8 changed files with 210 additions and 14 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

@@ -86,6 +86,8 @@ SELECT * FROM runes_entries
LEFT JOIN states ON runes_entries.rune_id = states.rune_id
WHERE (
runes_entries.terms = TRUE AND
COALESCE(runes_entries.terms_amount, 0) != 0 AND
COALESCE(runes_entries.terms_cap, 0) != 0 AND
states.mints < runes_entries.terms_cap AND
(
runes_entries.terms_height_start IS NULL OR runes_entries.terms_height_start <= @height::integer
@@ -99,9 +101,10 @@ SELECT * FROM runes_entries
) AND (
@search::text = '' OR
runes_entries.rune ILIKE @search::text || '%'
runes_entries.rune ILIKE '%' || @search::text || '%'
)
ORDER BY (states.mints / runes_entries.terms_cap::float) DESC
ORDER BY (COALESCE(runes_entries.premine, 0) + COALESCE(runes_entries.terms_amount, 0) * COALESCE(states.mints, 0)) /
(COALESCE(runes_entries.premine, 0) + COALESCE(runes_entries.terms_amount, 0) * COALESCE(runes_entries.terms_cap, 0))::float DESC
LIMIT @_limit OFFSET @_offset;
-- name: GetRuneIdFromRune :one

View File

@@ -437,6 +437,8 @@ SELECT runes_entries.rune_id, number, rune, spacers, premine, symbol, divisibili
LEFT JOIN states ON runes_entries.rune_id = states.rune_id
WHERE (
runes_entries.terms = TRUE AND
COALESCE(runes_entries.terms_amount, 0) != 0 AND
COALESCE(runes_entries.terms_cap, 0) != 0 AND
states.mints < runes_entries.terms_cap AND
(
runes_entries.terms_height_start IS NULL OR runes_entries.terms_height_start <= $1::integer
@@ -450,9 +452,10 @@ SELECT runes_entries.rune_id, number, rune, spacers, premine, symbol, divisibili
) AND (
$2::text = '' OR
runes_entries.rune ILIKE $2::text || '%'
runes_entries.rune ILIKE '%' || $2::text || '%'
)
ORDER BY (states.mints / runes_entries.terms_cap::float) DESC
ORDER BY (COALESCE(runes_entries.premine, 0) + COALESCE(runes_entries.terms_amount, 0) * COALESCE(states.mints, 0)) /
(COALESCE(runes_entries.premine, 0) + COALESCE(runes_entries.terms_amount, 0) * COALESCE(runes_entries.terms_cap, 0))::float DESC
LIMIT $4 OFFSET $3
`

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
}

View File

@@ -14,6 +14,13 @@ import (
"github.com/valyala/fasthttp"
)
var DefaultClient = fasthttp.Client{
MaxConnsPerHost: 10240, // default is 512
MaxConnWaitTimeout: 5 * time.Second, // default is no wating
ReadBufferSize: 4 * 1024,
WriteBufferSize: 4 * 1024,
}
type Config struct {
// Enable debug mode
Debug bool
@@ -143,7 +150,7 @@ func (h *Client) request(ctx context.Context, reqOptions RequestOptions) (*HttpR
fasthttp.ReleaseRequest(req)
}()
if err := fasthttp.Do(req, resp); err != nil {
if err := DefaultClient.Do(req, resp); err != nil {
return nil, errors.Wrapf(err, "url: %s", url)
}