feat: implement brc20 inscription dgs and repos

This commit is contained in:
Gaze
2024-05-28 14:57:44 +07:00
parent 3bb26d012b
commit 71641dd2fb
10 changed files with 813 additions and 42 deletions

View File

@@ -24,14 +24,15 @@ CREATE TABLE IF NOT EXISTS "brc20_indexed_blocks" (
"hash" TEXT NOT NULL,
"prev_hash" TEXT NOT NULL,
"event_hash" TEXT NOT NULL,
"cumulative_event_hash" TEXT NOT NULL,
"cumulative_event_hash" TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "brc20_processor_stats" (
"block_height" INT NOT NULL PRIMARY KEY,
"cursed_inscription_count" INT NOT NULL,
"blessed_inscription_count" INT NOT NULL,
)
"lost_sats" BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS "brc20_tickers" (
"tick" TEXT NOT NULL PRIMARY KEY, -- lowercase of original_tick
@@ -124,30 +125,32 @@ CREATE TABLE IF NOT EXISTS "brc20_inscription_entries" (
"parents" TEXT[], -- parent inscription id, 0.14 only supports 1 parent per inscription
"pointer" BIGINT,
"content" JSONB NOT NULL, -- can use jsonb because we only track brc20 inscriptions
"content_type" TEXT NOT NULL,
"content_encoding" TEXT,
"content_type" TEXT,
"cursed" BOOLEAN NOT NULL, -- inscriptions after jubilee are no longer cursed in 0.14, which affects inscription number
"cursed_for_brc20" BOOLEAN NOT NULL, -- however, inscriptions that would normally be cursed are still considered cursed for brc20
"created_at" TIMESTAMP NOT NULL,
"created_at_height" INT NOT NULL
);
CREATE TABLE IF NOT EXISTS "brc20_inscription_states" (
"id" TEXT NOT NULL PRIMARY KEY,
CREATE TABLE IF NOT EXISTS "brc20_inscription_entry_states" (
"id" TEXT NOT NULL,
"block_height" INT NOT NULL,
"transfer_count" INT NOT NULL,
PRIMARY KEY ("id", "block_height")
);
CREATE TABLE IF NOT EXISTS "brc20_inscription_transfers" (
"inscription_id" TEXT NOT NULL,
"block_height" INT NOT NULL,
"old_satpoint_tx_hash" TEXT NOT NULL,
"old_satpoint_out_idx" INT NOT NULL,
"old_satpoint_offset" BIGINT NOT NULL,
"new_satpoint_tx_hash" TEXT NOT NULL,
"new_satpoint_out_idx" INT NOT NULL,
"new_satpoint_offset" BIGINT NOT NULL,
"old_satpoint_tx_hash" TEXT,
"old_satpoint_out_idx" INT,
"old_satpoint_offset" BIGINT,
"new_satpoint_tx_hash" TEXT,
"new_satpoint_out_idx" INT,
"new_satpoint_offset" BIGINT,
"new_pkscript" TEXT NOT NULL,
"new_output_value" DECIMAL NOT NULL
"new_output_value" BIGINT NOT NULL,
"sent_as_fee" BOOLEAN NOT NULL,
PRIMARY KEY ("inscription_id", "block_height")
);

View File

@@ -0,0 +1,26 @@
-- name: GetLatestProcessorStats :one
SELECT * FROM "brc20_processor_stats" ORDER BY block_height DESC LIMIT 1;
-- name: GetInscriptionsInOutPoint :many
SELECT * FROM "brc20_inscription_transfers" WHERE "new_satpoint_tx_hash" = @tx_hash AND "new_satpoint_out_idx" = @tx_out_idx;
-- name: GetInscriptionEntriesByIds :many
WITH "states" AS (
-- select latest state
SELECT DISTINCT ON ("id") * FROM "brc20_inscription_entry_states" WHERE "id" = ANY(@inscription_ids::text[]) ORDER BY "id", "block_height" DESC
)
SELECT * FROM "brc20_inscription_entries"
LEFT JOIN "states" ON "brc20_inscription_entries"."id" = states."id"
WHERE "brc20_inscription_entries"."id" = ANY(@inscription_ids::text[]);
-- name: CreateProcessorStats :exec
INSERT INTO "brc20_processor_stats" (block_height, cursed_inscription_count, blessed_inscription_count, lost_sats) VALUES ($1, $2, $3, $4);
-- name: CreateInscriptionEntries :batchexec
INSERT INTO "brc20_inscription_entries" ("id", "number", "sequence_number", "delegate", "metadata", "metaprotocol", "parents", "pointer", "content", "content_encoding", "content_type", "cursed", "cursed_for_brc20", "created_at", "created_at_height") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15);
-- name: CreateInscriptionEntryStates :batchexec
INSERT INTO "brc20_inscription_entry_states" ("id", "block_height", "transfer_count") VALUES ($1, $2, $3);
-- name: CreateInscriptionTransfers :batchexec
INSERT INTO "brc20_inscription_transfers" ("inscription_id", "block_height", "old_satpoint_tx_hash", "old_satpoint_out_idx", "old_satpoint_offset", "new_satpoint_tx_hash", "new_satpoint_out_idx", "new_satpoint_offset", "new_pkscript", "new_output_value", "sent_as_fee") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11);

View File

@@ -23,14 +23,13 @@ type BRC20DataGatewayWithTx interface {
type BRC20ReaderDataGateway interface {
GetProcessorStats(ctx context.Context) (*entity.ProcessorStats, error)
GetInscriptionsInOutPoint(ctx context.Context, outPoint wire.OutPoint) (map[ordinals.SatPoint]ordinals.InscriptionId, error)
GetInscriptionIdsInOutPoint(ctx context.Context, outPoint wire.OutPoint) (map[ordinals.SatPoint][]ordinals.InscriptionId, error)
GetInscriptionEntryById(ctx context.Context, id ordinals.InscriptionId) (*ordinals.InscriptionEntry, error)
}
type BRC20WriterDataGateway interface {
CreateProcessorStats(ctx context.Context, stats *entity.ProcessorStats) error
CreateInscriptionEntries(ctx context.Context, blockHeight uint64, entries []*ordinals.InscriptionEntry) error
CreateInscriptionEntryStates(ctx context.Context, blockHeight uint64, entryStates []*ordinals.InscriptionEntry) error
CreateInscriptionTransfers(ctx context.Context, transfers []*entity.InscriptionTransfer) error
CreateProcessorStats(ctx context.Context, stats *entity.ProcessorStats) error
}

View File

@@ -48,27 +48,29 @@ func (p *Processor) processTx(ctx context.Context, tx *types.Transaction, blockH
continue
}
inscriptions, err := p.getInscriptionsInOutPoint(ctx, inputOutPoint)
inscriptions, err := p.getInscriptionIdsInOutPoint(ctx, inputOutPoint)
if err != nil {
return errors.Wrap(err, "failed to get inscriptions in outpoint")
}
for satPoint, inscriptionId := range inscriptions {
for satPoint, inscriptionIds := range inscriptions {
offset := totalInputValue + satPoint.Offset
floatingInscriptions = append(floatingInscriptions, &Flotsam{
Offset: offset,
InscriptionId: inscriptionId,
Tx: tx,
OriginOld: &OriginOld{
OldSatPoint: satPoint,
},
})
if _, ok := inscribeOffsets[offset]; !ok {
inscribeOffsets[offset] = &struct {
inscriptionId ordinals.InscriptionId
count int
}{inscriptionId, 0}
for _, inscriptionId := range inscriptionIds {
floatingInscriptions = append(floatingInscriptions, &Flotsam{
Offset: offset,
InscriptionId: inscriptionId,
Tx: tx,
OriginOld: &OriginOld{
OldSatPoint: satPoint,
},
})
if _, ok := inscribeOffsets[offset]; !ok {
inscribeOffsets[offset] = &struct {
inscriptionId ordinals.InscriptionId
count int
}{inscriptionId, 0}
}
inscribeOffsets[offset].count++
}
inscribeOffsets[offset].count++
}
// offset on output to inscribe new inscriptions from this input
offset := totalInputValue
@@ -404,8 +406,8 @@ func (p *Processor) getOutPointValues(ctx context.Context, outPoints []wire.OutP
return outPointValues, nil
}
func (p *Processor) getInscriptionsInOutPoint(ctx context.Context, outPoint wire.OutPoint) (map[ordinals.SatPoint]ordinals.InscriptionId, error) {
inscriptions, err := p.brc20Dg.GetInscriptionsInOutPoint(ctx, outPoint)
func (p *Processor) getInscriptionIdsInOutPoint(ctx context.Context, outPoint wire.OutPoint) (map[ordinals.SatPoint][]ordinals.InscriptionId, error) {
inscriptions, err := p.brc20Dg.GetInscriptionIdsInOutPoint(ctx, outPoint)
if err != nil {
return nil, errors.Wrap(err, "failed to get inscriptions by outpoint")
}

View File

@@ -0,0 +1,153 @@
package postgres
import (
"context"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/modules/brc20/internal/datagateway"
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
"github.com/gaze-network/indexer-network/modules/brc20/internal/ordinals"
"github.com/gaze-network/indexer-network/modules/brc20/internal/repository/postgres/gen"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/samber/lo"
)
var _ datagateway.BRC20DataGateway = (*Repository)(nil)
func (r *Repository) GetProcessorStats(ctx context.Context) (*entity.ProcessorStats, error) {
model, err := r.queries.GetLatestProcessorStats(ctx)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, errors.WithStack(errs.NotFound)
}
return nil, errors.WithStack(err)
}
stats := mapProcessorStatsModelToType(model)
return &stats, nil
}
func (r *Repository) GetInscriptionIdsInOutPoint(ctx context.Context, outPoint wire.OutPoint) (map[ordinals.SatPoint][]ordinals.InscriptionId, error) {
models, err := r.queries.GetInscriptionsInOutPoint(ctx, gen.GetInscriptionsInOutPointParams{
TxHash: pgtype.Text{String: outPoint.Hash.String(), Valid: true},
TxOutIdx: pgtype.Int4{Int32: int32(outPoint.Index), Valid: true},
})
if err != nil {
return nil, errors.WithStack(err)
}
inscriptionIds := make(map[ordinals.SatPoint][]ordinals.InscriptionId)
for _, model := range models {
// sanity check
if !model.NewSatpointTxHash.Valid || !model.NewSatpointOutIdx.Valid || !model.NewSatpointOffset.Valid {
return nil, errors.New("invalid satpoint: missing required satpoint fields")
}
txHash, err := chainhash.NewHashFromStr(model.NewSatpointTxHash.String)
if err != nil {
return nil, errors.Wrap(err, "invalid satpoint: cannot parse txHash")
}
satPoint := ordinals.SatPoint{
OutPoint: wire.OutPoint{
Hash: *txHash,
Index: uint32(model.NewSatpointOutIdx.Int32),
},
Offset: uint64(model.NewSatpointOffset.Int64),
}
inscriptionId, err := ordinals.NewInscriptionIdFromString(model.InscriptionID)
if err != nil {
return nil, errors.Wrap(err, "invalid inscription id")
}
inscriptionIds[satPoint] = append(inscriptionIds[satPoint], inscriptionId)
}
return inscriptionIds, nil
}
func (r *Repository) GetInscriptionEntryById(ctx context.Context, id ordinals.InscriptionId) (*ordinals.InscriptionEntry, error) {
models, err := r.queries.GetInscriptionEntriesByIds(ctx, []string{id.String()})
if err != nil {
return nil, errors.WithStack(err)
}
if len(models) == 0 {
return nil, errors.WithStack(errs.NotFound)
}
if len(models) > 1 {
// sanity check
panic("multiple inscription entries found for the same id")
}
inscriptionEntry, err := mapInscriptionEntryModelToType(models[0])
if err != nil {
return nil, errors.WithStack(err)
}
return &inscriptionEntry, nil
}
func (r *Repository) CreateProcessorStats(ctx context.Context, stats *entity.ProcessorStats) error {
params := mapProcessorStatsTypeToParams(*stats)
if err := r.queries.CreateProcessorStats(ctx, params); err != nil {
return errors.WithStack(err)
}
return nil
}
func (r *Repository) CreateInscriptionEntries(ctx context.Context, blockHeight uint64, entries []*ordinals.InscriptionEntry) error {
inscriptionEntryParams := make([]gen.CreateInscriptionEntriesParams, 0)
for _, entry := range entries {
params, _, err := mapInscriptionEntryTypeToParams(*entry, blockHeight)
if err != nil {
return errors.Wrap(err, "cannot map inscription entry to create params")
}
inscriptionEntryParams = append(inscriptionEntryParams, params)
}
results := r.queries.CreateInscriptionEntries(ctx, inscriptionEntryParams)
var execErrors []error
results.Exec(func(i int, err error) {
if err != nil {
execErrors = append(execErrors, err)
}
})
if len(execErrors) > 0 {
return errors.Wrap(errors.Join(execErrors...), "error during exec")
}
return nil
}
func (r *Repository) CreateInscriptionEntryStates(ctx context.Context, blockHeight uint64, entryStates []*ordinals.InscriptionEntry) error {
inscriptionEntryStatesParams := make([]gen.CreateInscriptionEntryStatesParams, 0)
for _, entry := range entryStates {
_, params, err := mapInscriptionEntryTypeToParams(*entry, blockHeight)
if err != nil {
return errors.Wrap(err, "cannot map inscription entry to create params")
}
inscriptionEntryStatesParams = append(inscriptionEntryStatesParams, params)
}
results := r.queries.CreateInscriptionEntryStates(ctx, inscriptionEntryStatesParams)
var execErrors []error
results.Exec(func(i int, err error) {
if err != nil {
execErrors = append(execErrors, err)
}
})
if len(execErrors) > 0 {
return errors.Wrap(errors.Join(execErrors...), "error during exec")
}
return nil
}
func (r *Repository) CreateInscriptionTransfers(ctx context.Context, transfers []*entity.InscriptionTransfer) error {
params := lo.Map(transfers, func(transfer *entity.InscriptionTransfer, _ int) gen.CreateInscriptionTransfersParams {
return mapInscriptionTransferTypeToParams(*transfer)
})
results := r.queries.CreateInscriptionTransfers(ctx, params)
var execErrors []error
results.Exec(func(i int, err error) {
if err != nil {
execErrors = append(execErrors, err)
}
})
if len(execErrors) > 0 {
return errors.Wrap(errors.Join(execErrors...), "error during exec")
}
return nil
}

View File

@@ -0,0 +1,211 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// source: batch.go
package gen
import (
"context"
"errors"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
var (
ErrBatchAlreadyClosed = errors.New("batch already closed")
)
const createInscriptionEntries = `-- name: CreateInscriptionEntries :batchexec
INSERT INTO "brc20_inscription_entries" ("id", "number", "sequence_number", "delegate", "metadata", "metaprotocol", "parents", "pointer", "content", "content_encoding", "content_type", "cursed", "cursed_for_brc20", "created_at", "created_at_height") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
`
type CreateInscriptionEntriesBatchResults struct {
br pgx.BatchResults
tot int
closed bool
}
type CreateInscriptionEntriesParams struct {
Id string
Number int64
SequenceNumber int64
Delegate pgtype.Text
Metadata []byte
Metaprotocol pgtype.Text
Parents []string
Pointer pgtype.Int8
Content []byte
ContentEncoding pgtype.Text
ContentType pgtype.Text
Cursed bool
CursedForBrc20 bool
CreatedAt pgtype.Timestamp
CreatedAtHeight int32
}
func (q *Queries) CreateInscriptionEntries(ctx context.Context, arg []CreateInscriptionEntriesParams) *CreateInscriptionEntriesBatchResults {
batch := &pgx.Batch{}
for _, a := range arg {
vals := []interface{}{
a.Id,
a.Number,
a.SequenceNumber,
a.Delegate,
a.Metadata,
a.Metaprotocol,
a.Parents,
a.Pointer,
a.Content,
a.ContentEncoding,
a.ContentType,
a.Cursed,
a.CursedForBrc20,
a.CreatedAt,
a.CreatedAtHeight,
}
batch.Queue(createInscriptionEntries, vals...)
}
br := q.db.SendBatch(ctx, batch)
return &CreateInscriptionEntriesBatchResults{br, len(arg), false}
}
func (b *CreateInscriptionEntriesBatchResults) Exec(f func(int, error)) {
defer b.br.Close()
for t := 0; t < b.tot; t++ {
if b.closed {
if f != nil {
f(t, ErrBatchAlreadyClosed)
}
continue
}
_, err := b.br.Exec()
if f != nil {
f(t, err)
}
}
}
func (b *CreateInscriptionEntriesBatchResults) Close() error {
b.closed = true
return b.br.Close()
}
const createInscriptionEntryStates = `-- name: CreateInscriptionEntryStates :batchexec
INSERT INTO "brc20_inscription_entry_states" ("id", "block_height", "transfer_count") VALUES ($1, $2, $3)
`
type CreateInscriptionEntryStatesBatchResults struct {
br pgx.BatchResults
tot int
closed bool
}
type CreateInscriptionEntryStatesParams struct {
Id string
BlockHeight int32
TransferCount int32
}
func (q *Queries) CreateInscriptionEntryStates(ctx context.Context, arg []CreateInscriptionEntryStatesParams) *CreateInscriptionEntryStatesBatchResults {
batch := &pgx.Batch{}
for _, a := range arg {
vals := []interface{}{
a.Id,
a.BlockHeight,
a.TransferCount,
}
batch.Queue(createInscriptionEntryStates, vals...)
}
br := q.db.SendBatch(ctx, batch)
return &CreateInscriptionEntryStatesBatchResults{br, len(arg), false}
}
func (b *CreateInscriptionEntryStatesBatchResults) Exec(f func(int, error)) {
defer b.br.Close()
for t := 0; t < b.tot; t++ {
if b.closed {
if f != nil {
f(t, ErrBatchAlreadyClosed)
}
continue
}
_, err := b.br.Exec()
if f != nil {
f(t, err)
}
}
}
func (b *CreateInscriptionEntryStatesBatchResults) Close() error {
b.closed = true
return b.br.Close()
}
const createInscriptionTransfers = `-- name: CreateInscriptionTransfers :batchexec
INSERT INTO "brc20_inscription_transfers" ("inscription_id", "block_height", "old_satpoint_tx_hash", "old_satpoint_out_idx", "old_satpoint_offset", "new_satpoint_tx_hash", "new_satpoint_out_idx", "new_satpoint_offset", "new_pkscript", "new_output_value", "sent_as_fee") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`
type CreateInscriptionTransfersBatchResults struct {
br pgx.BatchResults
tot int
closed bool
}
type CreateInscriptionTransfersParams struct {
InscriptionID string
BlockHeight int32
OldSatpointTxHash pgtype.Text
OldSatpointOutIdx pgtype.Int4
OldSatpointOffset pgtype.Int8
NewSatpointTxHash pgtype.Text
NewSatpointOutIdx pgtype.Int4
NewSatpointOffset pgtype.Int8
NewPkscript string
NewOutputValue int64
SentAsFee bool
}
func (q *Queries) CreateInscriptionTransfers(ctx context.Context, arg []CreateInscriptionTransfersParams) *CreateInscriptionTransfersBatchResults {
batch := &pgx.Batch{}
for _, a := range arg {
vals := []interface{}{
a.InscriptionID,
a.BlockHeight,
a.OldSatpointTxHash,
a.OldSatpointOutIdx,
a.OldSatpointOffset,
a.NewSatpointTxHash,
a.NewSatpointOutIdx,
a.NewSatpointOffset,
a.NewPkscript,
a.NewOutputValue,
a.SentAsFee,
}
batch.Queue(createInscriptionTransfers, vals...)
}
br := q.db.SendBatch(ctx, batch)
return &CreateInscriptionTransfersBatchResults{br, len(arg), false}
}
func (b *CreateInscriptionTransfersBatchResults) Exec(f func(int, error)) {
defer b.br.Close()
for t := 0; t < b.tot; t++ {
if b.closed {
if f != nil {
f(t, ErrBatchAlreadyClosed)
}
continue
}
_, err := b.br.Exec()
if f != nil {
f(t, err)
}
}
}
func (b *CreateInscriptionTransfersBatchResults) Close() error {
b.closed = true
return b.br.Close()
}

View File

@@ -0,0 +1,160 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// source: data.sql
package gen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createProcessorStats = `-- name: CreateProcessorStats :exec
INSERT INTO "brc20_processor_stats" (block_height, cursed_inscription_count, blessed_inscription_count, lost_sats) VALUES ($1, $2, $3, $4)
`
type CreateProcessorStatsParams struct {
BlockHeight int32
CursedInscriptionCount int32
BlessedInscriptionCount int32
LostSats int64
}
func (q *Queries) CreateProcessorStats(ctx context.Context, arg CreateProcessorStatsParams) error {
_, err := q.db.Exec(ctx, createProcessorStats,
arg.BlockHeight,
arg.CursedInscriptionCount,
arg.BlessedInscriptionCount,
arg.LostSats,
)
return err
}
const getInscriptionEntriesByIds = `-- name: GetInscriptionEntriesByIds :many
WITH "states" AS (
-- select latest state
SELECT DISTINCT ON ("id") id, block_height, transfer_count FROM "brc20_inscription_entry_states" WHERE "id" = ANY($1::text[]) ORDER BY "id", "block_height" DESC
)
SELECT brc20_inscription_entries.id, number, sequence_number, delegate, metadata, metaprotocol, parents, pointer, content, content_encoding, content_type, cursed, cursed_for_brc20, created_at, created_at_height, states.id, block_height, transfer_count FROM "brc20_inscription_entries"
LEFT JOIN "states" ON "brc20_inscription_entries"."id" = states."id"
WHERE "brc20_inscription_entries"."id" = ANY($1::text[])
`
type GetInscriptionEntriesByIdsRow struct {
Id string
Number int64
SequenceNumber int64
Delegate pgtype.Text
Metadata []byte
Metaprotocol pgtype.Text
Parents []string
Pointer pgtype.Int8
Content []byte
ContentEncoding pgtype.Text
ContentType pgtype.Text
Cursed bool
CursedForBrc20 bool
CreatedAt pgtype.Timestamp
CreatedAtHeight int32
Id_2 pgtype.Text
BlockHeight pgtype.Int4
TransferCount pgtype.Int4
}
func (q *Queries) GetInscriptionEntriesByIds(ctx context.Context, inscriptionIds []string) ([]GetInscriptionEntriesByIdsRow, error) {
rows, err := q.db.Query(ctx, getInscriptionEntriesByIds, inscriptionIds)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetInscriptionEntriesByIdsRow
for rows.Next() {
var i GetInscriptionEntriesByIdsRow
if err := rows.Scan(
&i.Id,
&i.Number,
&i.SequenceNumber,
&i.Delegate,
&i.Metadata,
&i.Metaprotocol,
&i.Parents,
&i.Pointer,
&i.Content,
&i.ContentEncoding,
&i.ContentType,
&i.Cursed,
&i.CursedForBrc20,
&i.CreatedAt,
&i.CreatedAtHeight,
&i.Id_2,
&i.BlockHeight,
&i.TransferCount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getInscriptionsInOutPoint = `-- name: GetInscriptionsInOutPoint :many
SELECT inscription_id, block_height, old_satpoint_tx_hash, old_satpoint_out_idx, old_satpoint_offset, new_satpoint_tx_hash, new_satpoint_out_idx, new_satpoint_offset, new_pkscript, new_output_value, sent_as_fee FROM "brc20_inscription_transfers" WHERE "new_satpoint_tx_hash" = $1 AND "new_satpoint_out_idx" = $2
`
type GetInscriptionsInOutPointParams struct {
TxHash pgtype.Text
TxOutIdx pgtype.Int4
}
func (q *Queries) GetInscriptionsInOutPoint(ctx context.Context, arg GetInscriptionsInOutPointParams) ([]Brc20InscriptionTransfer, error) {
rows, err := q.db.Query(ctx, getInscriptionsInOutPoint, arg.TxHash, arg.TxOutIdx)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Brc20InscriptionTransfer
for rows.Next() {
var i Brc20InscriptionTransfer
if err := rows.Scan(
&i.InscriptionID,
&i.BlockHeight,
&i.OldSatpointTxHash,
&i.OldSatpointOutIdx,
&i.OldSatpointOffset,
&i.NewSatpointTxHash,
&i.NewSatpointOutIdx,
&i.NewSatpointOffset,
&i.NewPkscript,
&i.NewOutputValue,
&i.SentAsFee,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getLatestProcessorStats = `-- name: GetLatestProcessorStats :one
SELECT block_height, cursed_inscription_count, blessed_inscription_count, lost_sats FROM "brc20_processor_stats" ORDER BY block_height DESC LIMIT 1
`
func (q *Queries) GetLatestProcessorStats(ctx context.Context) (Brc20ProcessorStat, error) {
row := q.db.QueryRow(ctx, getLatestProcessorStats)
var i Brc20ProcessorStat
err := row.Scan(
&i.BlockHeight,
&i.CursedInscriptionCount,
&i.BlessedInscriptionCount,
&i.LostSats,
)
return i, err
}

View File

@@ -15,6 +15,7 @@ type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
SendBatch(context.Context, *pgx.Batch) pgx.BatchResults
}
func New(db DBTX) *Queries {

View File

@@ -54,28 +54,42 @@ type Brc20IndexerState struct {
CreatedAt pgtype.Timestamptz
}
type Brc20Inscription struct {
type Brc20InscriptionEntry struct {
Id string
Number int64
SequenceNumber int64
Delegate pgtype.Text
Metadata []byte
Metaprotocol pgtype.Text
Parent pgtype.Text
Parents []string
Pointer pgtype.Int8
Content []byte
ContentType string
TransferCount int32
ContentEncoding pgtype.Text
ContentType pgtype.Text
Cursed bool
CursedForBrc20 bool
CreatedAt pgtype.Timestamp
CreatedAtHeight int32
}
type Brc20InscriptionLocation struct {
InscriptionID string
type Brc20InscriptionEntryState struct {
Id string
BlockHeight int32
TxHash string
TxIdx int32
SatOffset int64
TransferCount int32
}
type Brc20InscriptionTransfer struct {
InscriptionID string
BlockHeight int32
OldSatpointTxHash pgtype.Text
OldSatpointOutIdx pgtype.Int4
OldSatpointOffset pgtype.Int8
NewSatpointTxHash pgtype.Text
NewSatpointOutIdx pgtype.Int4
NewSatpointOffset pgtype.Int8
NewPkscript string
NewOutputValue int64
SentAsFee bool
}
type Brc20MintEvent struct {
@@ -92,6 +106,13 @@ type Brc20MintEvent struct {
ParentID pgtype.Text
}
type Brc20ProcessorStat struct {
BlockHeight int32
CursedInscriptionCount int32
BlessedInscriptionCount int32
LostSats int64
}
type Brc20Ticker struct {
Tick string
OriginalTick string

View File

@@ -0,0 +1,195 @@
package postgres
import (
"encoding/hex"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/modules/brc20/internal/entity"
"github.com/gaze-network/indexer-network/modules/brc20/internal/ordinals"
"github.com/gaze-network/indexer-network/modules/brc20/internal/repository/postgres/gen"
"github.com/jackc/pgx/v5/pgtype"
"github.com/samber/lo"
)
func mapProcessorStatsModelToType(src gen.Brc20ProcessorStat) entity.ProcessorStats {
return entity.ProcessorStats{
BlockHeight: uint64(src.BlockHeight),
CursedInscriptionCount: uint64(src.CursedInscriptionCount),
BlessedInscriptionCount: uint64(src.BlessedInscriptionCount),
LostSats: uint64(src.LostSats),
}
}
func mapProcessorStatsTypeToParams(src entity.ProcessorStats) gen.CreateProcessorStatsParams {
return gen.CreateProcessorStatsParams{
BlockHeight: int32(src.BlockHeight),
CursedInscriptionCount: int32(src.CursedInscriptionCount),
BlessedInscriptionCount: int32(src.BlessedInscriptionCount),
LostSats: int64(src.LostSats),
}
}
func mapInscriptionEntryModelToType(src gen.GetInscriptionEntriesByIdsRow) (ordinals.InscriptionEntry, error) {
inscriptionId, err := ordinals.NewInscriptionIdFromString(src.Id)
if err != nil {
return ordinals.InscriptionEntry{}, errors.Wrap(err, "invalid inscription id")
}
var delegate, parent *ordinals.InscriptionId
if src.Delegate.Valid {
delegateValue, err := ordinals.NewInscriptionIdFromString(src.Delegate.String)
if err != nil {
return ordinals.InscriptionEntry{}, errors.Wrap(err, "invalid delegate id")
}
delegate = &delegateValue
}
// ord 0.14.0 supports only one parent
if len(src.Parents) > 0 {
parentValue, err := ordinals.NewInscriptionIdFromString(src.Parents[0])
if err != nil {
return ordinals.InscriptionEntry{}, errors.Wrap(err, "invalid parent id")
}
parent = &parentValue
}
inscription := ordinals.Inscription{
Content: src.Content,
ContentEncoding: lo.Ternary(src.ContentEncoding.Valid, src.ContentEncoding.String, ""),
ContentType: lo.Ternary(src.ContentType.Valid, src.ContentType.String, ""),
Delegate: delegate,
Metadata: src.Metadata,
Metaprotocol: lo.Ternary(src.Metaprotocol.Valid, src.Metaprotocol.String, ""),
Parent: parent,
Pointer: lo.Ternary(src.Pointer.Valid, lo.ToPtr(uint64(src.Pointer.Int64)), nil),
}
return ordinals.InscriptionEntry{
Id: inscriptionId,
Number: src.Number,
SequenceNumber: uint64(src.SequenceNumber),
Cursed: src.Cursed,
CursedForBRC20: src.CursedForBrc20,
CreatedAt: lo.Ternary(src.CreatedAt.Valid, src.CreatedAt.Time, time.Time{}),
CreatedAtHeight: uint64(src.CreatedAtHeight),
Inscription: inscription,
TransferCount: lo.Ternary(src.TransferCount.Valid, uint32(src.TransferCount.Int32), 0),
}, nil
}
func mapInscriptionEntryTypeToParams(src ordinals.InscriptionEntry, blockHeight uint64) (gen.CreateInscriptionEntriesParams, gen.CreateInscriptionEntryStatesParams, error) {
var delegate, metaprotocol, contentEncoding, contentType pgtype.Text
if src.Inscription.Delegate != nil {
delegate = pgtype.Text{String: src.Inscription.Delegate.String(), Valid: true}
}
if src.Inscription.Metaprotocol != "" {
metaprotocol = pgtype.Text{String: src.Inscription.Metaprotocol, Valid: true}
}
if src.Inscription.ContentEncoding != "" {
contentEncoding = pgtype.Text{String: src.Inscription.ContentEncoding, Valid: true}
}
if src.Inscription.ContentType != "" {
contentType = pgtype.Text{String: src.Inscription.ContentType, Valid: true}
}
var parents []string
if src.Inscription.Parent != nil {
parents = append(parents, src.Inscription.Parent.String())
}
var pointer pgtype.Int8
if src.Inscription.Pointer != nil {
pointer = pgtype.Int8{Int64: int64(*src.Inscription.Pointer), Valid: true}
}
return gen.CreateInscriptionEntriesParams{
Id: src.Id.String(),
Number: src.Number,
SequenceNumber: int64(src.SequenceNumber),
Delegate: delegate,
Metadata: src.Inscription.Metadata,
Metaprotocol: metaprotocol,
Parents: parents,
Pointer: pointer,
Content: src.Inscription.Content,
ContentEncoding: contentEncoding,
ContentType: contentType,
Cursed: src.Cursed,
CursedForBrc20: src.CursedForBRC20,
CreatedAt: lo.Ternary(!src.CreatedAt.IsZero(), pgtype.Timestamp{Time: src.CreatedAt, Valid: true}, pgtype.Timestamp{}),
CreatedAtHeight: int32(src.CreatedAtHeight),
}, gen.CreateInscriptionEntryStatesParams{
Id: src.Id.String(),
BlockHeight: int32(blockHeight),
TransferCount: int32(src.TransferCount),
}, nil
}
func mapInscriptionTransferModelToType(src gen.Brc20InscriptionTransfer) (entity.InscriptionTransfer, error) {
inscriptionId, err := ordinals.NewInscriptionIdFromString(src.InscriptionID)
if err != nil {
return entity.InscriptionTransfer{}, errors.Wrap(err, "invalid inscription id")
}
var oldSatPoint, newSatPoint ordinals.SatPoint
if src.OldSatpointTxHash.Valid {
if !src.OldSatpointOutIdx.Valid || !src.OldSatpointOffset.Valid {
return entity.InscriptionTransfer{}, errors.New("old satpoint out idx and offset must exist if hash exists")
}
txHash, err := chainhash.NewHashFromStr(src.OldSatpointTxHash.String)
if err != nil {
return entity.InscriptionTransfer{}, errors.Wrap(err, "invalid old satpoint tx hash")
}
oldSatPoint = ordinals.SatPoint{
OutPoint: wire.OutPoint{
Hash: *txHash,
Index: uint32(src.OldSatpointOutIdx.Int32),
},
Offset: uint64(src.OldSatpointOffset.Int64),
}
}
if src.NewSatpointTxHash.Valid {
if !src.NewSatpointOutIdx.Valid || !src.NewSatpointOffset.Valid {
return entity.InscriptionTransfer{}, errors.New("new satpoint out idx and offset must exist if hash exists")
}
txHash, err := chainhash.NewHashFromStr(src.NewSatpointTxHash.String)
if err != nil {
return entity.InscriptionTransfer{}, errors.Wrap(err, "invalid new satpoint tx hash")
}
newSatPoint = ordinals.SatPoint{
OutPoint: wire.OutPoint{
Hash: *txHash,
Index: uint32(src.NewSatpointOutIdx.Int32),
},
Offset: uint64(src.NewSatpointOffset.Int64),
}
}
newPkScript, err := hex.DecodeString(src.NewPkscript)
if err != nil {
return entity.InscriptionTransfer{}, errors.Wrap(err, "failed to parse pkscript")
}
return entity.InscriptionTransfer{
InscriptionId: inscriptionId,
BlockHeight: uint64(src.BlockHeight),
OldSatPoint: oldSatPoint,
NewSatPoint: newSatPoint,
NewPkScript: newPkScript,
NewOutputValue: uint64(src.NewOutputValue),
SentAsFee: src.SentAsFee,
}, nil
}
func mapInscriptionTransferTypeToParams(src entity.InscriptionTransfer) gen.CreateInscriptionTransfersParams {
return gen.CreateInscriptionTransfersParams{
InscriptionID: src.InscriptionId.String(),
BlockHeight: int32(src.BlockHeight),
OldSatpointTxHash: lo.Ternary(src.OldSatPoint.OutPoint.Hash != chainhash.Hash{}, pgtype.Text{String: src.OldSatPoint.OutPoint.Hash.String(), Valid: true}, pgtype.Text{}),
OldSatpointOutIdx: lo.Ternary(src.OldSatPoint.OutPoint.Index != 0, pgtype.Int4{Int32: int32(src.OldSatPoint.OutPoint.Index), Valid: true}, pgtype.Int4{}),
OldSatpointOffset: lo.Ternary(src.OldSatPoint.Offset != 0, pgtype.Int8{Int64: int64(src.OldSatPoint.Offset), Valid: true}, pgtype.Int8{}),
NewSatpointTxHash: lo.Ternary(src.NewSatPoint.OutPoint.Hash != chainhash.Hash{}, pgtype.Text{String: src.NewSatPoint.OutPoint.Hash.String(), Valid: true}, pgtype.Text{}),
NewSatpointOutIdx: lo.Ternary(src.NewSatPoint.OutPoint.Index != 0, pgtype.Int4{Int32: int32(src.NewSatPoint.OutPoint.Index), Valid: true}, pgtype.Int4{}),
NewSatpointOffset: lo.Ternary(src.NewSatPoint.Offset != 0, pgtype.Int8{Int64: int64(src.NewSatPoint.Offset), Valid: true}, pgtype.Int8{}),
NewPkscript: hex.EncodeToString(src.NewPkScript),
NewOutputValue: int64(src.NewOutputValue),
SentAsFee: src.SentAsFee,
}
}