mirror of
https://github.com/alexgo-io/gaze-indexer.git
synced 2026-01-12 22:43:22 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58f8497997 | ||
|
|
920f7fe07b | ||
|
|
0cb66232ef | ||
|
|
4074548b3e | ||
|
|
c5c9a7bdeb |
@@ -11,6 +11,7 @@ import (
|
||||
type PublicError struct {
|
||||
err error
|
||||
message string
|
||||
code string // code is optional, it can be used to identify the error type
|
||||
}
|
||||
|
||||
func (p PublicError) Error() string {
|
||||
@@ -21,6 +22,10 @@ func (p PublicError) Message() string {
|
||||
return p.message
|
||||
}
|
||||
|
||||
func (p PublicError) Code() string {
|
||||
return p.code
|
||||
}
|
||||
|
||||
func (p PublicError) Unwrap() error {
|
||||
return p.err
|
||||
}
|
||||
@@ -29,6 +34,10 @@ func NewPublicError(message string) error {
|
||||
return withstack.WithStackDepth(&PublicError{err: errors.New(message), message: message}, 1)
|
||||
}
|
||||
|
||||
func NewPublicErrorWithCode(message string, code string) error {
|
||||
return withstack.WithStackDepth(&PublicError{err: errors.New(message), message: message, code: code}, 1)
|
||||
}
|
||||
|
||||
func WithPublicMessage(err error, prefix string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
@@ -41,3 +50,16 @@ func WithPublicMessage(err error, prefix string) error {
|
||||
}
|
||||
return withstack.WithStackDepth(&PublicError{err: err, message: message}, 1)
|
||||
}
|
||||
|
||||
func WithPublicMessageCode(err error, prefix string, code string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
var message string
|
||||
if prefix != "" {
|
||||
message = fmt.Sprintf("%s: %s", prefix, err.Error())
|
||||
} else {
|
||||
message = err.Error()
|
||||
}
|
||||
return withstack.WithStackDepth(&PublicError{err: err, message: message, code: code}, 1)
|
||||
}
|
||||
|
||||
2
go.sum
2
go.sum
@@ -99,6 +99,7 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
@@ -232,6 +233,7 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
|
||||
@@ -3,6 +3,7 @@ package httphandler
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"slices"
|
||||
|
||||
@@ -90,7 +91,7 @@ func (h *HttpHandler) GetHolders(ctx *fiber.Ctx) (err error) {
|
||||
var ok bool
|
||||
runeId, ok = h.resolveRuneId(ctx.UserContext(), req.Id)
|
||||
if !ok {
|
||||
return errs.NewPublicError("unable to resolve rune id from \"id\"")
|
||||
return errs.NewPublicError(fmt.Sprintf("unable to resolve rune id \"%s\" from \"id\"", req.Id))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
@@ -14,8 +14,10 @@ import (
|
||||
)
|
||||
|
||||
type getTokenInfoRequest struct {
|
||||
Id string `params:"id"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
Id string `params:"id"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
AdditionalFieldsRaw string `query:"additionalFields"` // comma-separated list of additional fields
|
||||
AdditionalFields []string
|
||||
}
|
||||
|
||||
func (r *getTokenInfoRequest) Validate() error {
|
||||
@@ -28,6 +30,13 @@ func (r *getTokenInfoRequest) Validate() error {
|
||||
if !isRuneIdOrRuneName(r.Id) {
|
||||
errList = append(errList, errors.Errorf("id '%s' is not valid rune id or rune name", r.Id))
|
||||
}
|
||||
|
||||
if r.AdditionalFieldsRaw == "" {
|
||||
// temporarily set default value for backward compatibility
|
||||
r.AdditionalFieldsRaw = "holdersCount" // TODO: remove this default value after all clients are updated
|
||||
}
|
||||
r.AdditionalFields = strings.Split(r.AdditionalFieldsRaw, ",")
|
||||
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
@@ -52,7 +61,8 @@ type entry struct {
|
||||
}
|
||||
|
||||
type tokenInfoExtend struct {
|
||||
Entry entry `json:"entry"`
|
||||
HoldersCount *int64 `json:"holdersCount,omitempty"`
|
||||
Entry entry `json:"entry"`
|
||||
}
|
||||
|
||||
type getTokenInfoResult struct {
|
||||
@@ -68,7 +78,7 @@ type getTokenInfoResult struct {
|
||||
DeployedAtHeight uint64 `json:"deployedAtHeight"`
|
||||
CompletedAt *int64 `json:"completedAt"` // unix timestamp
|
||||
CompletedAtHeight *uint64 `json:"completedAtHeight"`
|
||||
HoldersCount int `json:"holdersCount"`
|
||||
HoldersCount int64 `json:"holdersCount"` // deprecated // TODO: remove later
|
||||
Extend tokenInfoExtend `json:"extend"`
|
||||
}
|
||||
|
||||
@@ -103,7 +113,7 @@ func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
|
||||
var ok bool
|
||||
runeId, ok = h.resolveRuneId(ctx.UserContext(), req.Id)
|
||||
if !ok {
|
||||
return errs.NewPublicError("unable to resolve rune id from \"id\"")
|
||||
return errs.NewPublicError(fmt.Sprintf("unable to resolve rune id \"%s\" from \"id\"", req.Id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,71 +122,78 @@ func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
return errs.NewPublicError("rune not found")
|
||||
}
|
||||
return errors.Wrap(err, "error during GetTokenInfoByHeight")
|
||||
return errors.Wrap(err, "error during GetRuneEntryByRuneIdAndHeight")
|
||||
}
|
||||
holdingBalances, err := h.usecase.GetBalancesByRuneId(ctx.UserContext(), runeId, blockHeight, -1, 0) // get all balances
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
return errs.NewPublicError("rune not found")
|
||||
var holdersCountPtr *int64
|
||||
if lo.Contains(req.AdditionalFields, "holdersCount") {
|
||||
holdersCount, err := h.usecase.GetTotalHoldersByRuneId(ctx.UserContext(), runeId, blockHeight)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
return errs.NewPublicError("rune not found")
|
||||
}
|
||||
return errors.Wrap(err, "error during GetBalancesByRuneId")
|
||||
}
|
||||
return errors.Wrap(err, "error during GetBalancesByRuneId")
|
||||
holdersCountPtr = &holdersCount
|
||||
}
|
||||
|
||||
holdingBalances = lo.Filter(holdingBalances, func(b *entity.Balance, _ int) bool {
|
||||
return !b.Amount.IsZero()
|
||||
})
|
||||
// sort by amount descending
|
||||
slices.SortFunc(holdingBalances, func(i, j *entity.Balance) int {
|
||||
return j.Amount.Cmp(i.Amount)
|
||||
})
|
||||
|
||||
totalSupply, err := runeEntry.Supply()
|
||||
result, err := createTokenInfoResult(runeEntry, holdersCountPtr)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot get total supply of rune")
|
||||
return errors.Wrap(err, "error during createTokenInfoResult")
|
||||
}
|
||||
mintedAmount, err := runeEntry.MintedAmount()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot get minted amount of rune")
|
||||
}
|
||||
circulatingSupply := mintedAmount.Sub(runeEntry.BurnedAmount)
|
||||
|
||||
terms := lo.FromPtr(runeEntry.Terms)
|
||||
resp := getTokenInfoResponse{
|
||||
Result: &getTokenInfoResult{
|
||||
Id: runeId,
|
||||
Name: runeEntry.SpacedRune,
|
||||
Symbol: string(runeEntry.Symbol),
|
||||
TotalSupply: totalSupply,
|
||||
CirculatingSupply: circulatingSupply,
|
||||
MintedAmount: mintedAmount,
|
||||
BurnedAmount: runeEntry.BurnedAmount,
|
||||
Decimals: runeEntry.Divisibility,
|
||||
DeployedAt: runeEntry.EtchedAt.Unix(),
|
||||
DeployedAtHeight: runeEntry.EtchingBlock,
|
||||
CompletedAt: lo.Ternary(runeEntry.CompletedAt.IsZero(), nil, lo.ToPtr(runeEntry.CompletedAt.Unix())),
|
||||
CompletedAtHeight: runeEntry.CompletedAtHeight,
|
||||
HoldersCount: len(holdingBalances),
|
||||
Extend: tokenInfoExtend{
|
||||
Entry: entry{
|
||||
Divisibility: runeEntry.Divisibility,
|
||||
Premine: runeEntry.Premine,
|
||||
Rune: runeEntry.SpacedRune.Rune,
|
||||
Spacers: runeEntry.SpacedRune.Spacers,
|
||||
Symbol: string(runeEntry.Symbol),
|
||||
Terms: entryTerms{
|
||||
Amount: lo.FromPtr(terms.Amount),
|
||||
Cap: lo.FromPtr(terms.Cap),
|
||||
HeightStart: terms.HeightStart,
|
||||
HeightEnd: terms.HeightEnd,
|
||||
OffsetStart: terms.OffsetStart,
|
||||
OffsetEnd: terms.OffsetEnd,
|
||||
},
|
||||
Turbo: runeEntry.Turbo,
|
||||
EtchingTxHash: runeEntry.EtchingTxHash.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
Result: result,
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(resp))
|
||||
}
|
||||
|
||||
func createTokenInfoResult(runeEntry *runes.RuneEntry, holdersCount *int64) (*getTokenInfoResult, error) {
|
||||
totalSupply, err := runeEntry.Supply()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot get total supply of rune")
|
||||
}
|
||||
mintedAmount, err := runeEntry.MintedAmount()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot get minted amount of rune")
|
||||
}
|
||||
circulatingSupply := mintedAmount.Sub(runeEntry.BurnedAmount)
|
||||
|
||||
terms := lo.FromPtr(runeEntry.Terms)
|
||||
|
||||
return &getTokenInfoResult{
|
||||
Id: runeEntry.RuneId,
|
||||
Name: runeEntry.SpacedRune,
|
||||
Symbol: string(runeEntry.Symbol),
|
||||
TotalSupply: totalSupply,
|
||||
CirculatingSupply: circulatingSupply,
|
||||
MintedAmount: mintedAmount,
|
||||
BurnedAmount: runeEntry.BurnedAmount,
|
||||
Decimals: runeEntry.Divisibility,
|
||||
DeployedAt: runeEntry.EtchedAt.Unix(),
|
||||
DeployedAtHeight: runeEntry.EtchingBlock,
|
||||
CompletedAt: lo.Ternary(runeEntry.CompletedAt.IsZero(), nil, lo.ToPtr(runeEntry.CompletedAt.Unix())),
|
||||
CompletedAtHeight: runeEntry.CompletedAtHeight,
|
||||
HoldersCount: lo.FromPtr(holdersCount),
|
||||
Extend: tokenInfoExtend{
|
||||
HoldersCount: holdersCount,
|
||||
Entry: entry{
|
||||
Divisibility: runeEntry.Divisibility,
|
||||
Premine: runeEntry.Premine,
|
||||
Rune: runeEntry.SpacedRune.Rune,
|
||||
Spacers: runeEntry.SpacedRune.Spacers,
|
||||
Symbol: string(runeEntry.Symbol),
|
||||
Terms: entryTerms{
|
||||
Amount: lo.FromPtr(terms.Amount),
|
||||
Cap: lo.FromPtr(terms.Cap),
|
||||
HeightStart: terms.HeightStart,
|
||||
HeightEnd: terms.HeightEnd,
|
||||
OffsetStart: terms.OffsetStart,
|
||||
OffsetEnd: terms.OffsetEnd,
|
||||
},
|
||||
Turbo: runeEntry.Turbo,
|
||||
EtchingTxHash: runeEntry.EtchingTxHash.String(),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
118
modules/runes/api/httphandler/get_token_info_batch.go
Normal file
118
modules/runes/api/httphandler/get_token_info_batch.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"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 getTokenInfoBatchRequest struct {
|
||||
Ids []string `json:"ids"`
|
||||
BlockHeight uint64 `json:"blockHeight"`
|
||||
AdditionalFields []string `json:"additionalFields"`
|
||||
}
|
||||
|
||||
const getTokenInfoBatchMaxQueries = 100
|
||||
|
||||
func (r *getTokenInfoBatchRequest) Validate() error {
|
||||
var errList []error
|
||||
|
||||
if len(r.Ids) == 0 {
|
||||
errList = append(errList, errors.New("ids cannot be empty"))
|
||||
}
|
||||
if len(r.Ids) > getTokenInfoBatchMaxQueries {
|
||||
errList = append(errList, errors.Errorf("cannot query more than %d ids", getTokenInfoBatchMaxQueries))
|
||||
}
|
||||
for i := range r.Ids {
|
||||
id, err := url.QueryUnescape(r.Ids[i])
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
r.Ids[i] = id
|
||||
if !isRuneIdOrRuneName(r.Ids[i]) {
|
||||
errList = append(errList, errors.Errorf("ids[%d]: id '%s' is not valid rune id or rune name", i, r.Ids[i]))
|
||||
}
|
||||
}
|
||||
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
type getTokenInfoBatchResult struct {
|
||||
List []*getTokenInfoResult `json:"list"`
|
||||
}
|
||||
type getTokenInfoBatchResponse = HttpResponse[getTokenInfoBatchResult]
|
||||
|
||||
func (h *HttpHandler) GetTokenInfoBatch(ctx *fiber.Ctx) (err error) {
|
||||
var req getTokenInfoBatchRequest
|
||||
if err := ctx.BodyParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
blockHeight := req.BlockHeight
|
||||
if blockHeight == 0 {
|
||||
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
return errs.NewPublicError("latest block not found")
|
||||
}
|
||||
return errors.Wrap(err, "error during GetLatestBlock")
|
||||
}
|
||||
blockHeight = uint64(blockHeader.Height)
|
||||
}
|
||||
|
||||
runeIds := make([]runes.RuneId, 0)
|
||||
for i, id := range req.Ids {
|
||||
runeId, ok := h.resolveRuneId(ctx.UserContext(), id)
|
||||
if !ok {
|
||||
return errs.NewPublicError(fmt.Sprintf("unable to resolve rune id \"%s\" from \"ids[%d]\"", id, i))
|
||||
}
|
||||
runeIds = append(runeIds, runeId)
|
||||
}
|
||||
|
||||
runeEntries, err := h.usecase.GetRuneEntryByRuneIdAndHeightBatch(ctx.UserContext(), runeIds, blockHeight)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetRuneEntryByRuneIdAndHeightBatch")
|
||||
}
|
||||
holdersCounts := make(map[runes.RuneId]int64)
|
||||
if lo.Contains(req.AdditionalFields, "holdersCount") {
|
||||
holdersCounts, err = h.usecase.GetTotalHoldersByRuneIds(ctx.UserContext(), runeIds, blockHeight)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetBalancesByRuneId")
|
||||
}
|
||||
}
|
||||
|
||||
results := make([]*getTokenInfoResult, 0, len(runeIds))
|
||||
|
||||
for _, runeId := range runeIds {
|
||||
runeEntry, ok := runeEntries[runeId]
|
||||
if !ok {
|
||||
return errs.NewPublicError(fmt.Sprintf("rune not found: %s", runeId))
|
||||
}
|
||||
var holdersCount *int64
|
||||
if lo.Contains(req.AdditionalFields, "holdersCount") {
|
||||
holdersCount = lo.ToPtr(holdersCounts[runeId])
|
||||
}
|
||||
|
||||
result, err := createTokenInfoResult(runeEntry, holdersCount)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during createTokenInfoResult")
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
resp := getTokenInfoBatchResponse{
|
||||
Result: &getTokenInfoBatchResult{
|
||||
List: results,
|
||||
},
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(resp))
|
||||
}
|
||||
@@ -32,22 +32,31 @@ func (s GetTokensScope) IsValid() bool {
|
||||
|
||||
type getTokensRequest struct {
|
||||
paginationRequest
|
||||
Search string `query:"search"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
Scope GetTokensScope `query:"scope"`
|
||||
Search string `query:"search"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
Scope GetTokensScope `query:"scope"`
|
||||
AdditionalFieldsRaw string `query:"additionalFields"` // comma-separated list of additional fields
|
||||
AdditionalFields []string
|
||||
}
|
||||
|
||||
func (req getTokensRequest) Validate() error {
|
||||
func (r *getTokensRequest) Validate() error {
|
||||
var errList []error
|
||||
if err := req.paginationRequest.Validate(); err != nil {
|
||||
if err := r.paginationRequest.Validate(); err != nil {
|
||||
errList = append(errList, err)
|
||||
}
|
||||
if req.Limit > getTokensMaxLimit {
|
||||
if r.Limit > getTokensMaxLimit {
|
||||
errList = append(errList, errors.Errorf("limit must be less than or equal to 1000"))
|
||||
}
|
||||
if req.Scope != "" && !req.Scope.IsValid() {
|
||||
errList = append(errList, errors.Errorf("invalid scope: %s", req.Scope))
|
||||
if r.Scope != "" && !r.Scope.IsValid() {
|
||||
errList = append(errList, errors.Errorf("invalid scope: %s", r.Scope))
|
||||
}
|
||||
|
||||
if r.AdditionalFieldsRaw == "" {
|
||||
// temporarily set default value for backward compatibility
|
||||
r.AdditionalFieldsRaw = "holdersCount" // TODO: remove this default value after all clients are updated
|
||||
}
|
||||
r.AdditionalFields = strings.Split(r.AdditionalFieldsRaw, ",")
|
||||
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
@@ -62,7 +71,7 @@ func (req *getTokensRequest) ParseDefault() error {
|
||||
}
|
||||
|
||||
type getTokensResult struct {
|
||||
List []getTokenInfoResult `json:"list"`
|
||||
List []*getTokenInfoResult `json:"list"`
|
||||
}
|
||||
|
||||
type getTokensResponse = HttpResponse[getTokensResult]
|
||||
@@ -111,62 +120,31 @@ func (h *HttpHandler) GetTokens(ctx *fiber.Ctx) (err error) {
|
||||
}
|
||||
|
||||
runeIds := lo.Map(entries, func(item *runes.RuneEntry, _ int) runes.RuneId { return item.RuneId })
|
||||
totalHolders, err := h.usecase.GetTotalHoldersByRuneIds(ctx.UserContext(), runeIds, blockHeight)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetTotalHoldersByRuneIds")
|
||||
holdersCounts := make(map[runes.RuneId]int64)
|
||||
if lo.Contains(req.AdditionalFields, "holdersCount") {
|
||||
holdersCounts, err = h.usecase.GetTotalHoldersByRuneIds(ctx.UserContext(), runeIds, blockHeight)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetTotalHoldersByRuneIds")
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]getTokenInfoResult, 0, len(entries))
|
||||
results := make([]*getTokenInfoResult, 0, len(entries))
|
||||
for _, ent := range entries {
|
||||
totalSupply, err := ent.Supply()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot get total supply of rune")
|
||||
var holdersCount *int64
|
||||
if lo.Contains(req.AdditionalFields, "holdersCount") {
|
||||
holdersCount = lo.ToPtr(holdersCounts[ent.RuneId])
|
||||
}
|
||||
mintedAmount, err := ent.MintedAmount()
|
||||
result, err := createTokenInfoResult(ent, holdersCount)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot get minted amount of rune")
|
||||
return errors.Wrap(err, "error during createTokenInfoResult")
|
||||
}
|
||||
circulatingSupply := mintedAmount.Sub(ent.BurnedAmount)
|
||||
|
||||
terms := lo.FromPtr(ent.Terms)
|
||||
result = append(result, getTokenInfoResult{
|
||||
Id: ent.RuneId,
|
||||
Name: ent.SpacedRune,
|
||||
Symbol: string(ent.Symbol),
|
||||
TotalSupply: totalSupply,
|
||||
CirculatingSupply: circulatingSupply,
|
||||
MintedAmount: mintedAmount,
|
||||
BurnedAmount: ent.BurnedAmount,
|
||||
Decimals: ent.Divisibility,
|
||||
DeployedAt: ent.EtchedAt.Unix(),
|
||||
DeployedAtHeight: ent.EtchingBlock,
|
||||
CompletedAt: lo.Ternary(ent.CompletedAt.IsZero(), nil, lo.ToPtr(ent.CompletedAt.Unix())),
|
||||
CompletedAtHeight: ent.CompletedAtHeight,
|
||||
HoldersCount: int(totalHolders[ent.RuneId]),
|
||||
Extend: tokenInfoExtend{
|
||||
Entry: entry{
|
||||
Divisibility: ent.Divisibility,
|
||||
Premine: ent.Premine,
|
||||
Rune: ent.SpacedRune.Rune,
|
||||
Spacers: ent.SpacedRune.Spacers,
|
||||
Symbol: string(ent.Symbol),
|
||||
Terms: entryTerms{
|
||||
Amount: lo.FromPtr(terms.Amount),
|
||||
Cap: lo.FromPtr(terms.Cap),
|
||||
HeightStart: terms.HeightStart,
|
||||
HeightEnd: terms.HeightEnd,
|
||||
OffsetStart: terms.OffsetStart,
|
||||
OffsetEnd: terms.OffsetEnd,
|
||||
},
|
||||
Turbo: ent.Turbo,
|
||||
},
|
||||
},
|
||||
})
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(getTokensResponse{
|
||||
Result: &getTokensResult{
|
||||
List: result,
|
||||
List: results,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ func (h *HttpHandler) GetTransactions(ctx *fiber.Ctx) (err error) {
|
||||
var ok bool
|
||||
runeId, ok = h.resolveRuneId(ctx.UserContext(), req.Id)
|
||||
if !ok {
|
||||
return errs.NewPublicError("unable to resolve rune id from \"id\"")
|
||||
return errs.NewPublicError(fmt.Sprintf("unable to resolve rune id \"%s\" from \"id\"", req.Id))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ func (h *HttpHandler) Mount(router fiber.Router) error {
|
||||
r.Get("/transactions", h.GetTransactions)
|
||||
r.Get("/transactions/hash/:hash", h.GetTransactionByHash)
|
||||
r.Get("/holders/:id", h.GetHolders)
|
||||
r.Post("/info/batch", h.GetTokenInfoBatch)
|
||||
r.Get("/info/:id", h.GetTokenInfo)
|
||||
r.Get("/utxos/wallet/:wallet", h.GetUTXOs)
|
||||
r.Post("/utxos/output/batch", h.GetUTXOsOutputByLocationBatch)
|
||||
|
||||
@@ -33,3 +33,13 @@ func (u *Usecase) GetTotalHoldersByRuneIds(ctx context.Context, runeIds []runes.
|
||||
}
|
||||
return holders, nil
|
||||
}
|
||||
|
||||
func (u *Usecase) GetTotalHoldersByRuneId(ctx context.Context, runeId runes.RuneId, blockHeight uint64) (int64, error) {
|
||||
holders, err := u.runesDg.GetTotalHoldersByRuneIds(ctx, []runes.RuneId{runeId}, blockHeight)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to get total holders by rune ids")
|
||||
}
|
||||
|
||||
// defaults to zero holders if not found
|
||||
return holders[runeId], nil
|
||||
}
|
||||
|
||||
@@ -40,11 +40,11 @@ func (u *Usecase) GetRuneEntryByRuneIdAndHeight(ctx context.Context, runeId rune
|
||||
}
|
||||
|
||||
func (u *Usecase) GetRuneEntryByRuneIdAndHeightBatch(ctx context.Context, runeIds []runes.RuneId, blockHeight uint64) (map[runes.RuneId]*runes.RuneEntry, error) {
|
||||
runeEntry, err := u.runesDg.GetRuneEntryByRuneIdAndHeightBatch(ctx, runeIds, blockHeight)
|
||||
runeEntries, err := u.runesDg.GetRuneEntryByRuneIdAndHeightBatch(ctx, runeIds, blockHeight)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get rune entries by rune ids and height")
|
||||
}
|
||||
return runeEntry, nil
|
||||
return runeEntries, nil
|
||||
}
|
||||
|
||||
func (u *Usecase) GetRuneEntries(ctx context.Context, search string, blockHeight uint64, limit, offset int32) ([]*runes.RuneEntry, error) {
|
||||
|
||||
191
pkg/bip322/bip322.go
Normal file
191
pkg/bip322/bip322.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package bip322
|
||||
|
||||
// This package is forked from https://github.com/unisat-wallet/libbrc20-indexer/blob/v1.1.0/utils/bip322/verify.go,
|
||||
// with a few modifications to make the interface more friendly with Gaze types.
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/pkg/btcutils"
|
||||
)
|
||||
|
||||
func GetSha256(data []byte) (hash []byte) {
|
||||
sha := sha256.New()
|
||||
sha.Write(data[:])
|
||||
hash = sha.Sum(nil)
|
||||
return
|
||||
}
|
||||
|
||||
func GetTagSha256(data []byte) (hash []byte) {
|
||||
tag := []byte("BIP0322-signed-message")
|
||||
hashTag := GetSha256(tag)
|
||||
var msg []byte
|
||||
msg = append(msg, hashTag...)
|
||||
msg = append(msg, hashTag...)
|
||||
msg = append(msg, data...)
|
||||
return GetSha256(msg)
|
||||
}
|
||||
|
||||
func PrepareTx(pkScript []byte, message string) (toSign *wire.MsgTx, err error) {
|
||||
// Create a new transaction to spend
|
||||
toSpend := wire.NewMsgTx(0)
|
||||
|
||||
// Decode the message hash
|
||||
messageHash := GetTagSha256([]byte(message))
|
||||
|
||||
// Create the script for to_spend
|
||||
builder := txscript.NewScriptBuilder()
|
||||
builder.AddOp(txscript.OP_0)
|
||||
builder.AddData(messageHash)
|
||||
scriptSig, err := builder.Script()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Create a TxIn with the outpoint 000...000:FFFFFFFF
|
||||
prevOutHash, _ := chainhash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000000")
|
||||
prevOut := wire.NewOutPoint(prevOutHash, wire.MaxPrevOutIndex)
|
||||
txIn := wire.NewTxIn(prevOut, scriptSig, nil)
|
||||
txIn.Sequence = 0
|
||||
|
||||
toSpend.AddTxIn(txIn)
|
||||
toSpend.AddTxOut(wire.NewTxOut(0, pkScript))
|
||||
|
||||
// Create a transaction for to_sign
|
||||
toSign = wire.NewMsgTx(0)
|
||||
hash := toSpend.TxHash()
|
||||
|
||||
prevOutSpend := wire.NewOutPoint((*chainhash.Hash)(hash.CloneBytes()), 0)
|
||||
|
||||
txSignIn := wire.NewTxIn(prevOutSpend, nil, nil)
|
||||
txSignIn.Sequence = 0
|
||||
toSign.AddTxIn(txSignIn)
|
||||
|
||||
// Create the script for to_sign
|
||||
builderPk := txscript.NewScriptBuilder()
|
||||
builderPk.AddOp(txscript.OP_RETURN)
|
||||
scriptPk, err := builderPk.Script()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
toSign.AddTxOut(wire.NewTxOut(0, scriptPk))
|
||||
return toSign, nil
|
||||
}
|
||||
|
||||
func VerifyMessage(address *btcutils.Address, signature []byte, message string) bool {
|
||||
if len(signature) == 0 {
|
||||
// empty signature is invalid
|
||||
return false
|
||||
}
|
||||
|
||||
// BIP322 signature format is the serialized witness of the toSign transaction.
|
||||
// [0x02] [SIGNATURE_LEN, ...(signature that go into witness[0])] [PUBLIC_KEY_LEN, ...(public key that was used to sign the message, go to witness[1])]
|
||||
witness, err := DeserializeWitnessSignature(signature)
|
||||
if err != nil {
|
||||
// invalid signature
|
||||
return false
|
||||
}
|
||||
|
||||
return verifySignatureWitness(witness, address.ScriptPubKey(), message)
|
||||
}
|
||||
|
||||
// verifySignatureWitness
|
||||
// signature: 64B, pkScript: 33B, message: any
|
||||
func verifySignatureWitness(witness wire.TxWitness, pkScript []byte, message string) bool {
|
||||
toSign, err := PrepareTx(pkScript, message)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
toSign.TxIn[0].Witness = witness
|
||||
prevFetcher := txscript.NewCannedPrevOutputFetcher(
|
||||
pkScript, 0,
|
||||
)
|
||||
hashCache := txscript.NewTxSigHashes(toSign, prevFetcher)
|
||||
vm, err := txscript.NewEngine(pkScript, toSign, 0, txscript.StandardVerifyFlags, nil, hashCache, 0, prevFetcher)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if err := vm.Execute(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func SignMessage(privateKey *btcec.PrivateKey, address *btcutils.Address, message string) ([]byte, error) {
|
||||
var witness wire.TxWitness
|
||||
var err error
|
||||
switch address.Type() {
|
||||
case btcutils.AddressP2TR:
|
||||
witness, _, err = SignSignatureTaproot(privateKey, message)
|
||||
case btcutils.AddressP2WPKH:
|
||||
witness, _, err = SignSignatureP2WPKH(privateKey, message)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
signature, err := SerializeWitnessSignature(witness)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
return signature, nil
|
||||
}
|
||||
|
||||
func SignSignatureTaproot(privKey *btcec.PrivateKey, message string) (witness wire.TxWitness, pkScript []byte, err error) {
|
||||
pubKey := txscript.ComputeTaprootKeyNoScript(privKey.PubKey())
|
||||
|
||||
pkScript, err = PayToTaprootScript(pubKey)
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
toSign, err := PrepareTx(pkScript, message)
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
prevFetcher := txscript.NewCannedPrevOutputFetcher(
|
||||
pkScript, 0,
|
||||
)
|
||||
sigHashes := txscript.NewTxSigHashes(toSign, prevFetcher)
|
||||
|
||||
witness, err = txscript.TaprootWitnessSignature(
|
||||
toSign, sigHashes, 0, 0, pkScript,
|
||||
txscript.SigHashDefault, privKey,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
}
|
||||
return witness, pkScript, nil
|
||||
}
|
||||
|
||||
func SignSignatureP2WPKH(privKey *btcec.PrivateKey, message string) (witness wire.TxWitness, pkScript []byte, err error) {
|
||||
pubKey := privKey.PubKey()
|
||||
pkScript, err = PayToWitnessScript(pubKey)
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
toSign, err := PrepareTx(pkScript, message)
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
prevFetcher := txscript.NewCannedPrevOutputFetcher(
|
||||
pkScript, 0,
|
||||
)
|
||||
sigHashes := txscript.NewTxSigHashes(toSign, prevFetcher)
|
||||
|
||||
witness, err = txscript.WitnessSignature(toSign, sigHashes,
|
||||
0, 0, pkScript, txscript.SigHashAll,
|
||||
privKey, true)
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
}
|
||||
return witness, pkScript, nil
|
||||
}
|
||||
145
pkg/bip322/bip322_test.go
Normal file
145
pkg/bip322/bip322_test.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package bip322
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/gaze-network/indexer-network/pkg/btcutils"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVerifyMessage(t *testing.T) {
|
||||
type testcase struct {
|
||||
Address string
|
||||
Message string
|
||||
Signature string // base64
|
||||
Expected bool
|
||||
}
|
||||
testcases := []testcase{
|
||||
{
|
||||
Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
|
||||
Message: "",
|
||||
Signature: "AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=",
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
|
||||
Message: "",
|
||||
Signature: "AkgwRQIhAPkJ1Q4oYS0htvyuSFHLxRQpFAY56b70UvE7Dxazen0ZAiAtZfFz1S6T6I23MWI2lK/pcNTWncuyL8UL+oMdydVgzAEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy",
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
|
||||
Message: "Hello World",
|
||||
Signature: "AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=",
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
|
||||
Message: "Hello World",
|
||||
Signature: "AkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy",
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
|
||||
Message: "",
|
||||
Signature: "INVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVA",
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
|
||||
Message: "",
|
||||
Signature: "AkgwRQIhAPkJ1Q4oYS0htvyuSFHLxRQpFAY56b70UvE7Dxazen0ZAiAtZfFz1S6T6I23MWI2lK/pcNTWncuyL8UL+oMdydVgzAEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDLXXXX",
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
|
||||
Message: "Hello World",
|
||||
Signature: "BkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDLXXXX",
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3",
|
||||
Message: "",
|
||||
Signature: "AUDVvVp7mCtPZtoORKYcMM+idx9yy5+z4TGeoI/PWEUscd5x0QYJ6IPQ/anBSMWPWSRPqHVrEjOIWhP9FsZSMFdG",
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3",
|
||||
Message: "",
|
||||
Signature: "AUDYeG/k6AL9pNuhgK8aJqxIqBIObX867yc3QgdfS70sWEdUg0Msv0Ps24Pt5aQmcI2wZdwI3Egp5tA5PW+wTOw6",
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3",
|
||||
Message: "Hello World",
|
||||
Signature: "AUCkOlzIYSN6T+QzENjlp61Pa2l4EyDDH8c4pFANOwoh3oGi/iZHscAExUSePhbS94KIMgcg+yNp+LsckO+AfLQQ",
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3",
|
||||
Message: "Hello World",
|
||||
Signature: "AUD5MwxtURP3tAip3fS5vVRwa4L15wEyTIG0BQ3DPktJpXvQe7Sh8kf+mVaO4ldEP+vhiVZ/sXvOHEbQQnsiYpCq",
|
||||
Expected: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(fmt.Sprintf("%s_%s", tc.Address, tc.Message), func(t *testing.T) {
|
||||
address, err := btcutils.SafeNewAddress(tc.Address)
|
||||
require.NoError(t, err)
|
||||
signature, err := base64.StdEncoding.DecodeString(tc.Signature)
|
||||
require.NoError(t, err)
|
||||
|
||||
verified := VerifyMessage(&address, signature, tc.Message)
|
||||
assert.Equal(t, tc.Expected, verified)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignMessage(t *testing.T) {
|
||||
type testcase struct {
|
||||
PrivateKey *btcec.PrivateKey
|
||||
Address string
|
||||
Message string
|
||||
}
|
||||
|
||||
testcases := []testcase{
|
||||
{
|
||||
PrivateKey: lo.Must(btcutil.DecodeWIF("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k")).PrivKey,
|
||||
Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
|
||||
Message: "",
|
||||
},
|
||||
{
|
||||
PrivateKey: lo.Must(btcutil.DecodeWIF("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k")).PrivKey,
|
||||
Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
|
||||
Message: "Hello World",
|
||||
},
|
||||
{
|
||||
PrivateKey: lo.Must(btcutil.DecodeWIF("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k")).PrivKey,
|
||||
Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3",
|
||||
Message: "",
|
||||
},
|
||||
{
|
||||
PrivateKey: lo.Must(btcutil.DecodeWIF("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k")).PrivKey,
|
||||
Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3",
|
||||
Message: "Hello World",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(fmt.Sprintf("%s_%s", tc.Address, tc.Message), func(t *testing.T) {
|
||||
address, err := btcutils.SafeNewAddress(tc.Address)
|
||||
require.NoError(t, err)
|
||||
signature, err := SignMessage(tc.PrivateKey, &address, tc.Message)
|
||||
require.NoError(t, err)
|
||||
|
||||
verified := VerifyMessage(&address, signature, tc.Message)
|
||||
assert.True(t, verified)
|
||||
})
|
||||
}
|
||||
}
|
||||
77
pkg/bip322/bip322_util.go
Normal file
77
pkg/bip322/bip322_util.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package bip322
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
)
|
||||
|
||||
func SerializeWitnessSignature(witness wire.TxWitness) ([]byte, error) {
|
||||
result := new(bytes.Buffer)
|
||||
buf := make([]byte, 8)
|
||||
|
||||
if err := wire.WriteVarIntBuf(result, 0, uint64(len(witness)), buf); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
for _, item := range witness {
|
||||
if err := wire.WriteVarBytesBuf(result, 0, item, buf); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
return result.Bytes(), nil
|
||||
}
|
||||
|
||||
func DeserializeWitnessSignature(serialized []byte) (wire.TxWitness, error) {
|
||||
if len(serialized) == 0 {
|
||||
return nil, errors.Wrap(errs.ArgumentRequired, "serialized witness is required")
|
||||
}
|
||||
witness := make(wire.TxWitness, 0)
|
||||
|
||||
current := 0
|
||||
witnessLen := int(serialized[current])
|
||||
current++
|
||||
for i := 0; i < witnessLen; i++ {
|
||||
if current >= len(serialized) {
|
||||
return nil, errors.Wrap(errs.InvalidArgument, "invalid serialized witness data: not enough bytes")
|
||||
}
|
||||
witnessItemLen := int(serialized[current])
|
||||
current++
|
||||
if current+witnessItemLen > len(serialized) {
|
||||
return nil, errors.Wrap(errs.InvalidArgument, "invalid serialized witness data: not enough bytes")
|
||||
}
|
||||
witnessItem := serialized[current : current+witnessItemLen]
|
||||
current += witnessItemLen
|
||||
witness = append(witness, witnessItem)
|
||||
}
|
||||
return witness, nil
|
||||
}
|
||||
|
||||
// PayToTaprootScript creates a pk script for a pay-to-taproot output key.
|
||||
func PayToTaprootScript(taprootKey *btcec.PublicKey) ([]byte, error) {
|
||||
script, err := txscript.NewScriptBuilder().
|
||||
AddOp(txscript.OP_1).
|
||||
AddData(schnorr.SerializePubKey(taprootKey)).
|
||||
Script()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
return script, nil
|
||||
}
|
||||
|
||||
// PayToWitnessScript creates a pk script for a pay-to-wpkh output key.
|
||||
func PayToWitnessScript(pubkey *btcec.PublicKey) ([]byte, error) {
|
||||
script, err := txscript.NewScriptBuilder().
|
||||
AddOp(txscript.OP_0).
|
||||
AddData(btcutil.Hash160(pubkey.SerializeCompressed())).
|
||||
Script()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
return script, nil
|
||||
}
|
||||
Reference in New Issue
Block a user