mirror of
https://github.com/alexgo-io/gaze-indexer.git
synced 2026-04-30 04:35:13 +08:00
feat: implement get transactions
This commit is contained in:
@@ -13,6 +13,14 @@ type getBalancesByAddressRequest struct {
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
}
|
||||
|
||||
func (r getBalancesByAddressRequest) Validate() error {
|
||||
var errList []error
|
||||
if r.Wallet == "" {
|
||||
errList = append(errList, errors.New("'wallet' is required"))
|
||||
}
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
type balance struct {
|
||||
Amount string `json:"amount"`
|
||||
Id string `json:"id"`
|
||||
@@ -28,14 +36,6 @@ type getBalancesByAddressResult struct {
|
||||
|
||||
type getBalancesByAddressResponse = HttpResponse[getBalancesByAddressResult]
|
||||
|
||||
func (r getBalancesByAddressRequest) Validate() error {
|
||||
var errList []error
|
||||
if r.Wallet == "" {
|
||||
errList = append(errList, errors.New("'wallet' is required"))
|
||||
}
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
func (h *HttpHandler) GetBalancesByAddress(ctx *fiber.Ctx) (err error) {
|
||||
var req getBalancesByAddressRequest
|
||||
if err := ctx.ParamsParser(&req); err != nil {
|
||||
|
||||
273
modules/runes/api/httphandler/get_transactions.go
Normal file
273
modules/runes/api/httphandler/get_transactions.go
Normal file
@@ -0,0 +1,273 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/runes"
|
||||
"github.com/gaze-network/uint128"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type getTransactionsRequest struct {
|
||||
Wallet string `query:"wallet"`
|
||||
Id string `query:"id"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
}
|
||||
|
||||
type outPointBalance struct {
|
||||
PkScript string `json:"pkScript"`
|
||||
Address string `json:"address"`
|
||||
Id runes.RuneId `json:"id"`
|
||||
Amount uint128.Uint128 `json:"amount"`
|
||||
Index uint32 `json:"index"`
|
||||
}
|
||||
|
||||
type terms struct {
|
||||
Amount *uint128.Uint128 `json:"amount"`
|
||||
Cap *uint128.Uint128 `json:"cap"`
|
||||
HeightStart *uint64 `json:"heightStart"`
|
||||
HeightEnd *uint64 `json:"heightEnd"`
|
||||
OffsetStart *uint64 `json:"offsetStart"`
|
||||
OffsetEnd *uint64 `json:"offsetEnd"`
|
||||
}
|
||||
|
||||
type etching struct {
|
||||
Divisibility *uint8 `json:"divisibility"`
|
||||
Premine *uint128.Uint128 `json:"premine"`
|
||||
Rune *runes.Rune `json:"rune"`
|
||||
Spacers *uint32 `json:"spacers"`
|
||||
Symbol *string `json:"symbol"`
|
||||
Terms *terms `json:"terms"`
|
||||
Turbo bool `json:"turbo"`
|
||||
}
|
||||
|
||||
type edict struct {
|
||||
Id runes.RuneId `json:"id"`
|
||||
Amount uint128.Uint128 `json:"amount"`
|
||||
Output int `json:"output"`
|
||||
}
|
||||
|
||||
type runestone struct {
|
||||
Cenotaph bool `json:"cenotaph"`
|
||||
Flaws []string `json:"flaws"`
|
||||
Etching *etching `json:"etching"`
|
||||
Edicts []edict `json:"edicts"`
|
||||
Mint *runes.RuneId `json:"mint"`
|
||||
Pointer *uint64 `json:"pointer"`
|
||||
}
|
||||
|
||||
type runeTransactionExtend struct {
|
||||
Runestone *runestone `json:"runestone"`
|
||||
}
|
||||
|
||||
type transaction struct {
|
||||
TxHash string `json:"txHash"`
|
||||
BlockHeight uint64 `json:"blockHeight"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Inputs []outPointBalance `json:"inputs"`
|
||||
Outputs []outPointBalance `json:"outputs"`
|
||||
Mints map[string]uint128.Uint128 `json:"mints"`
|
||||
Burns map[string]uint128.Uint128 `json:"burns"`
|
||||
Extend runeTransactionExtend `json:"extend"`
|
||||
}
|
||||
|
||||
type getTransactionsResult struct {
|
||||
List []transaction `json:"list"`
|
||||
}
|
||||
|
||||
type getTransactionsResponse = HttpResponse[getTransactionsResult]
|
||||
|
||||
func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
|
||||
var req getTransactionsRequest
|
||||
if err := ctx.ParamsParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := ctx.QueryParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
var pkScript []byte
|
||||
if req.Wallet != "" {
|
||||
var ok bool
|
||||
pkScript, ok = h.resolvePkScript(h.network, req.Wallet)
|
||||
if !ok {
|
||||
return errs.NewPublicError("unable to resolve pkscript from \"wallet\"")
|
||||
}
|
||||
}
|
||||
|
||||
blockHeight := req.BlockHeight
|
||||
if blockHeight == 0 {
|
||||
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetLatestBlock")
|
||||
}
|
||||
blockHeight = uint64(blockHeader.Height)
|
||||
}
|
||||
|
||||
var runeId runes.RuneId
|
||||
if req.Id != "" {
|
||||
var ok bool
|
||||
runeId, ok = h.resolveRuneId(ctx.UserContext(), req.Id)
|
||||
if !ok {
|
||||
return errs.NewPublicError("unable to resolve rune id from \"id\"")
|
||||
}
|
||||
}
|
||||
|
||||
txs, err := h.usecase.GetTransactionsByHeight(ctx.UserContext(), blockHeight)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetTransactionsByHeight")
|
||||
}
|
||||
|
||||
filteredTxs := make([]*entity.RuneTransaction, 0)
|
||||
isTxContainPkScript := func(tx *entity.RuneTransaction) bool {
|
||||
for _, input := range tx.Inputs {
|
||||
if bytes.Equal(input.PkScript, pkScript) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, output := range tx.Outputs {
|
||||
if bytes.Equal(output.PkScript, pkScript) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
isTxContainRuneId := func(tx *entity.RuneTransaction) bool {
|
||||
for _, input := range tx.Inputs {
|
||||
if input.RuneId == runeId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, output := range tx.Outputs {
|
||||
if output.RuneId == runeId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for runeId := range tx.Mints {
|
||||
if runeId == runeId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for runeId := range tx.Burns {
|
||||
if runeId == runeId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if tx.Runestone != nil {
|
||||
if tx.Runestone.Mint != nil && *tx.Runestone.Mint == runeId {
|
||||
return true
|
||||
}
|
||||
// returns true if this tx etched this runeId
|
||||
if tx.Runestone.Etching != nil && tx.BlockHeight == runeId.BlockHeight && tx.Index == runeId.TxIndex {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
for _, tx := range txs {
|
||||
if pkScript != nil && !isTxContainPkScript(tx) {
|
||||
continue
|
||||
}
|
||||
if runeId != (runes.RuneId{}) && isTxContainRuneId(tx) {
|
||||
continue
|
||||
}
|
||||
filteredTxs = append(filteredTxs, tx)
|
||||
}
|
||||
|
||||
txList := make([]transaction, 0, len(filteredTxs))
|
||||
for _, tx := range filteredTxs {
|
||||
respTx := transaction{
|
||||
TxHash: tx.Hash.String(),
|
||||
BlockHeight: tx.BlockHeight,
|
||||
Timestamp: tx.Timestamp.Unix(),
|
||||
Inputs: make([]outPointBalance, 0, len(tx.Inputs)),
|
||||
Outputs: make([]outPointBalance, 0, len(tx.Outputs)),
|
||||
Mints: make(map[string]uint128.Uint128, len(tx.Mints)),
|
||||
Burns: make(map[string]uint128.Uint128, len(tx.Burns)),
|
||||
Extend: runeTransactionExtend{},
|
||||
}
|
||||
for _, input := range tx.Inputs {
|
||||
address := addressFromPkScript(input.PkScript, h.network)
|
||||
respTx.Inputs = append(respTx.Inputs, outPointBalance{
|
||||
PkScript: hex.EncodeToString(input.PkScript),
|
||||
Address: address,
|
||||
Id: input.RuneId,
|
||||
Amount: input.Amount,
|
||||
Index: input.Index,
|
||||
})
|
||||
}
|
||||
for _, output := range tx.Outputs {
|
||||
address := addressFromPkScript(output.PkScript, h.network)
|
||||
respTx.Outputs = append(respTx.Outputs, outPointBalance{
|
||||
PkScript: hex.EncodeToString(output.PkScript),
|
||||
Address: address,
|
||||
Id: output.RuneId,
|
||||
Amount: output.Amount,
|
||||
Index: output.Index,
|
||||
})
|
||||
}
|
||||
for id, amount := range tx.Mints {
|
||||
respTx.Mints[id.String()] = amount
|
||||
}
|
||||
for id, amount := range tx.Burns {
|
||||
respTx.Burns[id.String()] = amount
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
txList = append(txList, respTx)
|
||||
}
|
||||
|
||||
resp := getTransactionsResponse{
|
||||
Result: &getTransactionsResult{
|
||||
List: txList,
|
||||
},
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(resp))
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"github.com/gaze-network/indexer-network/common"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/runes"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/usecase"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
|
||||
)
|
||||
|
||||
type HttpHandler struct {
|
||||
@@ -62,6 +64,21 @@ func (h *HttpHandler) resolvePkScript(network common.Network, wallet string) ([]
|
||||
return pkScript, true
|
||||
}
|
||||
|
||||
// TODO: extract this function somewhere else
|
||||
// addressFromPkScript returns the address from the given pkScript. If the pkScript is invalid or not standard, it returns empty string.
|
||||
func addressFromPkScript(pkScript []byte, network common.Network) string {
|
||||
_, addrs, _, err := txscript.ExtractPkScriptAddrs(pkScript, network.ChainParams())
|
||||
if err != nil {
|
||||
logger.Debug("unable to extract address from pkscript", slogx.Error(err))
|
||||
return ""
|
||||
}
|
||||
if len(addrs) != 1 {
|
||||
logger.Debug("invalid number of addresses extracted from pkscript. Expected only 1.", slogx.Int("numAddresses", len(addrs)))
|
||||
return ""
|
||||
}
|
||||
return addrs[0].EncodeAddress()
|
||||
}
|
||||
|
||||
func (h *HttpHandler) resolveRuneId(ctx context.Context, id string) (runes.RuneId, bool) {
|
||||
if id == "" {
|
||||
return runes.RuneId{}, false
|
||||
|
||||
@@ -8,5 +8,6 @@ func (h *HttpHandler) Mount(router fiber.Router) error {
|
||||
r := router.Group("/v2/runes")
|
||||
|
||||
r.Get("/balances/wallet/:wallet", h.GetBalancesByAddress)
|
||||
r.Get("/transactions", h.GetTransactions)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -301,12 +301,6 @@ func extractModelRuneTxAndRunestone(src gen.GetRuneTransactionsByHeightRow) (gen
|
||||
var runestone *gen.RunesRunestone
|
||||
if src.TxHash.Valid {
|
||||
// these fields should never be null
|
||||
if !src.Etching.Valid {
|
||||
return gen.RunesTransaction{}, nil, errors.New("runestone etching bool is null")
|
||||
}
|
||||
if !src.EtchingTerms.Valid {
|
||||
return gen.RunesTransaction{}, nil, errors.New("runestone etching terms bool is null")
|
||||
}
|
||||
if !src.Cenotaph.Valid {
|
||||
return gen.RunesTransaction{}, nil, errors.New("runestone cenotaph is null")
|
||||
}
|
||||
|
||||
@@ -19,5 +19,42 @@ func (f FlawFlag) Mask() Flaws {
|
||||
return 1 << f
|
||||
}
|
||||
|
||||
var flawMessages = map[FlawFlag]string{
|
||||
FlawFlagEdictOutput: "edict output greater than transaction output count",
|
||||
FlawFlagEdictRuneId: "invalid runeId in edict",
|
||||
FlawFlagInvalidScript: "invalid script in OP_RETURN",
|
||||
FlawFlagOpCode: "non-pushdata opcode in OP_RETURN",
|
||||
FlawFlagSupplyOverflow: "supply overflows uint128",
|
||||
FlawFlagTrailingIntegers: "trailing integers in body",
|
||||
FlawFlagTruncatedField: "field with missing value",
|
||||
FlawFlagUnrecognizedEvenTag: "unrecognized even tag",
|
||||
FlawFlagUnrecognizedFlag: "unrecognized field",
|
||||
FlawFlagVarInt: "invalid varint",
|
||||
}
|
||||
|
||||
func (f FlawFlag) String() string {
|
||||
return flawMessages[f]
|
||||
}
|
||||
|
||||
// Flaws is a bitmask of flaws that caused a runestone to be a cenotaph.
|
||||
type Flaws uint32
|
||||
|
||||
func (f Flaws) Collect() []FlawFlag {
|
||||
var flags []FlawFlag
|
||||
// collect from list of all flags
|
||||
for flag := range flawMessages {
|
||||
if f&flag.Mask() != 0 {
|
||||
flags = append(flags, flag)
|
||||
}
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
func (f Flaws) CollectAsString() []string {
|
||||
flawFlags := f.Collect()
|
||||
flawMsgs := make([]string, 0, len(flawFlags))
|
||||
for _, flag := range flawFlags {
|
||||
flawMsgs = append(flawMsgs, flag.String())
|
||||
}
|
||||
return flawMsgs
|
||||
}
|
||||
|
||||
16
modules/runes/usecase/get_transactions.go
Normal file
16
modules/runes/usecase/get_transactions.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
|
||||
)
|
||||
|
||||
func (u *Usecase) GetTransactionsByHeight(ctx context.Context, height uint64) ([]*entity.RuneTransaction, error) {
|
||||
txs, err := u.runesDg.GetRuneTransactionsByHeight(ctx, height)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error during GetTransactionsByHeight")
|
||||
}
|
||||
return txs, nil
|
||||
}
|
||||
Reference in New Issue
Block a user