Skip to content

Commit

Permalink
feat: add ordinal endpoint(ordinal service + unisat) and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremy-babylonlabs authored and jrwbabylonlab committed Jul 24, 2024
1 parent 618f311 commit 4a53add
Show file tree
Hide file tree
Showing 33 changed files with 1,279 additions and 68 deletions.
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ run-unprocessed-events-replay-local:
--replay

generate-mock-interface:
cd internal/db && mockery --name=DBClient --output=../../tests/mocks --outpkg=dbmock --filename=mock_db_client.go
cd internal/db && mockery --name=DBClient --output=../../tests/mocks --outpkg=mocks --filename=mock_db_client.go
cd internal/clients/ordinals && mockery --name=OrdinalsClientInterface --output=../../../tests/mocks --outpkg=mocks --filename=mock_ordinal_client.go
cd internal/clients/unisat && mockery --name=UnisatClientInterface --output=../../../tests/mocks --outpkg=mocks --filename=mock_unisat_client.go

tests:
./bin/local-startup.sh;
Expand Down
8 changes: 6 additions & 2 deletions cmd/staking-api-service/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/babylonchain/staking-api-service/cmd/staking-api-service/cli"
"github.com/babylonchain/staking-api-service/cmd/staking-api-service/scripts"
"github.com/babylonchain/staking-api-service/internal/api"
"github.com/babylonchain/staking-api-service/internal/clients"
"github.com/babylonchain/staking-api-service/internal/config"
"github.com/babylonchain/staking-api-service/internal/db/model"
"github.com/babylonchain/staking-api-service/internal/observability/metrics"
Expand Down Expand Up @@ -58,12 +59,15 @@ func main() {
if err != nil {
log.Fatal().Err(err).Msg("error while setting up staking db model")
}
services, err := services.New(ctx, cfg, params, finalityProviders)

// initialize clients package which is used to interact with external services
clients := clients.New(cfg)
services, err := services.New(ctx, cfg, params, finalityProviders, clients)
if err != nil {
log.Fatal().Err(err).Msg("error while setting up staking services layer")
}
// Start the event queue processing
queues := queue.New(&cfg.Queue, services)
queues := queue.New(cfg.Queue, services)

// Check if the replay flag is set
if cli.GetReplayFlag() {
Expand Down
13 changes: 12 additions & 1 deletion config/config-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ server:
write-timeout: 60s
read-timeout: 60s
idle-timeout: 60s
allowed-origins: [ "*" ]
allowed-origins: ["*"]
log-level: debug
btc-net: "mainnet"
max-content-length: 4096
Expand All @@ -27,3 +27,14 @@ queue:
metrics:
host: 0.0.0.0
port: 2112
assets:
max_utxos: 100
ordinals:
host: "http://ord-poc.devnet.babylonchain.io"
port: 8888
timeout: 1000
unisat:
host: "https://open-api.unisat.io"
limit: 100
timeout: 1000
token: "add your token as ASSETS_UNISAT_TOKEN in environment variables"
13 changes: 12 additions & 1 deletion config/config-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ server:
write-timeout: 60s
read-timeout: 60s
idle-timeout: 60s
allowed-origins: [ "*" ]
allowed-origins: ["*"]
log-level: debug
btc-net: "signet"
max-content-length: 4096
Expand All @@ -27,3 +27,14 @@ queue:
metrics:
host: 0.0.0.0
port: 2112
assets:
max_utxos: 100
ordinals:
host: "http://ord-poc.devnet.babylonchain.io"
port: 8888
timeout: 5000
unisat:
host: "https://open-api-testnet.unisat.io"
limit: 100
timeout: 5000
token: "add your token as ASSETS_UNISAT_TOKEN in .env"
8 changes: 6 additions & 2 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,14 +622,18 @@ const docTemplate = `{
"VALIDATION_ERROR",
"NOT_FOUND",
"BAD_REQUEST",
"FORBIDDEN"
"FORBIDDEN",
"UNPROCESSABLE_ENTITY",
"REQUEST_TIMEOUT"
],
"x-enum-varnames": [
"InternalServiceError",
"ValidationError",
"NotFound",
"BadRequest",
"Forbidden"
"Forbidden",
"UnprocessableEntity",
"RequestTimeout"
]
}
}
Expand Down
55 changes: 55 additions & 0 deletions docs/ordinals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Ordinals in API system

The Babylon Staking API allows for the option to deploy additional endpoints
that check whether a UTXO contains an inscription or not, with the aim to help
staking applications identify whether they should avoid spending a particular UTXO.
This is accomplished through a connection to the
[Ordinal Service](https://github.com/ordinals/ord) and
a connection to the Unisat API.
Due to Unisat being a payed service and applying rate limits,
the API initially tries to get the status of a UTXO through the Ordinals Service,
and if that fails, then contacts the Unisat API,
effectively using it as a back-up mechanism to handle downtime from the Ordinals Service.

NOTE: To enable the optional ordinal API endpoint, you will need to provide the
`ordinal` and `unisat` configurations under `assets`

## Ordinal service Client

WIP

## Unisat Service Client

You can find more information about Unisat's Ordinal/BRC-20/Runes related endpoints at:
https://docs.unisat.io/

In our service, we only utilize the following endpoint:
- `/v1/indexer/address/{{address}}/inscription-utxo-data`

### How to Use It

1. Log in via https://developer.unisat.io/account/login (create an account if you don't have one).
2. Copy the `API-Key`.
3. Set the key as an environment variable named `UNISAT_TOKEN`.
4. Configure the values for `unisat.host`, `limit`, `timeout`, etc. Refer to `config-docker.yml`.
5. Ensure you also set up the `ordinals` configuration, as this is a dependency.
6. Call the POST endpoint `/v1/ordinals/verify-utxos` as shown in the example below:
7. The calls to unisat will only be triggered if the ordinal service is not responding or returning errors
```POST
{
"utxos": [
{
"txid": "143c33b4ff4450a60648aec6b4d086639322cb093195226c641ae4f0ae33c3f5",
"vout": 2
},
{
"txid": "be3877c8dedd716f026cc77ef3f04f940b40b064d1928247cff5bb08ef1ba58e",
"vout": 0
},
{
"txid": "d7f65a37f59088b3b4e4bc119727daa0a0dd8435a645c49e6a665affc109539d",
"vout": 0
}
],
"address": "tb1pyqjxwcdv6pfcaj2l565ludclz2pwu2k5azs6uznz8kml74kkma6qm0gzlv"
}
8 changes: 6 additions & 2 deletions docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -611,14 +611,18 @@
"VALIDATION_ERROR",
"NOT_FOUND",
"BAD_REQUEST",
"FORBIDDEN"
"FORBIDDEN",
"UNPROCESSABLE_ENTITY",
"REQUEST_TIMEOUT"
],
"x-enum-varnames": [
"InternalServiceError",
"ValidationError",
"NotFound",
"BadRequest",
"Forbidden"
"Forbidden",
"UnprocessableEntity",
"RequestTimeout"
]
}
}
Expand Down
4 changes: 4 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -214,13 +214,17 @@ definitions:
- NOT_FOUND
- BAD_REQUEST
- FORBIDDEN
- UNPROCESSABLE_ENTITY
- REQUEST_TIMEOUT
type: string
x-enum-varnames:
- InternalServiceError
- ValidationError
- NotFound
- BadRequest
- Forbidden
- UnprocessableEntity
- RequestTimeout
info:
contact: {}
paths:
Expand Down
57 changes: 57 additions & 0 deletions internal/api/handlers/ordinals.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package handlers

import (
"encoding/json"
"net/http"

"github.com/babylonchain/staking-api-service/internal/types"
"github.com/babylonchain/staking-api-service/internal/utils"
"github.com/btcsuite/btcd/chaincfg"
)

type VerifyUTXOsRequestPayload struct {
Address string `json:"address"`
UTXOs []types.UTXOIdentifier `json:"utxos"`
}

func parseRequestPayload(request *http.Request, maxUTXOs uint32, netParam *chaincfg.Params) (*VerifyUTXOsRequestPayload, *types.Error) {
var payload VerifyUTXOsRequestPayload
if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
return nil, types.NewErrorWithMsg(http.StatusBadRequest, types.BadRequest, "invalid input format")
}
utxos := payload.UTXOs
if len(utxos) == 0 {
return nil, types.NewErrorWithMsg(http.StatusBadRequest, types.BadRequest, "empty UTXO array")
}

if uint32(len(utxos)) > maxUTXOs {
return nil, types.NewErrorWithMsg(http.StatusBadRequest, types.BadRequest, "too many UTXOs in the request")
}

for _, utxo := range utxos {
if !utils.IsValidTxHash(utxo.Txid) {
return nil, types.NewErrorWithMsg(http.StatusBadRequest, types.BadRequest, "invalid UTXO txid")
} else if utxo.Vout < 0 {
return nil, types.NewErrorWithMsg(http.StatusBadRequest, types.BadRequest, "invalid UTXO vout")
}
}

if err := utils.IsValidBtcAddress(payload.Address, netParam); err != nil {
return nil, types.NewErrorWithMsg(http.StatusBadRequest, types.BadRequest, err.Error())
}
return &payload, nil
}

func (h *Handler) VerifyUTXOs(request *http.Request) (*Result, *types.Error) {
inputs, err := parseRequestPayload(request, h.config.Assets.MaxUTXOs, h.config.Server.BTCNetParam)
if err != nil {
return nil, err
}

results, err := h.services.VerifyUTXOs(request.Context(), inputs.UTXOs, inputs.Address)
if err != nil {
return nil, err
}

return NewResult(results), nil
}
6 changes: 6 additions & 0 deletions internal/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,11 @@ func (a *Server) SetupRoutes(r *chi.Mux) {
r.Get("/v1/staker/delegation/check", registerHandler(handlers.CheckStakerDelegationExist))
r.Get("/v1/delegation", registerHandler(handlers.GetDelegationByTxHash))

// Only register these routes if the asset has been configured
// The endpoints are used to check ordinals within the UTXOs
if a.cfg.Assets != nil {
r.Post("/v1/ordinals/verify-utxos", registerHandler(handlers.VerifyUTXOs))
}

r.Get("/swagger/*", httpSwagger.WrapHandler)
}
4 changes: 3 additions & 1 deletion internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
type Server struct {
httpServer *http.Server
handlers *handlers.Handler
cfg *config.Config
}

func New(
Expand Down Expand Up @@ -52,6 +53,7 @@ func New(
server := &Server{
httpServer: srv,
handlers: handlers,
cfg: cfg,
}
server.SetupRoutes(r)
return server, nil
Expand All @@ -60,4 +62,4 @@ func New(
func (a *Server) Start() error {
log.Info().Msgf("Starting server on %s", a.httpServer.Addr)
return a.httpServer.ListenAndServe()
}
}
Loading

0 comments on commit 4a53add

Please sign in to comment.