diff --git a/BUILD.bazel b/BUILD.bazel
index e0f69d7ad0..2d9d2337cd 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -170,6 +170,8 @@ pkg_tar(
"//dispatcher/cmd/dispatcher",
"//gateway/cmd/gateway",
"//hbird/cmd/hummingbird",
+ "//marketplace/cmd/marketplace",
+ "//marketplace_client",
"//router/cmd/router",
"//scion-pki/cmd/scion-pki",
"//scion/cmd/scion",
diff --git a/MODULE.bazel b/MODULE.bazel
index d56460b75c..478c02ccb1 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -60,9 +60,11 @@ use_repo(
"com_github_go_chi_chi_v5",
"com_github_go_chi_cors",
"com_github_go_viper_mapstructure_v2",
+ "com_github_golang_jwt_jwt",
"com_github_golang_mock",
"com_github_google_go_cmp",
"com_github_gopacket_gopacket",
+ "com_github_gorilla_sessions",
"com_github_grpc_ecosystem_go_grpc_middleware",
"com_github_grpc_ecosystem_go_grpc_prometheus",
"com_github_grpc_ecosystem_grpc_opentracing",
diff --git a/go.mod b/go.mod
index 311d839a6b..3d238e09cc 100644
--- a/go.mod
+++ b/go.mod
@@ -61,6 +61,8 @@ require (
zgo.at/zcache/v2 v2.1.0
)
+require github.com/gorilla/securecookie v1.1.2 // indirect
+
require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.9-20250912141014-52f32327d4b0.1 // indirect
buf.build/go/protovalidate v1.0.0 // indirect
@@ -79,9 +81,11 @@ require (
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
+ github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/cel-go v0.26.1 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
+ github.com/gorilla/sessions v1.4.0
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index ea00251b91..1b5892b250 100644
--- a/go.sum
+++ b/go.sum
@@ -97,6 +97,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
+github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@@ -137,6 +139,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopacket/gopacket v1.3.1 h1:ZppWyLrOJNZPe5XkdjLbtuTkfQoxQ0xyMJzQCqtqaPU=
github.com/gopacket/gopacket v1.3.1/go.mod h1:3I13qcqSpB2R9fFQg866OOgzylYkZxLTmkvcXhvf6qg=
+github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
+github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
+github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
+github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
diff --git a/marketplace/BUILD.bazel b/marketplace/BUILD.bazel
new file mode 100644
index 0000000000..fb0722b0bc
--- /dev/null
+++ b/marketplace/BUILD.bazel
@@ -0,0 +1,50 @@
+load("@rules_go//go:def.bzl", "go_library")
+load("//tools:go.bzl", "go_test")
+
+go_library(
+ name = "go_default_library",
+ srcs = [
+ "account.go",
+ "auth.go",
+ "config.go",
+ "redeem.go",
+ "redemption_service.go",
+ "service.go",
+ "trust_db_wrapper.go",
+ ],
+ importpath = "github.com/scionproto/scion/marketplace",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//marketplace/db:go_default_library",
+ "//marketplace/storage:go_default_library",
+ "//pkg/addr:go_default_library",
+ "//pkg/hummingbird/registration:go_default_library",
+ "//pkg/log:go_default_library",
+ "//pkg/private/serrors:go_default_library",
+ "//pkg/proto/hummingbird:go_default_library",
+ "//pkg/scrypto/cppki:go_default_library",
+ "//pkg/slayers/path/hummingbird:go_default_library",
+ "//private/config:go_default_library",
+ "//private/env:go_default_library",
+ "//private/storage:go_default_library",
+ "//private/storage/trust:go_default_library",
+ "//private/trust:go_default_library",
+ "@com_connectrpc_connect//:go_default_library",
+ "@com_github_golang_jwt_jwt//:go_default_library",
+ "@org_golang_google_protobuf//types/known/timestamppb:go_default_library",
+ "@org_golang_x_crypto//bcrypt:go_default_library",
+ ],
+)
+
+go_test(
+ name = "go_default_test",
+ srcs = [
+ "export_test.go",
+ "redemption_service_test.go",
+ ],
+ embed = [":go_default_library"],
+ deps = [
+ "//marketplace/db:go_default_library",
+ "@com_github_stretchr_testify//assert:go_default_library",
+ ],
+)
diff --git a/marketplace/account.go b/marketplace/account.go
new file mode 100644
index 0000000000..9c6c9e079e
--- /dev/null
+++ b/marketplace/account.go
@@ -0,0 +1,198 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package marketplace
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ "connectrpc.com/connect"
+ "github.com/golang-jwt/jwt"
+ "github.com/scionproto/scion/marketplace/db"
+ "github.com/scionproto/scion/pkg/addr"
+ "github.com/scionproto/scion/pkg/hummingbird/registration"
+ "github.com/scionproto/scion/pkg/private/serrors"
+ "github.com/scionproto/scion/pkg/proto/hummingbird"
+ "golang.org/x/crypto/bcrypt"
+)
+
+func AccountManagerInterceptor(verifier *TokenVerifier) connect.UnaryInterceptorFunc {
+ return func(next connect.UnaryFunc) connect.UnaryFunc {
+ return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
+ authority := req.Header().Get(":authority")
+ if authority == "" {
+ authority = req.Header().Get("host")
+ }
+
+ ctx = context.WithValue(ctx, "authority", authority)
+ authHeader := req.Header().Get("Authorization")
+ method := req.Spec().Procedure
+ fmt.Printf("AccountManagerInterceptor Interceptor for %s\n", method)
+ requiredScope, found := methodScopes[method]
+ if !found {
+ //no rules apply
+ fmt.Println("no rules apply for", method)
+ return next(ctx, req)
+ }
+ if strings.HasPrefix(authHeader, "Bearer ") {
+ tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
+ if tokenStr != "" {
+ var err error
+ ctx, err = verifier.contextFromJwt(ctx, tokenStr, requiredScope)
+ if err != nil {
+ return nil, err
+ }
+ }
+ }
+ return next(ctx, req)
+ }
+ }
+}
+
+func (s *Service) CreateChallenge(ctx context.Context, req *connect.Request[hummingbird.CreateChallengeRequest]) (*connect.Response[hummingbird.CreateChallengeResponse], error) {
+ if s.registrationService == nil {
+ return nil, connect.NewError(connect.CodeUnimplemented, serrors.New("Registration not configured"))
+ }
+ challenge, err := s.registrationService.CreateChallenge(ctx, addr.IA(req.Msg.Ia))
+ if err != nil {
+ return nil, connect.NewError(connect.CodeInvalidArgument, err)
+ }
+ return &connect.Response[hummingbird.CreateChallengeResponse]{
+ Msg: challenge,
+ }, nil
+}
+
+func (s *Service) RegisterAS(ctx context.Context, req *connect.Request[hummingbird.RegisterASRequest]) (*connect.Response[hummingbird.RegisterASResponse], error) {
+ if s.registrationService == nil {
+ return nil, connect.NewError(connect.CodeUnimplemented, serrors.New("Registration not configured"))
+ }
+ authority, ok := ctx.Value("authority").(string)
+ if !ok || authority == "" {
+ return nil, connect.NewError(connect.CodeInvalidArgument, serrors.New("Request must provide HTTP Host or HTTP2 :authority header"))
+ }
+ fmt.Println("Register AS", "Authority", authority)
+ ia, err := s.registrationService.RegisterAS(ctx, req.Msg.Id, req.Msg.SignedChallenge, authority)
+ if err != nil {
+ return nil, connect.NewError(connect.CodeInvalidArgument, err)
+ }
+ dbUser, err := s.store.GetASUser(ctx, ia)
+ if err != nil {
+ fmt.Println(err)
+
+ return nil, connect.NewError(connect.CodeInternal, err)
+ }
+ if dbUser == nil {
+ dbUser = &db.DBASUser{
+ IA: ia,
+ TokenVersion: 0,
+ }
+ _, err = s.store.CreateASUser(ctx, dbUser)
+ if err != nil {
+ fmt.Println(err)
+ return nil, connect.NewError(connect.CodeInvalidArgument, err)
+ }
+ }
+ now := time.Now()
+ publisherToken, err := s.signer.GenerateToken(jwt.MapClaims{
+ "sub": ia.String(),
+ "scope": registration.ScopeAssetPublisher,
+ "exp": now.Add(time.Hour * 24 * 7).Unix(),
+ "iat": now.Unix(),
+ "ver": dbUser.TokenVersion,
+ })
+ if err != nil {
+ fmt.Println(err)
+ return nil, connect.NewError(connect.CodeInvalidArgument, err)
+ }
+ redemptionToken, err := s.signer.GenerateToken(jwt.MapClaims{
+ "sub": ia.String(),
+ "scope": registration.ScopeRedemptionService,
+ "exp": now.Add(time.Hour * 24 * 7).Unix(),
+ "iat": now.Unix(),
+ "ver": dbUser.TokenVersion,
+ })
+ if err != nil {
+ fmt.Println(err)
+ return nil, connect.NewError(connect.CodeInvalidArgument, err)
+ }
+ return &connect.Response[hummingbird.RegisterASResponse]{
+ Msg: &hummingbird.RegisterASResponse{
+ JwtPublisher: publisherToken,
+ JwtRedemption: redemptionToken,
+ },
+ }, nil
+}
+
+func (s *Service) ResetJWT(ctx context.Context, req *connect.Request[hummingbird.JWTResetRequest]) (*connect.Response[hummingbird.JWTResetResponse], error) {
+ ver, ok := ctx.Value("ver").(int64)
+ if !ok {
+ return nil, connect.NewError(connect.CodeUnauthenticated, serrors.New("invalid token"))
+ }
+ v := ctx.Value("user")
+ switch x := v.(type) {
+ case nil:
+ return nil, connect.NewError(connect.CodeUnauthenticated, serrors.New("invalid token"))
+ case int64:
+ _, err := s.store.IncrementUserJWTVersion(ctx, x, ver)
+ if err != nil {
+ return nil, connect.NewError(connect.CodeFailedPrecondition, err)
+ }
+ case addr.IA:
+ _, err := s.store.IncrementASJWTVersion(ctx, x, ver)
+ if err != nil {
+ return nil, connect.NewError(connect.CodeFailedPrecondition, err)
+ }
+ kickRedemptionService := func() {
+ s.mtx.Lock()
+ defer s.mtx.Unlock()
+ peer, found := s.redemptionServerPeers[x]
+ if !found {
+ return
+ }
+ peer.mtx.Lock()
+ defer peer.mtx.Unlock()
+ if peer.cancelOldConnection != nil {
+ peer.cancelOldConnection()
+ }
+ }
+ kickRedemptionService()
+ default:
+ return nil, connect.NewError(connect.CodeInvalidArgument, serrors.New("invalid token"))
+ }
+ return &connect.Response[hummingbird.JWTResetResponse]{Msg: &hummingbird.JWTResetResponse{}}, nil
+}
+
+func (s *Service) SetPassword(ctx context.Context, req *connect.Request[hummingbird.SetPasswordRequest]) (*connect.Response[hummingbird.SetPasswordResponse], error) {
+ ia, ok := ctx.Value("user").(addr.IA)
+ if !ok {
+ return nil, connect.NewError(connect.CodeUnauthenticated, serrors.New("invalid token"))
+ }
+ hash, err := bcrypt.GenerateFromPassword(
+ []byte(req.Msg.Password),
+ 12,
+ )
+ if err != nil {
+ return nil, connect.NewError(connect.CodeInvalidArgument, serrors.New("invalid token"))
+ }
+ _, err = s.store.SetASAuthenticationToken(ctx, ia, string(hash))
+ if err != nil {
+ return nil, connect.NewError(connect.CodeInvalidArgument, err)
+ }
+ return &connect.Response[hummingbird.SetPasswordResponse]{
+ Msg: &hummingbird.SetPasswordResponse{},
+ }, nil
+}
diff --git a/marketplace/auth.go b/marketplace/auth.go
new file mode 100644
index 0000000000..7ca7b74c02
--- /dev/null
+++ b/marketplace/auth.go
@@ -0,0 +1,249 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package marketplace
+
+import (
+ "context"
+ "fmt"
+ "strconv"
+ "strings"
+
+ "connectrpc.com/connect"
+ "github.com/golang-jwt/jwt"
+ "github.com/scionproto/scion/marketplace/storage"
+ "github.com/scionproto/scion/pkg/addr"
+ "github.com/scionproto/scion/pkg/hummingbird/registration"
+)
+
+var methodScopes = map[string]string{
+ "/proto.hummingbird.v1.MarketplaceService/PublishAsset": "AssetPublisher",
+ "/proto.hummingbird.v1.MarketplaceService/UpdateAssets": "AssetPublisher",
+ "/proto.hummingbird.v1.MarketplaceService/Statistics": "AssetPublisher",
+ "/proto.hummingbird.v1.MarketplaceService/SearchAssets": "User,AssetPublisher",
+ "/proto.hummingbird.v1.MarketplaceService/SplitAsset": "User",
+ "/proto.hummingbird.v1.MarketplaceService/CombineAssets": "User",
+ "/proto.hummingbird.v1.MarketplaceService/BuyAssets": "User",
+ "/proto.hummingbird.v1.MarketplaceService/FetchReservations": "User",
+ "/proto.hummingbird.v1.MarketplaceService/RedeemAsset": "User",
+ "/proto.hummingbird.v1.RedemptionService/RedeemASAsset": "RedemptionService",
+ "/proto.hummingbird.v1.RedemptionService/DelegateRedemption": "RedemptionService",
+ "/proto.hummingbird.v1.AccountService/ResetJWT": "User,AssetPublisher,RedemptionService",
+ "/proto.hummingbird.v1.AccountService/SetPassword": "AssetPublisher,RedemptionService",
+}
+
+type AuthInterceptor struct {
+ TokenVerifier *TokenVerifier
+}
+
+func NewAuthInterceptor(v *TokenVerifier) *AuthInterceptor {
+ return &AuthInterceptor{
+ TokenVerifier: v,
+ }
+}
+
+type TokenVerifier struct {
+ Store *storage.MarketplaceStorage
+ JWTVerifier *registration.Verifier
+}
+
+func (a *TokenVerifier) verifyTokenVersion(ctx context.Context, user any, claims jwt.MapClaims, scopes map[string]bool) (int64, error) {
+ tokenVersion := int64(0)
+ if scopes["AssetPublisher"] || scopes["RedemptionService"] {
+ dbUser, err := a.Store.GetASUser(ctx, user.(addr.IA))
+ if err != nil {
+ return 0, connect.NewError(
+ connect.CodeUnauthenticated,
+ fmt.Errorf("invalid token"),
+ )
+ }
+ tokenVersion = dbUser.TokenVersion
+ } else if scopes["User"] {
+ dbAccount, err := a.Store.GetAccountByAccountID(ctx, user.(int64))
+ if err != nil {
+ return 0, connect.NewError(
+ connect.CodeUnauthenticated,
+ fmt.Errorf("invalid token"),
+ )
+ }
+ tokenVersion = dbAccount.TokenVersion
+ } else {
+ return 0, connect.NewError(
+ connect.CodeUnauthenticated,
+ fmt.Errorf("invalid token"),
+ )
+ }
+ versionClaim, ok := claims["ver"].(float64)
+ if !ok || int64(versionClaim) < tokenVersion {
+ return 0, connect.NewError(
+ connect.CodeUnauthenticated,
+ fmt.Errorf("invalid token"),
+ )
+ }
+ return int64(versionClaim), nil
+}
+
+func (a *AuthInterceptor) WrapStreamingClient(
+ next connect.StreamingClientFunc,
+) connect.StreamingClientFunc {
+ return next
+}
+
+func (t *TokenVerifier) contextFromJwt(ctx context.Context, tokenStr string, requiredScope string) (context.Context, error) {
+ token, err := t.JWTVerifier.VerifyToken(tokenStr)
+ if err != nil || !token.Valid {
+ fmt.Println("invalid token")
+ return nil, connect.NewError(
+ connect.CodeUnauthenticated,
+ fmt.Errorf("invalid token"),
+ )
+ }
+
+ claims, ok := token.Claims.(jwt.MapClaims)
+ if !ok {
+ return nil, connect.NewError(
+ connect.CodeUnauthenticated,
+ fmt.Errorf("invalid claims"),
+ )
+ }
+
+ user, ok := claims["sub"].(string)
+ if !ok || user == "" {
+ return nil, connect.NewError(
+ connect.CodeUnauthenticated,
+ fmt.Errorf("missing subject"),
+ )
+ }
+ scopeStr, ok := claims["scope"].(string)
+ if !ok || scopeStr == "" {
+ return nil, connect.NewError(
+ connect.CodePermissionDenied,
+ fmt.Errorf("missing scopes"),
+ )
+ }
+ scopes := parseScopes(scopeStr)
+ if requiredScope != "" {
+ found := false
+ for _, r := range strings.Split(requiredScope, ",") {
+ if scopes[r] {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return nil, connect.NewError(connect.CodePermissionDenied,
+ fmt.Errorf("missing scope: %s", requiredScope))
+ }
+ }
+ if scopes["User"] {
+ userid, err := strconv.ParseInt(user, 10, 64)
+ if err != nil {
+ return nil, connect.NewError(connect.CodePermissionDenied,
+ fmt.Errorf("invalid token"))
+ }
+ tokenVer, err := t.verifyTokenVersion(ctx, userid, claims, scopes)
+ if err != nil {
+ return nil, err
+ }
+ ctx = context.WithValue(ctx, "ver", tokenVer)
+ ctx = context.WithValue(ctx, "user", userid)
+ } else {
+ ia, err := addr.ParseIA(user)
+ if err != nil {
+ return nil, connect.NewError(connect.CodePermissionDenied,
+ fmt.Errorf("invalid scope"))
+ }
+ tokenVer, err := t.verifyTokenVersion(ctx, ia, claims, scopes)
+ if err != nil {
+ return nil, err
+ }
+ ctx = context.WithValue(ctx, "ver", tokenVer)
+ ctx = context.WithValue(ctx, "user", ia)
+ }
+
+ return ctx, nil
+}
+
+func (a *AuthInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc {
+ return func(
+ ctx context.Context,
+ req connect.AnyRequest,
+ ) (connect.AnyResponse, error) {
+
+ method := req.Spec().Procedure
+ fmt.Printf("Auth Interceptor for %s\n", method)
+ requiredScope, found := methodScopes[method]
+ if !found {
+ //no rules apply
+ fmt.Println("no rules apply for", method)
+ return next(ctx, req)
+ }
+ authHeader := req.Header().Get("Authorization")
+
+ if !strings.HasPrefix(authHeader, "Bearer ") {
+ return nil, connect.NewError(
+ connect.CodeUnauthenticated,
+ fmt.Errorf("missing bearer token"),
+ )
+ }
+ tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
+ ctx, err := a.TokenVerifier.contextFromJwt(ctx, tokenStr, requiredScope)
+ if err != nil {
+ return nil, err
+ }
+ return next(ctx, req)
+ }
+}
+
+func (a *AuthInterceptor) WrapStreamingHandler(
+ next connect.StreamingHandlerFunc,
+) connect.StreamingHandlerFunc {
+
+ return func(ctx context.Context, conn connect.StreamingHandlerConn) error {
+ method := conn.Spec().Procedure
+ fmt.Printf("Auth Interceptor for %s\n", method)
+ requiredScope, found := methodScopes[method]
+ if !found {
+ //no rules apply
+ fmt.Println("no rules apply for", method)
+ return next(ctx, conn)
+ }
+ authHeader := conn.RequestHeader().Get("Authorization")
+
+ if !strings.HasPrefix(authHeader, "Bearer ") {
+ return connect.NewError(
+ connect.CodeUnauthenticated,
+ fmt.Errorf("missing bearer token"),
+ )
+ }
+
+ tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
+ ctx, err := a.TokenVerifier.contextFromJwt(ctx, tokenStr, requiredScope)
+ if err != nil {
+ return err
+ }
+
+ return next(ctx, conn)
+ }
+}
+
+func parseScopes(scopeStr string) map[string]bool {
+ scopes := make(map[string]bool)
+ for s := range strings.SplitSeq(scopeStr, ",") {
+ if s != "" {
+ scopes[s] = true
+ }
+ }
+ return scopes
+}
diff --git a/marketplace/cmd/marketplace/BUILD.bazel b/marketplace/cmd/marketplace/BUILD.bazel
new file mode 100644
index 0000000000..2e8f0915b0
--- /dev/null
+++ b/marketplace/cmd/marketplace/BUILD.bazel
@@ -0,0 +1,45 @@
+load("@rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["main.go"],
+ importpath = "github.com/scionproto/scion/marketplace/cmd/marketplace",
+ visibility = ["//visibility:private"],
+ deps = [
+ "//marketplace:go_default_library",
+ "//marketplace/storage:go_default_library",
+ "//marketplace/webapp:go_default_library",
+ "//pkg/connect:go_default_library",
+ "//pkg/grpc:go_default_library",
+ "//pkg/hummingbird/registration:go_default_library",
+ "//pkg/log:go_default_library",
+ "//pkg/private/serrors:go_default_library",
+ "//pkg/proto/control_plane:go_default_library",
+ "//pkg/proto/hummingbird:go_default_library",
+ "//pkg/scrypto/cppki:go_default_library",
+ "//pkg/segment/iface:go_default_library",
+ "//pkg/snet:go_default_library",
+ "//pkg/snet/squic:go_default_library",
+ "//private/app:go_default_library",
+ "//private/app/appnet:go_default_library",
+ "//private/app/launcher:go_default_library",
+ "//private/storage:go_default_library",
+ "//private/topology:go_default_library",
+ "//private/trust:go_default_library",
+ "//private/trust/grpc:go_default_library",
+ "//private/trust/happy:go_default_library",
+ "@com_connectrpc_connect//:go_default_library",
+ "@com_github_quic_go_quic_go//:go_default_library",
+ "@com_github_quic_go_quic_go//http3:go_default_library",
+ "@com_github_scionproto_scion//pkg/proto/hummingbird/v1/hummingbirdconnect:go_default_library",
+ "@com_github_scionproto_scion//pkg/proto/control_plane/v1/control_planeconnect:go_default_library",
+ "@org_golang_google_grpc//:go_default_library",
+ "@org_golang_x_sync//errgroup:go_default_library",
+ ],
+)
+
+go_binary(
+ name = "marketplace",
+ embed = [":go_default_library"],
+ visibility = ["//visibility:public"],
+)
diff --git a/marketplace/cmd/marketplace/main.go b/marketplace/cmd/marketplace/main.go
new file mode 100644
index 0000000000..c0791eb513
--- /dev/null
+++ b/marketplace/cmd/marketplace/main.go
@@ -0,0 +1,563 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "context"
+ "crypto/ed25519"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/tls"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "fmt"
+ "math/big"
+ "net"
+ "net/http"
+ "net/netip"
+ "os"
+ "path"
+ "time"
+
+ "connectrpc.com/connect"
+ "golang.org/x/sync/errgroup"
+ "google.golang.org/grpc"
+
+ "github.com/quic-go/quic-go"
+ "github.com/quic-go/quic-go/http3"
+ "github.com/scionproto/scion/marketplace"
+ marketplacestorage "github.com/scionproto/scion/marketplace/storage"
+ "github.com/scionproto/scion/marketplace/webapp"
+ libgrpc "github.com/scionproto/scion/pkg/grpc"
+ "github.com/scionproto/scion/pkg/hummingbird/registration"
+ "github.com/scionproto/scion/pkg/log"
+ "github.com/scionproto/scion/pkg/private/serrors"
+ "github.com/scionproto/scion/pkg/proto/control_plane"
+ "github.com/scionproto/scion/pkg/proto/control_plane/v1/control_planeconnect"
+ "github.com/scionproto/scion/pkg/proto/hummingbird"
+ "github.com/scionproto/scion/pkg/proto/hummingbird/v1/hummingbirdconnect"
+ "github.com/scionproto/scion/pkg/scrypto/cppki"
+ "github.com/scionproto/scion/pkg/segment/iface"
+ "github.com/scionproto/scion/pkg/snet"
+ "github.com/scionproto/scion/pkg/snet/squic"
+ "github.com/scionproto/scion/private/app"
+ "github.com/scionproto/scion/private/app/appnet"
+ "github.com/scionproto/scion/private/app/launcher"
+ "github.com/scionproto/scion/private/storage"
+ "github.com/scionproto/scion/private/topology"
+ "github.com/scionproto/scion/private/trust"
+ trustgrpc "github.com/scionproto/scion/private/trust/grpc"
+ "github.com/scionproto/scion/private/trust/happy"
+)
+
+const APIMajorVersion = uint32(0)
+const APIMinorVersion = uint32(1)
+
+var globalCfg marketplace.Config
+
+func main() {
+ application := launcher.Application{
+ ApplicationBase: launcher.ApplicationBase{
+ TOMLConfig: &globalCfg,
+ ShortName: "Hummingbird Marketplace",
+ Main: realMain,
+ },
+ }
+ application.Run()
+}
+
+type controlServiceFetcherGrpc struct {
+ client control_plane.TrustMaterialServiceClient
+}
+
+func (f *controlServiceFetcherGrpc) Chains(ctx context.Context, query trust.ChainQuery,
+ _ net.Addr) ([][]*x509.Certificate, error) {
+
+ rep, err := f.client.Chains(ctx, trustgrpc.ChainQueryToReq(query))
+ if err != nil {
+ return nil, serrors.Wrap("fetching chains over connect", err)
+ }
+ chains, _, err := trustgrpc.RepToChains(rep.Chains)
+ if err != nil {
+ return nil, serrors.Wrap("parsing chains", err)
+ }
+ if err := trustgrpc.CheckChainsMatchQuery(query, chains); err != nil {
+ return nil, serrors.Wrap("chains do not match query", err)
+ }
+ return chains, nil
+}
+
+func (f *controlServiceFetcherGrpc) TRC(ctx context.Context, id cppki.TRCID,
+ _ net.Addr) (cppki.SignedTRC, error) {
+
+ rep, err := f.client.TRC(ctx, trustgrpc.IDToReq(id))
+ if err != nil {
+ return cppki.SignedTRC{}, serrors.Wrap("fetching chains over connect", err)
+ }
+ //nolint:forbidigo // generated field.
+ trc, err := cppki.DecodeSignedTRC(rep.Trc)
+ if err != nil {
+ return cppki.SignedTRC{}, serrors.Wrap("parse TRC reply", err)
+ }
+ if trc.TRC.ID != id && !(id.Base == 0 && id.Serial == 0) {
+ return cppki.SignedTRC{}, serrors.New("received wrong TRC", "expected", id,
+ "actual", trc.TRC.ID)
+ }
+ return trc, nil
+}
+
+type controlServiceFetcherConnect struct {
+ client control_planeconnect.TrustMaterialServiceClient
+}
+
+func (f *controlServiceFetcherConnect) Chains(ctx context.Context, query trust.ChainQuery,
+ _ net.Addr) ([][]*x509.Certificate, error) {
+
+ rep, err := f.client.Chains(ctx, connect.NewRequest(trustgrpc.ChainQueryToReq(query)))
+ if err != nil {
+ return nil, serrors.Wrap("fetching chains over connect", err)
+ }
+ chains, _, err := trustgrpc.RepToChains(rep.Msg.Chains)
+ if err != nil {
+ return nil, serrors.Wrap("parsing chains", err)
+ }
+ if err := trustgrpc.CheckChainsMatchQuery(query, chains); err != nil {
+ return nil, serrors.Wrap("chains do not match query", err)
+ }
+ return chains, nil
+}
+
+func (f *controlServiceFetcherConnect) TRC(ctx context.Context, id cppki.TRCID,
+ _ net.Addr) (cppki.SignedTRC, error) {
+
+ rep, err := f.client.TRC(ctx, connect.NewRequest(trustgrpc.IDToReq(id)))
+ if err != nil {
+ return cppki.SignedTRC{}, serrors.Wrap("fetching chains over connect", err)
+ }
+ //nolint:forbidigo // generated field.
+ trc, err := cppki.DecodeSignedTRC(rep.Msg.Trc)
+ if err != nil {
+ return cppki.SignedTRC{}, serrors.Wrap("parse TRC reply", err)
+ }
+ if trc.TRC.ID != id && !(id.Base == 0 && id.Serial == 0) {
+ return cppki.SignedTRC{}, serrors.New("received wrong TRC", "expected", id,
+ "actual", trc.TRC.ID)
+ }
+ return trc, nil
+}
+
+type recurser struct {
+}
+
+func (r *recurser) AllowRecursion(peer net.Addr) error {
+ return nil
+}
+
+func realMain(ctx context.Context) error {
+ var snetTopo snet.Topology
+ var err error
+ topo, err := topology.NewLoader(topology.LoaderCfg{
+ File: globalCfg.General.Topology(),
+ Validator: &topology.DefaultValidator{},
+ })
+ if err != nil {
+ return serrors.Wrap("creating topology loader", err)
+ }
+ trustDB, err := storage.NewInMemoryTrustStorage()
+ if err != nil {
+ return err
+ }
+ _, err = trust.LoadTRCs(context.Background(), path.Join(globalCfg.General.ConfigDir, "certs"), trustDB)
+ if err != nil {
+ return err
+ }
+ var regService *registration.Service
+
+ startPort, endPort := topo.PortRange()
+ snetTopo.PortRange = snet.TopologyPortRange{
+ Start: startPort,
+ End: endPort,
+ }
+ snetTopo.LocalIA = topo.IA()
+ snetTopo.Interface = func(u uint16) (netip.AddrPort, bool) {
+ i, found := topo.InterfaceInfoMap()[iface.ID(u)]
+ if !found {
+ return netip.AddrPort{}, false
+ }
+ return i.InternalAddr, true
+ }
+ if !globalCfg.Marketplace.DisableASRegistration {
+ connectConn := control_planeconnect.NewTrustMaterialServiceClient(http.DefaultClient, fmt.Sprintf("http://%s", topo.ControlServiceAddresses()[0].String()))
+ cpConn, err := grpc.DialContext(ctx, topo.ControlServiceAddresses()[0].String(), grpc.WithInsecure())
+ if err != nil {
+ return err
+ }
+ cpTrustMaterialClient := control_plane.NewTrustMaterialServiceClient(cpConn)
+ fetcher := happy.Fetcher{
+ Connect: &controlServiceFetcherConnect{
+ client: connectConn,
+ },
+ Grpc: &controlServiceFetcherGrpc{
+ client: cpTrustMaterialClient,
+ },
+ }
+ trustDB = marketplace.FromTrustDB(trustDB, fetcher, topo.ControlServiceAddresses()[0])
+ trustProvider := trust.FetchingProvider{
+ DB: trustDB,
+ Fetcher: fetcher,
+ Router: trust.LocalRouter{
+ IA: topo.IA(),
+ },
+ Recurser: &recurser{},
+ }
+ regService = registration.NewService(trustProvider)
+ }
+
+ store, err := marketplacestorage.NewStorage(globalCfg.MarketplaceDB, globalCfg.Marketplace.TransactionFeeRelative,
+ globalCfg.Marketplace.TransactionFeeAbsolute, globalCfg.Marketplace.SplitCombineFeeAbsolute, globalCfg.Marketplace.DelegationHourlyFee)
+ if err != nil {
+ return err
+ }
+ cert, err := loadOrCreateCertificate(path.Join(globalCfg.General.ConfigDir, "server.crt"), path.Join(globalCfg.General.ConfigDir, "server.key"))
+ if err != nil {
+ return err
+ }
+ signingPubKey, signingPrivKey, err := getJwtKeys()
+ if err != nil {
+ return err
+ }
+ jwtSigner := registration.NewSigner(signingPrivKey)
+ tokenVerifier := &marketplace.TokenVerifier{
+ Store: store,
+ JWTVerifier: registration.NewVerifier(signingPubKey),
+ }
+
+ trustVerifer := trust.NewTLSCryptoVerifier(trustDB)
+ service, err := marketplace.NewService(ctx, &marketplace.MarketplaceInfo{
+ ApiMajorVersion: APIMajorVersion,
+ ApiMinorVersion: APIMinorVersion,
+ Currency: globalCfg.Marketplace.Currency,
+ StatisticsTimeGranularity: globalCfg.Marketplace.StatisticsTimeGranularity,
+ SupportsRedemptionDelegation: globalCfg.Marketplace.SupportsRedemptionDelegation,
+ CurrencyExponent: globalCfg.Marketplace.CurrencyExponent,
+ PricingStrategy: hummingbird.PricingStrategy_static_pricing,
+ TransactionFeeRelative: globalCfg.Marketplace.TransactionFeeRelative,
+ TransactionFeeAbsolute: globalCfg.Marketplace.TransactionFeeAbsolute,
+ SplitCombineFeeAbsolute: globalCfg.Marketplace.SplitCombineFeeAbsolute,
+ DelegationHourlyFee: globalCfg.Marketplace.DelegationHourlyFee,
+ }, store, regService, jwtSigner)
+ if err != nil {
+ return err
+ }
+
+ mux := http.NewServeMux()
+ apiPath1, handler1 := hummingbirdconnect.NewMarketplaceServiceHandler(service, connect.WithInterceptors(marketplace.NewAuthInterceptor(tokenVerifier)))
+ apiPath2, handler2 := hummingbirdconnect.NewRedemptionServiceHandler(service, connect.WithInterceptors(marketplace.NewAuthInterceptor(tokenVerifier)))
+
+ mux.Handle(apiPath1, handler1)
+ mux.Handle(apiPath2, handler2)
+
+ accountPath, accountHandler := hummingbirdconnect.NewAccountServiceHandler(service, connect.WithInterceptors(marketplace.AccountManagerInterceptor(tokenVerifier)))
+
+ webapp.Init(jwtSigner, store, mux, globalCfg.Marketplace.DisableUserRegistration)
+ mux.Handle(accountPath, accountHandler)
+ server := &http.Server{
+ Addr: globalCfg.Marketplace.APIAddr,
+ Handler: loggingMiddleware(mux),
+ TLSConfig: &tls.Config{
+ Certificates: []tls.Certificate{cert},
+ },
+ }
+ var cleanup app.Cleanup
+ g, errCtx := errgroup.WithContext(ctx)
+ g.Go(func() error {
+ return server.ListenAndServeTLS("", "")
+ })
+ log.Info(fmt.Sprintf("HTTPS server running on %s\n", globalCfg.Marketplace.APIAddr))
+ if globalCfg.Marketplace.SCIONAPIAddr != "" {
+ err = StartSCIONServer(errCtx, snetTopo, 1400, globalCfg.Marketplace.SCIONAPIAddr, g, trustVerifer, &cert, mux, &cleanup)
+ if err != nil {
+ return err
+ }
+ }
+ cleanup.Add(func() error {
+ ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
+ defer cancel()
+ if err := server.Shutdown(ctx); err != nil && ctx.Err() == nil {
+ return err
+ }
+ return nil
+ })
+ g.Go(func() error {
+ defer log.HandlePanic()
+ <-errCtx.Done()
+ return cleanup.Do()
+ })
+
+ g.Wait()
+ return nil
+}
+
+func loggingMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ fmt.Printf("%s: %s %s%s\n",
+ r.Proto,
+ r.Method,
+ r.URL.Path,
+ func() string {
+ if r.URL.RawQuery != "" {
+ return "?" + r.URL.RawQuery
+ }
+ return ""
+ }(),
+ )
+
+ next.ServeHTTP(w, r)
+ })
+}
+
+func StartSCIONServer(ctx context.Context, topo snet.Topology, mtu uint16, addrString string, g *errgroup.Group, trustVerifier *trust.TLSCryptoVerifier, cert *tls.Certificate, mux *http.ServeMux, cleanup *app.Cleanup) error {
+ addr, err := net.ResolveUDPAddr("udp", addrString)
+ if err != nil {
+ return err
+ }
+ nc := appnet.NetworkConfig{
+ Topology: topo,
+ IA: topo.LocalIA,
+ QUIC: appnet.QUIC{
+ TLSVerifier: trustVerifier,
+ GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
+ return cert, nil
+ },
+ },
+ Public: addr,
+ MTU: mtu,
+ }
+ quicStack, err := nc.QUICStack(ctx)
+ if err != nil {
+ return err
+ }
+ quicServer := grpc.NewServer(
+ grpc.Creds(libgrpc.PassThroughCredentials{}),
+ libgrpc.UnaryServerInterceptor(),
+ libgrpc.DefaultMaxConcurrentStreams(),
+ )
+ cleanup.Add(func() error { quicServer.GracefulStop(); return nil })
+ cleanup.Add(func() error { return quicStack.Listener.Close() })
+ grpcConns := make(chan *quic.Conn)
+ g.Go(func() error {
+ defer log.HandlePanic()
+ listener := quicStack.Listener
+ connectServer := http3.Server{
+ Handler: loggingMiddleware(mux),
+ }
+ cleanup.Add(func() error {
+ ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
+ defer cancel()
+ if err := connectServer.Shutdown(ctx); err != nil && ctx.Err() == nil {
+ return err
+ }
+ return nil
+ })
+
+ for {
+ conn, err := listener.Accept(ctx)
+ if err == quic.ErrServerClosed {
+ return http.ErrServerClosed
+ }
+ if err != nil {
+ return err
+ }
+ go func() {
+ defer log.HandlePanic()
+ if conn.ConnectionState().TLS.NegotiatedProtocol != "h3" {
+ grpcConns <- conn
+ return
+ }
+ if err := connectServer.ServeQUICConn(conn); err != nil {
+ log.Debug("Error handling connectrpc connection", "err", err)
+ }
+ }()
+ }
+ })
+ g.Go(func() error {
+ defer log.HandlePanic()
+ grpcListener := squic.NewConnListener(grpcConns, quicStack.Listener.Addr())
+ if err := quicServer.Serve(grpcListener); err != nil {
+ return serrors.Wrap("serving gRPC/SCION API", err)
+ }
+ return nil
+ })
+ return nil
+}
+
+func saveSignatureKeys(pubKey ed25519.PublicKey, privKey ed25519.PrivateKey) error {
+ if err := os.MkdirAll("gen/marketplace", 0755); err != nil {
+ return err
+ }
+ privBytes, err := x509.MarshalPKCS8PrivateKey(privKey)
+ if err != nil {
+ return err
+ }
+
+ privBlock := &pem.Block{
+ Type: "PRIVATE KEY",
+ Bytes: privBytes,
+ }
+
+ if err := os.WriteFile(path.Join(globalCfg.General.ConfigDir, "signature_priv.pem"), pem.EncodeToMemory(privBlock), 0644); err != nil {
+ return err
+ }
+
+ pubBytes, err := x509.MarshalPKIXPublicKey(pubKey)
+ if err != nil {
+ return err
+ }
+
+ pubBlock := &pem.Block{
+ Type: "PUBLIC KEY",
+ Bytes: pubBytes,
+ }
+ if err := os.WriteFile(path.Join(globalCfg.General.ConfigDir, "signature_pub.pem"), pem.EncodeToMemory(pubBlock), 0644); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func getJwtKeys() (ed25519.PublicKey, ed25519.PrivateKey, error) {
+ pubFile := path.Join(globalCfg.General.ConfigDir, "signature_pub.pem")
+ privFile := path.Join(globalCfg.General.ConfigDir, "signature_priv.pem")
+ if _, err := os.Stat(privFile); os.IsNotExist(err) {
+ signingPubKey, signingPrivKey, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ return nil, nil, err
+ }
+ return signingPubKey, signingPrivKey, saveSignatureKeys(signingPubKey, signingPrivKey)
+ }
+ privData, err := os.ReadFile(privFile)
+ if err != nil {
+ return nil, nil, err
+ }
+ block, _ := pem.Decode(privData)
+ if block == nil {
+ return nil, nil, fmt.Errorf("failed to decode PEM")
+ }
+
+ privkey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ priv, ok := privkey.(ed25519.PrivateKey)
+ if !ok {
+ return nil, nil, fmt.Errorf("not an Ed25519 private key")
+ }
+
+ pubData, err := os.ReadFile(pubFile)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ block, _ = pem.Decode(pubData)
+ if block == nil {
+ return nil, nil, fmt.Errorf("failed to decode PEM")
+ }
+
+ pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ pub, ok := pubKey.(ed25519.PublicKey)
+ if !ok {
+ return nil, nil, fmt.Errorf("not an Ed25519 public key")
+ }
+
+ return pub, priv, nil
+}
+func loadOrCreateCertificate(certFile, keyFile string) (tls.Certificate, error) {
+ // Try existing files first.
+ if _, err := os.Stat(certFile); err == nil {
+ return tls.LoadX509KeyPair(certFile, keyFile)
+ }
+
+ // Generate new key.
+ priv, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ return tls.Certificate{}, err
+ }
+
+ template := x509.Certificate{
+ SerialNumber: big.NewInt(1),
+ Subject: pkix.Name{
+ CommonName: "marketplace.local",
+ },
+ NotBefore: time.Now(),
+ NotAfter: time.Now().Add(365 * 24 * time.Hour),
+
+ KeyUsage: x509.KeyUsageKeyEncipherment |
+ x509.KeyUsageDigitalSignature,
+ ExtKeyUsage: []x509.ExtKeyUsage{
+ x509.ExtKeyUsageServerAuth,
+ },
+
+ DNSNames: []string{"marketplace.local"},
+ }
+
+ derBytes, err := x509.CreateCertificate(
+ rand.Reader,
+ &template,
+ &template,
+ &priv.PublicKey,
+ priv,
+ )
+ if err != nil {
+ return tls.Certificate{}, err
+ }
+
+ // Write certificate.
+ certOut, err := os.Create(certFile)
+ if err != nil {
+ return tls.Certificate{}, err
+ }
+ defer certOut.Close()
+
+ err = pem.Encode(certOut, &pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: derBytes,
+ })
+ if err != nil {
+ return tls.Certificate{}, err
+ }
+
+ // Write private key.
+ keyOut, err := os.OpenFile(keyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+ if err != nil {
+ return tls.Certificate{}, err
+ }
+ defer keyOut.Close()
+
+ err = pem.Encode(keyOut, &pem.Block{
+ Type: "RSA PRIVATE KEY",
+ Bytes: x509.MarshalPKCS1PrivateKey(priv),
+ })
+ if err != nil {
+ return tls.Certificate{}, err
+ }
+
+ return tls.LoadX509KeyPair(certFile, keyFile)
+}
diff --git a/marketplace/config.go b/marketplace/config.go
new file mode 100644
index 0000000000..93696a7b3b
--- /dev/null
+++ b/marketplace/config.go
@@ -0,0 +1,85 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package marketplace
+
+import (
+ "io"
+
+ marketplacestorage "github.com/scionproto/scion/marketplace/storage"
+ "github.com/scionproto/scion/pkg/log"
+ "github.com/scionproto/scion/private/config"
+ "github.com/scionproto/scion/private/env"
+)
+
+type Config struct {
+ General env.General `toml:"general,omitempty"`
+ Logging log.Config `toml:"log,omitempty"`
+ Marketplace MarketplaceConfig `toml:"marketplace,omitempty"`
+ MarketplaceDB marketplacestorage.DBConfig `toml:"marketplace_db,omitempty"`
+}
+
+func (cfg *Config) InitDefaults() {
+ config.InitAll(
+ &cfg.General,
+ &cfg.Logging,
+ &cfg.Marketplace,
+ )
+}
+func (cfg *Config) Validate() error {
+ return config.ValidateAll(
+ &cfg.General,
+ &cfg.Logging,
+ &cfg.Marketplace,
+ )
+}
+func (cfg *Config) Sample(dst io.Writer, path config.Path, _ config.CtxMap) {
+
+}
+
+type MarketplaceConfig struct {
+ APIAddr string `toml:"api_addr,omitempty"`
+ SCIONAPIAddr string `toml:"scion_api_addr,omitempty"`
+ Currency string `toml:"currency,omitempty"`
+ StatisticsTimeGranularity uint32 `toml:"statistics_time_granularity,omitempty"`
+ SupportsRedemptionDelegation bool `toml:"supports_redemption_delegation,omitempty"`
+ CurrencyExponent uint32 `toml:"currency_exponent,omitempty"`
+ TransactionFeeRelative float32 `toml:"transaction_fee_relative,omitempty"`
+ TransactionFeeAbsolute uint64 `toml:"transaction_fee_absolute,omitempty"`
+ SplitCombineFeeAbsolute uint64 `toml:"split_combine_fee_absolute,omitempty"`
+ DelegationHourlyFee uint64 `toml:"delegation_hourly_fee,omitempty"`
+ DisableUserRegistration bool `toml:"disable_user_registration,omitempty"`
+ DisableASRegistration bool `toml:"disable_as_registration,omitempty"`
+}
+
+func (cfg *MarketplaceConfig) InitDefaults() {
+ if cfg.APIAddr == "" {
+ cfg.APIAddr = "localhost:8888"
+ }
+ if cfg.SCIONAPIAddr == "" {
+ cfg.SCIONAPIAddr = "localhost:9888"
+ }
+ if cfg.Currency == "" {
+ cfg.Currency = "CHF"
+ }
+ if cfg.StatisticsTimeGranularity == 0 {
+ cfg.StatisticsTimeGranularity = 86400
+ }
+}
+func (cfg *MarketplaceConfig) Validate() error {
+ return nil
+}
+func (cfg *MarketplaceConfig) Sample(dst io.Writer, path config.Path, _ config.CtxMap) {
+
+}
diff --git a/marketplace/db/BUILD.bazel b/marketplace/db/BUILD.bazel
new file mode 100644
index 0000000000..a6a49b2d3f
--- /dev/null
+++ b/marketplace/db/BUILD.bazel
@@ -0,0 +1,20 @@
+load("@rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = [
+ "schema.go",
+ "sqlite.go",
+ "types.go",
+ ],
+ embedsrcs = [
+ "schema.sql",
+ ],
+ importpath = "github.com/scionproto/scion/marketplace/db",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//pkg/addr:go_default_library",
+ "//pkg/private/serrors:go_default_library",
+ "//private/storage/db:go_default_library",
+ ],
+)
diff --git a/marketplace/db/schema.go b/marketplace/db/schema.go
new file mode 100644
index 0000000000..4ce05d95c4
--- /dev/null
+++ b/marketplace/db/schema.go
@@ -0,0 +1,26 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package db
+
+import (
+ _ "embed"
+)
+
+const (
+ SchemaVersion = 1
+)
+
+//go:embed schema.sql
+var Schema string
diff --git a/marketplace/db/schema.sql b/marketplace/db/schema.sql
new file mode 100644
index 0000000000..bb60480c19
--- /dev/null
+++ b/marketplace/db/schema.sql
@@ -0,0 +1,74 @@
+CREATE TABLE IF NOT EXISTS Assets(
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ account_id INTEGER,
+ isd_id INTEGER NOT NULL,
+ as_id INTEGER NOT NULL,
+ bandwidth INTEGER NOT NULL,
+ bandwidth_min INTEGER NOT NULL,
+ bandwidth_max INTEGER NOT NULL CHECK (bandwidth_max >= bandwidth_min),
+ price INTEGER NOT NULL,
+ time_granularity INTEGER NOT NULL,
+ time_min_duration INTEGER NOT NULL,
+ starts_at TEXT NOT NULL,
+ stops_at TEXT NOT NULL,
+ ingress INTEGER,
+ egress INTEGER,
+ state INTEGER NOT NULL DEFAULT 0,
+ FOREIGN KEY (account_id) REFERENCES Accounts(id)
+);
+CREATE INDEX IF NOT EXISTS idx_asset_owner ON Assets(account_id);
+CREATE INDEX IF NOT EXISTS idx_asset_validity ON Assets(starts_at, stops_at);
+CREATE INDEX IF NOT EXISTS idx_assets_ia_validity_start ON Assets(isd_id, as_id, starts_at, stops_at);
+CREATE INDEX IF NOT EXISTS idx_assets_ia_validity_end ON Assets(isd_id, as_id, stops_at, starts_at);
+CREATE TABLE IF NOT EXISTS Reservations(
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ reservation_id INTEGER NOT NULL,
+ account_id INTEGER NOT NULL,
+ isd_id INTEGER NOT NULL,
+ as_id INTEGER NOT NULL,
+ ingress INTEGER NOT NULL,
+ egress INTEGER NOT NULL,
+ bandwidth INTEGER NOT NULL,
+ bw_encoded INTEGER NOT NULL,
+ starts_at TEXT NOT NULL,
+ stops_at TEXT NOT NULL,
+ key BLOB NOT NULL,
+ FOREIGN KEY (account_id) REFERENCES Accounts(id)
+);
+CREATE INDEX IF NOT EXISTS idx_reservations_owner ON Reservations(account_id);
+CREATE INDEX IF NOT EXISTS idx_reservations_used ON Reservations(isd_id, as_id, starts_at, stops_at);
+CREATE TABLE IF NOT EXISTS Users(
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL UNIQUE,
+ pw_hash TEXT NOT NULL
+);
+CREATE INDEX IF NOT EXISTS idx_users_name ON Users(name);
+CREATE TABLE IF NOT EXISTS Accounts(
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ scope TEXT,
+ balance INTEGER NOT NULL DEFAULT 0 CHECK (balance >= 0),
+ jwt_version INTEGER NOT NULL DEFAULT 0,
+ UNIQUE(user_id, scope),
+ FOREIGN KEY(user_id) REFERENCES Users(id)
+);
+CREATE INDEX IF NOT EXISTS idx_accounts ON Accounts(user_id);
+CREATE TABLE IF NOT EXISTS Ases(
+ isd_id INTEGER NOT NULL,
+ as_id INTEGER NOT NULL,
+ pw_hash TEXT NOT NULL DEFAULT '',
+ jwt_version INTEGER NOT NULL DEFAULT 0,
+ balance INTEGER NOT NULL DEFAULT 0 CHECK (balance >= 0),
+ PRIMARY KEY(isd_id, as_id)
+);
+CREATE TABLE IF NOT EXISTS Redemption_Delegations(
+ isd_id INTEGER NOT NULL,
+ as_id INTEGER NOT NULL,
+ res_id_limit INTEGER NOT NULL,
+ expiration TEXT NOT NULL CHECK (expiration <= paid_until),
+ paid_until TEXT NOT NULL,
+ key BLOB NOT NULL,
+ encodings BLOB NOT NULL,
+ PRIMARY KEY(isd_id, as_id)
+);
+CREATE INDEX IF NOT EXISTS idx_redemption_expiration ON Redemption_Delegations(expiration);
\ No newline at end of file
diff --git a/marketplace/db/sqlite.go b/marketplace/db/sqlite.go
new file mode 100644
index 0000000000..7098033d68
--- /dev/null
+++ b/marketplace/db/sqlite.go
@@ -0,0 +1,1067 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package db
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "io"
+ "strings"
+ "time"
+
+ "github.com/scionproto/scion/pkg/addr"
+ "github.com/scionproto/scion/pkg/private/serrors"
+ "github.com/scionproto/scion/private/storage/db"
+)
+
+type MarketplaceDB interface {
+ io.Closer
+ InsertAsset(ctx context.Context, a *DBAsset) (int64, error)
+ InsertReservation(ctx context.Context, r *DBReservation) (int64, error)
+ Search(ctx context.Context, params *AssetQuery) ([]*DBAsset, error)
+ FetchReservations(ctx context.Context, params *ReservationQuery) ([]*DBReservation, error)
+ GetUser(ctx context.Context, id int64) (*DBUser, error)
+ GetUserByName(ctx context.Context, name string) (*DBUser, error)
+ GetAccountsByUser(ctx context.Context, id int64) ([]*DBAccount, error)
+ GetAccountByAccountID(ctx context.Context, id int64) (*DBAccount, error)
+ CreateAccount(ctx context.Context, account *DBAccount) (int64, error)
+ CreateASUser(ctx context.Context, user *DBASUser) (int64, error)
+ UpdateAccountMoney(ctx context.Context, id int64, amount int64) (int64, error)
+ UpdateASMoney(ctx context.Context, ia addr.IA, amount int64) (int64, error)
+ SearchAssetsForStatistics(ctx context.Context, params *StatisticsQuery) ([]*DBStat, error)
+ FindUsedReservations(ctx context.Context, params *UsedReservationsQuery) ([]*UsedReservation, error)
+ CreateOrUpdateRedemptionDelegations(ctx context.Context, r *RedemptionDelegation) (int64, error)
+ FindRedemptionDelegations(ctx context.Context) ([]*RedemptionDelegation, error)
+ IncrementASJWTVersion(ctx context.Context, ia addr.IA, current int64) (int64, error)
+ IncrementUserJWTVersion(ctx context.Context, userId int64, accountId int64) (int64, error)
+ IncrementAccountJWTVersion(ctx context.Context, userid int64, current int64) (int64, error)
+ GetASUser(ctx context.Context, ia addr.IA) (*DBASUser, error)
+ DeleteListedAsset(ctx context.Context, ia addr.IA, assetID int64) (int64, error)
+ SetASAuthenticationToken(ctx context.Context, ia addr.IA, auth string) (int64, error)
+ BeginTransaction(ctx context.Context, opts *sql.TxOptions) (*transaction, error)
+}
+
+type Backend struct {
+ db *db.Sqlite
+ *executor
+}
+
+type executor struct {
+ write db.Sqler
+ read interface {
+ QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
+ QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
+ }
+}
+
+func New(path string, cfg *db.SqliteConfig) (*Backend, error) {
+ db, err := db.NewSqlite(path, cfg)
+ if err != nil {
+ return nil, err
+ }
+ if err := db.Setup(Schema, SchemaVersion); err != nil {
+ return nil, err
+ }
+ return &Backend{
+ executor: &executor{
+ write: db.Full,
+ read: db.ReadOnly,
+ },
+ db: db,
+ }, nil
+}
+
+func (b *Backend) Close() error {
+ return b.db.Close()
+}
+
+func (b *Backend) BeginTransaction(ctx context.Context, opts *sql.TxOptions) (*transaction, error) {
+ tx, err := b.db.Full.BeginTx(ctx, opts)
+ if err != nil {
+ return nil, err
+ }
+ return &transaction{
+ executor: &executor{
+ write: tx,
+ read: tx,
+ },
+ tx: tx,
+ }, nil
+}
+
+type transaction struct {
+ *executor
+ tx *sql.Tx
+}
+
+func (tx *transaction) Commit() error {
+ return tx.tx.Commit()
+}
+
+func (tx *transaction) Rollback() error {
+ return tx.tx.Rollback()
+}
+
+func (e *executor) FetchReservations(ctx context.Context, params *ReservationQuery) ([]*DBReservation, error) {
+ if e.read == nil {
+ return nil, serrors.New("No database open")
+ }
+ stmt, args := e.buildReservationQuery(params)
+ rows, err := e.read.QueryContext(ctx, stmt, args...)
+ if err != nil {
+ return nil, serrors.New("Error looking up assets", "err", err, "q", stmt)
+ }
+ defer rows.Close()
+ var res []*DBReservation
+ for rows.Next() {
+ a := &DBReservation{}
+ var startsAtString string
+ var stopsAtString string
+ var isd uint16
+ var as uint64
+ err = rows.Scan(&a.ID, &a.AccountId, &a.ReservationID, &isd, &as, &a.Ingress, &a.Egress, &a.Bandwidth, &a.EncodedBandwidth, &startsAtString, &stopsAtString, &a.Key)
+ if err != nil {
+ return nil, serrors.Wrap("Error reading DB response", err)
+ }
+ a.IA, err = addr.IAFrom(addr.ISD(isd), addr.AS(as))
+ a.StartsAt, err = time.Parse(time.RFC3339, startsAtString)
+ if err != nil {
+ return nil, err
+ }
+ a.StopsAt, err = time.Parse(time.RFC3339, stopsAtString)
+ if err != nil {
+ return nil, err
+ }
+ res = append(res, a)
+ }
+ return res, nil
+}
+
+func (e *executor) buildReservationQuery(params *ReservationQuery) (string, []any) {
+ var args []any
+ where := []string{}
+ query := []string{
+ "SELECT r.id, r.account_id, r.reservation_id, r.isd_id, r.as_id, r.ingress, r.egress, r.bandwidth, r.bw_encoded, r.starts_at, r.stops_at, r.key FROM Reservations r",
+ }
+ query = append(query, "JOIN Accounts owner ON r.account_id = owner.id JOIN Accounts current ON current.id = ?")
+ where = append(where, "(current.scope IS NULL AND owner.user_id = current.user_id) OR (current.scope IS NOT NULL AND owner.id = current.id)")
+ args = append(args, params.AccountId)
+ if params.IA != nil {
+ where = append(where, "(r.isd_id=?) AND (r.as_id=?)")
+ args = append(args, int64(params.IA.ISD()), int64(params.IA.AS()))
+ }
+ if params.StartsAt != nil {
+ where = append(where, "(r.starts_at<=?)")
+ args = append(args, *params.StartsAt)
+ }
+ if params.StopsAt != nil {
+ where = append(where, "(r.stops_at>=?)")
+ args = append(args, *params.StopsAt)
+ }
+ if params.Ingress != nil {
+ where = append(where, "(r.ingress=?)")
+ args = append(args, *params.Ingress)
+ }
+ if params.Egress != nil {
+ where = append(where, "(r.egress=?)")
+ args = append(args, *params.Egress)
+ }
+ if params.Bandwidth != nil {
+ where = append(where, "(r.bandwidth>=?)")
+ args = append(args, *params.Bandwidth)
+ }
+ query = append(query, fmt.Sprintf("WHERE %s", strings.Join(where, "AND\n")))
+ return strings.Join(query, "\n"), args
+}
+
+func (e *executor) FindRedemptionDelegations(ctx context.Context) ([]*RedemptionDelegation, error) {
+ if e.read == nil {
+ return nil, serrors.New("No database open")
+ }
+ stmt := `SELECT isd_id, as_id, res_id_limit, expiration, paid_until, key, encodings FROM Redemption_Delegations WHERE expiration >= ?`
+ args := []any{time.Now().UTC().Format(time.RFC3339)}
+ rows, err := e.read.QueryContext(ctx, stmt, args...)
+ if err != nil {
+ return nil, serrors.New("Error looking up assets", "err", err, "q", stmt)
+ }
+ defer rows.Close()
+ var res []*RedemptionDelegation
+ for rows.Next() {
+ a := &RedemptionDelegation{}
+ var expirationString string
+ var paidUntilString string
+ var isd uint16
+ var as uint64
+ err = rows.Scan(&isd, &as, &a.ReservationIdLimit, &expirationString, &paidUntilString, &a.Key, &a.Encodings)
+ if err != nil {
+ return nil, serrors.Wrap("Error reading DB response", err)
+ }
+ a.Expiration, err = time.Parse(time.RFC3339, expirationString)
+ if err != nil {
+ return nil, err
+ }
+ a.PaidUntil, err = time.Parse(time.RFC3339, paidUntilString)
+ if err != nil {
+ return nil, err
+ }
+ a.IA, err = addr.IAFrom(addr.ISD(isd), addr.AS(as))
+ res = append(res, a)
+ }
+ return res, nil
+}
+
+func (e *executor) FindRedemptionDelegation(ctx context.Context, ia addr.IA) (*RedemptionDelegation, error) {
+ if e.read == nil {
+ return nil, serrors.New("No database open")
+ }
+ stmt := `SELECT isd_id, as_id, res_id_limit, expiration, paid_until, key, encodings FROM Redemption_Delegations WHERE expiration >= ? AND isd_id = ? AND as_id = ?`
+ rows, err := e.read.QueryContext(ctx, stmt, time.Now().Format(time.RFC3339), ia.ISD(), ia.AS())
+ if err != nil {
+ return nil, serrors.New("Error looking up assets", "err", err, "q", stmt)
+ }
+ defer rows.Close()
+ if !rows.Next() {
+ return nil, nil
+ }
+ a := &RedemptionDelegation{}
+ var expirationString string
+ var paidUntilString string
+ var isd uint16
+ var as uint64
+ err = rows.Scan(&isd, &as, &a.ReservationIdLimit, &expirationString, &paidUntilString, &a.Key, &a.Encodings)
+ if err != nil {
+ return nil, serrors.Wrap("Error reading DB response", err)
+ }
+ a.Expiration, err = time.Parse(time.RFC3339, expirationString)
+ if err != nil {
+ return nil, err
+ }
+ a.PaidUntil, err = time.Parse(time.RFC3339, paidUntilString)
+ if err != nil {
+ return nil, err
+ }
+ a.IA, err = addr.IAFrom(addr.ISD(isd), addr.AS(as))
+ return a, nil
+}
+
+func (e *executor) IncrementASJWTVersion(ctx context.Context, ia addr.IA, current int64) (int64, error) {
+ if e.write == nil {
+ return 0, serrors.New("No database open")
+ }
+ q := `UPDATE Ases SET jwt_version = jwt_version + 1 WHERE isd_id = ? AND as_id = ? AND jwt_version = ? RETURNING jwt_version`
+ rows, err := e.write.QueryContext(ctx, q, ia.ISD(), ia.AS(), current)
+ if err != nil {
+ return 0, err
+ }
+ defer rows.Close()
+ if !rows.Next() {
+ return 0, serrors.New("AS not found", "ia", ia)
+ }
+ var newVersion int64
+ err = rows.Scan(&newVersion)
+ if err != nil {
+ return 0, serrors.Wrap("Error reading DB response", err)
+ }
+ return newVersion, nil
+}
+
+func (e *executor) IncrementUserJWTVersion(ctx context.Context, userId int64, accountId int64) (int64, error) {
+ if e.write == nil {
+ return 0, serrors.New("No database open")
+ }
+ q := `UPDATE Accounts SET jwt_version = jwt_version + 1 WHERE id = ? AND user_id = ? RETURNING jwt_version`
+ rows, err := e.write.QueryContext(ctx, q, accountId, userId)
+ if err != nil {
+ return 0, err
+ }
+ defer rows.Close()
+ if !rows.Next() {
+ return 0, serrors.New("Account not found", "accountid", accountId)
+ }
+ var newVersion int64
+ err = rows.Scan(&newVersion)
+ if err != nil {
+ return 0, serrors.Wrap("Error reading DB response", err)
+ }
+ return newVersion, nil
+}
+
+func (e *executor) IncrementAccountJWTVersion(ctx context.Context, accountid int64, current int64) (int64, error) {
+ if e.write == nil {
+ return 0, serrors.New("No database open")
+ }
+ q := `UPDATE Accounts SET jwt_version = jwt_version + 1 WHERE id = ? AND jwt_version = ? RETURNING jwt_version`
+ rows, err := e.write.QueryContext(ctx, q, accountid, current)
+ if err != nil {
+ return 0, err
+ }
+ defer rows.Close()
+ if !rows.Next() {
+ return 0, serrors.New("Account not found", "accountid", accountid)
+ }
+ var newVersion int64
+ err = rows.Scan(&newVersion)
+ if err != nil {
+ return 0, serrors.Wrap("Error reading DB response", err)
+ }
+ return newVersion, nil
+}
+
+func (e *executor) CreateOrUpdateRedemptionDelegations(ctx context.Context, r *RedemptionDelegation) (int64, error) {
+ if e.write == nil {
+ return 0, serrors.New("No database open")
+ }
+ q := `
+ INSERT INTO Redemption_Delegations (isd_id, as_id, res_id_limit, expiration, paid_until, key, encodings)
+ VALUES (?,?,?,?,?,?,?)
+ ON CONFLICT(isd_id, as_id)
+ DO UPDATE SET
+ expiration = excluded.expiration,
+ paid_until = excluded.paid_until,
+ key = excluded.key,
+ encodings = excluded.encodings,
+ res_id_limit = excluded.res_id_limit;`
+ res, err := e.write.ExecContext(ctx, q, r.IA.ISD(), r.IA.AS(), r.ReservationIdLimit, r.Expiration.UTC().Format(time.RFC3339),
+ r.PaidUntil.UTC().Format(time.RFC3339), r.Key, r.Encodings)
+ if err != nil {
+ return 0, err
+ }
+ return res.RowsAffected()
+}
+
+func (e *executor) FindUsedReservations(ctx context.Context, params *UsedReservationsQuery) ([]*UsedReservation, error) {
+ if e.read == nil {
+ return nil, serrors.New("No database open")
+ }
+ stmt, args := e.buildUsedReservationsQuery(params)
+ rows, err := e.read.QueryContext(ctx, stmt, args...)
+ if err != nil {
+ return nil, serrors.New("Error looking up assets", "err", err, "q", stmt)
+ }
+ defer rows.Close()
+ var res []*UsedReservation
+ for rows.Next() {
+ a := &UsedReservation{}
+ var startsAtString string
+ var stopsAtString string
+ err = rows.Scan(&a.Id, &startsAtString, &stopsAtString)
+ if err != nil {
+ return nil, serrors.Wrap("Error reading DB response", err)
+ }
+ a.StartsAt, err = time.Parse(time.RFC3339, startsAtString)
+ if err != nil {
+ return nil, err
+ }
+ a.StopsAt, err = time.Parse(time.RFC3339, stopsAtString)
+ if err != nil {
+ return nil, err
+ }
+ res = append(res, a)
+ }
+ return res, nil
+}
+
+func (e *executor) buildUsedReservationsQuery(params *UsedReservationsQuery) (string, []any) {
+ var args []any
+ query := []string{
+ "SELECT id, starts_at, stops_at FROM Reservations",
+ "WHERE (isd_id = ?) AND (as_id = ?) AND (id < ?) AND (stops_at > ?)",
+ }
+ args = append(args, params.IA.ISD(), params.IA.AS(), params.Limit, time.Now().UTC().Format(time.RFC3339))
+ return strings.Join(query, "\n"), args
+}
+
+func (e *executor) SearchAssetsForStatistics(ctx context.Context, params *StatisticsQuery) ([]*DBStat, error) {
+ if e.read == nil {
+ return nil, serrors.New("No database open")
+ }
+ stmt, args := e.buildStatisticsQuery(params)
+ rows, err := e.read.QueryContext(ctx, stmt, args...)
+ if err != nil {
+ return nil, serrors.New("Error looking up assets", "err", err, "q", stmt)
+ }
+ defer rows.Close()
+ var res []*DBStat
+ for rows.Next() {
+ a := &DBStat{}
+ var startsAtString string
+ var stopsAtString string
+ err = rows.Scan(&a.OwnerId, &a.Bandwidth, &a.Price, &startsAtString, &stopsAtString)
+ if err != nil {
+ return nil, serrors.Wrap("Error reading DB response", err)
+ }
+ a.StartsAt, err = time.Parse(time.RFC3339, startsAtString)
+ if err != nil {
+ return nil, err
+ }
+ a.StopsAt, err = time.Parse(time.RFC3339, stopsAtString)
+ if err != nil {
+ return nil, err
+ }
+ res = append(res, a)
+ }
+ return res, nil
+}
+
+func (e *executor) buildStatisticsQuery(params *StatisticsQuery) (string, []any) {
+ var args []any
+ where := []string{}
+ query := []string{
+ "SELECT account_id, bandwidth, price, starts_at, stops_at FROM Assets",
+ }
+ where = append(where, "(isd_id=?) AND (as_id=?) AND (stops_at > ?) AND (starts_at <= ?)")
+ args = append(args, int64(params.IA.ISD()), int64(params.IA.AS()), params.WindowStart, params.WindowEnd)
+ if params.Ingress != nil {
+ where = append(where, "(ingress=?)")
+ args = append(args, *params.Ingress)
+ }
+ if params.Egress != nil {
+ where = append(where, "(egress=?)")
+ args = append(args, *params.Egress)
+ }
+ query = append(query, fmt.Sprintf("WHERE %s", strings.Join(where, "AND\n")))
+ return strings.Join(query, "\n"), args
+}
+
+func (e *executor) Search(ctx context.Context, params *AssetQuery) ([]*DBAsset, error) {
+ if e.read == nil {
+ return nil, serrors.New("No database open")
+ }
+ stmt, args := e.buildSearchQuery(params)
+ rows, err := e.read.QueryContext(ctx, stmt, args...)
+ if err != nil {
+ return nil, serrors.New("Error looking up assets", "err", err, "q", stmt)
+ }
+ defer rows.Close()
+ var res []*DBAsset
+ for rows.Next() {
+ a := &DBAsset{}
+ var startsAtString string
+ var stopsAtString string
+ var isd uint16
+ var as uint64
+ err = rows.Scan(&a.ID, &a.AccountId, &isd, &as, &a.Bandwidth, &a.BandwidthMin, &a.BandwidthMax, &a.Price, &a.TimeGranularity, &a.TimeMinDuration, &startsAtString, &stopsAtString, &a.IfIdIngress, &a.IfIdEgress)
+ if err != nil {
+ return nil, serrors.Wrap("Error reading DB response", err)
+ }
+ a.IA, err = addr.IAFrom(addr.ISD(isd), addr.AS(as))
+ a.StartAt, err = time.Parse(time.RFC3339, startsAtString)
+ if err != nil {
+ return nil, err
+ }
+ a.StopsAt, err = time.Parse(time.RFC3339, stopsAtString)
+ if err != nil {
+ return nil, err
+ }
+ res = append(res, a)
+ }
+ return res, nil
+}
+
+func (e *executor) buildSearchQuery(params *AssetQuery) (string, []any) {
+ var args []any
+ where := []string{}
+ query := []string{
+ "SELECT a.id, a.account_id, a.isd_id, a.as_id, a.bandwidth, a.bandwidth_min, a.bandwidth_max, a.price, a.time_granularity, a.time_min_duration, a.starts_at, a.stops_at, a.ingress, a.egress FROM Assets a",
+ }
+ where = append(where, "(a.state = 0)")
+ if params.AccountId == nil {
+ where = append(where, "(a.account_id IS NULL)")
+ } else {
+ query = append(query, "JOIN Accounts owner ON a.account_id = owner.id JOIN Accounts current ON current.id = ?")
+ where = append(where, "(current.scope IS NULL AND owner.user_id = current.user_id) OR (current.scope IS NOT NULL AND owner.id = current.id)")
+ args = append(args, *params.AccountId)
+ }
+ if params.IA != nil {
+ where = append(where, "(a.isd_id=?) AND (a.as_id=?)")
+ args = append(args, int64(params.IA.ISD()), int64(params.IA.AS()))
+ }
+ if params.StartsAt != nil {
+ where = append(where, "(a.starts_at<=?)")
+ args = append(args, *params.StartsAt)
+ }
+ if params.StopsAt != nil {
+ where = append(where, "(a.stops_at>=?)")
+ args = append(args, *params.StopsAt)
+ }
+ if params.Price != nil {
+ where = append(where, "(a.price<=?)")
+ args = append(args, *params.Price)
+ }
+ if params.MinRequiredBandwidth != nil {
+ where = append(where, "(a.bandwidth>=?)")
+ args = append(args, *params.MinRequiredBandwidth)
+ }
+ if params.Ingress != nil {
+ where = append(where, "(a.ingress=?)")
+ args = append(args, *params.Ingress)
+ }
+ if params.Egress != nil {
+ where = append(where, "(a.egress=?)")
+ args = append(args, *params.Egress)
+ }
+ query = append(query, fmt.Sprintf("WHERE %s", strings.Join(where, "AND\n")))
+ query = append(query, "ORDER BY LENGTH(a.id) ASC, a.id ASC")
+ return strings.Join(query, "\n"), args
+}
+
+func (e *executor) PrepareCombine(ctx context.Context, id int64, accountId int64) (*DBAsset, error) {
+ if e.write == nil {
+ return nil, serrors.New("No database open")
+ }
+ q := `
+ UPDATE assets
+ SET state = 4
+ WHERE id = ?
+ AND state = 0
+ AND account_id = ?
+ RETURNING
+ id,
+ account_id,
+ isd_id,
+ as_id,
+ bandwidth,
+ bandwidth_min,
+ bandwidth_max,
+ price,
+ time_granularity,
+ time_min_duration,
+ starts_at,
+ stops_at,
+ ingress,
+ egress;`
+ rows, err := e.write.QueryContext(ctx, q, id, accountId)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ if !rows.Next() {
+ return nil, serrors.New("asset not found", "id", id)
+ }
+ a := &DBAsset{}
+ var startsAtString string
+ var stopsAtString string
+ var isd uint16
+ var as uint64
+ err = rows.Scan(&a.ID, &a.AccountId, &isd, &as, &a.Bandwidth, &a.BandwidthMin, &a.BandwidthMax, &a.Price, &a.TimeGranularity, &a.TimeMinDuration, &startsAtString, &stopsAtString, &a.IfIdIngress, &a.IfIdEgress)
+ if err != nil {
+ return nil, serrors.Wrap("Error reading DB response", err)
+ }
+ a.IA, err = addr.IAFrom(addr.ISD(isd), addr.AS(as))
+ a.StartAt, err = time.Parse(time.RFC3339, startsAtString)
+ if err != nil {
+ return nil, err
+ }
+ a.StopsAt, err = time.Parse(time.RFC3339, stopsAtString)
+ if err != nil {
+ return nil, err
+ }
+ return a, nil
+}
+
+func (e *executor) PrepareSplit(ctx context.Context, id int64, accountId int64) (*DBAsset, error) {
+ if e.write == nil {
+ return nil, serrors.New("No database open")
+ }
+ q := `
+ UPDATE assets
+ SET state = 3
+ WHERE id = ?
+ AND state = 0
+ AND account_id = ?
+ RETURNING
+ id,
+ account_id,
+ isd_id,
+ as_id,
+ bandwidth,
+ bandwidth_min,
+ bandwidth_max,
+ price,
+ time_granularity,
+ time_min_duration,
+ starts_at,
+ stops_at,
+ ingress,
+ egress;`
+ rows, err := e.write.QueryContext(ctx, q, id, accountId)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ if !rows.Next() {
+ return nil, serrors.New("asset not found", "id", id)
+ }
+ a := &DBAsset{}
+ var startsAtString string
+ var stopsAtString string
+ var isd uint16
+ var as uint64
+ err = rows.Scan(&a.ID, &a.AccountId, &isd, &as, &a.Bandwidth, &a.BandwidthMin, &a.BandwidthMax, &a.Price, &a.TimeGranularity, &a.TimeMinDuration, &startsAtString, &stopsAtString, &a.IfIdIngress, &a.IfIdEgress)
+ if err != nil {
+ return nil, serrors.Wrap("Error reading DB response", err)
+ }
+ a.IA, err = addr.IAFrom(addr.ISD(isd), addr.AS(as))
+ a.StartAt, err = time.Parse(time.RFC3339, startsAtString)
+ if err != nil {
+ return nil, err
+ }
+ a.StopsAt, err = time.Parse(time.RFC3339, stopsAtString)
+ if err != nil {
+ return nil, err
+ }
+ return a, nil
+}
+
+func (e *executor) CheckoutAsset(ctx context.Context, id int64) (*DBAsset, error) {
+ if e.write == nil {
+ return nil, serrors.New("No database open")
+ }
+ q := `
+ UPDATE assets
+ SET state = 1
+ WHERE id = ?
+ AND state = 0
+ AND account_id IS NULL
+ RETURNING
+ id,
+ isd_id,
+ as_id,
+ bandwidth,
+ bandwidth_min,
+ bandwidth_max,
+ price,
+ time_granularity,
+ time_min_duration,
+ starts_at,
+ stops_at,
+ ingress,
+ egress;`
+ rows, err := e.write.QueryContext(ctx, q, id)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ if !rows.Next() {
+ return nil, serrors.New("asset not found", "id", id)
+ }
+ a := &DBAsset{}
+ var startsAtString string
+ var stopsAtString string
+ var isd uint16
+ var as uint64
+ err = rows.Scan(&a.ID, &isd, &as, &a.Bandwidth, &a.BandwidthMin, &a.BandwidthMax, &a.Price, &a.TimeGranularity, &a.TimeMinDuration, &startsAtString, &stopsAtString, &a.IfIdIngress, &a.IfIdEgress)
+ if err != nil {
+ return nil, serrors.Wrap("Error reading DB response", err)
+ }
+ a.IA, err = addr.IAFrom(addr.ISD(isd), addr.AS(as))
+ a.StartAt, err = time.Parse(time.RFC3339, startsAtString)
+ if err != nil {
+ return nil, err
+ }
+ a.StopsAt, err = time.Parse(time.RFC3339, stopsAtString)
+ if err != nil {
+ return nil, err
+ }
+ return a, nil
+}
+
+func (e *executor) UndoRedemption(ctx context.Context, account_id int64, id int64) (int64, error) {
+ if e.write == nil {
+ return 0, serrors.New("No database open")
+ }
+ q := `
+ UPDATE Assets
+ SET state = 0
+ WHERE id = ?
+ AND account_id = ?
+ AND state = 2`
+
+ res, err := e.write.ExecContext(ctx, q, id, account_id)
+ if err != nil {
+ return 0, err
+ }
+ return res.RowsAffected()
+}
+
+func (e *executor) PrepareRedemption(ctx context.Context, account_id int64, id int64) (*DBAsset, error) {
+ if e.write == nil {
+ return nil, serrors.New("No database open")
+ }
+ q := `
+ UPDATE assets
+ SET state = 2
+ WHERE id = ?
+ AND state = 0
+ AND account_id = ?
+ RETURNING
+ id,
+ isd_id,
+ as_id,
+ bandwidth,
+ bandwidth_min,
+ bandwidth_max,
+ price,
+ time_granularity,
+ time_min_duration,
+ starts_at,
+ stops_at,
+ ingress,
+ egress;`
+ rows, err := e.write.QueryContext(ctx, q, id, account_id)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ if !rows.Next() {
+ return nil, serrors.New("asset not found", "id", id)
+ }
+ a := &DBAsset{}
+ var startsAtString string
+ var stopsAtString string
+ var isd uint16
+ var as uint64
+ err = rows.Scan(&a.ID, &isd, &as, &a.Bandwidth, &a.BandwidthMin, &a.BandwidthMax, &a.Price, &a.TimeGranularity, &a.TimeMinDuration, &startsAtString, &stopsAtString, &a.IfIdIngress, &a.IfIdEgress)
+ if err != nil {
+ return nil, serrors.Wrap("Error reading DB response", err)
+ }
+ a.IA, err = addr.IAFrom(addr.ISD(isd), addr.AS(as))
+ a.StartAt, err = time.Parse(time.RFC3339, startsAtString)
+ if err != nil {
+ return nil, err
+ }
+ a.StopsAt, err = time.Parse(time.RFC3339, stopsAtString)
+ if err != nil {
+ return nil, err
+ }
+ return a, nil
+}
+
+func (e *executor) InsertReservation(ctx context.Context, r *DBReservation) (int64, error) {
+ if e.write == nil {
+ return 0, serrors.New("No database open")
+ }
+ q := `INSERT INTO Reservations (reservation_id, isd_id, as_id, ingress, egress, bandwidth, bw_encoded, starts_at, stops_at, key, account_id)
+ VALUES(?,?,?,?,?,?,?,?,?,?,?)`
+ res, err := e.write.ExecContext(ctx, q, r.ReservationID, r.IA.ISD(), r.IA.AS(), r.Ingress, r.Egress, r.Bandwidth, r.EncodedBandwidth,
+ r.StartsAt.UTC().Format(time.RFC3339), r.StopsAt.UTC().Format(time.RFC3339),
+ r.Key, r.AccountId)
+ if err != nil {
+ return 0, err
+ }
+ return res.RowsAffected()
+}
+
+func (e *executor) InsertAsset(ctx context.Context, a *DBAsset) (int64, error) {
+ if e.write == nil {
+ return 0, serrors.New("No database open")
+ }
+ var res sql.Result
+ var err error
+ if a.AccountId.Valid {
+ inst := `INSERT INTO Assets (isd_id, as_id, bandwidth, bandwidth_min, bandwidth_max, price, time_granularity,
+ time_min_duration, starts_at, stops_at, ingress, egress, account_id)
+ VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)`
+ res, err = e.write.ExecContext(ctx, inst, a.IA.ISD(), a.IA.AS(), a.Bandwidth, a.BandwidthMin, a.BandwidthMax, a.Price, a.TimeGranularity,
+ a.TimeMinDuration, a.StartAt.UTC().Format(time.RFC3339), a.StopsAt.UTC().Format(time.RFC3339), a.IfIdIngress, a.IfIdEgress, a.AccountId)
+ } else {
+ inst := `INSERT INTO Assets (isd_id, as_id, bandwidth, bandwidth_min, bandwidth_max, price, time_granularity,
+ time_min_duration, starts_at, stops_at, ingress, egress)
+ VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`
+ res, err = e.write.ExecContext(ctx, inst, a.IA.ISD(), a.IA.AS(), a.Bandwidth, a.BandwidthMin, a.BandwidthMax, a.Price, a.TimeGranularity,
+ a.TimeMinDuration, a.StartAt.UTC().Format(time.RFC3339), a.StopsAt.UTC().Format(time.RFC3339), a.IfIdIngress, a.IfIdEgress)
+ }
+ if err != nil {
+ return 0, err
+ }
+ return res.LastInsertId()
+}
+
+func (e *executor) DeleteListedAsset(ctx context.Context, ia addr.IA, assetID int64) (int64, error) {
+ if e.write == nil {
+ return 0, serrors.New("No database open")
+ }
+ inst := `DELETE FROM Assets WHERE id = ? AND isd_id = ? AND as_id = ? AND account_id IS NULL AND state = 0`
+ res, err := e.write.ExecContext(ctx, inst, assetID, ia.ISD(), ia.AS())
+ if err != nil {
+ return 0, err
+ }
+ return res.RowsAffected()
+}
+
+func (e *executor) RemoveAsset(ctx context.Context, assetID int64) error {
+ if e.write == nil {
+ return serrors.New("No database open")
+ }
+ inst := `DELETE FROM Assets WHERE id = ?`
+ _, err := e.write.ExecContext(ctx, inst, assetID)
+ return err
+}
+
+func (e *executor) GetUser(ctx context.Context, id int64) (*DBUser, error) {
+ if e.read == nil {
+ return nil, serrors.New("No database open")
+ }
+ q := `SELECT id, name, pw_hash FROM Users WHERE id=?`
+ rows, err := e.read.QueryContext(ctx, q, id)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ if !rows.Next() {
+ return nil, nil
+ }
+ user := &DBUser{}
+ err = rows.Scan(&user.ID, &user.Name, &user.PasswordHash)
+ if err != nil {
+ return nil, serrors.Wrap("Error reading DB response", err)
+ }
+ return user, nil
+}
+func (e *executor) GetUserByName(ctx context.Context, name string) (*DBUser, error) {
+ if e.read == nil {
+ return nil, serrors.New("No database open")
+ }
+ q := `SELECT id, name, pw_hash FROM Users WHERE name=?`
+ rows, err := e.read.QueryContext(ctx, q, name)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ if !rows.Next() {
+ return nil, nil
+ }
+ user := &DBUser{}
+ err = rows.Scan(&user.ID, &user.Name, &user.PasswordHash)
+ if err != nil {
+ return nil, serrors.Wrap("Error reading DB response", err)
+ }
+ return user, nil
+}
+
+func (e *executor) GetAccountByAccountID(ctx context.Context, id int64) (*DBAccount, error) {
+ if e.read == nil {
+ return nil, serrors.New("No database open")
+ }
+ q := `SELECT id, user_id, scope, balance, jwt_version FROM Accounts WHERE id = ?`
+ rows, err := e.read.QueryContext(ctx, q, id)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ if !rows.Next() {
+ return nil, serrors.New("account not found")
+ }
+ account := &DBAccount{}
+ err = rows.Scan(&account.ID, &account.UserID, &account.Scope, &account.Balance, &account.TokenVersion)
+ if err != nil {
+ return nil, serrors.Wrap("Error reading DB response", err)
+ }
+ return account, nil
+}
+
+func (e *executor) GetAccountsByUser(ctx context.Context, id int64) ([]*DBAccount, error) {
+ if e.read == nil {
+ return nil, serrors.New("No database open")
+ }
+ q := `SELECT id, scope, balance, jwt_version FROM Accounts WHERE user_id = ?`
+ rows, err := e.read.QueryContext(ctx, q, id)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ ret := []*DBAccount{}
+ for rows.Next() {
+ account := &DBAccount{}
+ err = rows.Scan(&account.ID, &account.Scope, &account.Balance, &account.TokenVersion)
+ if err != nil {
+ return nil, serrors.Wrap("Error reading DB response", err)
+ }
+ ret = append(ret, account)
+ }
+ return ret, nil
+}
+
+func (e *executor) CreateAccount(ctx context.Context, account *DBAccount) (int64, error) {
+ if e.write == nil {
+ return 0, serrors.New("No database open")
+ }
+ inst := `INSERT INTO Accounts (user_id, scope, balance) VALUES(?,?, ?)`
+ res, err := e.write.ExecContext(ctx, inst, account.UserID, account.Scope, account.Balance)
+ if err != nil {
+ return 0, err
+ }
+ return res.LastInsertId()
+}
+
+func (e *executor) CreateUser(ctx context.Context, user *DBUser) (int64, error) {
+ if e.write == nil {
+ return 0, serrors.New("No database open")
+ }
+ inst := `INSERT INTO Users (name, pw_hash)
+ VALUES(?,?) ON CONFLICT(name) DO NOTHING`
+ res, err := e.write.ExecContext(ctx, inst, user.Name, user.PasswordHash)
+ if err != nil {
+ return 0, err
+ }
+ return res.LastInsertId()
+}
+
+func (e *executor) CreateASUser(ctx context.Context, user *DBASUser) (int64, error) {
+ if e.write == nil {
+ return 0, serrors.New("No database open")
+ }
+ inst := `INSERT INTO Ases (isd_id, as_id) VALUES(?,?) ON CONFLICT DO NOTHING`
+ res, err := e.write.ExecContext(ctx, inst, user.IA.ISD(), user.IA.AS())
+ if err != nil {
+ return 0, err
+ }
+ return res.LastInsertId()
+}
+
+func (e *executor) GetASUser(ctx context.Context, ia addr.IA) (*DBASUser, error) {
+ if e.read == nil {
+ return nil, serrors.New("No database open")
+ }
+ q := `SELECT isd_id, as_id, pw_hash, jwt_version, balance FROM Ases WHERE isd_id=? AND as_id=?`
+ rows, err := e.read.QueryContext(ctx, q, ia.ISD(), ia.AS())
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ if !rows.Next() {
+ return nil, nil
+ }
+ user := &DBASUser{}
+ var isd uint16
+ var as uint64
+ err = rows.Scan(&isd, &as, &user.PasswordHash, &user.TokenVersion, &user.Balance)
+ if err != nil {
+ return nil, serrors.Wrap("Error reading DB response", err)
+ }
+ user.IA, err = addr.IAFrom(ia.ISD(), addr.AS(as))
+ if err != nil {
+ return nil, err
+ }
+ return user, nil
+}
+
+func (e *executor) UpdateAccountMoneyWithUser(ctx context.Context, userId int64, accountId int64, amount int64) (int64, error) {
+ if e.write == nil {
+ return 0, serrors.New("No database open")
+ }
+ inst := `UPDATE Accounts SET balance = balance + ? WHERE id = ? AND user_id = ?`
+ res, err := e.write.ExecContext(ctx, inst, amount, accountId, userId)
+ if err != nil {
+ return 0, err
+ }
+ return res.RowsAffected()
+}
+
+func (e *executor) UpdateAccountMoney(ctx context.Context, id int64, amount int64) (int64, error) {
+ if e.write == nil {
+ return 0, serrors.New("No database open")
+ }
+ inst := `UPDATE Accounts SET balance = balance + ? WHERE id = ?`
+ res, err := e.write.ExecContext(ctx, inst, amount, id)
+ if err != nil {
+ return 0, err
+ }
+ return res.RowsAffected()
+}
+
+func (e *executor) UpdateASMoney(ctx context.Context, ia addr.IA, amount int64) (int64, error) {
+ if e.write == nil {
+ return 0, serrors.New("No database open")
+ }
+ inst := `UPDATE Ases SET balance = balance + ? WHERE isd_id = ? AND as_id = ?`
+ res, err := e.write.ExecContext(ctx, inst, amount, ia.ISD(), ia.AS())
+ if err != nil {
+ return 0, err
+ }
+ return res.RowsAffected()
+}
+func (e *executor) SetASAuthenticationToken(ctx context.Context, ia addr.IA, auth string) (int64, error) {
+ if e.write == nil {
+ return 0, serrors.New("No database open")
+ }
+ inst := `UPDATE Ases SET pw_hash = ? WHERE isd_id = ? AND as_id = ?`
+ res, err := e.write.ExecContext(ctx, inst, auth, ia.ISD(), ia.AS())
+ if err != nil {
+ return 0, err
+ }
+ return res.RowsAffected()
+}
+
+func (e *executor) TransferAllAssetsToAccount(ctx context.Context, accountIdFrom int64, accountIdTo int64) (int64, error) {
+ if e.write == nil {
+ return 0, serrors.New("No database open")
+ }
+ inst := `UPDATE Assets SET account_id = ? WHERE account_id = ?`
+ res, err := e.write.ExecContext(ctx, inst, accountIdTo, accountIdFrom)
+ if err != nil {
+ return 0, err
+ }
+ return res.RowsAffected()
+}
+
+func (e *executor) TransferAllReservations(ctx context.Context, accountIdFrom int64, accountIdTo int64) (int64, error) {
+ if e.write == nil {
+ return 0, serrors.New("No database open")
+ }
+ inst := `UPDATE Reservations SET account_id = ? WHERE account_id = ?`
+ res, err := e.write.ExecContext(ctx, inst, accountIdTo, accountIdFrom)
+ if err != nil {
+ return 0, err
+ }
+ return res.RowsAffected()
+}
+
+func (e *executor) DeleteAccount(ctx context.Context, accountID int64) (int64, error) {
+ if e.write == nil {
+ return 0, serrors.New("No database open")
+ }
+ inst := `DELETE FROM Accounts WHERE id = ? AND scope IS NOT NULL`
+ res, err := e.write.ExecContext(ctx, inst, accountID)
+ if err != nil {
+ return 0, err
+ }
+ return res.RowsAffected()
+}
+
+func (e *executor) AssignAsset(ctx context.Context, assetID int64, accountIDFrom int64, accountIDTo int64) (int64, error) {
+ if e.write == nil {
+ return 0, serrors.New("No database open")
+ }
+ inst := `UPDATE Assets SET account_id = ? WHERE id=? AND account_id = ?`
+ res, err := e.write.ExecContext(ctx, inst, accountIDTo, assetID, accountIDFrom)
+ if err != nil {
+ return 0, err
+ }
+ return res.RowsAffected()
+}
+
+func (e *executor) AssignReservation(ctx context.Context, id int64, accountIDFrom int64, accountIDTo int64) (int64, error) {
+ if e.write == nil {
+ return 0, serrors.New("No database open")
+ }
+ inst := `UPDATE Reservations SET account_id = ? WHERE id=? AND account_id = ?`
+ res, err := e.write.ExecContext(ctx, inst, accountIDTo, id, accountIDFrom)
+ if err != nil {
+ return 0, err
+ }
+ return res.RowsAffected()
+}
diff --git a/marketplace/db/types.go b/marketplace/db/types.go
new file mode 100644
index 0000000000..1a4601399c
--- /dev/null
+++ b/marketplace/db/types.go
@@ -0,0 +1,148 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package db
+
+import (
+ "database/sql"
+ "encoding/binary"
+ "time"
+
+ "github.com/scionproto/scion/pkg/addr"
+)
+
+type AssetQuery struct {
+ AccountId *int64
+ IA *addr.IA
+ Ingress *uint32
+ Egress *uint32
+ MinRequiredBandwidth *uint32
+ StartsAt *string
+ StopsAt *string
+ Price *uint32
+}
+
+type UsedReservationsQuery struct {
+ IA addr.IA
+ Limit uint32
+}
+
+type UsedReservation struct {
+ Id uint32
+ StartsAt time.Time
+ StopsAt time.Time
+}
+
+type RedemptionDelegation struct {
+ IA addr.IA
+ Expiration time.Time
+ PaidUntil time.Time
+ ReservationIdLimit uint32
+ Key []byte
+ Encodings []byte
+}
+
+type ReservationQuery struct {
+ AccountId int64
+ IA *addr.IA
+ Ingress *uint32
+ Egress *uint32
+ StartsAt *string
+ StopsAt *string
+ Bandwidth *uint32
+}
+type StatisticsQuery struct {
+ IA addr.IA
+ WindowStart string
+ WindowEnd string
+ Ingress *uint32
+ Egress *uint32
+}
+type DBStat struct {
+ Price int64
+ Bandwidth int64
+ StartsAt time.Time
+ StopsAt time.Time
+ OwnerId sql.NullInt64
+}
+
+type DBReservation struct {
+ ID int64
+ ReservationID uint32
+ IA addr.IA
+ Ingress uint32
+ Egress uint32
+ Bandwidth uint32
+ EncodedBandwidth uint16
+ StartsAt time.Time
+ StopsAt time.Time
+ AccountId int64
+ Key []byte
+}
+
+type DBAsset struct {
+ ID int64
+ AccountId sql.NullInt64
+ IA addr.IA
+ Bandwidth uint32
+ BandwidthMin uint32
+ BandwidthMax uint32
+ StartAt time.Time
+ StopsAt time.Time
+ Price uint32
+ TimeGranularity uint32
+ TimeMinDuration uint32
+ IfIdIngress sql.NullInt32
+ IfIdEgress sql.NullInt32
+}
+
+type DBUser struct {
+ ID int64
+ Name string
+ PasswordHash string
+}
+
+type DBAccount struct {
+ ID int64
+ UserID int64
+ Scope *string
+ Balance int64
+ TokenVersion int64
+}
+
+type DBASUser struct {
+ IA addr.IA
+ PasswordHash string
+ TokenVersion int64
+ Balance int64
+}
+
+func (r *RedemptionDelegation) EncodingsToInts() []uint32 {
+ if len(r.Encodings)%4 != 0 {
+ panic("invalid data length")
+ }
+ nums := make([]uint32, len(r.Encodings)/4)
+ for i := range nums {
+ nums[i] = binary.LittleEndian.Uint32(r.Encodings[i*4:])
+ }
+ return nums
+}
+
+func (r *RedemptionDelegation) EncodeInts(nums []uint32) {
+ buf := make([]byte, len(nums)*4)
+ for i, n := range nums {
+ binary.LittleEndian.PutUint32(buf[i*4:], n)
+ }
+ r.Encodings = buf
+}
diff --git a/marketplace/export_test.go b/marketplace/export_test.go
new file mode 100644
index 0000000000..e8ae245516
--- /dev/null
+++ b/marketplace/export_test.go
@@ -0,0 +1,23 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package marketplace
+
+func (s *RedemptionService) SetEncodingPoints(encodings []uint32) {
+ s.encodingPoints = encodings
+}
+
+func (s *RedemptionService) EncodeBandwidth(bw uint32) uint16 {
+ return (s.encodeBandwidth(bw))
+}
diff --git a/marketplace/redeem.go b/marketplace/redeem.go
new file mode 100644
index 0000000000..458166f7fd
--- /dev/null
+++ b/marketplace/redeem.go
@@ -0,0 +1,272 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package marketplace
+
+import (
+ "context"
+ "fmt"
+ "sync"
+ "time"
+
+ "connectrpc.com/connect"
+ "github.com/scionproto/scion/marketplace/db"
+ "github.com/scionproto/scion/pkg/addr"
+ "github.com/scionproto/scion/pkg/private/serrors"
+ "github.com/scionproto/scion/pkg/proto/hummingbird"
+)
+
+func (s *Service) newRedemptionServerPeer(ia addr.IA) *RedemptionServerPeer {
+ peer := &RedemptionServerPeer{
+ ia: ia,
+ sendCh: make(chan *hummingbird.RedeemAssetFromASRequest, 64),
+ pending: make(map[uint64]chan *hummingbird.RedeemAssetFromASResponse),
+ }
+ s.redemptionServerPeers[ia] = peer
+ return peer
+}
+
+type RedemptionServerPeer struct {
+ delegatedServer *RedemptionService
+ ia addr.IA
+ sendCh chan *hummingbird.RedeemAssetFromASRequest
+ pending map[uint64]chan *hummingbird.RedeemAssetFromASResponse
+ requestID uint64
+ cancelOldConnection context.CancelFunc
+ mtx sync.Mutex
+}
+
+func (c *RedemptionServerPeer) Send(req *hummingbird.RedeemAssetFromASRequest) <-chan *hummingbird.RedeemAssetFromASResponse {
+ respCh := make(chan *hummingbird.RedeemAssetFromASResponse, 1)
+ c.mtx.Lock()
+ req.RequestId = c.requestID
+ c.requestID++
+ c.pending[req.RequestId] = respCh
+ c.mtx.Unlock()
+ c.sendCh <- req
+ return respCh
+}
+
+func (c *RedemptionServerPeer) ReturnResponse(msg *hummingbird.RedeemAssetFromASResponse) {
+ reqID := msg.RequestId
+ c.mtx.Lock()
+ defer c.mtx.Unlock()
+ ch, ok := c.pending[reqID]
+ if ok {
+ ch <- msg
+ close(ch)
+ delete(c.pending, reqID)
+ }
+}
+
+func (s *Service) startOrUpdateRedemptionDelegation(ctx context.Context, clientID addr.IA, state *RedemptionDelegationUpdate, dbSync bool) error {
+ fmt.Println("startOrUpdateRedemptionDelegation", clientID)
+ s.mtx.Lock()
+ defer s.mtx.Unlock()
+ client, found := s.redemptionServerPeers[clientID]
+ var err error
+ if !found {
+ // new peer
+ client = s.newRedemptionServerPeer(clientID)
+ client.mtx.Lock()
+ defer client.mtx.Unlock()
+ s.redemptionServerPeers[clientID] = client
+ if dbSync {
+ dbDelegation := &db.RedemptionDelegation{
+ IA: clientID,
+ Expiration: state.ExpirationTime,
+ ReservationIdLimit: state.ReservationIdLimit,
+ Key: state.Key,
+ }
+ dbDelegation.EncodeInts(state.EncodingPoints)
+ _, err = s.store.CreateOrUpdateRedemptionDelegations(ctx, dbDelegation)
+ if err != nil {
+ return err
+ }
+ }
+ client.delegatedServer, err = NewRedemptionService(ctx, client, s.store, clientID, state, client.sendCh, client.pending)
+ if err != nil {
+ return err
+ }
+
+ } else if client.delegatedServer != nil {
+ // the redemption server was already delegated, but we received an update request
+ client.mtx.Lock()
+ defer client.mtx.Unlock()
+ if dbSync {
+ dbDelegation := &db.RedemptionDelegation{
+ IA: clientID,
+ Expiration: state.ExpirationTime,
+ ReservationIdLimit: state.ReservationIdLimit,
+ Key: state.Key,
+ }
+ dbDelegation.EncodeInts(state.EncodingPoints)
+ _, err = s.store.CreateOrUpdateRedemptionDelegations(ctx, dbDelegation)
+ if err != nil {
+ return err
+ }
+ }
+ client.delegatedServer.UpdateChannel <- state
+ err = <-client.delegatedServer.UpdateResultChannel
+ if err != nil {
+ return err
+ }
+ } else {
+ // the redemption server was previously run by the AS, now it is delegated
+ client.mtx.Lock()
+ defer client.mtx.Unlock()
+ if client.cancelOldConnection != nil {
+ client.cancelOldConnection()
+ }
+ if dbSync {
+ dbDelegation := &db.RedemptionDelegation{
+ IA: clientID,
+ Expiration: state.ExpirationTime,
+ ReservationIdLimit: state.ReservationIdLimit,
+ Key: state.Key,
+ }
+ dbDelegation.EncodeInts(state.EncodingPoints)
+ _, err = s.store.CreateOrUpdateRedemptionDelegations(ctx, dbDelegation)
+ if err != nil {
+ return err
+ }
+ }
+ client.delegatedServer, err = NewRedemptionService(ctx, client, s.store, clientID, state, client.sendCh, client.pending)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (s *Service) stopRedemptionDelegation(ctx context.Context, clientID addr.IA) error {
+ fmt.Println("stopRedemptionDelegation", clientID)
+ s.mtx.Lock()
+ defer s.mtx.Unlock()
+ client, found := s.redemptionServerPeers[clientID]
+ if !found {
+ return serrors.New("cannot stop redemption delegation of unkown peer", "ia", clientID)
+ }
+ client.mtx.Lock()
+ defer client.mtx.Unlock()
+ if client.delegatedServer == nil {
+ return serrors.New("cannot stop redemption delegation without active delegation")
+ }
+ client.delegatedServer.UpdateChannel <- &RedemptionDelegationUpdate{ExpirationTime: time.Time{}}
+ _ = <-client.delegatedServer.UpdateResultChannel
+ client.delegatedServer = nil
+ _, err := s.store.CreateOrUpdateRedemptionDelegations(ctx, &db.RedemptionDelegation{
+ IA: clientID,
+ Expiration: time.Time{},
+ })
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (s *Service) DelegateRedemption(ctx context.Context, req *connect.Request[hummingbird.DelegateRedemptionRequest]) (*connect.Response[hummingbird.DelegateRedemptionResponse], error) {
+ fmt.Println("DelegateRedemption")
+ clientID, ok := ctx.Value("user").(addr.IA)
+ if !ok {
+ return nil, connect.NewError(connect.CodePermissionDenied, serrors.New("ia not provided"))
+ }
+ expTime := req.Msg.ExpirationTime.AsTime()
+ if expTime.Before(time.Now()) {
+ err := s.stopRedemptionDelegation(ctx, clientID)
+ if err != nil {
+ return nil, connect.NewError(connect.CodeInvalidArgument, err)
+ }
+ return &connect.Response[hummingbird.DelegateRedemptionResponse]{
+ Msg: &hummingbird.DelegateRedemptionResponse{},
+ }, nil
+ }
+ err := s.startOrUpdateRedemptionDelegation(ctx, clientID, &RedemptionDelegationUpdate{
+ ExpirationTime: req.Msg.ExpirationTime.AsTime(),
+ ReservationIdLimit: req.Msg.ReservationIdUpperBound,
+ Key: req.Msg.Key,
+ EncodingPoints: req.Msg.EncodingPoints,
+ }, true)
+ if err != nil {
+ return nil, connect.NewError(connect.CodeInvalidArgument, err)
+ }
+ return &connect.Response[hummingbird.DelegateRedemptionResponse]{
+ Msg: &hummingbird.DelegateRedemptionResponse{
+ ExpirationTime: req.Msg.ExpirationTime,
+ },
+ }, nil
+}
+
+func (s *Service) RedeemASAsset(ctx context.Context, stream *connect.BidiStream[hummingbird.RedeemAssetFromASResponse, hummingbird.RedeemAssetFromASRequest]) error {
+ fmt.Println("RedeemAsset (AS)")
+ clientID, ok := ctx.Value("user").(addr.IA)
+ if !ok {
+ return connect.NewError(connect.CodePermissionDenied, serrors.New("ia not provided"))
+ }
+ s.mtx.Lock()
+ client, found := s.redemptionServerPeers[clientID]
+ if !found {
+ client = s.newRedemptionServerPeer(clientID)
+ s.redemptionServerPeers[clientID] = client
+ }
+ s.mtx.Unlock()
+ s.stopRedemptionDelegation(ctx, clientID)
+ client.mtx.Lock()
+ if client.cancelOldConnection != nil {
+ client.cancelOldConnection()
+ }
+ cancelCtx, cancelF := context.WithCancel(ctx)
+ client.cancelOldConnection = cancelF
+ client.mtx.Unlock()
+ _, err := stream.Receive()
+ if err != nil {
+ return err
+ }
+ fmt.Println("AS redemption service connected:", clientID)
+
+ go func() {
+ for {
+ select {
+ case <-cancelCtx.Done():
+ return
+ default:
+ msg, err := stream.Receive()
+ if err != nil {
+ fmt.Println("receive error", err)
+ return
+ }
+ if client.delegatedServer != nil {
+ continue
+ }
+ client.ReturnResponse(msg)
+ }
+ }
+ }()
+ for {
+ select {
+ case <-cancelCtx.Done():
+ return nil
+ case req := <-client.sendCh:
+ if req == nil {
+ return serrors.New("send channel closed")
+ }
+ if client.delegatedServer != nil {
+ continue
+ }
+ if err := stream.Send(req); err != nil {
+ return err
+ }
+ }
+ }
+}
diff --git a/marketplace/redemption_service.go b/marketplace/redemption_service.go
new file mode 100644
index 0000000000..928f25e160
--- /dev/null
+++ b/marketplace/redemption_service.go
@@ -0,0 +1,306 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package marketplace
+
+import (
+ "container/heap"
+ "context"
+ "crypto/aes"
+ "crypto/cipher"
+ "fmt"
+ "slices"
+ "sort"
+ "time"
+
+ "github.com/scionproto/scion/marketplace/db"
+ "github.com/scionproto/scion/marketplace/storage"
+ "github.com/scionproto/scion/pkg/addr"
+ "github.com/scionproto/scion/pkg/private/serrors"
+ "github.com/scionproto/scion/pkg/proto/hummingbird"
+ hbird "github.com/scionproto/scion/pkg/slayers/path/hummingbird"
+)
+
+const (
+ AkBufferSize = 16
+)
+
+type RedemptionService struct {
+ SendChannel chan *hummingbird.RedeemAssetFromASRequest
+ Pending map[uint64]chan *hummingbird.RedeemAssetFromASResponse
+ // The update channel is used to propagate updates to the redemption service
+ // like different secret key or updated expiration date
+ // The result of the update is send over the UpdateResultChannel, which MUST
+ // be consumed by the entity who send the update over the UpdateChannel.
+ UpdateChannel chan *RedemptionDelegationUpdate
+ UpdateResultChannel chan error
+ client *RedemptionServerPeer
+ store *storage.MarketplaceStorage
+ cipher cipher.Block
+ resLimit uint32
+ resIdStore ReservationIdStore
+ encodingPoints []uint32
+ expiration time.Time
+}
+
+type RedemptionDelegationUpdate struct {
+ ExpirationTime time.Time
+ ReservationIdLimit uint32
+ Key []byte
+ EncodingPoints []uint32
+}
+
+type RedemptionDelegationUpdateResult struct {
+ ExpirationTime time.Time
+ OK bool
+}
+
+type ReservationIdStore interface {
+ Init(limit uint32, r []*db.UsedReservation) error
+ Next(now int64, start int64, end int64) (uint32, error)
+ Migrate(newLimit uint32, r []*db.UsedReservation) error
+ Close() error
+}
+
+func NewRedemptionService(ctx context.Context, client *RedemptionServerPeer, store *storage.MarketplaceStorage, ia addr.IA, initState *RedemptionDelegationUpdate, sendCh chan *hummingbird.RedeemAssetFromASRequest,
+ pending map[uint64]chan *hummingbird.RedeemAssetFromASResponse) (*RedemptionService, error) {
+ slices.Sort(initState.EncodingPoints)
+ blockCipher, err := aes.NewCipher(initState.Key)
+ if err != nil {
+ return nil, err
+ }
+ s := &RedemptionService{
+ SendChannel: sendCh,
+ Pending: pending,
+ UpdateChannel: make(chan *RedemptionDelegationUpdate),
+ UpdateResultChannel: make(chan error),
+ store: store,
+ cipher: blockCipher,
+ resLimit: initState.ReservationIdLimit,
+ encodingPoints: initState.EncodingPoints,
+ client: client,
+ expiration: initState.ExpirationTime,
+ resIdStore: &UsedIDStore{},
+ }
+ res, err := s.store.FindUsedReservations(ctx, &db.UsedReservationsQuery{
+ IA: ia,
+ Limit: initState.ReservationIdLimit,
+ })
+ if err != nil {
+ return nil, err
+ }
+ err = s.resIdStore.Init(initState.ReservationIdLimit, res)
+ if err != nil {
+ return nil, err
+ }
+ go s.readRoutine()
+ return s, nil
+}
+
+func (s *RedemptionService) readRoutine() {
+ var err error
+ for {
+ select {
+ case u := <-s.UpdateChannel:
+ if u.ExpirationTime.Before(time.Now()) {
+ s.UpdateResultChannel <- nil
+ return
+ }
+ s.UpdateResultChannel <- s.handleUpdate(u)
+ case r := <-s.SendChannel:
+ now := time.Now()
+ if s.expiration.Before(now) {
+ select {
+ case s.SendChannel <- r:
+ default:
+ }
+ return
+ }
+ if err = s.handleRequest(now, r); err != nil {
+ fmt.Println(err)
+ return
+ }
+ }
+ }
+}
+
+func (s *RedemptionService) handleUpdate(u *RedemptionDelegationUpdate) error {
+ var res []*db.UsedReservation
+ var err error
+ if u.ExpirationTime != s.expiration {
+ res, err = s.store.FindUsedReservations(context.TODO(), &db.UsedReservationsQuery{
+ IA: s.client.ia,
+ Limit: u.ReservationIdLimit,
+ })
+ if err != nil {
+ return err
+ }
+ s.expiration = u.ExpirationTime
+ }
+ err = s.resIdStore.Migrate(u.ReservationIdLimit, res)
+ if err != nil {
+ return err
+ }
+ slices.Sort(u.EncodingPoints)
+ s.encodingPoints = u.EncodingPoints
+ s.cipher, err = aes.NewCipher(u.Key)
+ return err
+}
+
+func (s *RedemptionService) handleRequest(now time.Time, r *hummingbird.RedeemAssetFromASRequest) error {
+ resId, err := s.resIdStore.Next(now.Unix(), r.StartsAt.Seconds, r.StopsAt.Seconds)
+ if err != nil {
+ return err
+ }
+
+ unixStart := uint32(r.StartsAt.Seconds)
+ unixEnd := uint32(r.StopsAt.Seconds)
+ durSeconds := unixEnd - unixStart
+ encoded_bw := s.encodeBandwidth(r.Bandwidth)
+
+ var buff [16]byte
+ ak := hbird.DeriveAuthKey(s.cipher, resId, encoded_bw, uint16(r.IngressId), uint16(r.EgressId), unixStart, uint16(durSeconds), buff[:])
+ s.client.mtx.Lock()
+ ch, ok := s.Pending[r.RequestId]
+ if ok {
+ ch <- &hummingbird.RedeemAssetFromASResponse{
+ Result: &hummingbird.RedeemAssetFromASResponse_ResInfo{
+ ResInfo: &hummingbird.ReservationInfo{
+ ReservationId: resId,
+ BandwithRounded: s.encodingPoints[encoded_bw],
+ BwDataplaneEncoding: uint32(encoded_bw),
+ AuthenticationKey: ak,
+ },
+ },
+ RequestId: r.RequestId,
+ }
+ close(ch)
+ delete(s.client.pending, r.RequestId)
+ }
+ s.client.mtx.Unlock()
+ return nil
+}
+
+func (s *RedemptionService) encodeBandwidth(bw uint32) uint16 {
+ return min(uint16(len(s.encodingPoints)-1), uint16(sort.Search(len(s.encodingPoints), func(i int) bool {
+ return s.encodingPoints[i] >= bw
+ })))
+}
+
+type entry struct {
+ id uint32
+ expiration int64
+}
+
+type ExpiryHeap []*entry
+
+func (h ExpiryHeap) Len() int {
+ return len(h)
+}
+
+func (h ExpiryHeap) Less(i, j int) bool {
+ return h[i].expiration < h[j].expiration
+}
+
+func (h ExpiryHeap) Swap(i, j int) {
+ h[i], h[j] = h[j], h[i]
+}
+
+func (h *ExpiryHeap) Push(x any) {
+ *h = append(*h, x.(*entry))
+}
+
+func (h *ExpiryHeap) Pop() any {
+ old := *h
+ n := len(old)
+
+ e := old[n-1]
+ *h = old[:n-1]
+
+ return e
+}
+
+type UsedIDStore struct {
+ usedIds map[uint32]struct{}
+ expirations ExpiryHeap
+ next uint32
+ limit uint32
+}
+
+func (s *UsedIDStore) Init(limit uint32, r []*db.UsedReservation) error {
+ s.usedIds = make(map[uint32]struct{})
+ s.limit = limit
+ s.next = 0
+ for _, res := range r {
+ s.usedIds[res.Id] = struct{}{}
+ s.expirations = append(s.expirations, &entry{
+ id: res.Id,
+ expiration: res.StopsAt.Unix(),
+ })
+ }
+ heap.Init(&s.expirations)
+ return nil
+}
+
+func (s *UsedIDStore) Next(now int64, start int64, end int64) (uint32, error) {
+ if len(s.expirations) != 0 && s.expirations[0].expiration <= now {
+ e := heap.Pop(&s.expirations).(*entry)
+ heap.Push(&s.expirations, &entry{
+ id: e.id,
+ expiration: end,
+ })
+ return e.id, nil
+ }
+ for ; s.next < s.limit; s.next++ {
+ _, found := s.usedIds[s.next]
+ if !found {
+ s.usedIds[s.next] = struct{}{}
+ heap.Push(&s.expirations, &entry{
+ id: s.next,
+ expiration: end,
+ })
+ tmp := s.next
+ s.next++
+ return tmp, nil
+ }
+ }
+ return 0, serrors.New("no free reservation id")
+}
+
+func (s *UsedIDStore) Migrate(newLimit uint32, r []*db.UsedReservation) error {
+ if s.next > newLimit {
+ return serrors.New("newLimit too small")
+ }
+ if r != nil {
+ clear(s.usedIds)
+ clear(s.expirations)
+ s.next = 0
+ for _, res := range r {
+ s.usedIds[res.Id] = struct{}{}
+ s.expirations = append(s.expirations, &entry{
+ id: res.Id,
+ expiration: res.StopsAt.Unix(),
+ })
+ }
+ heap.Init(&s.expirations)
+ }
+
+ s.limit = newLimit
+ return nil
+}
+
+func (s *UsedIDStore) Close() error {
+ clear(s.usedIds)
+ return nil
+}
diff --git a/marketplace/redemption_service_test.go b/marketplace/redemption_service_test.go
new file mode 100644
index 0000000000..324064c70e
--- /dev/null
+++ b/marketplace/redemption_service_test.go
@@ -0,0 +1,80 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package marketplace_test
+
+import (
+ "math"
+ "testing"
+ "time"
+
+ "github.com/scionproto/scion/marketplace"
+ "github.com/scionproto/scion/marketplace/db"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestIDStore(t *testing.T) {
+ base, err := time.Parse(time.RFC3339, "2026-07-16T00:00:00Z")
+ b := base.Unix()
+ assert.NoError(t, err)
+ store := marketplace.UsedIDStore{}
+ err = store.Init(3, []*db.UsedReservation{
+ {
+ Id: 0,
+ StartsAt: base,
+ StopsAt: base.Add(time.Second * 3),
+ },
+ })
+ assert.NoError(t, err)
+ nextId, err := store.Next(b, b, b+1)
+ assert.NoError(t, err)
+ assert.Equal(t, uint32(1), nextId)
+ nextId, err = store.Next(b, b, b+2)
+ assert.NoError(t, err)
+ assert.Equal(t, uint32(2), nextId)
+ nextId, err = store.Next(b+1, b+1, b+2)
+ assert.NoError(t, err)
+ assert.Equal(t, uint32(1), nextId)
+ nextId, err = store.Next(b+1, b+1, b+2)
+ assert.Error(t, err)
+}
+
+func TestEncoding(t *testing.T) {
+ const (
+ minBwKbps = 10.0
+ maxBwKbps = 10_000_000.0
+ levels = 1024
+ logEncodingStart = 60
+ )
+ var step = math.Pow(maxBwKbps/(minBwKbps+float64(logEncodingStart)), 1.0/float64(levels-logEncodingStart-1))
+ indexToBwKbps := func(i int) int {
+ if i < logEncodingStart {
+ return i + minBwKbps
+ }
+ bw := (minBwKbps + logEncodingStart) * math.Pow(step, float64(i-logEncodingStart))
+ return int(math.Ceil(bw))
+ }
+ encodings := make([]uint32, levels)
+ for i := 0; i < levels; i++ {
+ encodings[i] = uint32(indexToBwKbps(i))
+ }
+ s := &marketplace.RedemptionService{}
+ s.SetEncodingPoints(encodings)
+ assert.Equal(t, uint16(0), s.EncodeBandwidth(minBwKbps))
+ assert.Equal(t, uint16(1), s.EncodeBandwidth(minBwKbps+1))
+ assert.Equal(t, uint16(59), s.EncodeBandwidth(minBwKbps+59))
+ assert.Equal(t, uint16(60), s.EncodeBandwidth(minBwKbps+60))
+ assert.Equal(t, uint16(levels-1), s.EncodeBandwidth(maxBwKbps))
+ assert.Equal(t, uint16(levels-1), s.EncodeBandwidth(math.MaxUint32))
+}
diff --git a/marketplace/service.go b/marketplace/service.go
new file mode 100644
index 0000000000..dd5338b03e
--- /dev/null
+++ b/marketplace/service.go
@@ -0,0 +1,654 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package marketplace
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "strconv"
+ "sync"
+ "time"
+
+ "connectrpc.com/connect"
+ "github.com/scionproto/scion/marketplace/db"
+ "github.com/scionproto/scion/marketplace/storage"
+ "github.com/scionproto/scion/pkg/addr"
+ "github.com/scionproto/scion/pkg/hummingbird/registration"
+ "github.com/scionproto/scion/pkg/log"
+ "github.com/scionproto/scion/pkg/private/serrors"
+ "github.com/scionproto/scion/pkg/proto/hummingbird"
+ "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+type Service struct {
+ redemptionServerPeers map[addr.IA]*RedemptionServerPeer
+ mtx sync.Mutex
+ info *MarketplaceInfo
+ store *storage.MarketplaceStorage
+ registrationService *registration.Service
+ signer *registration.Signer
+}
+
+type MarketplaceInfo struct {
+ ApiMajorVersion uint32
+ ApiMinorVersion uint32
+ Currency string
+ StatisticsTimeGranularity uint32
+ SupportsRedemptionDelegation bool
+ CurrencyExponent uint32
+ PricingStrategy hummingbird.PricingStrategy
+ TransactionFeeRelative float32
+ TransactionFeeAbsolute uint64
+ SplitCombineFeeAbsolute uint64
+ DelegationHourlyFee uint64
+}
+
+func NewService(ctx context.Context, info *MarketplaceInfo, store *storage.MarketplaceStorage, regService *registration.Service, signer *registration.Signer) (*Service, error) {
+ s := &Service{
+ redemptionServerPeers: make(map[addr.IA]*RedemptionServerPeer),
+ info: info,
+ store: store,
+ registrationService: regService,
+ signer: signer,
+ }
+ if s.info.SupportsRedemptionDelegation {
+ d, err := store.FindRedemptionDelegations(ctx)
+ if err != nil {
+ return nil, err
+ }
+ for _, delegation := range d {
+ delegation.EncodingsToInts()
+ peer := s.newRedemptionServerPeer(delegation.IA)
+ err = s.startOrUpdateRedemptionDelegation(ctx, peer.ia, &RedemptionDelegationUpdate{
+ ExpirationTime: delegation.Expiration,
+ ReservationIdLimit: delegation.ReservationIdLimit,
+ Key: delegation.Key,
+ EncodingPoints: delegation.EncodingsToInts(),
+ }, false)
+ if err != nil {
+ return nil, err
+ }
+ }
+ }
+ return s, nil
+}
+
+func (s *Service) CombineAssets(ctx context.Context, req *connect.Request[hummingbird.CombineAssetRequest]) (*connect.Response[hummingbird.CombineAssetResponse], error) {
+ user_id, ok := ctx.Value("user").(int64)
+ if !ok {
+ return nil, connect.NewError(connect.CodePermissionDenied, serrors.New("user_id not provided"))
+ }
+ assetId1, err := strconv.ParseInt(req.Msg.AssetId_1, 10, 64)
+ if err != nil {
+ return nil, connect.NewError(connect.CodeInvalidArgument, err)
+ }
+ assetId2, err := strconv.ParseInt(req.Msg.AssetId_2, 10, 64)
+ if err != nil {
+ return nil, connect.NewError(connect.CodeInvalidArgument, err)
+ }
+ if assetId1 == assetId2 {
+ return nil, connect.NewError(connect.CodeInvalidArgument, serrors.New("cannot combine asset with itself"))
+ }
+ combinedId, err := s.store.CombineAssets(ctx, user_id, assetId1, assetId2)
+ if err != nil {
+ return nil, connect.NewError(connect.CodeInvalidArgument, err)
+ }
+ return &connect.Response[hummingbird.CombineAssetResponse]{
+ Msg: &hummingbird.CombineAssetResponse{
+ AssetId: strconv.FormatInt(combinedId, 10),
+ },
+ }, nil
+}
+
+func (s *Service) SplitAsset(ctx context.Context, req *connect.Request[hummingbird.SplitAssetRequest]) (*connect.Response[hummingbird.SplitAssetResponse], error) {
+ userId, ok := ctx.Value("user").(int64)
+ if !ok {
+ return nil, connect.NewError(connect.CodePermissionDenied, serrors.New("user_id not provided"))
+ }
+ assetId, err := strconv.ParseInt(req.Msg.AssetId, 10, 64)
+ if err != nil {
+ return nil, connect.NewError(connect.CodeInvalidArgument, err)
+ }
+ var id1, id2 int64
+ switch req.Msg.SplitOption.(type) {
+ case *hummingbird.SplitAssetRequest_BwSplit:
+ bwSplit := req.Msg.GetBwSplit()
+ id1, id2, err = s.store.SplitAsset(ctx, userId, assetId, &bwSplit, nil)
+ case *hummingbird.SplitAssetRequest_TimeSplit:
+ timeSplit := req.Msg.GetTimeSplit().AsTime()
+ id1, id2, err = s.store.SplitAsset(ctx, userId, assetId, nil, &timeSplit)
+ default:
+ return nil, connect.NewError(connect.CodeInvalidArgument, serrors.New("invalid split request"))
+ }
+ if err != nil {
+ return nil, connect.NewError(connect.CodeInvalidArgument, err)
+ }
+ return &connect.Response[hummingbird.SplitAssetResponse]{
+ Msg: &hummingbird.SplitAssetResponse{
+ AssetId_1: strconv.FormatInt(id1, 10),
+ AssetId_2: strconv.FormatInt(id2, 10),
+ },
+ }, nil
+}
+
+func (s *Service) BuyAssets(ctx context.Context, req *connect.Request[hummingbird.BuyAssetsRequest]) (*connect.Response[hummingbird.BuyAssetsResponse], error) {
+ fmt.Println("BuyAssets")
+ user, ok := ctx.Value("user").(int64)
+ if !ok {
+ return nil, connect.NewError(connect.CodePermissionDenied, serrors.New("user_id not provided"))
+ }
+ boughtAssetIDs, totalCost, err := s.store.BuyAssets(ctx, user, req.Msg.Assets, req.Msg.MaxPrice)
+ if err != nil {
+ return nil, connect.NewError(connect.CodeInvalidArgument, err)
+ }
+ boughtAssets := make([]*hummingbird.BoughtAsset, 0, len(boughtAssetIDs))
+ for _, a := range boughtAssetIDs {
+ boughtAssets = append(boughtAssets, &hummingbird.BoughtAsset{
+ AssetId: strconv.FormatInt(a, 10),
+ })
+ }
+ return &connect.Response[hummingbird.BuyAssetsResponse]{
+ Msg: &hummingbird.BuyAssetsResponse{
+ Assets: boughtAssets,
+ Cost: uint64(totalCost),
+ },
+ }, nil
+}
+
+func (s *Service) FetchReservations(ctx context.Context, req *connect.Request[hummingbird.FetchReservationsRequest]) (*connect.Response[hummingbird.FetchReservationsResponse], error) {
+ user, ok := ctx.Value("user").(int64)
+ if !ok {
+ return nil, connect.NewError(connect.CodePermissionDenied, serrors.New("user_id not provided"))
+ }
+ var startsAt *string
+ var stopsAt *string
+ if req.Msg.StartsAt != nil {
+ start := req.Msg.StartsAt.AsTime().UTC().Format(time.RFC3339)
+ startsAt = &start
+ }
+ if req.Msg.StopsAt != nil {
+ stop := req.Msg.StopsAt.AsTime().UTC().Format(time.RFC3339)
+ stopsAt = &stop
+ }
+ var ia *addr.IA
+ if req.Msg.Ia != nil {
+ tmp := addr.IA(*req.Msg.Ia)
+ ia = &tmp
+ }
+ reservations, err := s.store.FetchReservations(ctx, &db.ReservationQuery{
+ IA: ia,
+ Ingress: req.Msg.IngressId,
+ Egress: req.Msg.EgressId,
+ StartsAt: startsAt,
+ StopsAt: stopsAt,
+ Bandwidth: req.Msg.Bandwidth,
+ AccountId: user,
+ })
+ if err != nil {
+ return nil, connect.NewError(connect.CodeInvalidArgument, err)
+ }
+ resp := make([]*hummingbird.Reservation, 0, len(reservations))
+ for _, reservation := range reservations {
+ resp = append(resp, &hummingbird.Reservation{
+ ReservationId: reservation.ReservationID,
+ Ia: uint64(reservation.IA),
+ IngressId: reservation.Ingress,
+ EgressId: reservation.Egress,
+ Bandwidth: reservation.Bandwidth,
+ DataplaneEncoding: uint32(reservation.EncodedBandwidth),
+ StartsAt: timestamppb.New(reservation.StartsAt),
+ StopsAt: timestamppb.New(reservation.StopsAt),
+ AuthenticationKey: reservation.Key,
+ })
+ }
+ return &connect.Response[hummingbird.FetchReservationsResponse]{
+ Msg: &hummingbird.FetchReservationsResponse{
+ Reservations: resp,
+ },
+ }, nil
+}
+
+func (s *Service) Info(context.Context, *connect.Request[hummingbird.MarketplaceInfoRequest]) (*connect.Response[hummingbird.MarketplaceInfoResponse], error) {
+ fmt.Println("Info")
+ return &connect.Response[hummingbird.MarketplaceInfoResponse]{
+ Msg: &hummingbird.MarketplaceInfoResponse{
+ ApiMajorVersion: s.info.ApiMajorVersion,
+ ApiMinorVersion: s.info.ApiMinorVersion,
+ Currency: s.info.Currency,
+ MaxStatisticsGranularity: s.info.StatisticsTimeGranularity,
+ SupportsRedemptionDelegation: s.info.SupportsRedemptionDelegation,
+ CurrencyExponent: s.info.CurrencyExponent,
+ PricingStrategy: s.info.PricingStrategy,
+ TransactionFeeRelative: s.info.TransactionFeeRelative,
+ TransactionFeeAbsolute: s.info.TransactionFeeAbsolute,
+ SplitCombineFeeAbsolute: s.info.SplitCombineFeeAbsolute,
+ DelegationHourlyFee: s.info.DelegationHourlyFee,
+ },
+ }, nil
+}
+
+func (s *Service) UpdateAssets(ctx context.Context, req *connect.Request[hummingbird.UpdateAssetsRequest]) (*connect.Response[hummingbird.UpdateAssetsResponse], error) {
+ fmt.Println("UpdateAssets")
+ ia, ok := ctx.Value("user").(addr.IA)
+ if !ok {
+ return nil, connect.NewError(connect.CodePermissionDenied, serrors.New("ia not provided"))
+ }
+ handleAsset := func(update *hummingbird.AssetUpdate) (string, error) {
+ assetId, err := strconv.ParseInt(update.AssetId, 10, 64)
+ if err != nil {
+ return "", err
+ }
+ switch t := update.Operation.(type) {
+ case *hummingbird.AssetUpdate_Remove:
+ x, err := s.store.DeleteListedAsset(ctx, ia, assetId)
+ if err != nil {
+ return "", err
+ }
+ if x != 1 {
+ return "", serrors.New("no modifiable asset with that ID found")
+ }
+ return "", nil
+ case *hummingbird.AssetUpdate_Update:
+ dbAsset := &db.DBAsset{
+ ID: assetId,
+ IA: ia,
+ Bandwidth: t.Update.Bandwidth,
+ BandwidthMin: t.Update.BandwidthMin,
+ BandwidthMax: t.Update.BandwidthMax,
+ StartAt: t.Update.StartsAt.AsTime(),
+ StopsAt: t.Update.StopsAt.AsTime(),
+ Price: t.Update.Price,
+ TimeGranularity: t.Update.TimeGranularity,
+ TimeMinDuration: t.Update.TimeMinDuration,
+ }
+ if t.Update.IfIdIngress != nil {
+ dbAsset.IfIdIngress = sql.NullInt32{
+ Valid: true,
+ Int32: int32(*t.Update.IfIdIngress),
+ }
+ }
+ if t.Update.IfIdEgress != nil {
+ dbAsset.IfIdEgress = sql.NullInt32{
+ Valid: true,
+ Int32: int32(*t.Update.IfIdEgress),
+ }
+ }
+ newId, err := s.store.UpdateListedAsset(ctx, dbAsset)
+ if err != nil {
+ return "", err
+ }
+ return strconv.FormatInt(newId, 10), nil
+ default:
+ return "", serrors.New("unkown update")
+ }
+ }
+ res := make([]*hummingbird.UpdateAssetResult, 0, len(req.Msg.Assets))
+ for _, update := range req.Msg.Assets {
+ newId, err := handleAsset(update)
+ if err != nil {
+ res = append(res, &hummingbird.UpdateAssetResult{
+ ResultType: &hummingbird.UpdateAssetResult_Error{
+ Error: err.Error(),
+ },
+ })
+ } else {
+ res = append(res, &hummingbird.UpdateAssetResult{
+ ResultType: &hummingbird.UpdateAssetResult_NewId{
+ NewId: newId,
+ },
+ })
+ }
+ }
+ return &connect.Response[hummingbird.UpdateAssetsResponse]{
+ Msg: &hummingbird.UpdateAssetsResponse{
+ Result: res,
+ },
+ }, nil
+}
+
+func (s *Service) PublishAsset(ctx context.Context, req *connect.Request[hummingbird.PublishAssetRequest]) (*connect.Response[hummingbird.PublishAssetResponse], error) {
+ fmt.Println("PublishAsset")
+ ia, ok := ctx.Value("user").(addr.IA)
+ if !ok {
+ return nil, connect.NewError(connect.CodePermissionDenied, serrors.New("ia not provided"))
+ }
+ dbAsset := &db.DBAsset{
+ IA: ia,
+ Bandwidth: req.Msg.Asset.Bandwidth,
+ BandwidthMin: req.Msg.Asset.BandwidthMin,
+ BandwidthMax: req.Msg.Asset.BandwidthMax,
+ StartAt: req.Msg.Asset.StartsAt.AsTime().Truncate(time.Second),
+ StopsAt: req.Msg.Asset.StopsAt.AsTime().Truncate(time.Second),
+ Price: req.Msg.Asset.Price,
+ TimeGranularity: req.Msg.Asset.TimeGranularity,
+ TimeMinDuration: req.Msg.Asset.TimeMinDuration,
+ IfIdIngress: sql.NullInt32{},
+ IfIdEgress: sql.NullInt32{},
+ }
+ if !dbAsset.StopsAt.After(dbAsset.StartAt) {
+ return nil, connect.NewError(connect.CodeInvalidArgument, serrors.New("stopsAt must come after startsAt"))
+ }
+ if req.Msg.Asset.IfIdIngress != nil {
+ dbAsset.IfIdIngress.Int32 = int32(*req.Msg.Asset.IfIdIngress)
+ dbAsset.IfIdIngress.Valid = true
+ }
+ if req.Msg.Asset.IfIdEgress != nil {
+ dbAsset.IfIdEgress.Int32 = int32(*req.Msg.Asset.IfIdEgress)
+ dbAsset.IfIdEgress.Valid = true
+ }
+ assetID, err := s.store.PublishAsset(ctx, dbAsset)
+ if err != nil {
+ return nil, connect.NewError(connect.CodeInvalidArgument, err)
+ }
+
+ return &connect.Response[hummingbird.PublishAssetResponse]{
+ Msg: &hummingbird.PublishAssetResponse{
+ AssetId: strconv.FormatInt(assetID, 10),
+ },
+ }, nil
+}
+
+func (s *Service) RedeemAsset(ctx context.Context, req *connect.Request[hummingbird.RedeemAssetRequest]) (*connect.Response[hummingbird.RedeemAssetResponse], error) {
+ fmt.Println("RedeemAsset")
+ user, ok := ctx.Value("user").(int64)
+ if !ok {
+ return nil, connect.NewError(connect.CodePermissionDenied, serrors.New("user_id not provided"))
+ }
+ var assets []*db.DBAsset
+ var err error
+ switch t := req.Msg.Interfaces.(type) {
+ case *hummingbird.RedeemAssetRequest_Pair:
+ assets, err = s.store.PrepareRedemption(ctx, user, &t.Pair.IngressAssetId, &t.Pair.EgressAssetId, nil)
+ case *hummingbird.RedeemAssetRequest_IfPairAssetId:
+ assets, err = s.store.PrepareRedemption(ctx, user, nil, nil, &t.IfPairAssetId)
+ default:
+ return nil, connect.NewError(connect.CodeInvalidArgument, serrors.New("invalid interface pair"))
+ }
+ if err != nil {
+ return nil, connect.NewError(connect.CodeInvalidArgument, err)
+ }
+ var bw uint32
+ var ingressID uint32
+ var egressID uint32
+ var startsAt time.Time
+ var stopsAt time.Time
+ var ia addr.IA
+ if len(assets) == 1 {
+ // interface pair
+ pairAsset := assets[0]
+ bw = pairAsset.Bandwidth
+ ingressID = uint32(pairAsset.IfIdIngress.Int32)
+ egressID = uint32(pairAsset.IfIdEgress.Int32)
+ startsAt = pairAsset.StartAt
+ stopsAt = pairAsset.StopsAt
+ ia = pairAsset.IA
+ } else if len(assets) == 2 {
+ // ingress and egress assets
+ ingressAsset := assets[0]
+ egressAsset := assets[1]
+ bw = min(ingressAsset.Bandwidth, egressAsset.Bandwidth)
+ ingressID = uint32(ingressAsset.IfIdIngress.Int32)
+ egressID = uint32(egressAsset.IfIdEgress.Int32)
+ ia = ingressAsset.IA
+ if ingressAsset.StartAt.Before(egressAsset.StartAt) {
+ startsAt = egressAsset.StartAt
+ } else {
+ startsAt = ingressAsset.StartAt
+ }
+ if ingressAsset.StopsAt.Before(egressAsset.StopsAt) {
+ stopsAt = ingressAsset.StopsAt
+ } else {
+ stopsAt = egressAsset.StopsAt
+ }
+ } else {
+ return nil, connect.NewError(connect.CodeInvalidArgument, serrors.New("invalid assets"))
+ }
+ undoRedemption := func() error {
+ // we use context.Background here because if the client disconnected in the meantime,
+ // we cannot undo the redemption using the request's context.
+ switch t := req.Msg.Interfaces.(type) {
+ case *hummingbird.RedeemAssetRequest_Pair:
+ err = s.store.UndoRedemption(context.Background(), user, &t.Pair.IngressAssetId, &t.Pair.EgressAssetId, nil)
+ case *hummingbird.RedeemAssetRequest_IfPairAssetId:
+ err = s.store.UndoRedemption(context.Background(), user, nil, nil, &t.IfPairAssetId)
+ }
+ if err != nil {
+ log.Error("Error while undoing redemption", "err", err)
+ }
+ return err
+ }
+ if stopsAt.Before(startsAt) {
+ return nil, connect.NewError(connect.CodeInvalidArgument, serrors.Join(serrors.New("stops at before starts at"), undoRedemption()))
+ }
+ peer, found := s.redemptionServerPeers[ia]
+ if !found {
+ return nil, connect.NewError(connect.CodeUnavailable, serrors.Join(serrors.New("Redemption service not reachable"), undoRedemption()))
+ }
+ respCh := peer.Send(&hummingbird.RedeemAssetFromASRequest{
+ Bandwidth: bw,
+ IngressId: ingressID,
+ EgressId: egressID,
+ StartsAt: timestamppb.New(startsAt),
+ StopsAt: timestamppb.New(stopsAt),
+ })
+ select {
+ case resp := <-respCh:
+ if resp == nil {
+ return nil, connect.NewError(connect.CodeUnavailable, serrors.Join(serrors.New("Redemption service not available"), undoRedemption()))
+ }
+ switch r := resp.Result.(type) {
+ case *hummingbird.RedeemAssetFromASResponse_Error:
+ return nil, connect.NewError(connect.CodeUnavailable, serrors.Join(serrors.New(r.Error), undoRedemption()))
+ case *hummingbird.RedeemAssetFromASResponse_ResInfo:
+ n, err := s.store.InsertReservation(ctx, &db.DBReservation{
+ ReservationID: r.ResInfo.ReservationId,
+ IA: ia,
+ Ingress: ingressID,
+ Egress: egressID,
+ Bandwidth: r.ResInfo.BandwithRounded,
+ EncodedBandwidth: uint16(r.ResInfo.BwDataplaneEncoding),
+ StartsAt: startsAt,
+ StopsAt: stopsAt,
+ AccountId: user,
+ Key: r.ResInfo.AuthenticationKey,
+ })
+ if err != nil {
+ return nil, connect.NewError(connect.CodeUnavailable, serrors.Join(err, undoRedemption()))
+ }
+ if n != 1 {
+ return nil, connect.NewError(connect.CodeUnavailable, serrors.Join(serrors.New("reservation could not be stored"), undoRedemption()))
+ }
+ return &connect.Response[hummingbird.RedeemAssetResponse]{
+ Msg: &hummingbird.RedeemAssetResponse{
+ AuthenticationKey: r.ResInfo.AuthenticationKey,
+ ReservationId: r.ResInfo.ReservationId,
+ BandwidthRounded: r.ResInfo.BandwithRounded,
+ BwDataplaneEncoding: r.ResInfo.BwDataplaneEncoding,
+ },
+ }, nil
+ }
+ case <-time.After(30 * time.Second):
+ return nil, connect.NewError(connect.CodeUnavailable, serrors.Join(serrors.New("Redemption service not available"), undoRedemption()))
+ }
+ return nil, connect.NewError(connect.CodeUnavailable, serrors.Join(serrors.New("Redemption service not available"), undoRedemption()))
+}
+
+func ceilDuration(base time.Duration, multiple time.Duration) time.Duration {
+ truncated := base.Truncate(multiple)
+ if truncated == base {
+ return base
+ }
+ return truncated + multiple
+}
+func ceilTime(base time.Time, multiple time.Duration) time.Time {
+ truncated := base.Truncate(multiple)
+ if truncated.Equal(base) {
+ return base
+ }
+ return truncated.Add(multiple)
+}
+
+func (s *Service) Statistics(ctx context.Context, req *connect.Request[hummingbird.StatisticsRequest]) (*connect.Response[hummingbird.StatisticsResponse], error) {
+ fmt.Println("Statistics")
+ ia, ok := ctx.Value("user").(addr.IA)
+ if !ok {
+ return nil, connect.NewError(connect.CodePermissionDenied, serrors.New("ia not provided"))
+ }
+ step := ceilDuration(time.Duration(req.Msg.Step)*time.Second, (time.Duration(s.info.StatisticsTimeGranularity) * time.Second))
+ windowStart := req.Msg.Start.AsTime().UTC().Truncate(time.Duration(s.info.StatisticsTimeGranularity))
+ windowEnd := ceilTime(req.Msg.End.AsTime().UTC(), time.Duration(s.info.StatisticsTimeGranularity))
+ num_intervals := int(windowEnd.Sub(windowStart) / step)
+ fmt.Println(step, windowStart, windowEnd)
+ if num_intervals > 1024 {
+ return nil, connect.NewError(connect.CodeResourceExhausted, serrors.New("too many intervals"))
+ }
+ income := make([]uint64, num_intervals)
+ bwBought := make([]uint64, num_intervals)
+ bwListed := make([]uint64, num_intervals)
+ assets, err := s.store.Statistics(ctx, &db.StatisticsQuery{
+ IA: ia,
+ WindowStart: windowStart.Format(time.RFC3339),
+ WindowEnd: windowEnd.Format(time.RFC3339),
+ Ingress: req.Msg.IfIdIngress,
+ Egress: req.Msg.IfIdEgress,
+ })
+ if err != nil {
+ return nil, connect.NewError(connect.CodeInvalidArgument, err)
+ }
+
+ for _, asset := range assets {
+ start := asset.StartsAt
+ stop := asset.StopsAt
+ if asset.StartsAt.Before(windowStart) {
+ start = windowStart
+ }
+ if asset.StopsAt.After(windowEnd) {
+ stop = windowEnd
+ }
+ first := int(start.Sub(windowStart) / step)
+
+ last := int(stop.Sub(windowStart) / step)
+ if first < 0 || last > num_intervals {
+ continue
+ }
+ if stop.Equal(windowStart.Add(time.Duration(last) * step)) {
+ last--
+ }
+ for i := first; i <= last && i < num_intervals; i++ {
+ intervalStart := windowStart.Add(time.Duration(i) * step)
+ intervalEnd := intervalStart.Add(step)
+ overlapStart := start
+ overlapEnd := stop
+ if asset.StartsAt.Before(intervalStart) {
+ overlapStart = intervalStart
+ }
+ if asset.StopsAt.After(intervalEnd) {
+ overlapEnd = intervalEnd
+ }
+ duration := uint64(overlapEnd.Sub(overlapStart).Seconds())
+ bwTimesDuration := uint64(asset.Bandwidth) * duration
+ if !asset.OwnerId.Valid {
+ bwListed[i] += bwTimesDuration
+ } else {
+ bwBought[i] += bwTimesDuration
+ income[i] += bwTimesDuration * uint64(asset.Price)
+ }
+ }
+ }
+ respEntries := make([]*hummingbird.StatisticsResponseEntry, num_intervals)
+ for i := 0; i < num_intervals; i++ {
+ respEntries[i] = &hummingbird.StatisticsResponseEntry{
+ Revenue: income[i],
+ BandwidthUtilization: float64(bwBought[i]) / float64(bwBought[i]+bwListed[i]),
+ }
+ }
+ return &connect.Response[hummingbird.StatisticsResponse]{
+ Msg: &hummingbird.StatisticsResponse{
+ Statistics: respEntries,
+ },
+ }, nil
+}
+
+func (s *Service) SearchAssets(ctx context.Context, req *connect.Request[hummingbird.SearchAssetsRequest]) (*connect.Response[hummingbird.SearchAssetsResponse], error) {
+ fmt.Println("SearchAssets")
+ var owner_id *int64
+ switch user := ctx.Value("user").(type) {
+ case int64:
+ if req.Msg.Owned {
+ owner_id = &user
+ }
+ case addr.IA:
+ default:
+ return nil, connect.NewError(connect.CodePermissionDenied, serrors.New("user_id not provided"))
+ }
+ var startsAt *string
+ var stopsAt *string
+ if req.Msg.StartsAtLatest != nil {
+ start := req.Msg.StartsAtLatest.AsTime().UTC().Format(time.RFC3339)
+ startsAt = &start
+ }
+ if req.Msg.StopsAtEarliest != nil {
+ stop := req.Msg.StopsAtEarliest.AsTime().UTC().Format(time.RFC3339)
+ stopsAt = &stop
+ }
+ var ia *addr.IA
+ if req.Msg.Ia != nil {
+ tmp := addr.IA(*req.Msg.Ia)
+ ia = &tmp
+ }
+ assets, err := s.store.Search(ctx, &db.AssetQuery{
+ AccountId: owner_id,
+ IA: ia,
+ Ingress: req.Msg.IfIdIngress,
+ Egress: req.Msg.IfIdEgress,
+ MinRequiredBandwidth: req.Msg.MinRequiredBw,
+ Price: req.Msg.Price,
+ StartsAt: startsAt,
+ StopsAt: stopsAt,
+ })
+ if err != nil {
+ return nil, connect.NewError(connect.CodeInvalidArgument, err)
+ }
+ repAssets := make([]*hummingbird.SearchAsset, 0, len(assets))
+ for _, asset := range assets {
+ a := &hummingbird.SearchAsset{
+ AssetId: strconv.FormatInt(asset.ID, 10),
+ Ia: uint64(asset.IA),
+ Bandwidth: asset.Bandwidth,
+ BandwidthMin: asset.BandwidthMin,
+ BandwidthMax: asset.BandwidthMax,
+ TimeMinDuration: asset.TimeMinDuration,
+ StartsAt: timestamppb.New(asset.StartAt),
+ StopsAt: timestamppb.New(asset.StopsAt),
+ TimeGranularity: asset.TimeGranularity,
+ Price: asset.Price,
+ }
+ if asset.IfIdIngress.Valid {
+ ingress := uint32(asset.IfIdIngress.Int32)
+ a.IfIdIngress = &ingress
+ }
+ if asset.IfIdEgress.Valid {
+ egress := uint32(asset.IfIdEgress.Int32)
+ a.IfIdEgress = &egress
+ }
+ repAssets = append(repAssets, a)
+ }
+ return &connect.Response[hummingbird.SearchAssetsResponse]{
+ Msg: &hummingbird.SearchAssetsResponse{
+ Assets: repAssets,
+ },
+ }, nil
+}
diff --git a/marketplace/static/script.js b/marketplace/static/script.js
new file mode 100644
index 0000000000..795825a82d
--- /dev/null
+++ b/marketplace/static/script.js
@@ -0,0 +1,163 @@
+function editBalance(id, name) {
+ editingAccountId = id;
+ mainAccountId = document.getElementById("main-account-id").textContent;
+ document.getElementById("balance-editor").style.display = "block";
+ document.getElementById("balance-user-name").textContent = name;
+ document.getElementById("balance-value").value = 0;
+}
+async function saveBalance() {
+ const balance = document.getElementById("balance-value").value;
+
+ const response = await fetch(`/account/balance`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded"
+ },
+ body: "from=" + encodeURIComponent(mainAccountId) + "&to=" + encodeURIComponent(editingAccountId) + "&amount=" + encodeURIComponent(balance)
+ });
+
+ if (!response.ok) {
+ if (response.status == 401) {
+ location.reload();
+ return;
+ }
+ document.getElementById("error").textContent = await response.text();
+ return;
+ }
+
+ location.reload();
+}
+async function requestJWT(id, name) {
+ const response = await fetch("/account/token", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded"
+ },
+ body: "id=" + encodeURIComponent(id)
+ });
+
+ if (!response.ok) {
+ if (response.status == 401) {
+ location.reload();
+ return;
+ }
+ document.getElementById("error").textContent = await response.text()
+ return;
+ }
+
+ const token = await response.text();
+
+ document.getElementById("jwt-token").innerText = token;
+ document.getElementById("jwt-container").style.display = "block";
+ document.getElementById("jwt-user-name").textContent = name;
+}
+async function deleteAccount(id, name) {
+ if (!confirm(`Delete account "${name}"?`)) {
+ return;
+ }
+
+ const response = await fetch("/account/delete", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded"
+ },
+ body: "id=" + encodeURIComponent(id)
+ });
+
+ if (!response.ok) {
+ if (response.status == 401) {
+ location.reload();
+ return;
+ }
+ document.getElementById("error").textContent = await response.text();
+ return;
+ }
+
+ location.reload();
+}
+async function resetJWT(id, name) {
+ if (!confirm(`Reset JWT token for account "${name}"?`)) {
+ return;
+ }
+
+ const response = await fetch("/account/resetjwt", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded"
+ },
+ body: "id=" + encodeURIComponent(id)
+ });
+
+ if (!response.ok) {
+ if (response.status == 401) {
+ location.reload();
+ return;
+ }
+ document.getElementById("error").textContent = await response.text();
+ return;
+ }
+ location.reload();
+}
+function copyJWT() {
+ const token = document.getElementById("jwt-token").innerText;
+
+ navigator.clipboard.writeText(token)
+ .then(() => {
+ const element = document.getElementById("jwt-token");
+ const oldText = element.dataset.originalText || token;
+
+ element.dataset.originalText = oldText;
+ element.innerText = "Copied!";
+
+ setTimeout(() => {
+ element.innerText = oldText;
+ }, 500);
+ })
+ .catch(err => {
+ document.getElementById("error").textContent = "Could not copy token: " + err;
+ });
+}
+
+async function assignAsset(id, accountid, select) {
+ const response = await fetch("/assets/assign", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded"
+ },
+ body: "id=" + encodeURIComponent(id) + "&from=" + encodeURIComponent(accountid) + "&to=" + encodeURIComponent(select.value)
+ });
+
+ if (!response.ok) {
+ if (response.status == 401) {
+ location.reload();
+ return;
+ }
+ document.getElementById("error").textContent = await response.text();
+ return;
+ }
+ location.reload();
+}
+async function assignReservation(id, accountid, select) {
+ const response = await fetch("/reservations/assign", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded"
+ },
+ body: "id=" + encodeURIComponent(id) + "&from=" + encodeURIComponent(accountid) + "&to=" + encodeURIComponent(select.value)
+ });
+
+ if (!response.ok) {
+ if (response.status == 401) {
+ location.reload();
+ return;
+ }
+ document.getElementById("error").textContent = await response.text();
+ return;
+ }
+ location.reload();
+}
+function reloadWithId(id) {
+ const url = new URL(window.location.href);
+ url.searchParams.set("id", id);
+ window.location.href = url.toString();
+}
\ No newline at end of file
diff --git a/marketplace/static/style.css b/marketplace/static/style.css
new file mode 100644
index 0000000000..6919584167
--- /dev/null
+++ b/marketplace/static/style.css
@@ -0,0 +1,289 @@
+/* ---------- Reset ---------- */
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+/* ---------- Theme ---------- */
+
+:root {
+ --bg: #f4f7fa;
+ --surface: #ffffff;
+ --text: #334155;
+ --heading: #1e293b;
+
+ --primary: #2563eb;
+ --primary-hover: #1d4ed8;
+
+ --danger: #dc2626;
+ --danger-hover: #b91c1c;
+
+ --border: #e2e8f0;
+
+ --shadow: 0 8px 25px rgba(0, 0, 0, .08);
+ --radius: 12px;
+
+ --page-width: 950px;
+}
+
+/* ---------- Base ---------- */
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ background: var(--bg);
+ color: var(--text);
+ line-height: 1.5;
+}
+
+h1,
+h2,
+h3 {
+ color: var(--heading);
+ margin-bottom: 1rem;
+}
+
+p {
+ margin-bottom: .75rem;
+}
+
+/* ---------- Navigation ---------- */
+
+nav {
+ background: var(--heading);
+ color: white;
+
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ padding: 16px 32px;
+}
+
+.nav-left {
+ display: flex;
+ gap: 20px;
+}
+
+.nav-right {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+nav a {
+ color: white;
+ text-decoration: none;
+ font-weight: 500;
+}
+
+nav a:hover {
+ text-decoration: underline;
+}
+
+/* ---------- Layout ---------- */
+
+.page {
+ width: min(1200px, calc(100% - 40px));
+ margin: 40px auto;
+}
+
+.card {
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
+ padding: 30px;
+ margin-bottom: 24px;
+}
+
+.card.small {
+ max-width: 450px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+/* ---------- Forms ---------- */
+
+label {
+ font-weight: 500;
+}
+
+input {
+ padding: 10px 12px;
+ border: 1px solid #cbd5e1;
+ border-radius: 6px;
+ font-size: 1rem;
+}
+
+input:focus {
+ outline: none;
+ border-color: var(--primary);
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, .15);
+}
+
+.form-vertical {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ width: fit-content;
+ margin: 0 auto;
+}
+
+.form-vertical label {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.form-vertical input {
+ width: 300px;
+ max-width: 100%;
+}
+
+.form-vertical button {
+ width: auto;
+}
+
+.form-horizontal {
+ display: flex;
+ flex-direction: row;
+ gap: 16px;
+ align-items: end;
+}
+
+.form-horizontal label {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ flex: 1;
+}
+
+/* ---------- Buttons ---------- */
+
+button {
+ border: none;
+ border-radius: 6px;
+
+ background: var(--primary);
+ color: white;
+
+ cursor: pointer;
+
+ padding: 10px 16px;
+ font-size: .95rem;
+ font-weight: 500;
+
+ transition: background .15s;
+}
+
+button:hover {
+ background: var(--primary-hover);
+}
+
+.table-actions {
+ display: flex;
+ gap: 8px;
+}
+
+/* ---------- Table ---------- */
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+th {
+ text-align: left;
+ padding: 14px;
+ background: #f8fafc;
+ color: var(--heading);
+ border-bottom: 2px solid var(--border);
+}
+
+td {
+ padding: 14px;
+ border-bottom: 1px solid var(--border);
+}
+
+tbody tr:hover {
+ background: #f8fafc;
+}
+
+/* ---------- JWT ---------- */
+
+#jwt-token {
+ background: #f8fafc;
+ border: 1px solid var(--border);
+
+ border-radius: 6px;
+
+ padding: 16px;
+
+ font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
+ font-size: .9rem;
+
+ word-break: break-all;
+ white-space: pre-wrap;
+ cursor: pointer;
+ transition: background .15s;
+}
+
+#jwt-token:hover {
+ background: #eef2ff;
+}
+
+/* ---------- Balance ---------- */
+
+#balance-value {
+ width: 180px;
+ margin-bottom: 16px;
+}
+
+/* ---------- Messages ---------- */
+
+.error {
+ margin-top: 16px;
+
+ color: var(--danger);
+ font-weight: 600;
+}
+
+.balance {
+ font-size: 2rem;
+ font-weight: 700;
+ color: #2563eb;
+ margin-bottom: 20px;
+}
+
+button.danger {
+ background: var(--danger);
+}
+
+button.danger:hover {
+ background: var(--danger-hover);
+}
+
+select {
+ background-color: white;
+ border: 1px solid #cbd5e1;
+ border-radius: 6px;
+ padding: 10px 40px 10px 12px;
+ font-size: 1rem;
+ color: var(--text);
+ cursor: pointer;
+}
+
+select:hover {
+ border-color: #94a3b8;
+}
+
+select:focus {
+ outline: none;
+ border-color: var(--primary);
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, .15);
+}
+
+select option {
+ padding: 8px;
+}
\ No newline at end of file
diff --git a/marketplace/storage/BUILD.bazel b/marketplace/storage/BUILD.bazel
new file mode 100644
index 0000000000..b2b85aa5bf
--- /dev/null
+++ b/marketplace/storage/BUILD.bazel
@@ -0,0 +1,19 @@
+load("@rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = [
+ "split.go",
+ "storage.go",
+ ],
+ importpath = "github.com/scionproto/scion/marketplace/storage",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//marketplace/db:go_default_library",
+ "//pkg/addr:go_default_library",
+ "//pkg/private/serrors:go_default_library",
+ "//pkg/proto/hummingbird:go_default_library",
+ "//private/config:go_default_library",
+ "//private/storage/db:go_default_library",
+ ],
+)
diff --git a/marketplace/storage/split.go b/marketplace/storage/split.go
new file mode 100644
index 0000000000..40e2c863bf
--- /dev/null
+++ b/marketplace/storage/split.go
@@ -0,0 +1,136 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package storage
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/scionproto/scion/marketplace/db"
+)
+
+type RequestedSplit struct {
+ ExactFrom time.Time
+ ExactTo time.Time
+ ExactBandwidth uint32
+}
+
+type AssetSegment struct {
+ StartsAt time.Time
+ StopsAt time.Time
+ Bandwidth uint32
+}
+
+type SplitResult struct {
+ Split AssetSegment
+ Remainders []AssetSegment
+}
+
+func validateSplit(asset *db.DBAsset, p RequestedSplit) error {
+ if p.ExactFrom.Before(asset.StartAt) || p.ExactTo.After(asset.StopsAt) {
+ return fmt.Errorf("purchase outside asset bounds")
+ }
+ if !p.ExactFrom.Before(p.ExactTo) {
+ return fmt.Errorf("invalid validity range")
+ }
+ if p.ExactFrom.Nanosecond() != 0 || p.ExactTo.Nanosecond() != 0 {
+ return fmt.Errorf("timestamps must be second precision")
+ }
+ if p.ExactBandwidth > asset.Bandwidth {
+ return fmt.Errorf("purchase amount exceeds asset amount")
+ }
+
+ return nil
+}
+
+func SplitAsset(asset *db.DBAsset, split RequestedSplit) (*SplitResult, error) {
+ if !asset.StartAt.Before(asset.StopsAt) {
+ return nil, fmt.Errorf("invalid asset range")
+ }
+ if err := validateSplit(asset, split); err != nil {
+ return nil, err
+ }
+ remainingAsset := AssetSegment{
+ Bandwidth: asset.Bandwidth,
+ StartsAt: asset.StartAt,
+ StopsAt: asset.StopsAt,
+ }
+ splitResult := &SplitResult{}
+ if split.ExactBandwidth != asset.Bandwidth {
+ splitResult.Remainders = append(splitResult.Remainders, AssetSegment{
+ StartsAt: remainingAsset.StartsAt,
+ StopsAt: remainingAsset.StopsAt,
+ Bandwidth: remainingAsset.Bandwidth - split.ExactBandwidth,
+ })
+ remainingAsset.Bandwidth = split.ExactBandwidth
+ }
+ if split.ExactFrom.Equal(remainingAsset.StartsAt) {
+ // no left remainder asset exists
+ if split.ExactTo.Equal(remainingAsset.StopsAt) {
+ // no right remainder asset exists -> split = asset
+ splitResult.Split = AssetSegment{
+ Bandwidth: remainingAsset.Bandwidth,
+ StartsAt: remainingAsset.StartsAt,
+ StopsAt: remainingAsset.StopsAt,
+ }
+ } else {
+ // only a right remainder asset exists
+ splitResult.Split = AssetSegment{
+ Bandwidth: remainingAsset.Bandwidth,
+ StartsAt: remainingAsset.StartsAt,
+ StopsAt: split.ExactTo,
+ }
+ splitResult.Remainders = append(splitResult.Remainders, AssetSegment{
+ Bandwidth: remainingAsset.Bandwidth,
+ StartsAt: split.ExactTo,
+ StopsAt: remainingAsset.StopsAt,
+ })
+ }
+ } else {
+ // a left remainder asset exists
+ if split.ExactTo.Equal(remainingAsset.StopsAt) {
+ // no right remainder asset exists -> only a left remainder exists
+ splitResult.Split = AssetSegment{
+ Bandwidth: remainingAsset.Bandwidth,
+ StartsAt: split.ExactFrom,
+ StopsAt: remainingAsset.StopsAt,
+ }
+ splitResult.Remainders = append(splitResult.Remainders, AssetSegment{
+ Bandwidth: remainingAsset.Bandwidth,
+ StartsAt: remainingAsset.StartsAt,
+ StopsAt: split.ExactTo,
+ })
+ } else {
+ // a left remainder and a right remainder exists
+ splitResult.Split = AssetSegment{
+ Bandwidth: remainingAsset.Bandwidth,
+ StartsAt: split.ExactFrom,
+ StopsAt: split.ExactTo,
+ }
+ splitResult.Remainders = append(splitResult.Remainders,
+ AssetSegment{
+ Bandwidth: remainingAsset.Bandwidth,
+ StartsAt: remainingAsset.StartsAt,
+ StopsAt: split.ExactFrom,
+ }, AssetSegment{
+ Bandwidth: remainingAsset.Bandwidth,
+ StartsAt: split.ExactTo,
+ StopsAt: remainingAsset.StopsAt,
+ })
+ }
+ }
+ return splitResult, nil
+
+}
diff --git a/marketplace/storage/storage.go b/marketplace/storage/storage.go
new file mode 100644
index 0000000000..7a99596c54
--- /dev/null
+++ b/marketplace/storage/storage.go
@@ -0,0 +1,897 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package storage
+
+import (
+ "context"
+ "database/sql"
+ "math"
+ "strconv"
+ "time"
+
+ marketplacedb "github.com/scionproto/scion/marketplace/db"
+ "github.com/scionproto/scion/pkg/addr"
+ "github.com/scionproto/scion/pkg/private/serrors"
+ "github.com/scionproto/scion/pkg/proto/hummingbird"
+ "github.com/scionproto/scion/private/config"
+ "github.com/scionproto/scion/private/storage/db"
+)
+
+type DBConfig struct {
+ config.NoDefaulter
+ Connection string `toml:"connection,omitempty"`
+ MaxOpenReadConns int `toml:"max_open_read_conns,omitempty"`
+ MaxIdleReadConns int `toml:"max_idle_read_conns,omitempty"`
+ allowEmptyConn bool
+}
+
+type MarketplaceStorage struct {
+ db marketplacedb.MarketplaceDB
+ transactionFeeRelative float32
+ transactionFeeAbsolute uint64
+ splitCombineFeeAbsolute uint64
+ delegationHourlyFee uint64
+}
+
+func NewStorage(c DBConfig, transactionFeeRelative float32, transactionFeeAbsolute uint64, splitCombineFeeAbsolute uint64, delegationHourlyFee uint64) (
+ *MarketplaceStorage, error) {
+ db, err := marketplacedb.New(c.Connection, &db.SqliteConfig{
+ MaxOpenReadConns: c.MaxOpenReadConns,
+ MaxIdleReadConns: c.MaxIdleReadConns,
+ })
+ if err != nil {
+ return nil, err
+ }
+ return &MarketplaceStorage{
+ db: db,
+ transactionFeeRelative: transactionFeeRelative,
+ transactionFeeAbsolute: transactionFeeAbsolute,
+ splitCombineFeeAbsolute: splitCombineFeeAbsolute,
+ delegationHourlyFee: delegationHourlyFee,
+ }, nil
+}
+
+func (s *MarketplaceStorage) Search(ctx context.Context, params *marketplacedb.AssetQuery) ([]*marketplacedb.DBAsset, error) {
+ return s.db.Search(ctx, params)
+}
+
+func (s *MarketplaceStorage) FetchReservations(ctx context.Context, params *marketplacedb.ReservationQuery) ([]*marketplacedb.DBReservation, error) {
+ return s.db.FetchReservations(ctx, params)
+}
+
+func (s *MarketplaceStorage) PublishAsset(ctx context.Context, a *marketplacedb.DBAsset) (int64, error) {
+ err := validateAsset(a)
+ if err != nil {
+ return 0, err
+ }
+ return s.db.InsertAsset(ctx, a)
+}
+func (s *MarketplaceStorage) UpdateListedAsset(ctx context.Context, a *marketplacedb.DBAsset) (int64, error) {
+ err := validateAsset(a)
+ if err != nil {
+ return 0, err
+ }
+ tx, err := s.db.BeginTransaction(ctx, &sql.TxOptions{})
+ if err != nil {
+ return 0, err
+ }
+ x, err := tx.DeleteListedAsset(ctx, a.IA, a.ID)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ if x != 1 {
+ return 0, serrors.Join(serrors.New("no modifiable asset with that ID found"), tx.Rollback())
+ }
+ newId, err := tx.InsertAsset(ctx, a)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ err = tx.Commit()
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ return newId, nil
+}
+func (s *MarketplaceStorage) DeleteListedAsset(ctx context.Context, ia addr.IA, assetID int64) (int64, error) {
+ return s.db.DeleteListedAsset(ctx, ia, assetID)
+}
+func (s *MarketplaceStorage) GetAccountsByUser(ctx context.Context, id int64) ([]*marketplacedb.DBAccount, error) {
+ return s.db.GetAccountsByUser(ctx, id)
+}
+func (s *MarketplaceStorage) GetUser(ctx context.Context, id int64) (*marketplacedb.DBUser, error) {
+ return s.db.GetUser(ctx, id)
+}
+
+func (s *MarketplaceStorage) GetASUser(ctx context.Context, ia addr.IA) (*marketplacedb.DBASUser, error) {
+ return s.db.GetASUser(ctx, ia)
+}
+
+func (s *MarketplaceStorage) SetASAuthenticationToken(ctx context.Context, ia addr.IA, auth string) (int64, error) {
+ return s.db.SetASAuthenticationToken(ctx, ia, auth)
+}
+
+func (s *MarketplaceStorage) GetUserByName(ctx context.Context, name string) (*marketplacedb.DBUser, error) {
+ return s.db.GetUserByName(ctx, name)
+}
+
+func (s *MarketplaceStorage) CreateAccount(ctx context.Context, account *marketplacedb.DBAccount) (int64, error) {
+ return s.db.CreateAccount(ctx, account)
+}
+
+func (s *MarketplaceStorage) GetAccountByAccountID(ctx context.Context, id int64) (*marketplacedb.DBAccount, error) {
+ return s.db.GetAccountByAccountID(ctx, id)
+}
+
+func (s *MarketplaceStorage) DeleteAccount(ctx context.Context, user_id int64, accountId int64) (int64, error) {
+ tx, err := s.db.BeginTransaction(ctx, &sql.TxOptions{})
+ if err != nil {
+ return 0, err
+ }
+ allUserAccounts, err := tx.GetAccountsByUser(ctx, user_id)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ var targetAccount *marketplacedb.DBAccount
+ var mainAccount *marketplacedb.DBAccount
+ for _, account := range allUserAccounts {
+ if account.ID == accountId {
+ targetAccount = account
+ } else if account.Scope == nil {
+ mainAccount = account
+ }
+ }
+ if targetAccount == nil || mainAccount == nil {
+ return 0, serrors.Join(serrors.New("invalid account"), tx.Rollback())
+ }
+ _, err = tx.TransferAllAssetsToAccount(ctx, targetAccount.ID, mainAccount.ID)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ _, err = tx.TransferAllReservations(ctx, targetAccount.ID, mainAccount.ID)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ _, err = tx.UpdateAccountMoney(ctx, mainAccount.ID, targetAccount.Balance)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ x, err := tx.DeleteAccount(ctx, targetAccount.ID)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ if x != 1 {
+ return 0, serrors.Join(serrors.New("account not found"), tx.Rollback())
+ }
+ err = tx.Commit()
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ return 1, nil
+}
+
+func (s *MarketplaceStorage) TransferMoneyBetweenAccounts(ctx context.Context, user_id int64, accountFrom int64, accountTo int64, balance int64) (int64, error) {
+ tx, err := s.db.BeginTransaction(ctx, &sql.TxOptions{})
+ if err != nil {
+ return 0, err
+ }
+ x, err := tx.UpdateAccountMoneyWithUser(ctx, user_id, accountFrom, -balance)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ if x != 1 {
+ return 0, serrors.Join(serrors.New("account not found"), tx.Rollback())
+ }
+ x, err = tx.UpdateAccountMoneyWithUser(ctx, user_id, accountTo, balance)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ if x != 1 {
+ return 0, serrors.Join(serrors.New("account not found"), tx.Rollback())
+ }
+ err = tx.Commit()
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ return 1, nil
+}
+
+func (s *MarketplaceStorage) CreateUser(ctx context.Context, user *marketplacedb.DBUser) (int64, error) {
+ tx, err := s.db.BeginTransaction(ctx, &sql.TxOptions{})
+ if err != nil {
+ return 0, err
+ }
+ userId, err := tx.CreateUser(ctx, user)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ baseAccount := &marketplacedb.DBAccount{
+ UserID: userId,
+ Scope: nil,
+ }
+ _, err = tx.CreateAccount(ctx, baseAccount)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ err = tx.Commit()
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ return userId, nil
+}
+
+func (s *MarketplaceStorage) CreateASUser(ctx context.Context, user *marketplacedb.DBASUser) (int64, error) {
+ return s.db.CreateASUser(ctx, user)
+}
+func ceilDuration(base time.Duration, multiple time.Duration) time.Duration {
+ truncated := base.Truncate(multiple)
+ if truncated == base {
+ return base
+ }
+ return truncated + multiple
+}
+func (s *MarketplaceStorage) CreateOrUpdateRedemptionDelegations(ctx context.Context, r *marketplacedb.RedemptionDelegation) (int64, error) {
+ tx, err := s.db.BeginTransaction(ctx, &sql.TxOptions{})
+ if err != nil {
+ return 0, err
+ }
+ var paidUntil time.Time
+ dbDelegation, err := tx.FindRedemptionDelegation(ctx, r.IA)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ if dbDelegation == nil {
+ paidUntil = time.Now()
+ } else {
+ paidUntil = dbDelegation.PaidUntil
+ }
+ if s.delegationHourlyFee == 0 {
+ // service is free, just update paidUntil accordingly to ensure database constraints do not complain
+ if r.Expiration.After(paidUntil) {
+ r.PaidUntil = r.Expiration
+ }
+ } else {
+ // service requires payment
+ if r.Expiration.After(paidUntil) {
+ // expiration is later than currently paid period, so requires further payment
+ paymentDuration := ceilDuration(r.Expiration.Sub(paidUntil), time.Hour)
+ numHours := uint64(paymentDuration / time.Hour)
+ _, err = tx.UpdateASMoney(ctx, r.IA, -int64(numHours*s.delegationHourlyFee))
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ r.PaidUntil = paidUntil.Add(paymentDuration)
+ } else {
+ // user updated expiration but this is still within the period he already paid, so skip billing.
+ r.PaidUntil = paidUntil
+ }
+ }
+ id, err := tx.CreateOrUpdateRedemptionDelegations(ctx, r)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ err = tx.Commit()
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ return id, nil
+}
+func (s *MarketplaceStorage) FindRedemptionDelegations(ctx context.Context) ([]*marketplacedb.RedemptionDelegation, error) {
+ return s.db.FindRedemptionDelegations(ctx)
+}
+
+func mulInt64(a, b int64) (int64, bool) {
+ if a == 0 || b == 0 {
+ return 0, true
+ }
+ if a == math.MinInt64 && b == -1 {
+ return 0, false
+ }
+ if b == math.MinInt64 && a == -1 {
+ return 0, false
+ }
+ if a > math.MaxInt64/b || a < math.MinInt64/b {
+ return 0, false
+ }
+ return a * b, true
+}
+
+func addInt64(a, b int64) (int64, bool) {
+ if (b > 0 && a > math.MaxInt64-b) ||
+ (b < 0 && a < math.MinInt64-b) {
+ return 0, false // overflow
+ }
+ return a + b, true
+}
+
+func (s *MarketplaceStorage) totalPrice(price uint32, bw uint32, startsAt time.Time, stopsAt time.Time) (int64, int64, bool) {
+ splitDuration := int64(stopsAt.Sub(startsAt).Seconds())
+ a, safe := mulInt64(int64(price), splitDuration)
+ if !safe {
+ return 0, 0, false
+ }
+ costWithoutFee, safe := mulInt64(a, int64(bw))
+ if !safe {
+ return 0, 0, false
+ }
+ fee := int64(float64(costWithoutFee)*float64(s.transactionFeeRelative)) + int64(s.transactionFeeAbsolute)
+ return costWithoutFee, fee, true
+}
+func (s *MarketplaceStorage) IncrementASJWTVersion(ctx context.Context, ia addr.IA, current int64) (int64, error) {
+ return s.db.IncrementASJWTVersion(ctx, ia, current)
+}
+
+func (s *MarketplaceStorage) IncrementAccountJWTVersion(ctx context.Context, accountId int64, current int64) (int64, error) {
+ return s.db.IncrementAccountJWTVersion(ctx, accountId, current)
+}
+
+func (s *MarketplaceStorage) IncrementUserJWTVersion(ctx context.Context, userId int64, accountId int64) (int64, error) {
+ return s.db.IncrementUserJWTVersion(ctx, userId, accountId)
+}
+
+func gcd(a, b uint32) uint32 {
+ for b != 0 {
+ a, b = b, a%b
+ }
+ return a
+}
+
+func lcm(a, b uint32) uint32 {
+ return a / gcd(a, b) * b
+}
+
+func (s *MarketplaceStorage) CombineAssets(ctx context.Context, accountID int64, assetId1 int64, assetId2 int64) (int64, error) {
+ tx, err := s.db.BeginTransaction(ctx, &sql.TxOptions{})
+ if err != nil {
+ return 0, err
+ }
+ _, err = tx.UpdateAccountMoney(ctx, accountID, -int64(s.splitCombineFeeAbsolute))
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ a1, err := tx.PrepareCombine(ctx, assetId1, accountID)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ a2, err := tx.PrepareCombine(ctx, assetId2, accountID)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ if a1.IA != a2.IA {
+ return 0, serrors.Join(serrors.New("asset must have same IA"), tx.Rollback())
+ }
+ if a1.IfIdIngress.Valid && a1.IfIdIngress.Int32 != a2.IfIdIngress.Int32 {
+ return 0, serrors.Join(serrors.New("asset must have same ingress"), tx.Rollback())
+ }
+ if a1.IfIdEgress.Valid && a1.IfIdEgress.Int32 != a2.IfIdEgress.Int32 {
+ return 0, serrors.Join(serrors.New("asset must have same egress"), tx.Rollback())
+ }
+ var combinedAsset *marketplacedb.DBAsset
+ if a1.StartAt.Equal(a2.StartAt) && a1.StopsAt.Equal(a2.StopsAt) {
+ combinedAsset = &marketplacedb.DBAsset{
+ AccountId: a1.AccountId,
+ IA: a1.IA,
+ Bandwidth: a1.Bandwidth + a2.Bandwidth,
+ BandwidthMin: max(a1.BandwidthMin, a2.BandwidthMin),
+ BandwidthMax: min(a1.BandwidthMax, a2.BandwidthMax),
+ StartAt: a1.StartAt,
+ StopsAt: a1.StopsAt,
+ Price: 0,
+ TimeGranularity: lcm(a1.TimeGranularity, a2.TimeGranularity),
+ TimeMinDuration: max(a1.TimeMinDuration, a2.TimeMinDuration),
+ IfIdIngress: a1.IfIdIngress,
+ IfIdEgress: a1.IfIdEgress,
+ }
+ } else if a1.Bandwidth == a2.Bandwidth {
+ if a1.StartAt.Equal(a2.StopsAt) {
+ combinedAsset = &marketplacedb.DBAsset{
+ AccountId: a1.AccountId,
+ IA: a1.IA,
+ Bandwidth: a1.Bandwidth,
+ BandwidthMin: max(a1.BandwidthMin, a2.BandwidthMin),
+ BandwidthMax: min(a1.BandwidthMax, a2.BandwidthMax),
+ StartAt: a2.StartAt,
+ StopsAt: a1.StopsAt,
+ Price: 0,
+ TimeGranularity: lcm(a1.TimeGranularity, a2.TimeGranularity),
+ TimeMinDuration: max(a1.TimeMinDuration, a2.TimeMinDuration),
+ IfIdIngress: a1.IfIdIngress,
+ IfIdEgress: a1.IfIdEgress,
+ }
+ } else if a2.StartAt.Equal(a1.StopsAt) {
+ combinedAsset = &marketplacedb.DBAsset{
+ AccountId: a1.AccountId,
+ IA: a1.IA,
+ Bandwidth: a1.Bandwidth,
+ BandwidthMin: max(a1.BandwidthMin, a2.BandwidthMin),
+ BandwidthMax: min(a1.BandwidthMax, a2.BandwidthMax),
+ StartAt: a1.StartAt,
+ StopsAt: a2.StopsAt,
+ Price: 0,
+ TimeGranularity: lcm(a1.TimeGranularity, a2.TimeGranularity),
+ TimeMinDuration: max(a1.TimeMinDuration, a2.TimeMinDuration),
+ IfIdIngress: a1.IfIdIngress,
+ IfIdEgress: a1.IfIdEgress,
+ }
+ } else {
+ return 0, serrors.Join(serrors.New("asset cannot be combined"), tx.Rollback())
+ }
+ } else {
+ return 0, serrors.Join(serrors.New("asset cannot be combined"), tx.Rollback())
+ }
+ err = tx.RemoveAsset(ctx, assetId1)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ err = tx.RemoveAsset(ctx, assetId2)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ newId, err := tx.InsertAsset(ctx, combinedAsset)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ err = tx.Commit()
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ return newId, nil
+}
+
+func (s *MarketplaceStorage) SplitAsset(ctx context.Context, accountID int64, assetId int64, bwSplit *uint32, timeSplit *time.Time) (int64, int64, error) {
+ if bwSplit == nil && timeSplit == nil {
+ return 0, 0, serrors.New("invalid split request")
+ }
+ tx, err := s.db.BeginTransaction(ctx, &sql.TxOptions{})
+ if err != nil {
+ return 0, 0, err
+ }
+ _, err = tx.UpdateAccountMoney(ctx, accountID, -int64(s.splitCombineFeeAbsolute))
+ if err != nil {
+ return 0, 0, serrors.Join(err, tx.Rollback())
+ }
+ dbAsset, err := tx.PrepareSplit(ctx, assetId, accountID)
+ if err != nil {
+ return 0, 0, serrors.Join(err, tx.Rollback())
+ }
+ var requestedSplit RequestedSplit
+
+ if bwSplit != nil {
+ requestedSplit = RequestedSplit{
+ ExactBandwidth: *bwSplit,
+ ExactFrom: dbAsset.StartAt,
+ ExactTo: dbAsset.StopsAt,
+ }
+
+ } else {
+ requestedSplit = RequestedSplit{
+ ExactBandwidth: dbAsset.Bandwidth,
+ ExactFrom: dbAsset.StartAt,
+ ExactTo: *timeSplit,
+ }
+ }
+ splitResult, err := SplitAsset(dbAsset, requestedSplit)
+ if err != nil {
+ return 0, 0, serrors.Join(err, tx.Rollback())
+ }
+ if len(splitResult.Remainders) != 1 {
+ return 0, 0, serrors.Join(serrors.New("invalid split result"), tx.Rollback())
+ }
+ asset1 := &marketplacedb.DBAsset{
+ AccountId: dbAsset.AccountId,
+ IA: dbAsset.IA,
+ BandwidthMin: dbAsset.BandwidthMin,
+ BandwidthMax: dbAsset.BandwidthMax,
+ Price: dbAsset.Price,
+ TimeGranularity: dbAsset.TimeGranularity,
+ TimeMinDuration: dbAsset.TimeMinDuration,
+ IfIdIngress: dbAsset.IfIdIngress,
+ IfIdEgress: dbAsset.IfIdEgress,
+ Bandwidth: splitResult.Split.Bandwidth,
+ StartAt: splitResult.Split.StartsAt,
+ StopsAt: splitResult.Split.StopsAt,
+ }
+ asset2 := &marketplacedb.DBAsset{
+ AccountId: dbAsset.AccountId,
+ IA: dbAsset.IA,
+ BandwidthMin: dbAsset.BandwidthMin,
+ BandwidthMax: dbAsset.BandwidthMax,
+ Price: dbAsset.Price,
+ TimeGranularity: dbAsset.TimeGranularity,
+ TimeMinDuration: dbAsset.TimeMinDuration,
+ IfIdIngress: dbAsset.IfIdIngress,
+ IfIdEgress: dbAsset.IfIdEgress,
+ Bandwidth: splitResult.Remainders[0].Bandwidth,
+ StartAt: splitResult.Remainders[0].StartsAt,
+ StopsAt: splitResult.Remainders[0].StopsAt,
+ }
+ err = tx.RemoveAsset(ctx, assetId)
+ if err != nil {
+ return 0, 0, serrors.Join(err, tx.Rollback())
+ }
+ assetId1, err := tx.InsertAsset(ctx, asset1)
+ if err != nil {
+ return 0, 0, serrors.Join(err, tx.Rollback())
+ }
+ assetId2, err := tx.InsertAsset(ctx, asset2)
+ if err != nil {
+ return 0, 0, serrors.Join(err, tx.Rollback())
+ }
+ err = tx.Commit()
+ if err != nil {
+ return 0, 0, serrors.Join(err, tx.Rollback())
+ }
+ return assetId1, assetId2, nil
+}
+
+func (s *MarketplaceStorage) Statistics(ctx context.Context, params *marketplacedb.StatisticsQuery) ([]*marketplacedb.DBStat, error) {
+ return s.db.SearchAssetsForStatistics(ctx, params)
+}
+
+func (s *MarketplaceStorage) FindUsedReservations(ctx context.Context, params *marketplacedb.UsedReservationsQuery) ([]*marketplacedb.UsedReservation, error) {
+ return s.db.FindUsedReservations(ctx, params)
+}
+
+func (s *MarketplaceStorage) BuyAssets(ctx context.Context, accountID int64, assets []*hummingbird.BuyAsset, maxPrice uint64) ([]int64, int64, error) {
+ uniqueCheck := make(map[string]bool)
+ for _, asset := range assets {
+ if uniqueCheck[asset.AssetId] {
+ return nil, 0, serrors.New("Only a single split per asset per buy request allowed")
+ }
+ if asset.StopsAtExactly.AsTime().Before(asset.StartsAtExactly.AsTime()) {
+ return nil, 0, serrors.New("End of validity must come after start of validity")
+ }
+ uniqueCheck[asset.AssetId] = true
+ }
+ tx, err := s.db.BeginTransaction(ctx, &sql.TxOptions{})
+ if err != nil {
+ return nil, 0, err
+ }
+ boughtAssets := make([]int64, 0, 1)
+ costAcc := int64(0)
+ for _, asset := range assets {
+ assetId, err := strconv.ParseInt(asset.AssetId, 10, 64)
+ if err != nil {
+ return nil, 0, serrors.Join(err, tx.Rollback())
+ }
+ dbAsset, err := tx.CheckoutAsset(ctx, assetId)
+ if err != nil {
+ return nil, 0, serrors.Join(err, tx.Rollback())
+ }
+ split, err := SplitAsset(dbAsset, RequestedSplit{
+ ExactFrom: asset.StartsAtExactly.AsTime(),
+ ExactTo: asset.StopsAtExactly.AsTime(),
+ ExactBandwidth: asset.BandwidthExact,
+ })
+ if err != nil {
+ return nil, 0, serrors.Join(err, tx.Rollback())
+ }
+ newAsset := &marketplacedb.DBAsset{
+ AccountId: sql.NullInt64{
+ Int64: accountID,
+ Valid: true,
+ },
+ IA: dbAsset.IA,
+ BandwidthMin: dbAsset.BandwidthMin,
+ BandwidthMax: dbAsset.BandwidthMax,
+ TimeGranularity: dbAsset.TimeGranularity,
+ TimeMinDuration: dbAsset.TimeMinDuration,
+ IfIdIngress: dbAsset.IfIdIngress,
+ IfIdEgress: dbAsset.IfIdEgress,
+ Bandwidth: split.Split.Bandwidth,
+ StartAt: split.Split.StartsAt,
+ StopsAt: split.Split.StopsAt,
+ Price: dbAsset.Price,
+ }
+ id, err := tx.InsertAsset(ctx, newAsset)
+ if err != nil {
+ return nil, 0, serrors.Join(err, tx.Rollback())
+ }
+ totalAssetPrice, fee, safe := s.totalPrice(dbAsset.Price, split.Split.Bandwidth, split.Split.StartsAt, split.Split.StopsAt)
+ if !safe {
+ return nil, 0, serrors.Join(serrors.New("total asset price would lead to integer overflow"), tx.Rollback())
+ }
+ pricePlusFee, safe := addInt64(totalAssetPrice, fee)
+ if !safe {
+ return nil, 0, serrors.Join(serrors.New("total asset price would lead to integer overflow"), tx.Rollback())
+ }
+ _, err = tx.UpdateASMoney(ctx, dbAsset.IA, totalAssetPrice)
+ if err != nil {
+ return nil, 0, serrors.Join(err, tx.Rollback())
+ }
+ costAcc, safe = addInt64(costAcc, pricePlusFee) // currently fee is deducted but the marketplace cannot really see their actual income
+ if !safe {
+ return nil, 0, serrors.Join(serrors.New("total asset price would lead to integer overflow"), tx.Rollback())
+ }
+ boughtAssets = append(boughtAssets, id)
+ for _, segment := range split.Remainders {
+ newAsset := &marketplacedb.DBAsset{
+ IA: dbAsset.IA,
+ BandwidthMin: dbAsset.BandwidthMin,
+ BandwidthMax: dbAsset.BandwidthMax,
+ TimeGranularity: dbAsset.TimeGranularity,
+ TimeMinDuration: dbAsset.TimeMinDuration,
+ IfIdIngress: dbAsset.IfIdIngress,
+ IfIdEgress: dbAsset.IfIdEgress,
+ Bandwidth: segment.Bandwidth,
+ StartAt: segment.StartsAt,
+ StopsAt: segment.StopsAt,
+ Price: dbAsset.Price,
+ }
+ _, err := tx.InsertAsset(ctx, newAsset)
+ if err != nil {
+ return nil, 0, serrors.Join(err, tx.Rollback())
+ }
+ }
+ err = tx.RemoveAsset(ctx, int64(dbAsset.ID))
+ if err != nil {
+ return nil, 0, serrors.Join(err, tx.Rollback())
+ }
+ }
+ if uint64(costAcc) > maxPrice {
+ return nil, 0, serrors.Join(serrors.New("cost higher than max price"), tx.Rollback())
+ }
+ _, err = tx.UpdateAccountMoney(ctx, accountID, -costAcc)
+ if err != nil {
+ return nil, 0, serrors.Join(err, tx.Rollback())
+ }
+ err = tx.Commit()
+ if err != nil {
+ return nil, 0, serrors.Join(err, tx.Rollback())
+ }
+ return boughtAssets, costAcc, nil
+}
+
+func (s *MarketplaceStorage) InsertReservation(ctx context.Context, r *marketplacedb.DBReservation) (int64, error) {
+ return s.db.InsertReservation(ctx, r)
+}
+
+func (s *MarketplaceStorage) UndoRedemption(ctx context.Context, user_id int64, ingressIDString *string, egressIDString *string, pairIDString *string) error {
+ tx, err := s.db.BeginTransaction(ctx, &sql.TxOptions{})
+ if err != nil {
+ return err
+ }
+ if ingressIDString != nil && egressIDString != nil {
+ ingressID, err := strconv.ParseInt(*ingressIDString, 10, 64)
+ if err != nil {
+ return serrors.Join(err, tx.Rollback())
+ }
+ egressID, err := strconv.ParseInt(*egressIDString, 10, 64)
+ if err != nil {
+ return serrors.Join(err, tx.Rollback())
+ }
+ n, err := tx.UndoRedemption(ctx, user_id, ingressID)
+ if err != nil {
+ return serrors.Join(err, tx.Rollback())
+ }
+ if n != 1 {
+ return serrors.Join(serrors.New("asset not found"), tx.Rollback())
+ }
+ n, err = tx.UndoRedemption(ctx, user_id, egressID)
+ if err != nil {
+ return serrors.Join(err, tx.Rollback())
+ }
+ if n != 1 {
+ return serrors.Join(serrors.New("asset not found"), tx.Rollback())
+ }
+ return tx.Commit()
+ } else if ingressIDString == nil && egressIDString == nil && pairIDString != nil {
+ pairID, err := strconv.ParseInt(*pairIDString, 10, 64)
+ if err != nil {
+ return serrors.Join(err, tx.Rollback())
+ }
+ n, err := tx.UndoRedemption(ctx, user_id, pairID)
+ if err != nil {
+ return serrors.Join(err, tx.Rollback())
+ }
+ if n != 1 {
+ return serrors.Join(serrors.New("asset not found"), tx.Rollback())
+ }
+ return tx.Commit()
+ }
+ return serrors.Join(serrors.New("invalid asset IDs"), tx.Rollback())
+}
+func validateAsset(a *marketplacedb.DBAsset) error {
+ duration := a.StopsAt.Sub(a.StartAt)
+ if a.Bandwidth == 0 {
+ return serrors.New("bandwidth is 0")
+ }
+ if duration == 0 {
+ return serrors.New("duration is 0")
+ }
+ if a.Bandwidth < a.BandwidthMin {
+ return serrors.New("bandwidth < min_bandwidth")
+ }
+ if a.Bandwidth > a.BandwidthMax {
+ return serrors.New("bandwith > max_bandiwdth")
+ }
+ if duration < time.Duration(a.TimeMinDuration)*time.Second {
+ return serrors.New("duration < time_min_duration")
+ }
+ if duration%(time.Duration(a.TimeGranularity)*time.Second) != 0 {
+ return serrors.New("duration not multiple of time granularity")
+ }
+ return nil
+}
+func (s *MarketplaceStorage) PrepareRedemption(ctx context.Context, user_id int64, ingressIDString *string, egressIDString *string, pairIDString *string) ([]*marketplacedb.DBAsset, error) {
+ tx, err := s.db.BeginTransaction(ctx, &sql.TxOptions{})
+ if err != nil {
+ return nil, err
+ }
+ if ingressIDString != nil && egressIDString != nil {
+ ingressID, err := strconv.ParseInt(*ingressIDString, 10, 64)
+ if err != nil {
+ return nil, serrors.Join(err, tx.Rollback())
+ }
+ egressID, err := strconv.ParseInt(*egressIDString, 10, 64)
+ if err != nil {
+ return nil, serrors.Join(err, tx.Rollback())
+ }
+ ingressAsset, err := tx.PrepareRedemption(ctx, user_id, ingressID)
+ if err != nil {
+ return nil, serrors.Join(err, tx.Rollback())
+ }
+ if !ingressAsset.IfIdIngress.Valid || ingressAsset.IfIdEgress.Valid {
+ return nil, serrors.Join(serrors.New("ingress asset is not an ingress asset"), tx.Rollback())
+ }
+ if err = validateAsset(ingressAsset); err != nil {
+ return nil, serrors.Join(err, tx.Rollback())
+ }
+ egressAsset, err := tx.PrepareRedemption(ctx, user_id, egressID)
+ if err != nil {
+ return nil, serrors.Join(err, tx.Rollback())
+ }
+ if !egressAsset.IfIdEgress.Valid || egressAsset.IfIdIngress.Valid {
+ return nil, serrors.Join(serrors.New("egress asset is not an egress asset"), tx.Rollback())
+ }
+ if err = validateAsset(egressAsset); err != nil {
+ return nil, serrors.Join(err, tx.Rollback())
+ }
+ err = tx.Commit()
+ if err != nil {
+ return nil, serrors.Join(err, tx.Rollback())
+ }
+ return []*marketplacedb.DBAsset{ingressAsset, egressAsset}, nil
+ } else if ingressIDString == nil && egressIDString == nil && pairIDString != nil {
+ pairID, err := strconv.ParseInt(*pairIDString, 10, 64)
+ if err != nil {
+ return nil, serrors.Join(err, tx.Rollback())
+ }
+ pairAsset, err := tx.PrepareRedemption(ctx, user_id, pairID)
+ if err != nil {
+ return nil, serrors.Join(err, tx.Rollback())
+ }
+ if !pairAsset.IfIdIngress.Valid || !pairAsset.IfIdEgress.Valid {
+ return nil, serrors.Join(serrors.New("pair asset is not a pair asset"), tx.Rollback())
+ }
+ if err = validateAsset(pairAsset); err != nil {
+ return nil, serrors.Join(err, tx.Rollback())
+ }
+ err = tx.Commit()
+ if err != nil {
+ return nil, serrors.Join(err, tx.Rollback())
+ }
+ return []*marketplacedb.DBAsset{pairAsset}, nil
+ }
+ return nil, serrors.Join(serrors.New("invalid asset IDs"), tx.Rollback())
+
+}
+
+func (s *MarketplaceStorage) DepositMoneyAndGet(ctx context.Context, id int64, amount int64) (*marketplacedb.DBUser, error) {
+ tx, err := s.db.BeginTransaction(ctx, &sql.TxOptions{})
+ if err != nil {
+ return nil, err
+ }
+ _, err = tx.UpdateAccountMoney(ctx, id, amount)
+ if err != nil {
+ return nil, serrors.Join(err, tx.Rollback())
+ }
+ user, err := tx.GetUser(ctx, id)
+ if err != nil {
+ return nil, serrors.Join(err, tx.Rollback())
+ }
+ err = tx.Commit()
+ if err != nil {
+ return nil, serrors.Join(err, tx.Rollback())
+ }
+ return user, nil
+}
+
+func (s *MarketplaceStorage) DepositMoneyAndGetAS(ctx context.Context, ia addr.IA, amount int64) (*marketplacedb.DBASUser, error) {
+ tx, err := s.db.BeginTransaction(ctx, &sql.TxOptions{})
+ if err != nil {
+ return nil, err
+ }
+ _, err = tx.UpdateASMoney(ctx, ia, amount)
+ if err != nil {
+ return nil, serrors.Join(err, tx.Rollback())
+ }
+ user, err := tx.GetASUser(ctx, ia)
+ if err != nil {
+ return nil, serrors.Join(err, tx.Rollback())
+ }
+ err = tx.Commit()
+ if err != nil {
+ return nil, serrors.Join(err, tx.Rollback())
+ }
+ return user, nil
+}
+
+func (s *MarketplaceStorage) AssignAsset(ctx context.Context, assetID int64, userID int64, accountIDFrom int64, accountIDTo int64) (int64, error) {
+ tx, err := s.db.BeginTransaction(ctx, &sql.TxOptions{})
+ if err != nil {
+ return 0, err
+ }
+ accounts, err := tx.GetAccountsByUser(ctx, userID)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ var fromAcc, toAcc *marketplacedb.DBAccount
+ for _, acc := range accounts {
+ if acc.ID == accountIDFrom {
+ fromAcc = acc
+ } else if acc.ID == accountIDTo {
+ toAcc = acc
+ }
+ }
+ if fromAcc == nil || toAcc == nil {
+ return 0, serrors.Join(serrors.New("invalid account or asset"), tx.Rollback())
+ }
+ x, err := tx.AssignAsset(ctx, assetID, fromAcc.ID, toAcc.ID)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ if x != 1 {
+ return 0, serrors.Join(serrors.New("invalid account or asset"), tx.Rollback())
+ }
+ err = tx.Commit()
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ return 1, nil
+}
+
+func (s *MarketplaceStorage) AssignReservation(ctx context.Context, id int64, userID int64, accountIDFrom int64, accountIDTo int64) (int64, error) {
+ tx, err := s.db.BeginTransaction(ctx, &sql.TxOptions{})
+ if err != nil {
+ return 0, err
+ }
+ accounts, err := tx.GetAccountsByUser(ctx, userID)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ var fromAcc, toAcc *marketplacedb.DBAccount
+ for _, acc := range accounts {
+ if acc.ID == accountIDFrom {
+ fromAcc = acc
+ } else if acc.ID == accountIDTo {
+ toAcc = acc
+ }
+ }
+ if fromAcc == nil || toAcc == nil {
+ return 0, serrors.Join(serrors.New("invalid account or reservation"), tx.Rollback())
+ }
+ x, err := tx.AssignReservation(ctx, id, fromAcc.ID, toAcc.ID)
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ if x != 1 {
+ return 0, serrors.Join(serrors.New("invalid account or reservation"), tx.Rollback())
+ }
+ err = tx.Commit()
+ if err != nil {
+ return 0, serrors.Join(err, tx.Rollback())
+ }
+ return 1, nil
+}
diff --git a/marketplace/templates/account.html b/marketplace/templates/account.html
new file mode 100644
index 0000000000..b454057c08
--- /dev/null
+++ b/marketplace/templates/account.html
@@ -0,0 +1,124 @@
+
+
+
+
+
+ Accounts
+
+
+
+
+
+
+
+
Accounts
+
{{.Error}}
+
+
+ Account balance
+ {{.Main.Balance}}
+ Token version
+ {{.Main.TokenVersion}}
+
+ {{.Main.ID}}
+
+
+
+
+ JWT Token
+
+ Account:
+
+
+
+
+
+ Sub Accounts
+
+
+
+ | Name |
+ Balance |
+ Token Version |
+ Actions |
+
+
+
+ {{range .Accounts}}
+
+ |
+ {{with .Scope}}
+ {{.}}
+ {{end}}
+ |
+ {{.Balance}} |
+ {{.TokenVersion}} |
+
+
+
+
+
+
+
+ |
+
+ {{end}}
+
+
+
+
+
+ Create new sub Account
+
+
+
+
+
+
\ No newline at end of file
diff --git a/marketplace/templates/asbalance.html b/marketplace/templates/asbalance.html
new file mode 100644
index 0000000000..6f8cecc438
--- /dev/null
+++ b/marketplace/templates/asbalance.html
@@ -0,0 +1,45 @@
+
+
+
+
+
+ View AS Account Balance
+
+
+
+
+
+
+
+ Account Balance
+
+ {{if .Error}}
+ {{.Error}}
+ {{end}}
+
+ Current balance
+
+ {{if .Balance}}
+ {{.Balance}}
+ {{end}}
+
+
+
+
+
\ No newline at end of file
diff --git a/marketplace/templates/aslogin.html b/marketplace/templates/aslogin.html
new file mode 100644
index 0000000000..b2d4b5973f
--- /dev/null
+++ b/marketplace/templates/aslogin.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+ Login
+
+
+
+
+
+
+
+ Login
+ {{if .Error}}
+ {{.Error}}
+ {{end}}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/marketplace/templates/assets.html b/marketplace/templates/assets.html
new file mode 100644
index 0000000000..e5649c8d65
--- /dev/null
+++ b/marketplace/templates/assets.html
@@ -0,0 +1,94 @@
+
+
+
+
+
+ Accounts
+
+
+
+
+
+
+
+
Assets
+
{{.Error}}
+
+ Filter by account
+
+
+
+ Assets
+
+
+
+ | ID |
+ IA |
+ Ingress |
+ Egress |
+ Starts at |
+ Stops at |
+ Bandwidth |
+ Account |
+
+
+
+ {{range $asset := .Assets}}
+
+ | {{.ID}} |
+ {{.IA}} |
+
+ {{with .IfIdIngress}}
+ {{.Value}}
+ {{end}}
+ |
+
+ {{with .IfIdEgress}}
+ {{.Value}}
+ {{end}}
+ |
+ {{.StartAt}} |
+ {{.StopsAt}} |
+ {{.Bandwidth}} |
+
+
+ |
+
+ {{end}}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/marketplace/templates/login.html b/marketplace/templates/login.html
new file mode 100644
index 0000000000..bb8305e27c
--- /dev/null
+++ b/marketplace/templates/login.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+ Login
+
+
+
+
+
+
+
+ Login
+ {{if .Error}}
+ {{.Error}}
+ {{end}}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/marketplace/templates/register.html b/marketplace/templates/register.html
new file mode 100644
index 0000000000..59b8e620ff
--- /dev/null
+++ b/marketplace/templates/register.html
@@ -0,0 +1,50 @@
+
+
+
+
+
+ Register
+
+
+
+
+
+
+
+ Register
+ {{if .Error}}
+ {{.Error}}
+ {{end}}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/marketplace/templates/reservations.html b/marketplace/templates/reservations.html
new file mode 100644
index 0000000000..0b71cfb827
--- /dev/null
+++ b/marketplace/templates/reservations.html
@@ -0,0 +1,86 @@
+
+
+
+
+
+ Accounts
+
+
+
+
+
+
+
+
Reservations
+
{{.Error}}
+
+ Filter by account
+
+
+
+ Reservations
+
+
+
+ | ID |
+ IA |
+ Ingress |
+ Egress |
+ Starts at |
+ Stops at |
+ Bandwidth |
+ Account |
+
+
+
+ {{range $reservation := .Reservations}}
+
+ | {{.ReservationID}} |
+ {{.IA}} |
+ {{.Ingress}} |
+ {{.Egress}} |
+ {{.StartsAt}} |
+ {{.StopsAt}} |
+ {{.Bandwidth}} |
+
+
+ |
+
+ {{end}}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/marketplace/trust_db_wrapper.go b/marketplace/trust_db_wrapper.go
new file mode 100644
index 0000000000..4aec3ea3df
--- /dev/null
+++ b/marketplace/trust_db_wrapper.go
@@ -0,0 +1,83 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package marketplace
+
+import (
+ "context"
+ "crypto/x509"
+ "net"
+
+ "github.com/scionproto/scion/pkg/log"
+ "github.com/scionproto/scion/pkg/private/serrors"
+ "github.com/scionproto/scion/pkg/scrypto/cppki"
+ "github.com/scionproto/scion/private/storage"
+ storageTrust "github.com/scionproto/scion/private/storage/trust"
+ "github.com/scionproto/scion/private/trust"
+)
+
+type DB_Wrapper struct {
+ db storage.TrustDB
+ fetcher trust.Fetcher
+ server net.Addr
+}
+
+func FromTrustDB(db storage.TrustDB, fetcher trust.Fetcher, server net.Addr) *DB_Wrapper {
+ return &DB_Wrapper{
+ db: db,
+ fetcher: fetcher,
+ server: server,
+ }
+}
+
+func (d *DB_Wrapper) Chain(ctx context.Context, b []byte) ([]*x509.Certificate, error) {
+ return d.db.Chain(ctx, b)
+}
+
+func (d *DB_Wrapper) Chains(ctx context.Context, q trust.ChainQuery) ([][]*x509.Certificate, error) {
+ return d.db.Chains(ctx, q)
+}
+
+func (d *DB_Wrapper) Close() error {
+ return d.db.Close()
+}
+
+func (d *DB_Wrapper) InsertChain(ctx context.Context, c []*x509.Certificate) (bool, error) {
+ return d.db.InsertChain(ctx, c)
+}
+
+func (d *DB_Wrapper) InsertTRC(ctx context.Context, trc cppki.SignedTRC) (bool, error) {
+ return d.db.InsertTRC(ctx, trc)
+}
+
+func (d *DB_Wrapper) SignedTRC(ctx context.Context, id cppki.TRCID) (cppki.SignedTRC, error) {
+ trc, err := d.db.SignedTRC(ctx, id)
+ if err == nil && !trc.IsZero() {
+ return trc, nil
+ }
+ log.FromCtx(ctx).Debug("fetch trc from server")
+ trc, err = d.fetcher.TRC(ctx, id, d.server)
+ if err != nil {
+ return trc, serrors.WrapNoStack("parsing TRC", err)
+ }
+ _, err = d.InsertTRC(ctx, trc)
+ if err != nil {
+ return trc, serrors.WrapNoStack("inserting TRC", err)
+ }
+ return trc, nil
+}
+
+func (d *DB_Wrapper) SignedTRCs(ctx context.Context, query storageTrust.TRCsQuery) (cppki.SignedTRCs, error) {
+ return d.db.SignedTRCs(ctx, query)
+}
diff --git a/marketplace/webapp/BUILD.bazel b/marketplace/webapp/BUILD.bazel
new file mode 100644
index 0000000000..9e09e7ccd6
--- /dev/null
+++ b/marketplace/webapp/BUILD.bazel
@@ -0,0 +1,18 @@
+load("@rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["web.go"],
+ importpath = "github.com/scionproto/scion/marketplace/webapp",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//marketplace/db:go_default_library",
+ "//marketplace/storage:go_default_library",
+ "//pkg/addr:go_default_library",
+ "//pkg/hummingbird/registration:go_default_library",
+ "//pkg/log:go_default_library",
+ "@com_github_golang_jwt_jwt//:go_default_library",
+ "@com_github_gorilla_sessions//:go_default_library",
+ "@org_golang_x_crypto//bcrypt:go_default_library",
+ ],
+)
diff --git a/marketplace/webapp/web.go b/marketplace/webapp/web.go
new file mode 100644
index 0000000000..d948b57940
--- /dev/null
+++ b/marketplace/webapp/web.go
@@ -0,0 +1,768 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package webapp
+
+import (
+ "crypto/rand"
+ "encoding/gob"
+ "fmt"
+ "html/template"
+ "net/http"
+ "regexp"
+ "strconv"
+ "time"
+
+ "github.com/golang-jwt/jwt"
+ "github.com/gorilla/sessions"
+ "golang.org/x/crypto/bcrypt"
+
+ "github.com/scionproto/scion/marketplace/db"
+ "github.com/scionproto/scion/marketplace/storage"
+ "github.com/scionproto/scion/pkg/addr"
+ "github.com/scionproto/scion/pkg/hummingbird/registration"
+ "github.com/scionproto/scion/pkg/log"
+)
+
+var templates = template.Must(template.ParseGlob("marketplace/templates/*.html"))
+
+func Init(signer *registration.Signer, store *storage.MarketplaceStorage, mux *http.ServeMux, disableUserRegistration bool) {
+ sessionKey := make([]byte, 32)
+ rand.Read(sessionKey)
+ sessionStore := sessions.NewCookieStore(sessionKey)
+ sessionStore.Options = &sessions.Options{
+ Secure: true,
+ HttpOnly: true,
+ MaxAge: 0,
+ SameSite: http.SameSiteLaxMode,
+ }
+ gob.Register(User{})
+ gob.Register(addr.IA(0))
+ h := &Handler{
+ sessions: sessionStore,
+ signer: signer,
+ store: store,
+ disableUserRegistration: disableUserRegistration,
+ }
+ mux.HandleFunc("/", h.accountHandler)
+ mux.HandleFunc("/login", h.loginHandler)
+ mux.HandleFunc("/register", h.registerHandler)
+ mux.HandleFunc("/account/balance", h.accountBalanceHandler)
+ mux.HandleFunc("/account/create", h.accountCreateHandler)
+ mux.HandleFunc("/account/delete", h.accountDeletionHandler)
+ mux.HandleFunc("/account/token", h.accountTokenHandler)
+ mux.HandleFunc("/account/resetjwt", h.accountResetJWTHandler)
+ mux.HandleFunc("/account", h.accountHandler)
+ mux.HandleFunc("/assets", h.assetsHandler)
+ mux.HandleFunc("/assets/assign", h.assignAssetHandler)
+ mux.HandleFunc("/reservations", h.reservationsHandler)
+ mux.HandleFunc("/reservations/assign", h.assignReservationHandler)
+ mux.HandleFunc("/logout", h.logoutHandler)
+ mux.HandleFunc("/aslogin", h.asLoginHandler)
+ mux.HandleFunc("/asbalance", h.asBalanceHandler)
+ mux.HandleFunc("/static/script.js", func(w http.ResponseWriter, r *http.Request) {
+ http.ServeFile(w, r, "./marketplace/static/script.js")
+ })
+ mux.HandleFunc("/static/style.css", func(w http.ResponseWriter, r *http.Request) {
+ http.ServeFile(w, r, "./marketplace/static/style.css")
+ })
+}
+
+type Handler struct {
+ sessions sessions.Store
+ store *storage.MarketplaceStorage
+ signer *registration.Signer
+ disableUserRegistration bool
+}
+type User struct {
+ ID int64
+ Name string
+}
+type Account struct {
+ ID int64
+ Scope *string
+ Balance int64
+ TokenVersion int64
+}
+
+func (h *Handler) SetSessionUser(w http.ResponseWriter, r *http.Request, user any) error {
+ session, _ := h.sessions.Get(r, "session_id")
+ session.Values["user"] = user
+ session.Values["iat"] = time.Now().Unix()
+ return session.Save(r, w)
+}
+
+func (h *Handler) GetSession(r *http.Request) (*sessions.Session, error) {
+ session, err := h.sessions.Get(r, "session_id")
+ if err != nil {
+ return nil, err
+ }
+ iat, ok := session.Values["iat"].(int64)
+ if !ok {
+ return nil, fmt.Errorf("session invalid")
+ }
+ if time.Now().Unix() > iat+3600 {
+ return nil, fmt.Errorf("session invalid")
+ }
+ return session, nil
+}
+
+func (h *Handler) asLoginHandler(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodGet {
+ templates.ExecuteTemplate(w, "aslogin.html", map[string]any{})
+ return
+ }
+
+ r.ParseForm()
+ username := r.FormValue("username")
+ password := r.FormValue("password")
+ ia, err := addr.ParseIA(username)
+ dbUser, err := h.store.GetASUser(r.Context(), ia)
+ if err != nil {
+ log.Debug("AS login handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ if dbUser == nil {
+ templates.ExecuteTemplate(w, "aslogin.html", map[string]any{
+ "Error": "invalid credentials",
+ })
+ return
+ }
+ err = bcrypt.CompareHashAndPassword(
+ []byte(dbUser.PasswordHash),
+ []byte(password),
+ )
+ if err != nil {
+ templates.ExecuteTemplate(w, "aslogin.html", map[string]any{
+ "Error": "invalid credentials",
+ })
+ return
+ }
+
+ err = h.SetSessionUser(w, r, dbUser.IA)
+ if err != nil {
+ log.Debug("AS login handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ http.Redirect(w, r, "/asbalance", http.StatusSeeOther)
+}
+
+func (h *Handler) asBalanceHandler(w http.ResponseWriter, r *http.Request) {
+ session, err := h.GetSession(r)
+ if err != nil {
+ http.Redirect(w, r, "/aslogin", http.StatusSeeOther)
+ return
+ }
+ ia, ok := session.Values["user"].(addr.IA)
+ if !ok {
+ http.Redirect(w, r, "/aslogin", http.StatusSeeOther)
+ return
+ }
+ dbUser, err := h.store.GetASUser(r.Context(), ia)
+ if err != nil {
+ log.Debug("AS balance handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ if dbUser == nil {
+ http.Redirect(w, r, "/aslogin", http.StatusSeeOther)
+ return
+ }
+ templates.ExecuteTemplate(w, "asbalance.html", map[string]any{
+ "Balance": strconv.Itoa(int(dbUser.Balance)),
+ "Username": ia.String(),
+ })
+}
+
+func (h *Handler) accountBalanceHandler(w http.ResponseWriter, r *http.Request) {
+ session, err := h.GetSession(r)
+ if err != nil {
+ http.Error(w, "invalid session", http.StatusUnauthorized)
+ return
+ }
+ user, ok := session.Values["user"].(User)
+ if !ok {
+ http.Error(w, "invalid session", http.StatusUnauthorized)
+ return
+ }
+ if r.Method != http.MethodPost {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ r.ParseForm()
+ accountFromID, err := strconv.ParseInt(r.FormValue("from"), 10, 64)
+ if err != nil {
+ log.Debug("User account balance handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ accountToID, err := strconv.ParseInt(r.FormValue("to"), 10, 64)
+ if err != nil {
+ log.Debug("User account balance handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ amount, err := strconv.ParseInt(r.FormValue("amount"), 10, 64)
+ if err != nil {
+ log.Debug("User account balance handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ _, err = h.store.TransferMoneyBetweenAccounts(r.Context(), user.ID, accountFromID, accountToID, amount)
+ if err != nil {
+ log.Debug("User account balance handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+}
+
+func (h *Handler) accountTokenHandler(w http.ResponseWriter, r *http.Request) {
+ session, err := h.GetSession(r)
+ if err != nil {
+ http.Error(w, "invalid session", http.StatusUnauthorized)
+ return
+ }
+ user, ok := session.Values["user"].(User)
+ if !ok {
+ http.Error(w, "invalid session", http.StatusUnauthorized)
+ return
+ }
+ if r.Method != http.MethodPost {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ r.ParseForm()
+ accountIDString := r.FormValue("id")
+ accountID, err := strconv.ParseInt(accountIDString, 10, 64)
+ if err != nil {
+ log.Debug("User account token handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ dbAccount, err := h.store.GetAccountByAccountID(r.Context(), accountID)
+ if err != nil {
+ log.Debug("User account token handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ if dbAccount.UserID != user.ID {
+ log.Debug("User account token handler. Account does not belong to user", "dbAccount.UserID", dbAccount.UserID, "user.ID", user.ID)
+ http.Error(w, "invalid account", http.StatusUnauthorized)
+ return
+ }
+ token, err := h.createToken(strconv.FormatInt(dbAccount.ID, 10), dbAccount.TokenVersion)
+ if err != nil {
+ log.Debug("User token handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ w.Write([]byte(token))
+}
+
+func (h *Handler) accountDeletionHandler(w http.ResponseWriter, r *http.Request) {
+ session, err := h.GetSession(r)
+ if err != nil {
+ http.Error(w, "invalid session", http.StatusUnauthorized)
+ return
+ }
+ user, ok := session.Values["user"].(User)
+ if !ok {
+ http.Error(w, "invalid session", http.StatusUnauthorized)
+ return
+ }
+ if r.Method != http.MethodPost {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ r.ParseForm()
+ accountId, err := strconv.ParseInt(r.FormValue("id"), 10, 64)
+ if err != nil {
+ log.Debug("User account deletion handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ _, err = h.store.DeleteAccount(r.Context(), user.ID, accountId)
+ if err != nil {
+ log.Debug("User account creation handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+}
+
+func (h *Handler) accountResetJWTHandler(w http.ResponseWriter, r *http.Request) {
+ session, err := h.GetSession(r)
+ if err != nil {
+ http.Error(w, "invalid session", http.StatusUnauthorized)
+ return
+ }
+ user, ok := session.Values["user"].(User)
+ if !ok {
+ http.Error(w, "invalid session", http.StatusUnauthorized)
+ return
+ }
+ if r.Method != http.MethodPost {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ r.ParseForm()
+ accountId, err := strconv.ParseInt(r.FormValue("id"), 10, 64)
+ if err != nil {
+ log.Debug("User account jwt reset handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ _, err = h.store.IncrementUserJWTVersion(r.Context(), user.ID, accountId)
+ if err != nil {
+ log.Debug("User account jwt reset handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+}
+
+var accountScopeRegex = regexp.MustCompile(`^[A-Za-z0-9_-]{1,20}$`)
+
+func (h *Handler) accountCreateHandler(w http.ResponseWriter, r *http.Request) {
+ session, err := h.GetSession(r)
+ if err != nil {
+ http.Redirect(w, r, "/login", http.StatusSeeOther)
+ return
+ }
+ user, ok := session.Values["user"].(User)
+ if !ok {
+ http.Redirect(w, r, "/login", http.StatusSeeOther)
+ return
+ }
+ if r.Method != http.MethodPost {
+ http.Redirect(w, r, "/account", http.StatusSeeOther)
+ return
+ }
+ r.ParseForm()
+ accountScope := r.FormValue("name")
+ if !accountScopeRegex.MatchString(accountScope) {
+ http.Error(w, "invalid name", http.StatusBadRequest)
+ return
+ }
+ _, err = h.store.CreateAccount(r.Context(), &db.DBAccount{
+ UserID: user.ID,
+ Scope: &accountScope,
+ })
+ if err != nil {
+ log.Debug("User account creation handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ http.Redirect(w, r, "/account", http.StatusSeeOther)
+}
+
+func (h *Handler) assignReservationHandler(w http.ResponseWriter, r *http.Request) {
+ session, err := h.GetSession(r)
+ if err != nil {
+ http.Error(w, "invalid session", http.StatusUnauthorized)
+ return
+ }
+ user, ok := session.Values["user"].(User)
+ if !ok {
+ http.Error(w, "invalid session", http.StatusUnauthorized)
+ return
+ }
+ if r.Method != http.MethodPost {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ r.ParseForm()
+ id, err := strconv.ParseInt(r.FormValue("id"), 10, 64)
+ if err != nil {
+ log.Debug("User assign reservation handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ fromAccountId, err := strconv.ParseInt(r.FormValue("from"), 10, 64)
+ if err != nil {
+ log.Debug("User assign reservation handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ toAccountId, err := strconv.ParseInt(r.FormValue("to"), 10, 64)
+ if err != nil {
+ log.Debug("User assign reservation handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ _, err = h.store.AssignReservation(r.Context(), id, user.ID, fromAccountId, toAccountId)
+ if err != nil {
+ log.Debug("User assign reservation handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+}
+
+func (h *Handler) assignAssetHandler(w http.ResponseWriter, r *http.Request) {
+ session, err := h.GetSession(r)
+ if err != nil {
+ http.Error(w, "invalid session", http.StatusUnauthorized)
+ return
+ }
+ user, ok := session.Values["user"].(User)
+ if !ok {
+ http.Error(w, "invalid session", http.StatusUnauthorized)
+ return
+ }
+ if r.Method != http.MethodPost {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ r.ParseForm()
+ assetId, err := strconv.ParseInt(r.FormValue("id"), 10, 64)
+ if err != nil {
+ log.Debug("User assign asset handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ fromAccountId, err := strconv.ParseInt(r.FormValue("from"), 10, 64)
+ if err != nil {
+ log.Debug("User assign asset handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ toAccountId, err := strconv.ParseInt(r.FormValue("to"), 10, 64)
+ if err != nil {
+ log.Debug("User assign asset handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ _, err = h.store.AssignAsset(r.Context(), assetId, user.ID, fromAccountId, toAccountId)
+ if err != nil {
+ log.Debug("User assign asset handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+}
+
+func (h *Handler) assetsHandler(w http.ResponseWriter, r *http.Request) {
+ session, err := h.GetSession(r)
+ if err != nil {
+ http.Redirect(w, r, "/login", http.StatusSeeOther)
+ return
+ }
+ user, ok := session.Values["user"].(User)
+ if !ok {
+ http.Redirect(w, r, "/login", http.StatusSeeOther)
+ return
+ }
+ r.ParseForm()
+ accountIdFilterString := r.FormValue("id")
+ var accountIdFilter int64
+ if accountIdFilterString != "" {
+ accountIdFilter, err = strconv.ParseInt(accountIdFilterString, 10, 64)
+ if err != nil {
+ log.Debug("User assign asset handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ }
+
+ dbAccounts, err := h.store.GetAccountsByUser(r.Context(), user.ID)
+ if err != nil {
+ log.Debug("User assets handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ var filterAccount *db.DBAccount
+ var mainAccount *db.DBAccount
+ for _, a := range dbAccounts {
+ if a.Scope == nil {
+ a.Scope = &user.Name
+ mainAccount = a
+ }
+ if accountIdFilterString != "" && a.ID == accountIdFilter {
+ filterAccount = a
+ }
+ }
+ if filterAccount == nil {
+ filterAccount = mainAccount
+ }
+ if mainAccount == nil {
+ http.Error(w, "forbidden", http.StatusForbidden)
+ return
+ }
+ assets, err := h.store.Search(r.Context(), &db.AssetQuery{
+ AccountId: &filterAccount.ID,
+ })
+ if err != nil {
+ log.Debug("User assets handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ templates.ExecuteTemplate(w, "assets.html", map[string]any{
+ "Username": user.Name,
+ "Assets": assets,
+ "Accounts": dbAccounts,
+ "Account": filterAccount,
+ })
+}
+
+func (h *Handler) reservationsHandler(w http.ResponseWriter, r *http.Request) {
+ session, err := h.GetSession(r)
+ if err != nil {
+ http.Redirect(w, r, "/login", http.StatusSeeOther)
+ return
+ }
+ user, ok := session.Values["user"].(User)
+ if !ok {
+ http.Redirect(w, r, "/login", http.StatusSeeOther)
+ return
+ }
+ r.ParseForm()
+ accountIdFilterString := r.FormValue("id")
+ var accountIdFilter int64
+ if accountIdFilterString != "" {
+ accountIdFilter, err = strconv.ParseInt(accountIdFilterString, 10, 64)
+ if err != nil {
+ log.Debug("User assign asset handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ }
+
+ dbAccounts, err := h.store.GetAccountsByUser(r.Context(), user.ID)
+ if err != nil {
+ log.Debug("User assets handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ var filterAccount *db.DBAccount
+ var mainAccount *db.DBAccount
+ for _, a := range dbAccounts {
+ if a.Scope == nil {
+ a.Scope = &user.Name
+ mainAccount = a
+ }
+ if accountIdFilterString != "" && a.ID == accountIdFilter {
+ filterAccount = a
+ }
+ }
+ if filterAccount == nil {
+ filterAccount = mainAccount
+ }
+ if mainAccount == nil {
+ http.Error(w, "forbidden", http.StatusForbidden)
+ return
+ }
+ reservations, err := h.store.FetchReservations(r.Context(), &db.ReservationQuery{
+ AccountId: filterAccount.ID,
+ })
+ if err != nil {
+ log.Debug("User assets handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ templates.ExecuteTemplate(w, "reservations.html", map[string]any{
+ "Username": user.Name,
+ "Reservations": reservations,
+ "Accounts": dbAccounts,
+ "Account": filterAccount,
+ })
+}
+
+func (h *Handler) accountHandler(w http.ResponseWriter, r *http.Request) {
+ session, err := h.GetSession(r)
+ if err != nil {
+ http.Redirect(w, r, "/login", http.StatusSeeOther)
+ return
+ }
+ user, ok := session.Values["user"].(User)
+ if !ok {
+ http.Redirect(w, r, "/login", http.StatusSeeOther)
+ return
+ }
+ accounts, err := h.store.GetAccountsByUser(r.Context(), user.ID)
+ if err != nil {
+ log.Debug("User account handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ accs := []Account{}
+ mainAccount := Account{}
+ for _, a := range accounts {
+ if a.Scope == nil {
+ mainAccount = Account{
+ ID: a.ID,
+ Balance: a.Balance,
+ TokenVersion: a.TokenVersion,
+ }
+ } else {
+ accs = append(accs, Account{
+ ID: a.ID,
+ Scope: a.Scope,
+ Balance: a.Balance,
+ TokenVersion: a.TokenVersion,
+ })
+ }
+
+ }
+ templates.ExecuteTemplate(w, "account.html", map[string]any{
+ "Username": user.Name,
+ "Main": mainAccount,
+ "Accounts": accs,
+ })
+}
+
+func (h *Handler) registerHandler(w http.ResponseWriter, r *http.Request) {
+ if h.disableUserRegistration {
+ templates.ExecuteTemplate(w, "register.html", map[string]any{
+ "Error": "User registration disabled",
+ })
+ return
+ }
+ if r.Method == http.MethodGet {
+ templates.ExecuteTemplate(w, "register.html", map[string]any{})
+ return
+ }
+
+ r.ParseForm()
+ username := r.FormValue("username")
+ password := r.FormValue("password")
+ if !accountScopeRegex.MatchString(username) {
+ templates.ExecuteTemplate(w, "register.html", map[string]any{
+ "Error": "Invalid username",
+ })
+ return
+ }
+ user, err := h.store.GetUserByName(r.Context(), username)
+ if err != nil {
+ log.Debug("User register handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ if user != nil {
+ templates.ExecuteTemplate(w, "register.html", map[string]any{
+ "Error": "Username already exists",
+ })
+ return
+ }
+ hash, err := bcrypt.GenerateFromPassword(
+ []byte(password),
+ 12,
+ )
+ if err != nil {
+ log.Debug("User register handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ userId, err := h.store.CreateUser(r.Context(), &db.DBUser{
+ Name: username,
+ PasswordHash: string(hash),
+ })
+ if err != nil {
+ log.Debug("User register handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ err = h.SetSessionUser(w, r, User{
+ ID: userId,
+ Name: username,
+ })
+ if err != nil {
+ log.Debug("User register handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ http.Redirect(w, r, "/account", http.StatusSeeOther)
+}
+
+func (h *Handler) logoutHandler(w http.ResponseWriter, r *http.Request) {
+ session, err := h.GetSession(r)
+ if err != nil {
+ http.Redirect(w, r, "/login", http.StatusSeeOther)
+ return
+ }
+ clear(session.Values)
+ session.Options.MaxAge = -1
+ err = session.Save(r, w)
+ if err != nil {
+ log.Debug("Logout handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ http.Redirect(w, r, "/login", http.StatusSeeOther)
+}
+
+func (h *Handler) loginHandler(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodGet {
+ templates.ExecuteTemplate(w, "login.html", map[string]any{})
+ return
+ }
+
+ r.ParseForm()
+ username := r.FormValue("username")
+ password := r.FormValue("password")
+
+ dbUser, err := h.store.GetUserByName(r.Context(), username)
+ if err != nil {
+ log.Debug("User login handler", "err", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ if dbUser == nil {
+ templates.ExecuteTemplate(w, "login.html", map[string]any{
+ "Error": "invalid credentials",
+ })
+ return
+ }
+ err = bcrypt.CompareHashAndPassword(
+ []byte(dbUser.PasswordHash),
+ []byte(password),
+ )
+ if err != nil {
+ templates.ExecuteTemplate(w, "login.html", map[string]any{
+ "Error": "invalid credentials",
+ })
+ return
+ }
+
+ err = h.SetSessionUser(w, r, User{
+ ID: dbUser.ID,
+ Name: username,
+ })
+ if err != nil {
+ templates.ExecuteTemplate(w, "login.html", map[string]any{
+ "Error": err.Error(),
+ })
+ return
+ }
+
+ http.Redirect(w, r, "/account", http.StatusSeeOther)
+}
+
+func (h *Handler) createToken(user string, tokenVersion int64) (string, error) {
+ claims := jwt.MapClaims{
+ "sub": user,
+ "scope": "User",
+ "exp": time.Now().Add(time.Hour * 24 * 7).Unix(),
+ "iat": time.Now().Unix(),
+ "ver": tokenVersion,
+ }
+ return h.signer.GenerateToken(claims)
+}
diff --git a/marketplace_client/BUILD.bazel b/marketplace_client/BUILD.bazel
new file mode 100644
index 0000000000..684b628794
--- /dev/null
+++ b/marketplace_client/BUILD.bazel
@@ -0,0 +1,34 @@
+load("@rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["main.go"],
+ importpath = "github.com/scionproto/scion/marketplace_client",
+ visibility = ["//visibility:private"],
+ deps = [
+ "//pkg/addr:go_default_library",
+ "//pkg/connect:go_default_library",
+ "//pkg/daemon:go_default_library",
+ "//pkg/daemon/types:go_default_library",
+ "//pkg/hummingbird/registration:go_default_library",
+ "//pkg/private/serrors:go_default_library",
+ "//pkg/proto/hummingbird:go_default_library",
+ "//pkg/snet:go_default_library",
+ "//pkg/snet/path:go_default_library",
+ "//pkg/snet/squic:go_default_library",
+ "//private/app/appnet:go_default_library",
+ "//private/storage:go_default_library",
+ "//private/trust:go_default_library",
+ "@com_connectrpc_connect//:go_default_library",
+ "@com_github_golang_jwt_jwt//:go_default_library",
+ "@com_github_quic_go_quic_go//http3:go_default_library",
+ "@com_github_scionproto_scion//pkg/proto/hummingbird/v1/hummingbirdconnect:go_default_library",
+ "@org_golang_google_protobuf//types/known/timestamppb:go_default_library",
+ ],
+)
+
+go_binary(
+ name = "marketplace_client",
+ embed = [":go_default_library"],
+ visibility = ["//visibility:public"],
+)
diff --git a/marketplace_client/main.go b/marketplace_client/main.go
new file mode 100644
index 0000000000..d355f8220f
--- /dev/null
+++ b/marketplace_client/main.go
@@ -0,0 +1,1297 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "bufio"
+ "context"
+ "crypto/tls"
+ "encoding/csv"
+ "encoding/hex"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "math"
+ "net"
+ "net/http"
+ "net/netip"
+ "os"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "connectrpc.com/connect"
+ "github.com/golang-jwt/jwt"
+ "github.com/quic-go/quic-go/http3"
+ "github.com/scionproto/scion/pkg/addr"
+ libconnect "github.com/scionproto/scion/pkg/connect"
+ "github.com/scionproto/scion/pkg/daemon"
+ "github.com/scionproto/scion/pkg/daemon/types"
+ "github.com/scionproto/scion/pkg/hummingbird/registration"
+ "github.com/scionproto/scion/pkg/private/serrors"
+ "github.com/scionproto/scion/pkg/proto/hummingbird"
+ "github.com/scionproto/scion/pkg/proto/hummingbird/v1/hummingbirdconnect"
+ "github.com/scionproto/scion/pkg/snet"
+ "github.com/scionproto/scion/pkg/snet/path"
+ "github.com/scionproto/scion/pkg/snet/squic"
+ "github.com/scionproto/scion/private/app/appnet"
+ "github.com/scionproto/scion/private/storage"
+ "github.com/scionproto/scion/private/trust"
+ "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+type jwtType int
+
+const (
+ User jwtType = iota
+ Publisher
+ RedemptionService
+)
+
+func authInterceptor(jwtToken string) connect.UnaryInterceptorFunc {
+ return func(next connect.UnaryFunc) connect.UnaryFunc {
+ return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
+ req.Header().Set("Authorization", "Bearer "+jwtToken)
+ return next(ctx, req)
+ }
+ }
+}
+func printOptions(t jwtType) {
+ fmt.Println("-> info")
+ switch t {
+ case User:
+ fmt.Println("-> search")
+ fmt.Println("-> buy")
+ fmt.Println("-> split")
+ fmt.Println("-> combine")
+ fmt.Println("-> redeem")
+ fmt.Println("-> reservation")
+ case Publisher:
+ fmt.Println("-> search")
+ fmt.Println("-> publish")
+ fmt.Println("-> update")
+ fmt.Println("-> statistics")
+ fmt.Println("-> password")
+ case RedemptionService:
+ fmt.Println("-> delegate")
+ }
+ fmt.Println("-> reset")
+ fmt.Println("-> exit")
+}
+
+func withSCION(ctx context.Context, localIA addr.IA, remote *snet.UDPAddr, serverName string, token string) (
+ hummingbirdconnect.MarketplaceServiceClient, hummingbirdconnect.RedemptionServiceClient, hummingbirdconnect.AccountServiceClient, error) {
+ var topo snet.Topology
+ var err error
+ var localPublic *net.UDPAddr
+ var querier snet.PathQuerier
+
+ connector, err := daemon.NewAutoConnector(ctx, daemon.WithDaemon(sciond))
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ localIA, err = connector.LocalIA(ctx)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ querier = daemon.Querier{Connector: connector, IA: localIA}
+ portStart, portEnd, err := connector.PortRange(ctx)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ interfaces, err := connector.Interfaces(ctx)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ topo = snet.Topology{
+ LocalIA: localIA,
+ PortRange: snet.TopologyPortRange{Start: portStart, End: portEnd},
+ Interface: func(u uint16) (netip.AddrPort, bool) {
+ a, found := interfaces[u]
+ return a, found
+ },
+ }
+ if remote.IA == localIA {
+ remote.Path = path.Empty{}
+ remote.NextHop = remote.Host
+ } else {
+ paths, err := querier.Query(ctx, remote.IA)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ if len(paths) == 0 {
+ return nil, nil, nil, serrors.New("no paths found to marketplace")
+ }
+ remote.Path = paths[0].Dataplane()
+ remote.NextHop = paths[0].UnderlayNextHop()
+ }
+
+ trustDB, err := storage.NewInMemoryTrustStorage()
+ if err != nil {
+ return nil, nil, nil, err
+ }
+
+ conn, err := net.Dial("udp", remote.NextHop.String())
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ localPublic, ok := conn.LocalAddr().(*net.UDPAddr)
+ if !ok {
+ return nil, nil, nil, serrors.New("localAddr not UDP addr")
+ }
+ conn.Close()
+
+ nc := appnet.NetworkConfig{
+ Topology: topo,
+ IA: topo.LocalIA,
+ QUIC: appnet.QUIC{
+ TLSVerifier: trust.NewTLSCryptoVerifier(trustDB),
+ },
+ MTU: 1400,
+ Public: &net.UDPAddr{
+ IP: localPublic.IP,
+ Port: 0,
+ Zone: localPublic.Zone,
+ },
+ }
+ quicStack, err := nc.QUICStack(ctx)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ var dialerFunc func(a net.Addr, opts ...squic.EarlyDialerOption) squic.EarlyDialer
+ if insecure {
+ dialerFunc = (&squic.EarlyDialerFactory{
+ Transport: quicStack.Dialer.Transport,
+ TLSConfig: &tls.Config{
+ NextProtos: []string{"h3", "SCION"},
+ InsecureSkipVerify: true,
+ },
+ Rewriter: &appnet.AddressRewriter{
+ Router: &snet.BaseRouter{
+ Querier: querier,
+ },
+ },
+ }).NewDialer
+ } else {
+ dialerFunc = (&squic.EarlyDialerFactory{
+ Transport: quicStack.Dialer.Transport,
+ TLSConfig: &tls.Config{
+ NextProtos: []string{"h3", "SCION"},
+ ServerName: serverName,
+ },
+ Rewriter: &appnet.AddressRewriter{
+ Router: &snet.BaseRouter{
+ Querier: querier,
+ },
+ },
+ }).NewDialer
+ }
+
+ dialer := dialerFunc(remote)
+ marketplaceClient := hummingbirdconnect.NewMarketplaceServiceClient(
+ libconnect.HTTPClient{
+ RoundTripper: &http3.Transport{
+ Dial: dialer.DialEarly,
+ },
+ }, libconnect.BaseUrl(remote), connect.WithInterceptors(authInterceptor(token)))
+ redemptionClient := hummingbirdconnect.NewRedemptionServiceClient(
+ libconnect.HTTPClient{
+ RoundTripper: &http3.Transport{
+ Dial: dialer.DialEarly,
+ },
+ }, libconnect.BaseUrl(remote), connect.WithInterceptors(authInterceptor(token)))
+ accountClient := hummingbirdconnect.NewAccountServiceClient(
+ libconnect.HTTPClient{
+ RoundTripper: &http3.Transport{
+ Dial: dialer.DialEarly,
+ },
+ }, libconnect.BaseUrl(remote), connect.WithInterceptors(authInterceptor(token)))
+ return marketplaceClient, redemptionClient, accountClient, nil
+}
+
+type HummingbirdNotes struct {
+ Hummingbird []HummingbirdNoteEntry `json:"hummingbird,omitempty"`
+}
+
+type HummingbirdNoteEntry struct {
+ Name string `json:"name"`
+ Protocol string `json:"protocol"`
+ Api string `json:"api"`
+ Website string `json:"website"`
+}
+
+func discoverMarketplaces(ctx context.Context, reader *bufio.Reader) (*HummingbirdNoteEntry, error) {
+ fmt.Println("Discovery will return a list of marketplaces that offer hummingbird reservations for a given AS.")
+ targetIA := readString(reader, "Reservation ISD-AS: ")
+ ia, err := addr.ParseIA(targetIA)
+ if err != nil {
+ return nil, err
+ }
+
+ var paths []snet.Path
+
+ connector, err := daemon.NewAutoConnector(ctx, daemon.WithDaemon(sciond))
+ if err != nil {
+ return nil, err
+ }
+ localIA, err := connector.LocalIA(ctx)
+ if err != nil {
+ return nil, err
+ }
+ paths, err = connector.Paths(ctx, ia, localIA, types.PathReqFlags{})
+ if err != nil {
+ return nil, err
+ }
+
+ marketplacesSet := map[HummingbirdNoteEntry]int{}
+ for _, path := range paths {
+ for _, note := range path.Metadata().Notes {
+ if note == "" {
+ continue
+ }
+ hummingbirdNotes := &HummingbirdNotes{}
+ err = json.Unmarshal([]byte(note), hummingbirdNotes)
+ if err != nil {
+ fmt.Println(err, note)
+ continue
+ }
+ for _, entry := range hummingbirdNotes.Hummingbird {
+ marketplacesSet[entry]++
+ }
+ }
+ }
+ if len(marketplacesSet) == 0 {
+ return nil, serrors.New("No marketplaces found")
+ }
+ marketplaces := make([]HummingbirdNoteEntry, 0, len(marketplacesSet))
+ for marketplace := range marketplacesSet {
+ marketplaces = append(marketplaces, marketplace)
+ }
+ sort.Slice(marketplaces, func(i, j int) bool {
+ return marketplacesSet[marketplaces[i]] < marketplacesSet[marketplaces[j]]
+ })
+ fmt.Println("Found marketplaces:")
+
+ for i, marketplace := range marketplaces {
+ fmt.Printf("%d: %s with website %s using protocol %s\n", i, marketplace.Name, marketplace.Website, marketplace.Protocol)
+ }
+ index := readUint64(reader, "Select marketplace: ")
+ if index >= uint64(len(marketplaces)) {
+ return nil, serrors.New("index out of range")
+ }
+ return &marketplaces[index], nil
+}
+
+func tokenType(tokenStr string) (string, jwtType, error) {
+ parseScopes := func(scopeStr string) map[string]bool {
+ scopes := make(map[string]bool)
+ for s := range strings.SplitSeq(scopeStr, ",") {
+ if s != "" {
+ scopes[s] = true
+ }
+ }
+ return scopes
+ }
+ parser := jwt.Parser{}
+ publisherClaims := jwt.MapClaims{}
+ token, _, err := parser.ParseUnverified(tokenStr, publisherClaims)
+ if err != nil {
+ return "", 0, err
+ }
+
+ claims, ok := token.Claims.(jwt.MapClaims)
+ if !ok {
+ return "", 0, serrors.New("invalid claims")
+ }
+
+ user, ok := claims["sub"].(string)
+ if !ok || user == "" {
+ return "", 0, serrors.New("missing subject")
+ }
+ scopeStr, ok := claims["scope"].(string)
+ if !ok || scopeStr == "" {
+ return "", 0, serrors.New("missing scope")
+ }
+ scopes := parseScopes(scopeStr)
+ if scopes[registration.ScopeUser] {
+ return user, User, nil
+ } else if scopes[registration.ScopeAssetPublisher] {
+ return user, Publisher, nil
+ } else if scopes[registration.ScopeRedemptionService] {
+ return user, RedemptionService, nil
+ } else {
+ return "", 0, serrors.New("unsupported JWT")
+ }
+}
+
+func parseAddr(s string) (addr.Addr, uint16, string, error) {
+ host, port, err := net.SplitHostPort(s)
+ if err != nil {
+ return addr.Addr{}, 0, "", serrors.Wrap("invalid address: split host:port", err, "addr", s)
+ }
+ splits := strings.Split(host, ",")
+ if len(splits) != 2 {
+ return addr.Addr{}, 0, "", serrors.Wrap("invalid address: split host:port", err, "addr", s)
+ }
+ _, err = netip.ParseAddr(splits[1])
+ if err != nil {
+ ipAddr, err := net.ResolveIPAddr("ip", splits[1])
+ if err != nil {
+ return addr.Addr{}, 0, "", serrors.Wrap("invalid address: split host:port", err, "addr", s)
+ }
+ s = fmt.Sprintf("[%s,%s]:%s", splits[0], ipAddr.String(), port)
+ }
+ fmt.Println(s)
+ a, p, err := addr.ParseAddrPort(s)
+ return a, p, splits[1], err
+}
+
+func userInteraction() {
+ ctx := context.Background()
+ reader := bufio.NewReader(os.Stdin)
+ url := readString(reader, "marketplace_api (leave empty to start discovery): ")
+ var err error
+ if url == "" {
+ if sciond == "" {
+ fmt.Println("marketplace discovery is only enabled when providing a SCION daemon address")
+ return
+ }
+
+ marketplace, err := discoverMarketplaces(ctx, reader)
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ url = marketplace.Api
+ }
+ token := ""
+ if !as_registration {
+ token = readString(reader, "jwt_token: ")
+ }
+ var marketplaceClient hummingbirdconnect.MarketplaceServiceClient
+ var redemptionClient hummingbirdconnect.RedemptionServiceClient
+ var accountClient hummingbirdconnect.AccountServiceClient
+ urlSplit := strings.Split(url, "://")
+ var httpHost string
+ api := ""
+ if len(urlSplit) == 1 {
+ api = urlSplit[0]
+ } else if len(urlSplit) == 2 {
+ api = urlSplit[1]
+ httpHost = api
+ } else {
+ fmt.Println("invalid url", url)
+ return
+ }
+ var localIA addr.IA
+ if as_registration {
+ localIAString := readString(reader, "local IA: ")
+ localIA, err = addr.ParseIA(localIAString)
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ }
+ scionAddr, port, serverName, err := parseAddr(api)
+ if err == nil {
+ remote := &snet.UDPAddr{
+ IA: scionAddr.IA,
+ Host: net.UDPAddrFromAddrPort(netip.AddrPortFrom(scionAddr.Host.IP(), port)),
+ }
+ baseUrlSplit := strings.Split(libconnect.BaseUrl(remote), "https://")
+ if len(baseUrlSplit) != 2 {
+ fmt.Println("base Url is invalid", baseUrlSplit)
+ return
+ }
+ httpHost = baseUrlSplit[1]
+ if sciond == "" {
+ fmt.Println("connecting over SCION requires a SCION daemon address")
+ return
+ }
+ marketplaceClient, redemptionClient, accountClient, err = withSCION(ctx, localIA, remote, serverName, token)
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ fmt.Println("use SCION connection")
+ } else {
+ if insecure {
+ marketplaceClient = hummingbirdconnect.NewMarketplaceServiceClient(&http.Client{
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: true,
+ },
+ },
+ }, url, connect.WithInterceptors(authInterceptor(token)))
+ redemptionClient = hummingbirdconnect.NewRedemptionServiceClient(&http.Client{
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: true,
+ },
+ },
+ }, url, connect.WithInterceptors(authInterceptor(token)))
+ accountClient = hummingbirdconnect.NewAccountServiceClient(&http.Client{
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: true,
+ },
+ },
+ }, url, connect.WithInterceptors(authInterceptor(token)))
+ } else {
+ marketplaceClient = hummingbirdconnect.NewMarketplaceServiceClient(http.DefaultClient,
+ url, connect.WithInterceptors(authInterceptor(token)))
+ redemptionClient = hummingbirdconnect.NewRedemptionServiceClient(http.DefaultClient,
+ url, connect.WithInterceptors(authInterceptor(token)))
+ accountClient = hummingbirdconnect.NewAccountServiceClient(http.DefaultClient,
+ url, connect.WithInterceptors(authInterceptor(token)))
+ }
+
+ fmt.Println("use TCP connection")
+ }
+ if as_registration {
+ trcDir := readString(reader, "trc directory: ")
+ certDir := readString(reader, "certificate directory: ")
+ keyRingDir := readString(reader, "keyring directory: ")
+ fmt.Println("httpHost", httpHost)
+ regClient := registration.NewClient(accountClient, httpHost)
+ publisherToken, redemptionToken, err := regClient.RegisterWithNewSigner(ctx, localIA, trcDir, certDir, keyRingDir)
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ fmt.Printf("Publisher Token: %s\nRedemptionService Token: %s\n", publisherToken, redemptionToken)
+ return
+ }
+ username, t, err := tokenType(token)
+ if err == nil {
+ fmt.Printf("Logged in as %s\n", username)
+ }
+
+ for {
+ printOptions(t)
+ option, err := reader.ReadString('\n')
+ option = strings.TrimSpace(option)
+ if err != nil {
+ fmt.Println(err)
+ continue
+ }
+ switch {
+ case option == "info":
+ handleInfo(ctx, marketplaceClient)
+ case option == "search":
+ handleSearch(ctx, reader, marketplaceClient)
+ case option == "buy":
+ handleBuy(ctx, reader, marketplaceClient)
+ case option == "split":
+ handleSplit(ctx, reader, marketplaceClient)
+ case option == "combine":
+ handleCombine(ctx, reader, marketplaceClient)
+ case option == "redeem":
+ handleRedeem(ctx, reader, marketplaceClient)
+ case option == "reservation":
+ handleReservation(ctx, reader, marketplaceClient)
+ case option == "publish":
+ handlePublish(ctx, reader, marketplaceClient)
+ case option == "statistics":
+ handleStatistics(ctx, reader, marketplaceClient)
+ case option == "delegate":
+ handleDelegate(ctx, reader, redemptionClient)
+ case option == "update":
+ handleUpdate(ctx, reader, marketplaceClient)
+ case option == "password":
+ handlePassword(ctx, reader, accountClient)
+ case option == "reset":
+ if success := handleResetJwt(ctx, reader, accountClient, t); success {
+ return
+ }
+ case option == "exit":
+ return
+ }
+ fmt.Println("----------")
+ }
+}
+
+func handlePassword(ctx context.Context, reader *bufio.Reader, c hummingbirdconnect.AccountServiceClient) {
+ fmt.Println("Handle set authentication token query")
+ password := readString(reader, "password: ")
+ _, err := c.SetPassword(ctx, &connect.Request[hummingbird.SetPasswordRequest]{
+ Msg: &hummingbird.SetPasswordRequest{
+ Password: password,
+ },
+ })
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ fmt.Println("Authentication Token updated!")
+}
+
+func handleResetJwt(ctx context.Context, reader *bufio.Reader, c hummingbirdconnect.AccountServiceClient, t jwtType) bool {
+ fmt.Println("Handle jwt reset query")
+ if t == Publisher || t == RedemptionService {
+ fmt.Println("Warning! Reseting the Token will also terminate the connection between the marketplace and the redemption service!")
+ }
+ if !readConfirm(reader) {
+ return false
+ }
+ _, err := c.ResetJWT(ctx, &connect.Request[hummingbird.JWTResetRequest]{})
+ if err != nil {
+ fmt.Println(err)
+ return false
+ }
+ fmt.Println("token reseted")
+ return true
+}
+
+func handleDelegate(ctx context.Context, reader *bufio.Reader, c hummingbirdconnect.RedemptionServiceClient) {
+ fmt.Println("Handle redemption delegation query")
+ expTime := readTime(reader, "Redemption until (2026-06-23T13:25:36Z): ")
+ idUpperBound := uint32(readUint64(reader, "Reservation ID upper bound: "))
+ hexStr := readString(reader, "Key in hexadecimal (a1b2c3): ")
+ key, err := hex.DecodeString(hexStr)
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ if len(key) != 16 {
+ fmt.Println("key has invalid length", "expected", 16, "got", len(key))
+ return
+ }
+ pathToEncodings := readString(reader, "path to encodings file(csv): ")
+ f, err := os.Open(pathToEncodings)
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ defer f.Close()
+
+ r := csv.NewReader(f)
+
+ record, err := r.Read()
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ encodings := make([]uint32, len(record))
+
+ for i, s := range record {
+ v, err := strconv.ParseUint(s, 10, 32)
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ encodings[i] = uint32(v)
+ }
+ if !readConfirm(reader) {
+ return
+ }
+
+ resp, err := c.DelegateRedemption(ctx, &connect.Request[hummingbird.DelegateRedemptionRequest]{
+ Msg: &hummingbird.DelegateRedemptionRequest{
+ ExpirationTime: timestamppb.New(expTime),
+ ReservationIdUpperBound: idUpperBound,
+ Key: key,
+ EncodingPoints: encodings,
+ },
+ })
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ fmt.Println("Delegated until:", resp.Msg.ExpirationTime.AsTime())
+}
+func handlePublish(ctx context.Context, reader *bufio.Reader, c hummingbirdconnect.MarketplaceServiceClient) {
+ fmt.Println("Handle publish asset query.")
+ asset := &hummingbird.PublisherAsset{
+ IfIdIngress: readOptionalUint32(reader, "ingress: "),
+ IfIdEgress: readOptionalUint32(reader, "egress: "),
+ Bandwidth: uint32(readUint64(reader, "bandwidth: ")),
+ BandwidthMin: uint32(readUint64(reader, "minimum bandwidth: ")),
+ BandwidthMax: uint32(readUint64(reader, "maximum bandwidth: ")),
+ StartsAt: timestamppb.New(readTime(reader, "Starts at (2026-06-23T13:25:36Z): ")),
+ StopsAt: timestamppb.New(readTime(reader, "Stops at (2026-06-23T13:25:36Z): ")),
+ Price: uint32(readUint64(reader, "price per kbit per second: ")),
+ TimeMinDuration: uint32(readUint64(reader, "minimum time duration: ")),
+ TimeGranularity: uint32(readUint64(reader, "time granularity: ")),
+ }
+ if !readConfirm(reader) {
+ return
+ }
+ resp, err := c.PublishAsset(ctx, &connect.Request[hummingbird.PublishAssetRequest]{
+ Msg: &hummingbird.PublishAssetRequest{
+ Asset: asset,
+ },
+ })
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ fmt.Printf("Published asset ID %s\n", resp.Msg.AssetId)
+}
+
+func ceilDuration(base time.Duration, multiple time.Duration) time.Duration {
+ truncated := base.Truncate(multiple)
+ if truncated == base {
+ return base
+ }
+ return truncated + multiple
+}
+func ceilTime(base time.Time, multiple time.Duration) time.Time {
+ truncated := base.Truncate(multiple)
+ if truncated.Equal(base) {
+ return base
+ }
+ return truncated.Add(multiple)
+}
+
+func printUpdateAssetOptions() {
+ fmt.Println("-> add")
+ fmt.Println("-> remove")
+ fmt.Println("-> list")
+ fmt.Println("-> submit")
+ fmt.Println("-> cancel")
+}
+
+func handleUpdate(ctx context.Context, reader *bufio.Reader, c hummingbirdconnect.MarketplaceServiceClient) {
+ fmt.Println("Handle update assets query")
+ assetUpdates := make([]*hummingbird.AssetUpdate, 0, 1)
+ for {
+ fmt.Printf("Currently %d assets in update query.\n", len(assetUpdates))
+ printUpdateAssetOptions()
+ option, err := reader.ReadString('\n')
+ option = strings.TrimSpace(option)
+ if err != nil {
+ fmt.Println(err)
+ continue
+ }
+ switch {
+ case option == "add":
+ fmt.Println("-> delete")
+ fmt.Println("-> update")
+ subOption, err := reader.ReadString('\n')
+ subOption = strings.TrimSpace(subOption)
+ if err != nil {
+ fmt.Println(err)
+ continue
+ }
+ switch {
+ case subOption == "delete":
+ assetUpdates = append(assetUpdates, &hummingbird.AssetUpdate{
+ AssetId: readString(reader, "AssetID: "),
+ Operation: &hummingbird.AssetUpdate_Remove{},
+ })
+ case subOption == "update":
+ assetUpdates = append(assetUpdates, &hummingbird.AssetUpdate{
+ AssetId: readString(reader, "AssetID: "),
+ Operation: &hummingbird.AssetUpdate_Update{
+ Update: &hummingbird.PublisherAsset{IfIdIngress: readOptionalUint32(reader, "ingress: "),
+ IfIdEgress: readOptionalUint32(reader, "egress: "),
+ Bandwidth: uint32(readUint64(reader, "bandwidth: ")),
+ BandwidthMin: uint32(readUint64(reader, "minimum bandwidth: ")),
+ BandwidthMax: uint32(readUint64(reader, "maximum bandwidth: ")),
+ StartsAt: timestamppb.New(readTime(reader, "Starts at (2026-06-23T13:25:36Z): ")),
+ StopsAt: timestamppb.New(readTime(reader, "Stops at (2026-06-23T13:25:36Z): ")),
+ Price: uint32(readUint64(reader, "price per kbit per second: ")),
+ TimeMinDuration: uint32(readUint64(reader, "minimum time duration: ")),
+ TimeGranularity: uint32(readUint64(reader, "time granularity: "))},
+ },
+ })
+ default:
+ break
+ }
+ case option == "remove":
+ index := readUint64(reader, "List index to remove: ")
+ if int(index) >= len(assetUpdates) {
+ fmt.Println("index invalid")
+ break
+ }
+ assetUpdates = append(assetUpdates[:index], assetUpdates[index+1:]...)
+ case option == "list":
+ for index, asset := range assetUpdates {
+ jsonAsset, _ := json.Marshal(asset)
+ fmt.Printf("%d:%s\n", index, string(jsonAsset))
+ }
+ case option == "submit":
+ if !readConfirm(reader) {
+ continue
+ }
+ rep, err := c.UpdateAssets(ctx, &connect.Request[hummingbird.UpdateAssetsRequest]{
+ Msg: &hummingbird.UpdateAssetsRequest{
+ Assets: assetUpdates,
+ },
+ })
+ if err != nil {
+ fmt.Println(err)
+ continue
+ }
+ for i, res := range rep.Msg.Result {
+ if i >= len(assetUpdates) {
+ break
+ }
+ switch t := res.ResultType.(type) {
+ case *hummingbird.UpdateAssetResult_NewId:
+ if t.NewId == "" {
+ fmt.Printf("%s: deleted\n", assetUpdates[i].AssetId)
+ } else {
+ fmt.Printf("%s: updated to -> %s\n", assetUpdates[i].AssetId, t.NewId)
+ }
+ case *hummingbird.UpdateAssetResult_Error:
+ fmt.Printf("%s: error: %s\n", assetUpdates[i].AssetId, t.Error)
+ default:
+ fmt.Printf("%s: unkown result", assetUpdates[i].AssetId)
+ }
+ }
+ return
+ case option == "cancel":
+ return
+ }
+ }
+}
+
+func handleStatistics(ctx context.Context, reader *bufio.Reader, c hummingbirdconnect.MarketplaceServiceClient) {
+ fmt.Println("Handle statistics query")
+ infoRep, err := c.Info(ctx, &connect.Request[hummingbird.MarketplaceInfoRequest]{})
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ startsAt := readTime(reader, "Starts at (2026-06-23T13:25:36Z): ").UTC().Truncate(time.Duration(infoRep.Msg.MaxStatisticsGranularity) * time.Second)
+ stopsAt := ceilTime(readTime(reader, "Stops at (2026-06-23T13:25:36Z): ").UTC(), time.Duration(infoRep.Msg.MaxStatisticsGranularity)*time.Second)
+ stepSize := readUint64(reader, "Step size: ")
+ ingress := readOptionalUint32(reader, "Ingress: ")
+ egress := readOptionalUint32(reader, "Egress: ")
+ if !readConfirm(reader) {
+ return
+ }
+ resp, err := c.Statistics(ctx, &connect.Request[hummingbird.StatisticsRequest]{
+ Msg: &hummingbird.StatisticsRequest{
+ Start: timestamppb.New(startsAt),
+ End: timestamppb.New(stopsAt),
+ Step: uint32(stepSize),
+ IfIdIngress: ingress,
+ IfIdEgress: egress,
+ },
+ })
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ step := ceilDuration(time.Duration(stepSize)*time.Second, time.Duration(infoRep.Msg.MaxStatisticsGranularity)*time.Second)
+ for i, stat := range resp.Msg.Statistics {
+ intervalStart := startsAt.Add(time.Duration(i) * step)
+ intervalEnd := intervalStart.Add(step)
+ fmt.Printf("[%s - %s], Revenue: %d, Utilization %f\n", intervalStart.Format(time.RFC3339),
+ intervalEnd.Format(time.RFC3339), stat.Revenue, stat.BandwidthUtilization)
+ }
+}
+func handleSplit(ctx context.Context, reader *bufio.Reader, c hummingbirdconnect.MarketplaceServiceClient) {
+ fmt.Println("Handle split asset query.")
+ assetID := readString(reader, "assetID: ")
+ splitOption := readOptionalString(reader, "spit using axis [bw,time]: ", nil)
+ if splitOption == nil {
+ fmt.Println("invalid split option.")
+ return
+ }
+ req := &hummingbird.SplitAssetRequest{
+ AssetId: assetID,
+ }
+ switch *splitOption {
+ case "bw":
+ bwSplit := readUint64(reader, "bw split: ")
+ req.SplitOption = &hummingbird.SplitAssetRequest_BwSplit{
+ BwSplit: uint32(bwSplit),
+ }
+ case "time":
+ timeSplit := readTime(reader, "time split (2026-06-23T13:25:36Z): ")
+ req.SplitOption = &hummingbird.SplitAssetRequest_TimeSplit{
+ TimeSplit: timestamppb.New(timeSplit),
+ }
+ default:
+ fmt.Println("invalid split option.")
+ return
+ }
+ if !readConfirm(reader) {
+ return
+ }
+ resp, err := c.SplitAsset(ctx, &connect.Request[hummingbird.SplitAssetRequest]{
+ Msg: req,
+ })
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ fmt.Printf("asset split into %s and %s\n", resp.Msg.AssetId_1, resp.Msg.AssetId_2)
+}
+func handleCombine(ctx context.Context, reader *bufio.Reader, c hummingbirdconnect.MarketplaceServiceClient) {
+ fmt.Println("Handle combine assets query.")
+ asset1 := readString(reader, "asset 1: ")
+ asset2 := readString(reader, "asset 2: ")
+ if !readConfirm(reader) {
+ return
+ }
+ resp, err := c.CombineAssets(ctx, &connect.Request[hummingbird.CombineAssetRequest]{
+ Msg: &hummingbird.CombineAssetRequest{
+ AssetId_1: asset1,
+ AssetId_2: asset2,
+ },
+ })
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ fmt.Printf("assets combined into %s\n", resp.Msg.AssetId)
+}
+func handleReservation(ctx context.Context, reader *bufio.Reader, c hummingbirdconnect.MarketplaceServiceClient) {
+ var ia *uint64
+ var ingress *uint32
+ var egress *uint32
+ var bw *uint32
+ var startsAt *time.Time
+ var stopsAt *time.Time
+ fmt.Println("Handle fetch reservation query. Filters are ignored if empty.")
+ ia = readOptionalIAUint64(reader, "IA: ")
+ ingress = readOptionalUint32(reader, "Ingress: ")
+ egress = readOptionalUint32(reader, "Egress: ")
+ bw = readOptionalUint32(reader, "BW: ")
+ startsAt = readOptionalTime(reader, "Starts At (2006-01-02T15:04:05Z): ")
+ stopsAt = readOptionalTime(reader, "Stops At (2006-01-02T15:04:05Z): ")
+
+ req := &hummingbird.FetchReservationsRequest{
+ Ia: ia,
+ IngressId: ingress,
+ EgressId: egress,
+ Bandwidth: bw,
+ }
+ if startsAt != nil {
+ req.StartsAt = timestamppb.New(*startsAt)
+ }
+ if stopsAt != nil {
+ req.StopsAt = timestamppb.New(*stopsAt)
+ }
+ rep, err := c.FetchReservations(ctx, &connect.Request[hummingbird.FetchReservationsRequest]{
+ Msg: req,
+ })
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ type Reservation struct {
+ ResId uint32
+ Ia addr.IA
+ IngressId uint32
+ EgressId uint32
+ Bw uint32
+ EncodedBw uint16
+ StartsAt time.Time
+ StopsAt time.Time
+ Ak []byte
+ }
+ transformed := make([]*Reservation, 0, len(rep.Msg.Reservations))
+ for _, res := range rep.Msg.Reservations {
+ transformed = append(transformed, &Reservation{
+ ResId: res.ReservationId,
+ Ia: addr.IA(res.Ia),
+ IngressId: res.IngressId,
+ EgressId: res.EgressId,
+ Bw: res.Bandwidth,
+ EncodedBw: uint16(res.DataplaneEncoding),
+ StartsAt: res.StartsAt.AsTime(),
+ StopsAt: res.StopsAt.AsTime(),
+ Ak: res.AuthenticationKey,
+ })
+ }
+ j, err := json.MarshalIndent(transformed, "", "\t")
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ fmt.Println(string(j))
+}
+
+func handleRedeem(ctx context.Context, reader *bufio.Reader, c hummingbirdconnect.MarketplaceServiceClient) {
+ fmt.Println("Redeem Query.")
+ var err error
+ option := readOptionalUint32(reader, "Select option:\n0: ingress and egress assets\n1: interface-pair asset:\n")
+ var rep *connect.Response[hummingbird.RedeemAssetResponse]
+ if option == nil {
+ return
+ } else if *option == 0 {
+ ingressAssetID := readString(reader, "Ingress Asset ID: ")
+ egressAssetID := readString(reader, "Egress Asset ID: ")
+ if !readConfirm(reader) {
+ return
+ }
+ rep, err = c.RedeemAsset(ctx, &connect.Request[hummingbird.RedeemAssetRequest]{
+ Msg: &hummingbird.RedeemAssetRequest{
+ Interfaces: &hummingbird.RedeemAssetRequest_Pair{
+ Pair: &hummingbird.IngressEgressPair{
+ IngressAssetId: ingressAssetID,
+ EgressAssetId: egressAssetID,
+ },
+ },
+ },
+ })
+ } else if *option == 1 {
+ assetID := readString(reader, "Interface-pair Asset ID: ")
+ if !readConfirm(reader) {
+ return
+ }
+ rep, err = c.RedeemAsset(ctx, &connect.Request[hummingbird.RedeemAssetRequest]{
+ Msg: &hummingbird.RedeemAssetRequest{
+ Interfaces: &hummingbird.RedeemAssetRequest_IfPairAssetId{
+ IfPairAssetId: assetID,
+ },
+ },
+ })
+ } else {
+ return
+ }
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ fmt.Println("Redemption Result:")
+ fmt.Printf("ResID: %d\nAk: %s\nBw: %d\nEncoding: %d\n", rep.Msg.ReservationId, rep.Msg.AuthenticationKey, rep.Msg.BandwidthRounded, rep.Msg.BwDataplaneEncoding)
+}
+func printBuyOptions() {
+ fmt.Println("-> add")
+ fmt.Println("-> remove")
+ fmt.Println("-> list")
+ fmt.Println("-> submit")
+ fmt.Println("-> cancel")
+}
+func handleBuy(ctx context.Context, reader *bufio.Reader, c hummingbirdconnect.MarketplaceServiceClient) {
+ fmt.Println("Buy Query.")
+ buyAssets := make([]*hummingbird.BuyAsset, 0, 1)
+ for {
+ fmt.Printf("Currently %d assets in the shopping cart.\n", len(buyAssets))
+ printBuyOptions()
+ option, err := reader.ReadString('\n')
+ option = strings.TrimSpace(option)
+ if err != nil {
+ fmt.Println(err)
+ continue
+ }
+ switch {
+ case option == "add":
+ buyAsset := &hummingbird.BuyAsset{
+ AssetId: readString(reader, "AssetID: "),
+ StartsAtExactly: timestamppb.New(readTime(reader, "Starts at exactly (2026-06-23T13:25:36Z): ")),
+ StopsAtExactly: timestamppb.New(readTime(reader, "Stops at exactly (2026-06-23T13:25:36Z): ")),
+ BandwidthExact: uint32(readUint64(reader, "BW exact: ")),
+ }
+ buyAssets = append(buyAssets, buyAsset)
+ case option == "remove":
+ index := readUint64(reader, "List index to remove: ")
+ if int(index) >= len(buyAssets) {
+ fmt.Println("index invalid")
+ break
+ }
+ buyAssets = append(buyAssets[:index], buyAssets[index+1:]...)
+ case option == "list":
+ for index, asset := range buyAssets {
+ jsonAsset, _ := json.Marshal(asset)
+ fmt.Printf("%d:%s\n", index, string(jsonAsset))
+ }
+ case option == "submit":
+ maxPrice := readUint64(reader, "Max Price: ")
+ if !readConfirm(reader) {
+ continue
+ }
+ rep, err := c.BuyAssets(ctx, &connect.Request[hummingbird.BuyAssetsRequest]{
+ Msg: &hummingbird.BuyAssetsRequest{
+ Assets: buyAssets,
+ MaxPrice: maxPrice,
+ },
+ })
+ if err != nil {
+ fmt.Println(err)
+ continue
+ }
+ fmt.Printf("Bought assets for a total cost of %d\n", rep.Msg.Cost)
+ fmt.Print("[")
+ for _, boughtAsset := range rep.Msg.Assets {
+ fmt.Printf("%s,", boughtAsset.AssetId)
+ }
+ fmt.Print("]\n")
+ return
+ case option == "cancel":
+ return
+ }
+ }
+}
+
+func handleSearch(ctx context.Context, reader *bufio.Reader, c hummingbirdconnect.MarketplaceServiceClient) {
+ var owned *bool
+ var ia *uint64
+ var ingress *uint32
+ var egress *uint32
+ var minReqBw *uint32
+ var price *uint32
+ var startsAtLatest *time.Time
+ var stopsAtEarliest *time.Time
+ fmt.Println("Search Query. Owned is mandatory, other filters are ignored if empty.")
+
+ owned = readOptionalBool(reader, "Owned (true,false): ")
+ ia = readOptionalIAUint64(reader, "IA: ")
+ ingress = readOptionalUint32(reader, "Ingress: ")
+ egress = readOptionalUint32(reader, "Egress: ")
+ minReqBw = readOptionalUint32(reader, "Min Required BW: ")
+ price = readOptionalUint32(reader, "Price: ")
+ startsAtLatest = readOptionalTime(reader, "Starts At Latest (2006-01-02T15:04:05Z): ")
+ stopsAtEarliest = readOptionalTime(reader, "Stops At Earliest (2006-01-02T15:04:05Z): ")
+ if owned == nil {
+ tmp := false
+ owned = &tmp
+ }
+ msg := &hummingbird.SearchAssetsRequest{
+ Owned: *owned,
+ Ia: ia,
+ IfIdIngress: ingress,
+ IfIdEgress: egress,
+ MinRequiredBw: minReqBw,
+ Price: price,
+ }
+ if startsAtLatest != nil {
+ msg.StartsAtLatest = timestamppb.New(*startsAtLatest)
+ }
+ if stopsAtEarliest != nil {
+ msg.StopsAtEarliest = timestamppb.New(*stopsAtEarliest)
+ }
+ rep, err := c.SearchAssets(ctx, &connect.Request[hummingbird.SearchAssetsRequest]{
+ Msg: msg,
+ })
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ transformed := make([]Asset, 0, len(rep.Msg.Assets))
+ for _, asset := range rep.Msg.Assets {
+ transformed = append(transformed, Asset{
+ ID: asset.AssetId,
+ IA: addr.IA(asset.Ia),
+ Bandwidth: asset.Bandwidth,
+ BandwidthMin: asset.BandwidthMin,
+ BandwidthMax: asset.BandwidthMax,
+ TimeMinDuration: asset.TimeMinDuration,
+ StartAt: asset.StartsAt.AsTime(),
+ StopsAt: asset.StopsAt.AsTime(),
+ Price: asset.Price,
+ IfIdIngress: asset.IfIdIngress,
+ IfIdEgress: asset.IfIdEgress,
+ TimeGranularity: asset.TimeGranularity,
+ })
+ }
+ j, err := json.MarshalIndent(transformed, "", "\t")
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ fmt.Println(string(j))
+}
+
+func handleInfo(ctx context.Context, c hummingbirdconnect.MarketplaceServiceClient) {
+ info, err := c.Info(ctx, &connect.Request[hummingbird.MarketplaceInfoRequest]{})
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ transform := func(base uint64) string {
+ return fmt.Sprintf("%g%s", float64(base)*math.Pow10(-int(info.Msg.CurrencyExponent)), info.Msg.Currency)
+ }
+ fmt.Printf("Version: %d.%d\nCurrency: %g %s\nPricing strategy: %s\n",
+ info.Msg.ApiMajorVersion, info.Msg.ApiMinorVersion, math.Pow10(-int(info.Msg.CurrencyExponent)),
+ info.Msg.Currency, info.Msg.PricingStrategy)
+ fmt.Printf("Transaction fees: %g%% + %s\nSplit or combine assets: %s\n",
+ 100*info.Msg.TransactionFeeRelative, transform(info.Msg.TransactionFeeAbsolute), transform(info.Msg.SplitCombineFeeAbsolute))
+ if info.Msg.SupportsRedemptionDelegation {
+ fmt.Printf("Redemption delegation hourly fee: %s\n", transform(info.Msg.DelegationHourlyFee))
+ }
+ fmt.Printf("Statistics granularity: %s\n", (time.Duration(info.Msg.MaxStatisticsGranularity) * time.Second).String())
+}
+
+func readConfirm(reader *bufio.Reader) bool {
+ fmt.Print("Confirm [yes,no]: ")
+ text, _ := reader.ReadString('\n')
+ text = strings.TrimSpace(text)
+ text = strings.ToLower(text)
+ if text == "yes" {
+ return true
+ }
+ return false
+}
+
+func readUint64(reader *bufio.Reader, prompt string) uint64 {
+ fmt.Print(prompt)
+
+ text, _ := reader.ReadString('\n')
+ text = strings.TrimSpace(text)
+
+ v, err := strconv.ParseUint(text, 10, 64)
+ if err != nil {
+ fmt.Println("Invalid uint64")
+ return 0
+ }
+
+ return v
+}
+
+func readOptionalIAUint64(reader *bufio.Reader, prompt string) *uint64 {
+ fmt.Print(prompt)
+
+ text, _ := reader.ReadString('\n')
+ text = strings.TrimSpace(text)
+
+ if text == "" {
+ return nil
+ }
+ ia, err := addr.ParseIA(text)
+ if err != nil {
+ fmt.Println(err)
+ return nil
+ }
+ v := uint64(ia)
+
+ return &v
+}
+
+func readString(reader *bufio.Reader, prompt string) string {
+ fmt.Print(prompt)
+
+ text, _ := reader.ReadString('\n')
+ text = strings.TrimSpace(text)
+
+ return text
+}
+
+func readOptionalString(reader *bufio.Reader, prompt string, defaultStr *string) *string {
+ fmt.Print(prompt)
+
+ text, _ := reader.ReadString('\n')
+ text = strings.TrimSpace(text)
+
+ if text == "" {
+ return defaultStr
+ }
+
+ return &text
+}
+
+func readOptionalUint32(reader *bufio.Reader, prompt string) *uint32 {
+ fmt.Print(prompt)
+
+ text, _ := reader.ReadString('\n')
+ text = strings.TrimSpace(text)
+
+ if text == "" {
+ return nil
+ }
+
+ v64, err := strconv.ParseUint(text, 10, 32)
+ if err != nil {
+ fmt.Println("Invalid uint32")
+ return nil
+ }
+
+ v := uint32(v64)
+ return &v
+}
+
+func readOptionalBool(reader *bufio.Reader, prompt string) *bool {
+ fmt.Print(prompt)
+
+ text, _ := reader.ReadString('\n')
+ text = strings.TrimSpace(text)
+
+ if text == "" {
+ return nil
+ }
+
+ v, err := strconv.ParseBool(text)
+ if err != nil {
+ fmt.Println("Invalid bool")
+ return nil
+ }
+
+ return &v
+}
+
+func readTime(reader *bufio.Reader, prompt string) time.Time {
+ fmt.Print(prompt)
+
+ text, _ := reader.ReadString('\n')
+ text = strings.TrimSpace(text)
+
+ if text == "" {
+ return time.Time{}
+ }
+
+ // Example format: 2026-05-20T15:04:05
+ t, err := time.Parse(time.RFC3339, text)
+ if err != nil {
+ fmt.Println("Invalid time format")
+ return time.Time{}
+ }
+
+ return t
+}
+
+func readOptionalTime(reader *bufio.Reader, prompt string) *time.Time {
+ fmt.Print(prompt)
+
+ text, _ := reader.ReadString('\n')
+ text = strings.TrimSpace(text)
+
+ if text == "" {
+ return nil
+ }
+
+ // Example format: 2026-05-20T15:04:05
+ t, err := time.Parse(time.RFC3339, text)
+ if err != nil {
+ fmt.Println("Invalid time format")
+ return nil
+ }
+
+ return &t
+}
+
+var insecure bool
+var as_registration bool
+var sciond string
+
+func main() {
+ flag.BoolVar(&insecure, "insecure", false, "indicates whether TLS insecure skip verify should be applied")
+ flag.BoolVar(&as_registration, "register", false, "start AS registration")
+ flag.StringVar(&sciond, "sciond", "", "address of the SCION daemon")
+ flag.Parse()
+ userInteraction()
+}
+
+type Asset struct {
+ ID string
+ IA addr.IA
+ Bandwidth uint32
+ BandwidthMin uint32
+ BandwidthMax uint32
+ StartAt time.Time
+ StopsAt time.Time
+ Price uint32
+ TimeGranularity uint32
+ TimeMinDuration uint32
+ IfIdIngress *uint32
+ IfIdEgress *uint32
+}
diff --git a/pkg/hummingbird/marketplace/BUILD.bazel b/pkg/hummingbird/marketplace/BUILD.bazel
new file mode 100644
index 0000000000..bd1fc94e18
--- /dev/null
+++ b/pkg/hummingbird/marketplace/BUILD.bazel
@@ -0,0 +1,37 @@
+load("@rules_go//go:def.bzl", "go_library")
+load("//tools:go.bzl", "go_test")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["client.go"],
+ importpath = "github.com/scionproto/scion/pkg/hummingbird/marketplace",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//pkg/addr:go_default_library",
+ "//pkg/connect:go_default_library",
+ "//pkg/log:go_default_library",
+ "//pkg/private/serrors:go_default_library",
+ "//pkg/proto/hummingbird:go_default_library",
+ "//pkg/snet:go_default_library",
+ "//pkg/snet/path:go_default_library",
+ "//pkg/snet/squic:go_default_library",
+ "//private/app/appnet:go_default_library",
+ "//private/storage:go_default_library",
+ "//private/trust:go_default_library",
+ "@com_connectrpc_connect//:go_default_library",
+ "@com_github_quic_go_quic_go//http3:go_default_library",
+ "@org_golang_google_protobuf//types/known/timestamppb:go_default_library",
+ "@com_github_scionproto_scion//pkg/proto/hummingbird/v1/hummingbirdconnect:go_default_library",
+ ],
+)
+
+go_test(
+ name = "go_default_test",
+ srcs = ["client_test.go"],
+ embed = [":go_default_library"],
+ deps = [
+ "//pkg/proto/hummingbird:go_default_library",
+ "@com_github_stretchr_testify//assert:go_default_library",
+ "@org_golang_google_protobuf//types/known/timestamppb:go_default_library",
+ ],
+)
diff --git a/pkg/hummingbird/marketplace/client.go b/pkg/hummingbird/marketplace/client.go
new file mode 100644
index 0000000000..ea4735d6e7
--- /dev/null
+++ b/pkg/hummingbird/marketplace/client.go
@@ -0,0 +1,1045 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package marketplace
+
+import (
+ "context"
+ "crypto/tls"
+ "fmt"
+ "math"
+ "net"
+ "net/http"
+ "net/netip"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "connectrpc.com/connect"
+ "github.com/quic-go/quic-go/http3"
+ "github.com/scionproto/scion/pkg/addr"
+ libconnect "github.com/scionproto/scion/pkg/connect"
+ "github.com/scionproto/scion/pkg/log"
+ "github.com/scionproto/scion/pkg/private/serrors"
+ "github.com/scionproto/scion/pkg/proto/hummingbird"
+ "github.com/scionproto/scion/pkg/proto/hummingbird/v1/hummingbirdconnect"
+ "github.com/scionproto/scion/pkg/snet"
+ "github.com/scionproto/scion/pkg/snet/path"
+ snetpath "github.com/scionproto/scion/pkg/snet/path"
+ "github.com/scionproto/scion/pkg/snet/squic"
+ "github.com/scionproto/scion/private/app/appnet"
+ "github.com/scionproto/scion/private/storage"
+ "github.com/scionproto/scion/private/trust"
+ "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+type MarketplaceClient struct {
+ token string
+ topo snet.Topology
+ client hummingbirdconnect.MarketplaceServiceClient
+}
+
+func (c *MarketplaceClient) withSCION(ctx context.Context, querier snet.PathQuerier, remote *snet.UDPAddr, serverName string, insecure bool) (
+ hummingbirdconnect.MarketplaceServiceClient, error) {
+
+ trustDB, err := storage.NewInMemoryTrustStorage()
+ if err != nil {
+ return nil, err
+ }
+ var localPublic *net.UDPAddr
+ var dp snet.DataplanePath
+ var nextHop *net.UDPAddr
+
+ if remote.IA == c.topo.LocalIA {
+ // marketplace is inside local AS
+ dp = path.Empty{}
+ nextHop = remote.Host
+ } else {
+ paths, err := querier.Query(ctx, remote.IA)
+ if err != nil {
+ return nil, err
+ }
+ if len(paths) == 0 {
+ return nil, serrors.New("no paths found to marketplace")
+ }
+ dp = paths[0].Dataplane()
+ nextHop = paths[0].UnderlayNextHop()
+ }
+ remote.Path = dp
+ remote.NextHop = nextHop
+
+ conn, err := net.Dial("udp", nextHop.String())
+ if err != nil {
+ return nil, err
+ }
+ localPublic, ok := conn.LocalAddr().(*net.UDPAddr)
+ if !ok {
+ return nil, serrors.New("localAddr not UDP addr")
+ }
+ conn.Close()
+
+ nc := appnet.NetworkConfig{
+ Topology: c.topo,
+ IA: c.topo.LocalIA,
+ QUIC: appnet.QUIC{
+ TLSVerifier: trust.NewTLSCryptoVerifier(trustDB),
+ },
+ MTU: 1400,
+ Public: &net.UDPAddr{
+ IP: localPublic.IP,
+ Port: 0,
+ Zone: localPublic.Zone,
+ },
+ }
+ quicStack, err := nc.QUICStack(ctx)
+ if err != nil {
+ return nil, err
+ }
+ var dialerFunc func(a net.Addr, opts ...squic.EarlyDialerOption) squic.EarlyDialer
+ if insecure {
+ dialerFunc = (&squic.EarlyDialerFactory{
+ Transport: quicStack.Dialer.Transport,
+ TLSConfig: &tls.Config{
+ NextProtos: []string{"h3", "SCION"},
+ InsecureSkipVerify: true,
+ },
+ Rewriter: &appnet.AddressRewriter{
+ Router: &snet.BaseRouter{
+ Querier: querier,
+ },
+ },
+ }).NewDialer
+ } else {
+ dialerFunc = (&squic.EarlyDialerFactory{
+ Transport: quicStack.Dialer.Transport,
+ TLSConfig: &tls.Config{
+ NextProtos: []string{"h3", "SCION"},
+ ServerName: serverName,
+ },
+ Rewriter: &appnet.AddressRewriter{
+ Router: &snet.BaseRouter{
+ Querier: querier,
+ },
+ },
+ }).NewDialer
+ }
+
+ dialer := dialerFunc(remote)
+ marketplaceClient := hummingbirdconnect.NewMarketplaceServiceClient(
+ libconnect.HTTPClient{
+ RoundTripper: &http3.Transport{
+ Dial: dialer.DialEarly,
+ },
+ }, libconnect.BaseUrl(remote), connect.WithInterceptors(authInterceptor(c.token)))
+ return marketplaceClient, nil
+}
+
+func authInterceptor(jwtToken string) connect.UnaryInterceptorFunc {
+ return func(next connect.UnaryFunc) connect.UnaryFunc {
+ return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
+ req.Header().Set("Authorization", "Bearer "+jwtToken)
+ return next(ctx, req)
+ }
+ }
+}
+
+func parseAddr(s string) (addr.Addr, uint16, string, error) {
+ host, port, err := net.SplitHostPort(s)
+ if err != nil {
+ return addr.Addr{}, 0, "", serrors.Wrap("invalid address: split host:port", err, "addr", s)
+ }
+ splits := strings.Split(host, ",")
+ if len(splits) != 2 {
+ return addr.Addr{}, 0, "", serrors.Wrap("invalid address: split host:port", err, "addr", s)
+ }
+ _, err = netip.ParseAddr(splits[1])
+ if err != nil {
+ ipAddr, err := net.ResolveIPAddr("ip", splits[1])
+ if err != nil {
+ return addr.Addr{}, 0, "", serrors.Wrap("invalid address: split host:port", err, "addr", s)
+ }
+ s = fmt.Sprintf("[%s,%s]:%s", splits[0], ipAddr.String(), port)
+ }
+ a, p, err := addr.ParseAddrPort(s)
+ return a, p, splits[1], err
+}
+
+// Creates a new marketplace client.
+// Valid URL forms:
+// https://127.0.0.1:8888
+// https://my-marketplace.local:8888
+// [1-ff00:0:111,127.0.0.1]:9888
+// [1-ff00:0:111,my-marketplace.local]:9888
+// If a SCION url is provided, the client will connect over the SCION network, otherwise over the public internet.
+// If a dns name is provided, dns resolution will be done using local dns resolver.
+// If insecure = true, server certificate validation will be disabled.
+// path querier and local topology is only required for scion connections
+func NewMarketplaceClient(ctx context.Context, url string, token string, querier snet.PathQuerier, topo snet.Topology, insecure bool) (*MarketplaceClient, error) {
+ urlSplit := strings.Split(url, "://")
+ api := ""
+ if len(urlSplit) == 1 {
+ api = urlSplit[0]
+ } else if len(urlSplit) == 2 {
+ api = urlSplit[1]
+ } else {
+ return nil, serrors.New("invalid url", "url", url)
+ }
+ c := &MarketplaceClient{
+ token: token,
+ topo: topo,
+ }
+ scionAddr, port, serverName, err := parseAddr(api)
+ if err == nil {
+ remote := &snet.UDPAddr{
+ IA: scionAddr.IA,
+ Host: net.UDPAddrFromAddrPort(netip.AddrPortFrom(scionAddr.Host.IP(), port)),
+ }
+ c.client, err = c.withSCION(ctx, querier, remote, serverName, insecure)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ if insecure {
+ c.client = hummingbirdconnect.NewMarketplaceServiceClient(&http.Client{
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: true,
+ },
+ },
+ }, url, connect.WithInterceptors(authInterceptor(token)))
+ } else {
+ c.client = hummingbirdconnect.NewMarketplaceServiceClient(http.DefaultClient,
+ url, connect.WithInterceptors(authInterceptor(token)))
+ }
+ }
+ return c, nil
+}
+
+type BuyMode int
+
+const (
+ FailOnError BuyMode = iota
+ ContinueOnError
+)
+
+type InterfacePair struct {
+ IA uint64
+ Ingress uint32
+ Egress uint32
+}
+
+func (c *MarketplaceClient) findExistingReservations(ctx context.Context, pairs []InterfacePair, bwInKbps uint32, startsAt time.Time, stopsAt time.Time) ([]*snetpath.Hop, error) {
+ req := &hummingbird.FetchReservationsRequest{
+ Bandwidth: &bwInKbps,
+ StartsAt: timestamppb.New(startsAt),
+ StopsAt: timestamppb.New(stopsAt),
+ }
+ res, err := c.client.FetchReservations(ctx, &connect.Request[hummingbird.FetchReservationsRequest]{
+ Msg: req,
+ })
+ if err != nil {
+ return nil, err
+ }
+ ret := make([]*snetpath.Hop, len(pairs))
+ for i, pair := range pairs {
+ for _, r := range res.Msg.Reservations {
+ if r.Ia == pair.IA && r.IngressId == pair.Ingress && r.EgressId == pair.Egress {
+ ret[i] = &snetpath.Hop{
+ BaseHop: snetpath.BaseHop{
+ IA: addr.IA(r.Ia),
+ Ingress: uint16(r.IngressId),
+ Egress: uint16(r.EgressId),
+ },
+ Flyover: &snetpath.FlyoverData{
+ ResID: r.ReservationId,
+ Ak: [16]byte(r.AuthenticationKey),
+ Bw: uint16(r.DataplaneEncoding),
+ StartTime: uint32(r.StartsAt.Seconds),
+ Duration: uint16(r.StopsAt.Seconds - r.StartsAt.Seconds),
+ },
+ }
+ break
+ }
+ }
+ }
+ return ret, nil
+}
+
+func filter[T any](s []*T, keep func(*T) bool) []*T {
+ result := make([]*T, 0, len(s))
+ for _, v := range s {
+ if keep(v) {
+ result = append(result, v)
+ }
+ }
+ return result
+}
+
+func gcd(a, b uint32) uint32 {
+ for b != 0 {
+ a, b = b, a%b
+ }
+ return a
+}
+
+func lcm(a, b uint32) uint32 {
+ return a / gcd(a, b) * b
+}
+
+func timeMin(a time.Time, b time.Time) time.Time {
+ if a.Before(b) {
+ return a
+ }
+ return b
+}
+
+func activeAt(elements []*hummingbird.SearchAsset, t time.Time, bw uint32) []*hummingbird.SearchAsset {
+ // Find first element with StartsAt > t
+ end := sort.Search(len(elements), func(i int) bool {
+ return elements[i].StartsAt.AsTime().After(t)
+ })
+
+ result := make([]*hummingbird.SearchAsset, 0)
+
+ for i := 0; i < end; i++ {
+ if t.Before(elements[i].StopsAt.AsTime()) && elements[i].Bandwidth >= bw {
+ result = append(result, elements[i])
+ }
+ }
+ return result
+}
+
+// recursiveSelect
+func (c *MarketplaceClient) recursiveSelectStart(assets []*hummingbird.SearchAsset, bwInKbps uint32,
+ startsAt time.Time, stopsAt time.Time, combineCost uint64) ([]*hummingbird.BuyAsset, uint64, bool) {
+
+ sort.Slice(assets, func(i, j int) bool {
+ return assets[i].StartsAt.Seconds < assets[j].StartsAt.Seconds
+ })
+ assetMap := make(map[string]*hummingbird.SearchAsset)
+ for _, asset := range assets {
+ assetMap[asset.AssetId] = asset
+ }
+ filterValidAt := func(t time.Time, assets []*hummingbird.SearchAsset) []*hummingbird.SearchAsset {
+ return activeAt(assets, t, bwInKbps)
+ }
+
+ nextAssets := filterValidAt(startsAt, assets)
+ if len(nextAssets) == 0 {
+ return nil, 0, false
+ }
+ allChains := []chain{}
+ for _, nextAsset := range nextAssets {
+ resultChains, ok := c.recursiveSelect(nextAsset, assets, bwInKbps, 1, 1, startsAt, stopsAt)
+ if !ok {
+ continue
+ }
+ for _, resultChain := range resultChains {
+ allChains = append(allChains, resultChain)
+ }
+ }
+ actualPrice := func(a *hummingbird.SearchAsset, duration uint32) uint32 {
+ granularityAdjustedDuration := duration + (a.TimeGranularity-(duration%a.TimeGranularity))%a.TimeGranularity
+ return a.Price * max(a.BandwidthMin, bwInKbps) * max(a.TimeMinDuration, duration, granularityAdjustedDuration)
+ }
+ bestCost := uint64(math.MaxInt64)
+ bestSelect := []*hummingbird.BuyAsset{}
+ for _, chain := range allChains {
+ currCost := uint64(0)
+ currSelect := []*hummingbird.BuyAsset{}
+ if len(chain.ids) == 1 {
+ asset := assetMap[chain.ids[0]]
+ currCost += uint64(actualPrice(asset, uint32(chain.stopsAt.Sub(startsAt).Seconds())))
+ currSelect = append(currSelect, &hummingbird.BuyAsset{
+ AssetId: asset.AssetId,
+ StartsAtExactly: timestamppb.New(startsAt),
+ StopsAtExactly: timestamppb.New(chain.stopsAt),
+ BandwidthExact: chain.bandwidthMin,
+ })
+ } else {
+ currCost = combineCost * uint64(len(chain.ids)-1)
+ currStartsAt := startsAt
+ i := 0
+ for ; i < len(chain.ids)-1; i++ {
+ currAsset := assetMap[chain.ids[i]]
+ nextAsset := assetMap[chain.ids[i+1]]
+ if currAsset.Price <= nextAsset.Price {
+ duration := uint32(timeMin(currAsset.StopsAt.AsTime(), chain.stopsAt).Sub(currStartsAt).Seconds())
+ currCost += uint64(actualPrice(currAsset, duration))
+
+ currSelect = append(currSelect, &hummingbird.BuyAsset{
+ AssetId: currAsset.AssetId,
+ StartsAtExactly: timestamppb.New(currStartsAt),
+ StopsAtExactly: timestamppb.New(timeMin(currAsset.StopsAt.AsTime(), chain.stopsAt)),
+ BandwidthExact: chain.bandwidthMin,
+ })
+ currStartsAt = timeMin(currAsset.StopsAt.AsTime(), chain.stopsAt)
+
+ } else {
+ duration := uint32(timeMin(nextAsset.StartsAt.AsTime(), chain.stopsAt).Sub(currStartsAt).Seconds())
+ currCost += uint64(actualPrice(currAsset, duration))
+ currSelect = append(currSelect, &hummingbird.BuyAsset{
+ AssetId: currAsset.AssetId,
+ StartsAtExactly: timestamppb.New(currStartsAt),
+ StopsAtExactly: timestamppb.New(timeMin(nextAsset.StartsAt.AsTime(), chain.stopsAt)),
+ BandwidthExact: chain.bandwidthMin,
+ })
+ currStartsAt = timeMin(nextAsset.StartsAt.AsTime(), chain.stopsAt)
+ }
+ }
+ lastAsset := assetMap[chain.ids[i]]
+ duration := uint32(timeMin(lastAsset.StopsAt.AsTime(), chain.stopsAt).Sub(currStartsAt).Seconds())
+ currCost += uint64(actualPrice(lastAsset, duration))
+
+ currSelect = append(currSelect, &hummingbird.BuyAsset{
+ AssetId: lastAsset.AssetId,
+ StartsAtExactly: timestamppb.New(currStartsAt),
+ StopsAtExactly: timestamppb.New(timeMin(lastAsset.StopsAt.AsTime(), chain.stopsAt)),
+ BandwidthExact: chain.bandwidthMin,
+ })
+ }
+ if currCost < bestCost {
+ bestCost = currCost
+ bestSelect = currSelect
+ }
+ }
+
+ return bestSelect, bestCost, len(bestSelect) != 0
+}
+
+type chain struct {
+ ids []string
+ timeGranularity uint32
+ timeMinDuration uint32
+ bandwidthMin uint32
+ stopsAt time.Time
+}
+
+func (c *MarketplaceClient) recursiveSelect(currentAsset *hummingbird.SearchAsset, otherAssets []*hummingbird.SearchAsset, previousBw uint32, previousTimeMinDuration uint32,
+ previousTimeGranularity uint32, globalStart time.Time, globalStop time.Time) ([]chain, bool) {
+ currBW := max(previousBw, currentAsset.BandwidthMin)
+ timeGranularity := lcm(previousTimeGranularity, currentAsset.TimeGranularity)
+ timeMinDuration := max(previousTimeMinDuration, currentAsset.TimeMinDuration)
+ filterValidAt := func(t time.Time, assets []*hummingbird.SearchAsset) []*hummingbird.SearchAsset {
+ return activeAt(assets, t, currBW)
+ }
+ remainder := uint32(globalStop.Sub(globalStart).Seconds()) % timeGranularity
+ if remainder != 0 {
+ addedDuration := timeGranularity - remainder
+ globalStop = globalStop.Add(time.Duration(addedDuration) * time.Second)
+ }
+ if uint32(globalStop.Sub(globalStart).Seconds()) < timeMinDuration {
+ globalStop = globalStart.Add(time.Duration(timeMinDuration) * time.Second)
+ }
+ if !currentAsset.StopsAt.AsTime().Before(globalStop) {
+ // with this asset we found a chain from start till end
+ return []chain{{ids: []string{currentAsset.AssetId}, bandwidthMin: currBW, timeGranularity: timeGranularity, timeMinDuration: timeMinDuration, stopsAt: globalStop}}, true
+ }
+ nextAssets := filterValidAt(currentAsset.StopsAt.AsTime(), otherAssets)
+ if len(nextAssets) == 0 {
+ fmt.Println("no assets found to be valid at ", currentAsset.StopsAt.AsTime())
+ return nil, false
+ }
+ allChains := []chain{}
+ for _, nextAsset := range nextAssets {
+ resultChains, ok := c.recursiveSelect(nextAsset, otherAssets, currBW, timeMinDuration, timeGranularity, globalStart, globalStop)
+ if !ok {
+ fmt.Println("recursive select failed")
+ continue
+ }
+ for _, resultChain := range resultChains {
+ if resultChain.bandwidthMin > currentAsset.Bandwidth {
+ fmt.Println("bandwidth cannot be satisfied")
+ continue
+ }
+ resultChain.ids = append([]string{currentAsset.AssetId}, resultChain.ids...)
+ allChains = append(allChains, resultChain)
+ }
+ }
+ return allChains, len(allChains) != 0
+
+}
+
+// checkoutAssetForInterfacePairWithCombine tries to find assets that can be combined on the time axis. It does not check for combinations on the bandwidth axis.
+func (c *MarketplaceClient) checkoutAssetForInterfacePairWithCombine(ctx context.Context, pair InterfacePair, bwInKbps uint32,
+ startsAt time.Time, stopsAt time.Time, combineCost uint64) ([]*hummingbird.BuyAsset, error) {
+
+ ingressAndEgressSuccess := false
+ var ingressBuyAssets, egressBuyAssets, pairBuyAssets []*hummingbird.BuyAsset
+ var ingressAssetPrice, egressAssetPrice, pairAssetPrice uint64
+ var ok bool
+ ingressAssetsResponse, err := c.client.SearchAssets(ctx, &connect.Request[hummingbird.SearchAssetsRequest]{
+ Msg: &hummingbird.SearchAssetsRequest{
+ Owned: false,
+ Ia: &pair.IA,
+ IfIdIngress: &pair.Ingress,
+ MinRequiredBw: &bwInKbps,
+ StartsAtLatest: timestamppb.New(stopsAt),
+ StopsAtEarliest: timestamppb.New(startsAt),
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+ ingressSearchAssets := filter(ingressAssetsResponse.Msg.Assets, func(a *hummingbird.SearchAsset) bool {
+ return a.IfIdEgress == nil
+ })
+ ingressBuyAssets, ingressAssetPrice, ok = c.recursiveSelectStart(ingressSearchAssets, bwInKbps, startsAt, stopsAt, combineCost)
+ if ok {
+ egressAssetsResponse, err := c.client.SearchAssets(ctx, &connect.Request[hummingbird.SearchAssetsRequest]{
+ Msg: &hummingbird.SearchAssetsRequest{
+ Owned: false,
+ Ia: &pair.IA,
+ IfIdEgress: &pair.Egress,
+ MinRequiredBw: &bwInKbps,
+ StartsAtLatest: timestamppb.New(stopsAt),
+ StopsAtEarliest: timestamppb.New(startsAt),
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+ egressSearchAssets := filter(egressAssetsResponse.Msg.Assets, func(a *hummingbird.SearchAsset) bool {
+ return a.IfIdIngress == nil
+ })
+ egressBuyAssets, egressAssetPrice, ok = c.recursiveSelectStart(egressSearchAssets, bwInKbps, startsAt, stopsAt, combineCost)
+ if ok {
+ ingressAndEgressSuccess = true
+ }
+ }
+
+ pairAssetsResponse, err := c.client.SearchAssets(ctx, &connect.Request[hummingbird.SearchAssetsRequest]{
+ Msg: &hummingbird.SearchAssetsRequest{
+ Owned: false,
+ Ia: &pair.IA,
+ IfIdIngress: &pair.Ingress,
+ IfIdEgress: &pair.Egress,
+ MinRequiredBw: &bwInKbps,
+ StartsAtLatest: timestamppb.New(stopsAt),
+ StopsAtEarliest: timestamppb.New(startsAt),
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+ pairBuyAssets, pairAssetPrice, ok = c.recursiveSelectStart(pairAssetsResponse.Msg.Assets, bwInKbps, startsAt, stopsAt, combineCost)
+ if ok {
+ if ingressAndEgressSuccess {
+ if ingressAssetPrice+egressAssetPrice < pairAssetPrice {
+ return append(ingressBuyAssets, egressBuyAssets...), nil
+ }
+ return pairBuyAssets, nil
+ } else {
+ return pairBuyAssets, nil
+ }
+ } else {
+ if ingressAndEgressSuccess {
+ return append(ingressBuyAssets, egressBuyAssets...), nil
+ } else {
+ return nil, serrors.New("no assets found")
+ }
+ }
+}
+
+// checkoutAssetForInterfacePair searches on the marketplace for assets that match the interface pair. It searches for ingress, egress and interface-pair assets.
+// It ensures that the selected assets when bought should be redeemable without further splits or combines necessary.
+// The function does not combine assets. If no single (ingress-asset, egress-asset) tuple or interface-pair asset can satisfy the request, no buy order is returned.
+func (c *MarketplaceClient) checkoutAssetForInterfacePair(ctx context.Context, pair InterfacePair, bwInKbps uint32, startsAt time.Time, stopsAt time.Time) ([]*hummingbird.BuyAsset, error) {
+ var ingressSearchAssets, egressSearchAssets, pairSearchAssets []*hummingbird.SearchAsset
+ ingressAssetsResponse, err := c.client.SearchAssets(ctx, &connect.Request[hummingbird.SearchAssetsRequest]{
+ Msg: &hummingbird.SearchAssetsRequest{
+ Owned: false,
+ Ia: &pair.IA,
+ IfIdIngress: &pair.Ingress,
+ MinRequiredBw: &bwInKbps,
+ StartsAtLatest: timestamppb.New(startsAt),
+ StopsAtEarliest: timestamppb.New(stopsAt),
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+ ingressSearchAssets = filter(ingressAssetsResponse.Msg.Assets, func(a *hummingbird.SearchAsset) bool {
+ return a.IfIdEgress == nil
+ })
+ if len(ingressSearchAssets) != 0 {
+ egressAssetsResponse, err := c.client.SearchAssets(ctx, &connect.Request[hummingbird.SearchAssetsRequest]{
+ Msg: &hummingbird.SearchAssetsRequest{
+ Owned: false,
+ Ia: &pair.IA,
+ IfIdEgress: &pair.Egress,
+ MinRequiredBw: &bwInKbps,
+ StartsAtLatest: timestamppb.New(startsAt),
+ StopsAtEarliest: timestamppb.New(stopsAt),
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+ egressSearchAssets = filter(egressAssetsResponse.Msg.Assets, func(a *hummingbird.SearchAsset) bool {
+ return a.IfIdIngress == nil
+ })
+ }
+
+ pairAssetsResponse, err := c.client.SearchAssets(ctx, &connect.Request[hummingbird.SearchAssetsRequest]{
+ Msg: &hummingbird.SearchAssetsRequest{
+ Owned: false,
+ Ia: &pair.IA,
+ IfIdIngress: &pair.Ingress,
+ IfIdEgress: &pair.Egress,
+ MinRequiredBw: &bwInKbps,
+ StartsAtLatest: timestamppb.New(startsAt),
+ StopsAtEarliest: timestamppb.New(stopsAt),
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+ pairSearchAssets = pairAssetsResponse.Msg.Assets
+
+ duration := uint32(stopsAt.Sub(startsAt).Seconds())
+ actualPrice := func(a *hummingbird.SearchAsset) uint32 {
+ granularityAdjustedDuration := duration + (a.TimeGranularity-(duration%a.TimeGranularity))%a.TimeGranularity
+ return a.Price * max(a.BandwidthMin, bwInKbps) * max(a.TimeMinDuration, duration, granularityAdjustedDuration)
+ }
+ filterAssets := func(assets []*hummingbird.SearchAsset) []*hummingbird.SearchAsset {
+ return filter(assets, func(a *hummingbird.SearchAsset) bool {
+ if duration < a.TimeMinDuration {
+ if a.StopsAt.Seconds <= startsAt.Unix()+int64(a.TimeMinDuration) {
+ // asset is not valid because when extending duration to the minimum duration, the stops at lies outside validity period
+ return false
+ }
+ }
+ remainder := duration % a.TimeGranularity
+ if remainder != 0 {
+ addedDuration := a.TimeGranularity - remainder
+ if a.StopsAt.Seconds <= startsAt.Unix()+int64(addedDuration) {
+ // asset is not valid because when extending duration to a multiple of time granularity, the stops at lies outside validity period
+ return false
+ }
+ }
+ if a.BandwidthMin > bwInKbps {
+ if a.Bandwidth < a.BandwidthMin {
+ // there is not enough bandwidth to satisfy bandwidth min
+ return false
+ }
+ }
+ return true
+ })
+ }
+ sortAssets := func(assets []*hummingbird.SearchAsset) []*hummingbird.SearchAsset {
+ sort.Slice(assets, func(i, j int) bool {
+ return actualPrice(assets[i]) < actualPrice(assets[j])
+ })
+ return assets
+ }
+ buildBuyRequest := func(a *hummingbird.SearchAsset) *hummingbird.BuyAsset {
+ addedDuration := uint32(0)
+ if duration < a.TimeMinDuration {
+ addedDuration = a.TimeMinDuration - duration
+ }
+ remainder := duration % a.TimeGranularity
+ if remainder != 0 {
+ addedDuration = a.TimeGranularity - remainder
+ }
+ return &hummingbird.BuyAsset{
+ AssetId: a.AssetId,
+ BandwidthExact: max(bwInKbps, a.BandwidthMin),
+ StartsAtExactly: timestamppb.New(startsAt),
+ StopsAtExactly: timestamppb.New(stopsAt.Add(time.Duration(addedDuration) * time.Second)),
+ }
+ }
+ ingressAssets := sortAssets(filterAssets(ingressSearchAssets))
+ egressAssets := sortAssets(filterAssets(egressSearchAssets))
+ pairAssets := sortAssets(filterAssets(pairSearchAssets))
+
+ order := []*hummingbird.BuyAsset{}
+ if len(pairAssets) != 0 {
+ if len(ingressAssets) != 0 && len(egressAssets) != 0 {
+ if actualPrice(pairAssets[0]) <= actualPrice(ingressAssets[0])+actualPrice(egressAssets[0]) {
+ order = append(order, buildBuyRequest(pairAssets[0]))
+ } else {
+ order = append(order, buildBuyRequest(ingressAssets[0]), buildBuyRequest(egressAssets[0]))
+ }
+ } else {
+ order = append(order, buildBuyRequest(pairAssets[0]))
+ }
+ } else if len(ingressAssets) != 0 && len(egressAssets) != 0 {
+ order = append(order, buildBuyRequest(ingressAssets[0]), buildBuyRequest(egressAssets[0]))
+ } else {
+ log.Debug("no assets found", "ia", addr.IA(pair.IA).String(), "ingress", pair.Ingress, "egress", pair.Egress, "startsat", startsAt, "stopsat", stopsAt)
+ return nil, serrors.New("no assets found", "ia", addr.IA(pair.IA).String(), "ingress", pair.Ingress, "egress", pair.Egress, "startsat", startsAt, "stopsat", stopsAt)
+ }
+ return order, nil
+}
+
+type assetInfo struct {
+ id string
+ startsAt time.Time
+ stopsAt time.Time
+}
+
+func (c *MarketplaceClient) ObtainReservationsForInterfacePairs(ctx context.Context, pairs []InterfacePair, bwInKbps uint32, startsAt time.Time, stopsAt time.Time,
+ maxPrice uint64, buyMode BuyMode, fetchReservations bool, combineAssets bool, num_retries int) ([]*snetpath.Hop, error) {
+ infoRep, err := c.client.Info(ctx, &connect.Request[hummingbird.MarketplaceInfoRequest]{})
+ if err != nil {
+ return nil, err
+ }
+ combineCost := uint64(infoRep.Msg.SplitCombineFeeAbsolute)
+ startsAt = startsAt.Truncate(time.Second)
+ stopsAt = stopsAt.Truncate(time.Second)
+ hops := make([]*snetpath.Hop, len(pairs))
+ if fetchReservations {
+ hops, err = c.findExistingReservations(ctx, pairs, bwInKbps, startsAt, stopsAt)
+ if err != nil {
+ return nil, err
+ }
+ }
+ var orders = []*hummingbird.BuyAsset{}
+ iaAssets := map[uint64][]assetInfo{}
+ var boughtAssets []*hummingbird.BoughtAsset
+ success := false
+ for retry := -1; retry < num_retries; retry++ {
+ orders = []*hummingbird.BuyAsset{}
+ for i, pair := range pairs {
+ if hops[i] != nil {
+ continue
+ }
+ if combineAssets {
+ o, err := c.checkoutAssetForInterfacePairWithCombine(ctx, pair, bwInKbps, startsAt, stopsAt, combineCost)
+ if err != nil {
+ if buyMode == FailOnError {
+ return nil, err
+ } else if buyMode == ContinueOnError {
+ continue
+ }
+ }
+ orders = append(orders, o...)
+ } else {
+ o, err := c.checkoutAssetForInterfacePair(ctx, pair, bwInKbps, startsAt, stopsAt)
+ if err != nil {
+ if buyMode == FailOnError {
+ return nil, err
+ } else if buyMode == ContinueOnError {
+ continue
+ }
+ }
+ orders = append(orders, o...)
+ }
+ }
+ if len(orders) == 0 {
+ log.FromCtx(ctx).Debug("Skip purchase since no assets are selected for purchasing")
+ } else {
+ log.FromCtx(ctx).Debug("Purchase", "assets", orders)
+ boughtAssetsResponse, err := c.client.BuyAssets(ctx, &connect.Request[hummingbird.BuyAssetsRequest]{
+ Msg: &hummingbird.BuyAssetsRequest{
+ Assets: orders,
+ MaxPrice: maxPrice,
+ },
+ })
+ if err != nil {
+ log.FromCtx(ctx).Debug("Error buying asset", "err", err)
+ continue
+ }
+ boughtAssets = boughtAssetsResponse.Msg.Assets
+ }
+ success = true
+ break
+ }
+ if !success {
+ return nil, err
+ }
+ var foundAssets []*hummingbird.SearchAsset
+ if combineAssets {
+ foundAssetsResp, err := c.client.SearchAssets(ctx, &connect.Request[hummingbird.SearchAssetsRequest]{
+ Msg: &hummingbird.SearchAssetsRequest{
+ Owned: true,
+ MinRequiredBw: &bwInKbps,
+ StartsAtLatest: timestamppb.New(stopsAt),
+ StopsAtEarliest: timestamppb.New(startsAt),
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+ foundAssets = foundAssetsResp.Msg.Assets
+ } else {
+ foundAssetsResp, err := c.client.SearchAssets(ctx, &connect.Request[hummingbird.SearchAssetsRequest]{
+ Msg: &hummingbird.SearchAssetsRequest{
+ Owned: true,
+ MinRequiredBw: &bwInKbps,
+ StartsAtLatest: timestamppb.New(startsAt),
+ StopsAtEarliest: timestamppb.New(stopsAt),
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+ foundAssets = foundAssetsResp.Msg.Assets
+ }
+
+ foundAssetsMap := make(map[uint64][]*hummingbird.SearchAsset)
+ for _, boughtAsset := range boughtAssets {
+ for _, foundAsset := range foundAssets {
+ if boughtAsset.AssetId == foundAsset.AssetId {
+ currentSlice, found := foundAssetsMap[foundAsset.Ia]
+ if found {
+ foundAssetsMap[foundAsset.Ia] = append(currentSlice, foundAsset)
+ } else {
+ foundAssetsMap[foundAsset.Ia] = []*hummingbird.SearchAsset{foundAsset}
+ }
+ break
+ }
+ }
+ }
+ if combineAssets {
+ for k, v := range foundAssetsMap {
+ ingressAssets := []*hummingbird.SearchAsset{}
+ egressAssets := []*hummingbird.SearchAsset{}
+ pairAssets := []*hummingbird.SearchAsset{}
+ for _, asset := range v {
+ if asset.IfIdIngress != nil {
+ if asset.IfIdEgress != nil {
+ pairAssets = append(pairAssets, asset)
+ } else {
+ ingressAssets = append(ingressAssets, asset)
+ }
+ } else {
+ egressAssets = append(egressAssets, asset)
+ }
+ }
+ if len(pairAssets) != 0 {
+ sort.Slice(pairAssets, func(i, j int) bool {
+ return pairAssets[i].StartsAt.Seconds < pairAssets[j].StartsAt.Seconds
+ })
+ currAssetId := pairAssets[0].AssetId
+ for i := 0; i < len(pairAssets)-1; i++ {
+ combineResp, err := c.client.CombineAssets(ctx, &connect.Request[hummingbird.CombineAssetRequest]{
+ Msg: &hummingbird.CombineAssetRequest{
+ AssetId_1: currAssetId,
+ AssetId_2: pairAssets[i+1].AssetId,
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+ currAssetId = combineResp.Msg.AssetId
+ }
+ iaAssets[k] = []assetInfo{{id: currAssetId, startsAt: pairAssets[0].StartsAt.AsTime(), stopsAt: pairAssets[len(pairAssets)-1].StopsAt.AsTime()}}
+ }
+ if len(ingressAssets) != 0 && len(ingressAssets) == len(egressAssets) {
+ sort.Slice(ingressAssets, func(i, j int) bool {
+ return ingressAssets[i].StartsAt.Seconds < ingressAssets[j].StartsAt.Seconds
+ })
+ currIngressAssetId := ingressAssets[0].AssetId
+ for i := 0; i < len(ingressAssets)-1; i++ {
+ combineResp, err := c.client.CombineAssets(ctx, &connect.Request[hummingbird.CombineAssetRequest]{
+ Msg: &hummingbird.CombineAssetRequest{
+ AssetId_1: currIngressAssetId,
+ AssetId_2: ingressAssets[i+1].AssetId,
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+ currIngressAssetId = combineResp.Msg.AssetId
+ }
+ sort.Slice(egressAssets, func(i, j int) bool {
+ return egressAssets[i].StartsAt.Seconds < egressAssets[j].StartsAt.Seconds
+ })
+ currEgressAssetId := egressAssets[0].AssetId
+ for i := 0; i < len(egressAssets)-1; i++ {
+ combineResp, err := c.client.CombineAssets(ctx, &connect.Request[hummingbird.CombineAssetRequest]{
+ Msg: &hummingbird.CombineAssetRequest{
+ AssetId_1: currEgressAssetId,
+ AssetId_2: egressAssets[i+1].AssetId,
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+ currEgressAssetId = combineResp.Msg.AssetId
+ }
+ iaAssets[k] = []assetInfo{
+ {id: currIngressAssetId, startsAt: ingressAssets[0].StartsAt.AsTime(), stopsAt: ingressAssets[len(ingressAssets)-1].StopsAt.AsTime()},
+ {id: currEgressAssetId, startsAt: egressAssets[0].StartsAt.AsTime(), stopsAt: egressAssets[len(egressAssets)-1].StopsAt.AsTime()},
+ }
+ }
+ }
+ } else {
+ for _, v := range foundAssetsMap {
+ for _, asset := range v {
+ current, found := iaAssets[asset.Ia]
+ if found {
+ iaAssets[asset.Ia] = append(current, assetInfo{id: asset.AssetId, stopsAt: asset.StopsAt.AsTime(), startsAt: asset.StartsAt.AsTime()})
+ } else {
+ iaAssets[asset.Ia] = []assetInfo{{id: asset.AssetId, stopsAt: asset.StopsAt.AsTime(), startsAt: asset.StartsAt.AsTime()}}
+ }
+ }
+ }
+ }
+ redeemedHops, err := c.redeemHopsConcurrently(ctx, pairs, iaAssets, startsAt, stopsAt)
+ if err != nil {
+ return nil, err
+ }
+ for i, hop := range hops {
+ if hop != nil {
+ redeemedHops[i] = hops[i]
+ }
+ }
+ return redeemedHops, nil
+}
+
+func later(a time.Time, b time.Time) time.Time {
+ if a.Before(b) {
+ return b
+ }
+ return a
+}
+func earlier(a time.Time, b time.Time) time.Time {
+ if a.Before(b) {
+ return a
+ }
+ return b
+}
+
+func (c *MarketplaceClient) redeemHopsConcurrently(ctx context.Context, pairs []InterfacePair, iaAssets map[uint64][]assetInfo, startsAt time.Time, stopsAt time.Time) ([]*snetpath.Hop, error) {
+ type requestInfo struct {
+ request *hummingbird.RedeemAssetRequest
+ startsAt time.Time
+ stopsAt time.Time
+ }
+ requests := make([]*requestInfo, len(pairs))
+ responses := make([]*hummingbird.RedeemAssetResponse, len(pairs))
+ errors := make([]error, len(pairs))
+ for i, pair := range pairs {
+ assets, found := iaAssets[pair.IA]
+ if !found || len(assets) == 0 {
+ continue
+ }
+ if len(assets) == 1 {
+ requests[i] = &requestInfo{
+ request: &hummingbird.RedeemAssetRequest{
+ Interfaces: &hummingbird.RedeemAssetRequest_IfPairAssetId{
+ IfPairAssetId: assets[0].id,
+ },
+ },
+ startsAt: assets[0].startsAt,
+ stopsAt: assets[0].stopsAt,
+ }
+ } else if len(assets) == 2 {
+ requests[i] = &requestInfo{
+ request: &hummingbird.RedeemAssetRequest{
+ Interfaces: &hummingbird.RedeemAssetRequest_Pair{
+ Pair: &hummingbird.IngressEgressPair{
+ IngressAssetId: assets[0].id,
+ EgressAssetId: assets[1].id,
+ },
+ },
+ },
+ startsAt: later(assets[0].startsAt, assets[1].startsAt),
+ stopsAt: earlier(assets[0].stopsAt, assets[1].stopsAt),
+ }
+ }
+ }
+ wg := sync.WaitGroup{}
+ wg.Add(len(pairs))
+ for i := range pairs {
+ go func(i int) {
+ defer wg.Done()
+ if requests[i] == nil {
+ return
+ }
+ rep, err := c.client.RedeemAsset(ctx, &connect.Request[hummingbird.RedeemAssetRequest]{
+ Msg: requests[i].request,
+ })
+ if err != nil {
+ errors[i] = err
+ return
+ }
+ responses[i] = rep.Msg
+ }(i)
+ }
+ wg.Wait()
+ ret := make([]*snetpath.Hop, len(pairs))
+ for i := 0; i < len(pairs); i++ {
+ if responses[i] != nil {
+ ret[i] = &snetpath.Hop{
+ BaseHop: snetpath.BaseHop{
+ IA: addr.IA(pairs[i].IA),
+ Ingress: uint16(pairs[i].Ingress),
+ Egress: uint16(pairs[i].Egress),
+ },
+ Flyover: &snetpath.FlyoverData{
+ ResID: responses[i].ReservationId,
+ Ak: [16]byte(responses[i].AuthenticationKey),
+ Bw: uint16(responses[i].BwDataplaneEncoding),
+ StartTime: uint32(requests[i].startsAt.Unix()),
+ Duration: uint16(requests[i].stopsAt.Sub(requests[i].startsAt) / time.Second),
+ },
+ }
+ } else if errors[i] != nil {
+ log.FromCtx(ctx).Debug("error redeeming asset", "asset ID", requests[i].request.Interfaces, "err", errors[i])
+ ret[i] = &snetpath.Hop{
+ BaseHop: snetpath.BaseHop{
+ IA: addr.IA(pairs[i].IA),
+ Ingress: uint16(pairs[i].Ingress),
+ Egress: uint16(pairs[i].Egress),
+ },
+ }
+ } else {
+ ret[i] = &snetpath.Hop{
+ BaseHop: snetpath.BaseHop{
+ IA: addr.IA(pairs[i].IA),
+ Ingress: uint16(pairs[i].Ingress),
+ Egress: uint16(pairs[i].Egress),
+ },
+ }
+ }
+ }
+ return ret, nil
+}
+
+// ObtainReservationsFullPath tries to obtain reservations for the full path provided.
+// buyMode defines whether it should try to continue searching and buying assets in other ASes if purchase of an asset for one AS fails (e.g. because no asset matching the filters exists)
+// fetchReservations enables fetching already owned reservations. If reservations that match IA, ingress, egress, bwInKbps, startsAt and stopsAt are found, those are used instead of buying new ones.
+// It could happen that an asset gets bought by some other user between searching and buying the asset ourselves. With num_retries we can repeat the search and buy step if this happens.
+func (c *MarketplaceClient) ObtainReservationsFullPath(ctx context.Context, path snet.Path, bwInKbps uint32, startsAt time.Time, stopsAt time.Time,
+ maxPrice uint64, buyMode BuyMode, fetchReservations bool, combineAssets bool, num_retries int) ([]*snetpath.Hop, error) {
+
+ pairs := []InterfacePair{}
+ ifaces := path.Metadata().Interfaces
+ pairs = append(pairs, InterfacePair{
+ IA: uint64(ifaces[0].IA),
+ Ingress: uint32(0),
+ Egress: uint32(ifaces[0].ID),
+ })
+ i := 1
+ for ; i < len(ifaces)-1; i += 2 {
+ pairs = append(pairs, InterfacePair{
+ IA: uint64(ifaces[i].IA),
+ Ingress: uint32(ifaces[i].ID),
+ Egress: uint32(ifaces[i+1].ID),
+ })
+ }
+ for ; i < len(ifaces); i++ {
+ pairs = append(pairs, InterfacePair{
+ IA: uint64(ifaces[i].IA),
+ Ingress: uint32(ifaces[i].ID),
+ Egress: 0,
+ })
+ }
+ return c.ObtainReservationsForInterfacePairs(ctx, pairs, bwInKbps, startsAt, stopsAt, maxPrice, buyMode, fetchReservations, combineAssets, num_retries)
+}
diff --git a/pkg/hummingbird/marketplace/client_test.go b/pkg/hummingbird/marketplace/client_test.go
new file mode 100644
index 0000000000..79cbea1a7d
--- /dev/null
+++ b/pkg/hummingbird/marketplace/client_test.go
@@ -0,0 +1,285 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package marketplace
+
+import (
+ "testing"
+ "time"
+
+ "github.com/scionproto/scion/pkg/proto/hummingbird"
+ "github.com/stretchr/testify/assert"
+ "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+func TestCombine(t *testing.T) {
+ type Test struct {
+ duration time.Duration
+ bw uint32
+ splitCombineCost uint64
+ assets []*hummingbird.SearchAsset
+ expectedResponse []*hummingbird.BuyAsset
+ }
+ start, _ := time.Parse(time.RFC3339, "2026-07-07T00:00:00Z")
+ tests := map[string]Test{
+ "baseCombine": {
+ duration: time.Second * 2,
+ bw: 1,
+ expectedResponse: []*hummingbird.BuyAsset{
+ {
+ AssetId: "1",
+ StartsAtExactly: timestamppb.New(start),
+ StopsAtExactly: timestamppb.New(start.Add(time.Second * 1)),
+ BandwidthExact: 1,
+ },
+ {
+ AssetId: "2",
+ StartsAtExactly: timestamppb.New(start.Add(time.Second * 1)),
+ StopsAtExactly: timestamppb.New(start.Add(time.Second * 2)),
+ BandwidthExact: 1,
+ },
+ },
+ assets: []*hummingbird.SearchAsset{
+ {
+ AssetId: "1",
+ Bandwidth: 1,
+ StartsAt: timestamppb.New(start),
+ StopsAt: timestamppb.New(start.Add(time.Second * 1)),
+ TimeGranularity: 1,
+ BandwidthMin: 1,
+ TimeMinDuration: 1,
+ Price: 1,
+ },
+ {
+ AssetId: "2",
+ Bandwidth: 1,
+ StartsAt: timestamppb.New(start.Add(time.Second * 1)),
+ StopsAt: timestamppb.New(start.Add(time.Second * 2)),
+ TimeGranularity: 1,
+ BandwidthMin: 1,
+ TimeMinDuration: 1,
+ Price: 1,
+ },
+ },
+ },
+ "baseCombineChooseCheaper": {
+ duration: time.Second * 2,
+ bw: 1,
+ expectedResponse: []*hummingbird.BuyAsset{
+ {
+ AssetId: "1",
+ StartsAtExactly: timestamppb.New(start),
+ StopsAtExactly: timestamppb.New(start.Add(time.Second * 1)),
+ BandwidthExact: 1,
+ },
+ {
+ AssetId: "3",
+ StartsAtExactly: timestamppb.New(start.Add(time.Second * 1)),
+ StopsAtExactly: timestamppb.New(start.Add(time.Second * 2)),
+ BandwidthExact: 1,
+ },
+ },
+ assets: []*hummingbird.SearchAsset{
+ {
+ AssetId: "1",
+ Bandwidth: 1,
+ StartsAt: timestamppb.New(start),
+ StopsAt: timestamppb.New(start.Add(time.Second * 1)),
+ TimeGranularity: 1,
+ BandwidthMin: 1,
+ TimeMinDuration: 1,
+ Price: 1,
+ },
+ {
+ AssetId: "2",
+ Bandwidth: 1,
+ StartsAt: timestamppb.New(start.Add(time.Second * 1)),
+ StopsAt: timestamppb.New(start.Add(time.Second * 2)),
+ TimeGranularity: 1,
+ BandwidthMin: 1,
+ TimeMinDuration: 1,
+ Price: 2,
+ },
+ {
+ AssetId: "3",
+ Bandwidth: 1,
+ StartsAt: timestamppb.New(start.Add(time.Second * 1)),
+ StopsAt: timestamppb.New(start.Add(time.Second * 2)),
+ TimeGranularity: 1,
+ BandwidthMin: 1,
+ TimeMinDuration: 1,
+ Price: 1,
+ },
+ },
+ },
+ "combineOverlappingSolutions": {
+ duration: time.Second * 5,
+ bw: 1,
+ expectedResponse: []*hummingbird.BuyAsset{
+ {
+ AssetId: "1",
+ StartsAtExactly: timestamppb.New(start),
+ StopsAtExactly: timestamppb.New(start.Add(time.Second * 1)),
+ BandwidthExact: 1,
+ },
+ {
+ AssetId: "2",
+ StartsAtExactly: timestamppb.New(start.Add(time.Second * 1)),
+ StopsAtExactly: timestamppb.New(start.Add(time.Second * 2)),
+ BandwidthExact: 1,
+ },
+ {
+ AssetId: "3",
+ StartsAtExactly: timestamppb.New(start.Add(time.Second * 2)),
+ StopsAtExactly: timestamppb.New(start.Add(time.Second * 4)),
+ BandwidthExact: 1,
+ },
+ {
+ AssetId: "4",
+ StartsAtExactly: timestamppb.New(start.Add(time.Second * 4)),
+ StopsAtExactly: timestamppb.New(start.Add(time.Second * 5)),
+ BandwidthExact: 1,
+ },
+ },
+ assets: []*hummingbird.SearchAsset{
+ {
+ AssetId: "1",
+ Bandwidth: 1,
+ StartsAt: timestamppb.New(start),
+ StopsAt: timestamppb.New(start.Add(time.Second * 1)),
+ TimeGranularity: 1,
+ BandwidthMin: 1,
+ TimeMinDuration: 1,
+ Price: 1,
+ },
+ {
+ AssetId: "2",
+ Bandwidth: 1,
+ StartsAt: timestamppb.New(start.Add(time.Second * 1)),
+ StopsAt: timestamppb.New(start.Add(time.Second * 2)),
+ TimeGranularity: 1,
+ BandwidthMin: 1,
+ TimeMinDuration: 1,
+ Price: 1,
+ },
+ {
+ AssetId: "3",
+ Bandwidth: 1,
+ StartsAt: timestamppb.New(start.Add(time.Second * 1)),
+ StopsAt: timestamppb.New(start.Add(time.Second * 4)),
+ TimeGranularity: 1,
+ BandwidthMin: 1,
+ TimeMinDuration: 1,
+ Price: 1,
+ },
+ {
+ AssetId: "4",
+ Bandwidth: 1,
+ StartsAt: timestamppb.New(start.Add(time.Second * 4)),
+ StopsAt: timestamppb.New(start.Add(time.Second * 5)),
+ TimeGranularity: 1,
+ BandwidthMin: 1,
+ TimeMinDuration: 1,
+ Price: 1,
+ },
+ },
+ },
+ "combineOverlappingSolutionsWithCombineCost": {
+ duration: time.Second * 5,
+ bw: 1,
+ splitCombineCost: 10,
+ expectedResponse: []*hummingbird.BuyAsset{
+ {
+ AssetId: "1",
+ StartsAtExactly: timestamppb.New(start),
+ StopsAtExactly: timestamppb.New(start.Add(time.Second * 1)),
+ BandwidthExact: 1,
+ },
+ {
+ AssetId: "3",
+ StartsAtExactly: timestamppb.New(start.Add(time.Second * 1)),
+ StopsAtExactly: timestamppb.New(start.Add(time.Second * 4)),
+ BandwidthExact: 1,
+ },
+ {
+ AssetId: "4",
+ StartsAtExactly: timestamppb.New(start.Add(time.Second * 4)),
+ StopsAtExactly: timestamppb.New(start.Add(time.Second * 5)),
+ BandwidthExact: 1,
+ },
+ },
+ assets: []*hummingbird.SearchAsset{
+ {
+ AssetId: "1",
+ Bandwidth: 1,
+ StartsAt: timestamppb.New(start),
+ StopsAt: timestamppb.New(start.Add(time.Second * 1)),
+ TimeGranularity: 1,
+ BandwidthMin: 1,
+ TimeMinDuration: 1,
+ Price: 1,
+ },
+ {
+ AssetId: "2",
+ Bandwidth: 1,
+ StartsAt: timestamppb.New(start.Add(time.Second * 1)),
+ StopsAt: timestamppb.New(start.Add(time.Second * 2)),
+ TimeGranularity: 1,
+ BandwidthMin: 1,
+ TimeMinDuration: 1,
+ Price: 1,
+ },
+ {
+ AssetId: "3",
+ Bandwidth: 1,
+ StartsAt: timestamppb.New(start.Add(time.Second * 1)),
+ StopsAt: timestamppb.New(start.Add(time.Second * 4)),
+ TimeGranularity: 1,
+ BandwidthMin: 1,
+ TimeMinDuration: 1,
+ Price: 1,
+ },
+ {
+ AssetId: "4",
+ Bandwidth: 1,
+ StartsAt: timestamppb.New(start.Add(time.Second * 4)),
+ StopsAt: timestamppb.New(start.Add(time.Second * 5)),
+ TimeGranularity: 1,
+ BandwidthMin: 1,
+ TimeMinDuration: 1,
+ Price: 1,
+ },
+ },
+ },
+ }
+ c := &MarketplaceClient{}
+ for name, tc := range tests {
+ t.Run(name, func(t *testing.T) {
+ assert.NotEqual(t, 0, tc.bw)
+ buyAssets, _, ok := c.recursiveSelectStart(tc.assets, tc.bw, start, start.Add(tc.duration), tc.splitCombineCost)
+ if tc.expectedResponse == nil {
+ assert.False(t, ok)
+ } else {
+ assert.NotEmpty(t, buyAssets)
+ assert.Len(t, buyAssets, len(tc.expectedResponse))
+ for i := range buyAssets {
+ assert.Equal(t, tc.expectedResponse[i].AssetId, buyAssets[i].AssetId)
+ assert.Equal(t, tc.expectedResponse[i].StartsAtExactly.Seconds, buyAssets[i].StartsAtExactly.Seconds)
+ assert.Equal(t, tc.expectedResponse[i].StopsAtExactly.Seconds, buyAssets[i].StopsAtExactly.Seconds)
+ assert.Equal(t, tc.expectedResponse[i].BandwidthExact, buyAssets[i].BandwidthExact)
+ }
+ }
+ })
+ }
+}
diff --git a/pkg/hummingbird/registration/BUILD.bazel b/pkg/hummingbird/registration/BUILD.bazel
new file mode 100644
index 0000000000..f1082088e2
--- /dev/null
+++ b/pkg/hummingbird/registration/BUILD.bazel
@@ -0,0 +1,25 @@
+load("@rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = [
+ "client.go",
+ "jwt_handler.go",
+ "service.go",
+ ],
+ importpath = "github.com/scionproto/scion/pkg/hummingbird/registration",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//pkg/addr:go_default_library",
+ "//pkg/log:go_default_library",
+ "//pkg/private/serrors:go_default_library",
+ "//pkg/proto/crypto:go_default_library",
+ "//pkg/proto/hummingbird:go_default_library",
+ "//pkg/scrypto/cppki:go_default_library",
+ "//private/storage:go_default_library",
+ "//private/trust:go_default_library",
+ "@com_connectrpc_connect//:go_default_library",
+ "@com_github_golang_jwt_jwt//:go_default_library",
+ "@com_github_scionproto_scion//pkg/proto/hummingbird/v1/hummingbirdconnect:go_default_library",
+ ],
+)
diff --git a/pkg/hummingbird/registration/client.go b/pkg/hummingbird/registration/client.go
new file mode 100644
index 0000000000..f995f58cff
--- /dev/null
+++ b/pkg/hummingbird/registration/client.go
@@ -0,0 +1,153 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package registration
+
+import (
+ "context"
+ "crypto"
+ "crypto/x509"
+ "encoding/pem"
+ "os"
+ "path/filepath"
+ "time"
+
+ "connectrpc.com/connect"
+ "github.com/scionproto/scion/pkg/addr"
+ "github.com/scionproto/scion/pkg/log"
+ "github.com/scionproto/scion/pkg/proto/hummingbird"
+ "github.com/scionproto/scion/pkg/proto/hummingbird/v1/hummingbirdconnect"
+ "github.com/scionproto/scion/pkg/scrypto/cppki"
+ "github.com/scionproto/scion/private/storage"
+ "github.com/scionproto/scion/private/trust"
+)
+
+type Client struct {
+ accountClient hummingbirdconnect.AccountServiceClient
+ name string
+}
+
+func NewClient(c hummingbirdconnect.AccountServiceClient, name string) *Client {
+ return &Client{
+ accountClient: c,
+ name: name,
+ }
+}
+
+// RegisterWithNewSigner is used if the application does not already have a trust.Signer. It loads the trcs, the certificates
+// and private keys from the provided folders, generates a signer and then performs the registration steps.
+// Returns the publisher JWT token, the redemption service JWT token, and the error state.
+func (c *Client) RegisterWithNewSigner(ctx context.Context, ia addr.IA, trcDir string,
+ certDir string, keyringDir string) (string, string, error) {
+
+ trustDB, err := storage.NewInMemoryTrustStorage()
+ if err != nil {
+ return "", "", err
+ }
+ loader := trust.TRCLoader{
+ Dir: trcDir,
+ DB: trustDB,
+ }
+ _, err = loader.Load(ctx)
+ if err != nil {
+ return "", "", err
+ }
+ _, err = trust.LoadChains(ctx, certDir, trustDB)
+ if err != nil {
+ return "", "", err
+ }
+ gen := trust.SignerGen{
+ IA: ia,
+ DB: trustDB,
+ KeyRing: loadingRing{
+ Dir: keyringDir,
+ },
+ ExtKeyUsage: x509.ExtKeyUsageClientAuth,
+ }
+ signers, err := gen.Generate(ctx)
+ if err != nil {
+ return "", "", err
+ }
+ now := time.Now()
+ signer, err := trust.LastExpiring(signers, cppki.Validity{
+ NotBefore: now,
+ NotAfter: now,
+ })
+ if err != nil {
+ return "", "", err
+ }
+ return c.Register(ctx, signer)
+}
+
+// Register uses the provided signer to perform the registration steps.
+// Returns the publisher JWT token, the redemption service JWT token, and the error state.
+func (c *Client) Register(ctx context.Context, signer trust.Signer) (string, string, error) {
+ challengeResponse, err := c.accountClient.CreateChallenge(ctx, &connect.Request[hummingbird.CreateChallengeRequest]{
+ Msg: &hummingbird.CreateChallengeRequest{
+ Ia: uint64(signer.IA),
+ },
+ })
+ if err != nil {
+ return "", "", err
+ }
+ signedMsg, err := signer.Sign(ctx, challengeResponse.Msg.Value, []byte(c.name), []byte(hummingbirdconnect.AccountServiceRegisterASProcedure))
+ if err != nil {
+ return "", "", err
+ }
+ registrationResponse, err := c.accountClient.RegisterAS(ctx, &connect.Request[hummingbird.RegisterASRequest]{
+ Msg: &hummingbird.RegisterASRequest{
+ Id: challengeResponse.Msg.Id,
+ SignedChallenge: signedMsg,
+ },
+ })
+ if err != nil {
+ return "", "", err
+ }
+ return registrationResponse.Msg.JwtPublisher, registrationResponse.Msg.JwtRedemption, nil
+}
+
+type loadingRing struct {
+ Dir string
+}
+
+// PrivateKeys loads all private keys that are in PKCS#8 format from the directory.
+func (r loadingRing) PrivateKeys(ctx context.Context) ([]crypto.Signer, error) {
+ files, err := filepath.Glob(filepath.Join(r.Dir, "*.key"))
+ if err != nil {
+ return nil, err
+ }
+
+ var signers []crypto.Signer
+ for _, file := range files {
+ raw, err := os.ReadFile(file)
+ if err != nil {
+ log.FromCtx(ctx).Info("Error reading key file", "file", file, "err", err)
+ continue
+ }
+ block, _ := pem.Decode(raw)
+ if block == nil || block.Type != "PRIVATE KEY" {
+ continue
+ }
+ key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
+ if err != nil {
+ continue
+ }
+ signer, ok := key.(crypto.Signer)
+ if !ok {
+ continue
+ }
+ signers = append(signers, signer)
+ }
+ return signers, nil
+}
diff --git a/pkg/hummingbird/registration/jwt_handler.go b/pkg/hummingbird/registration/jwt_handler.go
new file mode 100644
index 0000000000..f8cf6b4206
--- /dev/null
+++ b/pkg/hummingbird/registration/jwt_handler.go
@@ -0,0 +1,70 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package registration
+
+import (
+ "crypto/ed25519"
+ "fmt"
+
+ "github.com/golang-jwt/jwt"
+)
+
+const (
+ ScopeUser = "User"
+ ScopeAssetPublisher = "AssetPublisher"
+ ScopeRedemptionService = "RedemptionService"
+)
+
+type Signer struct {
+ privKey ed25519.PrivateKey
+}
+
+func NewSigner(privKey ed25519.PrivateKey) *Signer {
+ return &Signer{
+ privKey: privKey,
+ }
+}
+
+func (s *Signer) GenerateToken(claims jwt.MapClaims) (string, error) {
+ token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims)
+ signedToken, err := token.SignedString(s.privKey)
+ if err != nil {
+ return "", err
+ }
+ return signedToken, nil
+}
+
+type Verifier struct {
+ pubKey ed25519.PublicKey
+}
+
+func NewVerifier(pubKey ed25519.PublicKey) *Verifier {
+ return &Verifier{
+ pubKey: pubKey,
+ }
+}
+
+func (v *Verifier) VerifyToken(token string) (*jwt.Token, error) {
+ parsedToken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
+ if t.Method != jwt.SigningMethodEdDSA {
+ return nil, fmt.Errorf("unexpected signing method")
+ }
+ return v.pubKey, nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ return parsedToken, nil
+}
diff --git a/pkg/hummingbird/registration/service.go b/pkg/hummingbird/registration/service.go
new file mode 100644
index 0000000000..f387203a64
--- /dev/null
+++ b/pkg/hummingbird/registration/service.go
@@ -0,0 +1,174 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package registration
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/hex"
+ "slices"
+ "sync"
+ "time"
+
+ "github.com/golang-jwt/jwt"
+ "github.com/scionproto/scion/pkg/addr"
+ "github.com/scionproto/scion/pkg/private/serrors"
+ cryptopb "github.com/scionproto/scion/pkg/proto/crypto"
+ "github.com/scionproto/scion/pkg/proto/hummingbird"
+ "github.com/scionproto/scion/pkg/proto/hummingbird/v1/hummingbirdconnect"
+ "github.com/scionproto/scion/pkg/scrypto/cppki"
+ "github.com/scionproto/scion/private/trust"
+)
+
+var ChallengeLifetime = int64(10) //time in seconds
+
+type Service struct {
+ buckets [3]*bucket
+ challengeLifetime int64
+ trustProvider trust.Provider
+ mtx sync.RWMutex
+}
+type bucket struct {
+ challenges map[string]Challenge
+ mtx sync.RWMutex
+}
+
+func NewService(provider trust.Provider) *Service {
+ buckets := [3]*bucket{}
+ for i := range 3 {
+ buckets[i] = &bucket{
+ challenges: make(map[string]Challenge),
+ }
+ }
+ s := &Service{
+ buckets: buckets,
+ challengeLifetime: max(10, ChallengeLifetime),
+ trustProvider: provider,
+ }
+ s.startCleanupRoutine()
+ return s
+}
+
+type Challenge struct {
+ ID string
+ IA addr.IA
+ Nonce []byte
+}
+
+func (s *Service) startCleanupRoutine() {
+ go func() {
+ now := time.Now()
+ next := now.Truncate(time.Duration(s.challengeLifetime) * time.Second).Add(time.Duration(s.challengeLifetime+s.challengeLifetime/2) * time.Second)
+ sleepDuration := next.Sub(now)
+ time.Sleep(sleepDuration)
+ tick := time.NewTicker(time.Duration(s.challengeLifetime) * time.Second)
+ for {
+ t := <-tick.C
+ s.clear(t)
+ }
+ }()
+}
+
+func (s *Service) CreateChallenge(ctx context.Context, ia addr.IA) (*hummingbird.CreateChallengeResponse, error) {
+ nonce := make([]byte, 32)
+ challengeID := make([]byte, 32)
+ rand.Read(nonce)
+ rand.Read(challengeID)
+ challengeIDString := hex.EncodeToString(challengeID)
+
+ c := Challenge{
+ ID: challengeIDString,
+ IA: ia,
+ Nonce: nonce,
+ }
+ s.insert(time.Now(), c)
+ return &hummingbird.CreateChallengeResponse{
+ Id: challengeIDString,
+ Value: nonce,
+ }, nil
+}
+
+// get searches for the challenge in both current and previous bucket (validity might overlap)
+func (s *Service) get(now time.Time, id string) (Challenge, bool) {
+ bucketIndex := (now.Unix() % (3 * s.challengeLifetime)) / s.challengeLifetime
+ bucket := s.buckets[bucketIndex]
+ bucket.mtx.RLock()
+ defer bucket.mtx.RUnlock()
+ c, found := bucket.challenges[id]
+ if !found {
+ bucketIndex = (bucketIndex + 2) % 3
+ bucket = s.buckets[bucketIndex]
+ bucket.mtx.RLock()
+ defer bucket.mtx.RUnlock()
+ c, found = bucket.challenges[id]
+ return c, found
+ }
+ return c, true
+}
+
+// insert inserts challenge in current bucket
+func (s *Service) insert(now time.Time, c Challenge) {
+ bucketIndex := (now.Unix() % (3 * s.challengeLifetime)) / s.challengeLifetime
+ bucket := s.buckets[bucketIndex]
+ bucket.mtx.Lock()
+ defer bucket.mtx.Unlock()
+ bucket.challenges[c.ID] = c
+}
+
+// delete tries to delete challenge in both current and previous bucket (validity might overlap bucket)
+func (s *Service) delete(now time.Time, id string) {
+ bucketIndex := (now.Unix() % (3 * s.challengeLifetime)) / s.challengeLifetime
+ bucket := s.buckets[bucketIndex]
+ bucket.mtx.Lock()
+ delete(bucket.challenges, id)
+ bucket.mtx.Unlock()
+ bucketIndex = (bucketIndex + 2) % 3
+ bucket = s.buckets[bucketIndex]
+ bucket.mtx.Lock()
+ delete(bucket.challenges, id)
+ bucket.mtx.Unlock()
+}
+func (s *Service) clear(now time.Time) {
+ bucketIndex := (((now.Unix() % (3 * s.challengeLifetime)) / s.challengeLifetime) + 1) % 3
+ bucket := s.buckets[bucketIndex]
+ bucket.mtx.Lock()
+ defer bucket.mtx.Unlock()
+ clear(bucket.challenges)
+}
+
+func (s *Service) RegisterAS(ctx context.Context, challengeID string, signedMsg *cryptopb.SignedMessage, name string, claims ...jwt.MapClaims) (addr.IA, error) {
+ now := time.Now()
+ c, found := s.get(now, challengeID)
+ if !found {
+ return 0, serrors.New("challenge not found")
+ }
+ verifier := &trust.Verifier{
+ BoundIA: c.IA,
+ Engine: s.trustProvider,
+ BoundValidity: cppki.Validity{
+ NotBefore: now,
+ NotAfter: now,
+ },
+ }
+ msg, err := verifier.Verify(ctx, signedMsg, []byte(name), []byte(hummingbirdconnect.AccountServiceRegisterASProcedure))
+ if err != nil {
+ return 0, err
+ }
+ if slices.Compare(msg.Body, c.Nonce) != 0 {
+ return 0, serrors.New("wrong challenge")
+ }
+ s.delete(now, c.ID)
+ return c.IA, nil
+}
diff --git a/pkg/proto/hummingbird/BUILD.bazel b/pkg/proto/hummingbird/BUILD.bazel
new file mode 100644
index 0000000000..b5921686ab
--- /dev/null
+++ b/pkg/proto/hummingbird/BUILD.bazel
@@ -0,0 +1,12 @@
+load("@rules_go//proto:def.bzl", "go_proto_library")
+
+go_proto_library(
+ name = "go_default_library",
+ compiler = "@rules_go//proto:go_grpc",
+ importpath = "github.com/scionproto/scion/pkg/proto/hummingbird",
+ proto = "//proto/hummingbird/v1:hummingbird",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//pkg/proto/crypto:go_default_library",
+ ],
+)
diff --git a/pkg/proto/hummingbird/account.pb.go b/pkg/proto/hummingbird/account.pb.go
new file mode 100644
index 0000000000..a1ce2d5ca0
--- /dev/null
+++ b/pkg/proto/hummingbird/account.pb.go
@@ -0,0 +1,660 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.36.10
+// protoc v6.31.1
+// source: proto/hummingbird/v1/account.proto
+
+package hummingbird
+
+import (
+ context "context"
+ crypto "github.com/scionproto/scion/pkg/proto/crypto"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+ unsafe "unsafe"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type SetPasswordRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SetPasswordRequest) Reset() {
+ *x = SetPasswordRequest{}
+ mi := &file_proto_hummingbird_v1_account_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SetPasswordRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SetPasswordRequest) ProtoMessage() {}
+
+func (x *SetPasswordRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_account_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SetPasswordRequest.ProtoReflect.Descriptor instead.
+func (*SetPasswordRequest) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_account_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *SetPasswordRequest) GetPassword() string {
+ if x != nil {
+ return x.Password
+ }
+ return ""
+}
+
+type SetPasswordResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SetPasswordResponse) Reset() {
+ *x = SetPasswordResponse{}
+ mi := &file_proto_hummingbird_v1_account_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SetPasswordResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SetPasswordResponse) ProtoMessage() {}
+
+func (x *SetPasswordResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_account_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SetPasswordResponse.ProtoReflect.Descriptor instead.
+func (*SetPasswordResponse) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_account_proto_rawDescGZIP(), []int{1}
+}
+
+type RegisterASRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+ SignedChallenge *crypto.SignedMessage `protobuf:"bytes,2,opt,name=signed_challenge,json=signedChallenge,proto3" json:"signed_challenge,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *RegisterASRequest) Reset() {
+ *x = RegisterASRequest{}
+ mi := &file_proto_hummingbird_v1_account_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *RegisterASRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RegisterASRequest) ProtoMessage() {}
+
+func (x *RegisterASRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_account_proto_msgTypes[2]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RegisterASRequest.ProtoReflect.Descriptor instead.
+func (*RegisterASRequest) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_account_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *RegisterASRequest) GetId() string {
+ if x != nil {
+ return x.Id
+ }
+ return ""
+}
+
+func (x *RegisterASRequest) GetSignedChallenge() *crypto.SignedMessage {
+ if x != nil {
+ return x.SignedChallenge
+ }
+ return nil
+}
+
+type RegisterASResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ JwtPublisher string `protobuf:"bytes,1,opt,name=jwt_publisher,json=jwtPublisher,proto3" json:"jwt_publisher,omitempty"`
+ JwtRedemption string `protobuf:"bytes,2,opt,name=jwt_redemption,json=jwtRedemption,proto3" json:"jwt_redemption,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *RegisterASResponse) Reset() {
+ *x = RegisterASResponse{}
+ mi := &file_proto_hummingbird_v1_account_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *RegisterASResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RegisterASResponse) ProtoMessage() {}
+
+func (x *RegisterASResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_account_proto_msgTypes[3]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RegisterASResponse.ProtoReflect.Descriptor instead.
+func (*RegisterASResponse) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_account_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *RegisterASResponse) GetJwtPublisher() string {
+ if x != nil {
+ return x.JwtPublisher
+ }
+ return ""
+}
+
+func (x *RegisterASResponse) GetJwtRedemption() string {
+ if x != nil {
+ return x.JwtRedemption
+ }
+ return ""
+}
+
+type CreateChallengeRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Ia uint64 `protobuf:"varint,1,opt,name=ia,proto3" json:"ia,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CreateChallengeRequest) Reset() {
+ *x = CreateChallengeRequest{}
+ mi := &file_proto_hummingbird_v1_account_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CreateChallengeRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateChallengeRequest) ProtoMessage() {}
+
+func (x *CreateChallengeRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_account_proto_msgTypes[4]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateChallengeRequest.ProtoReflect.Descriptor instead.
+func (*CreateChallengeRequest) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_account_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *CreateChallengeRequest) GetIa() uint64 {
+ if x != nil {
+ return x.Ia
+ }
+ return 0
+}
+
+type CreateChallengeResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+ Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CreateChallengeResponse) Reset() {
+ *x = CreateChallengeResponse{}
+ mi := &file_proto_hummingbird_v1_account_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CreateChallengeResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateChallengeResponse) ProtoMessage() {}
+
+func (x *CreateChallengeResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_account_proto_msgTypes[5]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateChallengeResponse.ProtoReflect.Descriptor instead.
+func (*CreateChallengeResponse) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_account_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *CreateChallengeResponse) GetId() string {
+ if x != nil {
+ return x.Id
+ }
+ return ""
+}
+
+func (x *CreateChallengeResponse) GetValue() []byte {
+ if x != nil {
+ return x.Value
+ }
+ return nil
+}
+
+type JWTResetRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *JWTResetRequest) Reset() {
+ *x = JWTResetRequest{}
+ mi := &file_proto_hummingbird_v1_account_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *JWTResetRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*JWTResetRequest) ProtoMessage() {}
+
+func (x *JWTResetRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_account_proto_msgTypes[6]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use JWTResetRequest.ProtoReflect.Descriptor instead.
+func (*JWTResetRequest) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_account_proto_rawDescGZIP(), []int{6}
+}
+
+type JWTResetResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *JWTResetResponse) Reset() {
+ *x = JWTResetResponse{}
+ mi := &file_proto_hummingbird_v1_account_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *JWTResetResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*JWTResetResponse) ProtoMessage() {}
+
+func (x *JWTResetResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_account_proto_msgTypes[7]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use JWTResetResponse.ProtoReflect.Descriptor instead.
+func (*JWTResetResponse) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_account_proto_rawDescGZIP(), []int{7}
+}
+
+var File_proto_hummingbird_v1_account_proto protoreflect.FileDescriptor
+
+const file_proto_hummingbird_v1_account_proto_rawDesc = "" +
+ "\n" +
+ "\"proto/hummingbird/v1/account.proto\x12\x14proto.hummingbird.v1\x1a\x1cproto/crypto/v1/signed.proto\"0\n" +
+ "\x12SetPasswordRequest\x12\x1a\n" +
+ "\bpassword\x18\x01 \x01(\tR\bpassword\"\x15\n" +
+ "\x13SetPasswordResponse\"n\n" +
+ "\x11RegisterASRequest\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\tR\x02id\x12I\n" +
+ "\x10signed_challenge\x18\x02 \x01(\v2\x1e.proto.crypto.v1.SignedMessageR\x0fsignedChallenge\"`\n" +
+ "\x12RegisterASResponse\x12#\n" +
+ "\rjwt_publisher\x18\x01 \x01(\tR\fjwtPublisher\x12%\n" +
+ "\x0ejwt_redemption\x18\x02 \x01(\tR\rjwtRedemption\"(\n" +
+ "\x16CreateChallengeRequest\x12\x0e\n" +
+ "\x02ia\x18\x01 \x01(\x04R\x02ia\"?\n" +
+ "\x17CreateChallengeResponse\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" +
+ "\x05value\x18\x02 \x01(\fR\x05value\"\x11\n" +
+ "\x0fJWTResetRequest\"\x12\n" +
+ "\x10JWTResetResponse2\xa8\x03\n" +
+ "\x0eAccountService\x12[\n" +
+ "\bResetJWT\x12%.proto.hummingbird.v1.JWTResetRequest\x1a&.proto.hummingbird.v1.JWTResetResponse\"\x00\x12p\n" +
+ "\x0fCreateChallenge\x12,.proto.hummingbird.v1.CreateChallengeRequest\x1a-.proto.hummingbird.v1.CreateChallengeResponse\"\x00\x12a\n" +
+ "\n" +
+ "RegisterAS\x12'.proto.hummingbird.v1.RegisterASRequest\x1a(.proto.hummingbird.v1.RegisterASResponse\"\x00\x12d\n" +
+ "\vSetPassword\x12(.proto.hummingbird.v1.SetPasswordRequest\x1a).proto.hummingbird.v1.SetPasswordResponse\"\x00B3Z1github.com/scionproto/scion/pkg/proto/hummingbirdb\x06proto3"
+
+var (
+ file_proto_hummingbird_v1_account_proto_rawDescOnce sync.Once
+ file_proto_hummingbird_v1_account_proto_rawDescData []byte
+)
+
+func file_proto_hummingbird_v1_account_proto_rawDescGZIP() []byte {
+ file_proto_hummingbird_v1_account_proto_rawDescOnce.Do(func() {
+ file_proto_hummingbird_v1_account_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_hummingbird_v1_account_proto_rawDesc), len(file_proto_hummingbird_v1_account_proto_rawDesc)))
+ })
+ return file_proto_hummingbird_v1_account_proto_rawDescData
+}
+
+var file_proto_hummingbird_v1_account_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
+var file_proto_hummingbird_v1_account_proto_goTypes = []any{
+ (*SetPasswordRequest)(nil), // 0: proto.hummingbird.v1.SetPasswordRequest
+ (*SetPasswordResponse)(nil), // 1: proto.hummingbird.v1.SetPasswordResponse
+ (*RegisterASRequest)(nil), // 2: proto.hummingbird.v1.RegisterASRequest
+ (*RegisterASResponse)(nil), // 3: proto.hummingbird.v1.RegisterASResponse
+ (*CreateChallengeRequest)(nil), // 4: proto.hummingbird.v1.CreateChallengeRequest
+ (*CreateChallengeResponse)(nil), // 5: proto.hummingbird.v1.CreateChallengeResponse
+ (*JWTResetRequest)(nil), // 6: proto.hummingbird.v1.JWTResetRequest
+ (*JWTResetResponse)(nil), // 7: proto.hummingbird.v1.JWTResetResponse
+ (*crypto.SignedMessage)(nil), // 8: proto.crypto.v1.SignedMessage
+}
+var file_proto_hummingbird_v1_account_proto_depIdxs = []int32{
+ 8, // 0: proto.hummingbird.v1.RegisterASRequest.signed_challenge:type_name -> proto.crypto.v1.SignedMessage
+ 6, // 1: proto.hummingbird.v1.AccountService.ResetJWT:input_type -> proto.hummingbird.v1.JWTResetRequest
+ 4, // 2: proto.hummingbird.v1.AccountService.CreateChallenge:input_type -> proto.hummingbird.v1.CreateChallengeRequest
+ 2, // 3: proto.hummingbird.v1.AccountService.RegisterAS:input_type -> proto.hummingbird.v1.RegisterASRequest
+ 0, // 4: proto.hummingbird.v1.AccountService.SetPassword:input_type -> proto.hummingbird.v1.SetPasswordRequest
+ 7, // 5: proto.hummingbird.v1.AccountService.ResetJWT:output_type -> proto.hummingbird.v1.JWTResetResponse
+ 5, // 6: proto.hummingbird.v1.AccountService.CreateChallenge:output_type -> proto.hummingbird.v1.CreateChallengeResponse
+ 3, // 7: proto.hummingbird.v1.AccountService.RegisterAS:output_type -> proto.hummingbird.v1.RegisterASResponse
+ 1, // 8: proto.hummingbird.v1.AccountService.SetPassword:output_type -> proto.hummingbird.v1.SetPasswordResponse
+ 5, // [5:9] is the sub-list for method output_type
+ 1, // [1:5] is the sub-list for method input_type
+ 1, // [1:1] is the sub-list for extension type_name
+ 1, // [1:1] is the sub-list for extension extendee
+ 0, // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_proto_hummingbird_v1_account_proto_init() }
+func file_proto_hummingbird_v1_account_proto_init() {
+ if File_proto_hummingbird_v1_account_proto != nil {
+ return
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_hummingbird_v1_account_proto_rawDesc), len(file_proto_hummingbird_v1_account_proto_rawDesc)),
+ NumEnums: 0,
+ NumMessages: 8,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_proto_hummingbird_v1_account_proto_goTypes,
+ DependencyIndexes: file_proto_hummingbird_v1_account_proto_depIdxs,
+ MessageInfos: file_proto_hummingbird_v1_account_proto_msgTypes,
+ }.Build()
+ File_proto_hummingbird_v1_account_proto = out.File
+ file_proto_hummingbird_v1_account_proto_goTypes = nil
+ file_proto_hummingbird_v1_account_proto_depIdxs = nil
+}
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ context.Context
+var _ grpc.ClientConnInterface
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+const _ = grpc.SupportPackageIsVersion6
+
+// AccountServiceClient is the client API for AccountService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
+type AccountServiceClient interface {
+ ResetJWT(ctx context.Context, in *JWTResetRequest, opts ...grpc.CallOption) (*JWTResetResponse, error)
+ CreateChallenge(ctx context.Context, in *CreateChallengeRequest, opts ...grpc.CallOption) (*CreateChallengeResponse, error)
+ RegisterAS(ctx context.Context, in *RegisterASRequest, opts ...grpc.CallOption) (*RegisterASResponse, error)
+ SetPassword(ctx context.Context, in *SetPasswordRequest, opts ...grpc.CallOption) (*SetPasswordResponse, error)
+}
+
+type accountServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewAccountServiceClient(cc grpc.ClientConnInterface) AccountServiceClient {
+ return &accountServiceClient{cc}
+}
+
+func (c *accountServiceClient) ResetJWT(ctx context.Context, in *JWTResetRequest, opts ...grpc.CallOption) (*JWTResetResponse, error) {
+ out := new(JWTResetResponse)
+ err := c.cc.Invoke(ctx, "/proto.hummingbird.v1.AccountService/ResetJWT", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *accountServiceClient) CreateChallenge(ctx context.Context, in *CreateChallengeRequest, opts ...grpc.CallOption) (*CreateChallengeResponse, error) {
+ out := new(CreateChallengeResponse)
+ err := c.cc.Invoke(ctx, "/proto.hummingbird.v1.AccountService/CreateChallenge", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *accountServiceClient) RegisterAS(ctx context.Context, in *RegisterASRequest, opts ...grpc.CallOption) (*RegisterASResponse, error) {
+ out := new(RegisterASResponse)
+ err := c.cc.Invoke(ctx, "/proto.hummingbird.v1.AccountService/RegisterAS", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *accountServiceClient) SetPassword(ctx context.Context, in *SetPasswordRequest, opts ...grpc.CallOption) (*SetPasswordResponse, error) {
+ out := new(SetPasswordResponse)
+ err := c.cc.Invoke(ctx, "/proto.hummingbird.v1.AccountService/SetPassword", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// AccountServiceServer is the server API for AccountService service.
+type AccountServiceServer interface {
+ ResetJWT(context.Context, *JWTResetRequest) (*JWTResetResponse, error)
+ CreateChallenge(context.Context, *CreateChallengeRequest) (*CreateChallengeResponse, error)
+ RegisterAS(context.Context, *RegisterASRequest) (*RegisterASResponse, error)
+ SetPassword(context.Context, *SetPasswordRequest) (*SetPasswordResponse, error)
+}
+
+// UnimplementedAccountServiceServer can be embedded to have forward compatible implementations.
+type UnimplementedAccountServiceServer struct {
+}
+
+func (*UnimplementedAccountServiceServer) ResetJWT(context.Context, *JWTResetRequest) (*JWTResetResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method ResetJWT not implemented")
+}
+func (*UnimplementedAccountServiceServer) CreateChallenge(context.Context, *CreateChallengeRequest) (*CreateChallengeResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method CreateChallenge not implemented")
+}
+func (*UnimplementedAccountServiceServer) RegisterAS(context.Context, *RegisterASRequest) (*RegisterASResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method RegisterAS not implemented")
+}
+func (*UnimplementedAccountServiceServer) SetPassword(context.Context, *SetPasswordRequest) (*SetPasswordResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method SetPassword not implemented")
+}
+
+func RegisterAccountServiceServer(s *grpc.Server, srv AccountServiceServer) {
+ s.RegisterService(&_AccountService_serviceDesc, srv)
+}
+
+func _AccountService_ResetJWT_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(JWTResetRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(AccountServiceServer).ResetJWT(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/proto.hummingbird.v1.AccountService/ResetJWT",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(AccountServiceServer).ResetJWT(ctx, req.(*JWTResetRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _AccountService_CreateChallenge_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(CreateChallengeRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(AccountServiceServer).CreateChallenge(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/proto.hummingbird.v1.AccountService/CreateChallenge",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(AccountServiceServer).CreateChallenge(ctx, req.(*CreateChallengeRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _AccountService_RegisterAS_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(RegisterASRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(AccountServiceServer).RegisterAS(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/proto.hummingbird.v1.AccountService/RegisterAS",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(AccountServiceServer).RegisterAS(ctx, req.(*RegisterASRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _AccountService_SetPassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(SetPasswordRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(AccountServiceServer).SetPassword(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/proto.hummingbird.v1.AccountService/SetPassword",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(AccountServiceServer).SetPassword(ctx, req.(*SetPasswordRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+var _AccountService_serviceDesc = grpc.ServiceDesc{
+ ServiceName: "proto.hummingbird.v1.AccountService",
+ HandlerType: (*AccountServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "ResetJWT",
+ Handler: _AccountService_ResetJWT_Handler,
+ },
+ {
+ MethodName: "CreateChallenge",
+ Handler: _AccountService_CreateChallenge_Handler,
+ },
+ {
+ MethodName: "RegisterAS",
+ Handler: _AccountService_RegisterAS_Handler,
+ },
+ {
+ MethodName: "SetPassword",
+ Handler: _AccountService_SetPassword_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "proto/hummingbird/v1/account.proto",
+}
diff --git a/pkg/proto/hummingbird/marketplace.pb.go b/pkg/proto/hummingbird/marketplace.pb.go
new file mode 100644
index 0000000000..43438c434b
--- /dev/null
+++ b/pkg/proto/hummingbird/marketplace.pb.go
@@ -0,0 +1,2821 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.36.10
+// protoc v6.31.1
+// source: proto/hummingbird/v1/marketplace.proto
+
+package hummingbird
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+ reflect "reflect"
+ sync "sync"
+ unsafe "unsafe"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type PricingStrategy int32
+
+const (
+ PricingStrategy_static_pricing PricingStrategy = 0
+ PricingStrategy_dynamic_pricing PricingStrategy = 1
+)
+
+// Enum value maps for PricingStrategy.
+var (
+ PricingStrategy_name = map[int32]string{
+ 0: "static_pricing",
+ 1: "dynamic_pricing",
+ }
+ PricingStrategy_value = map[string]int32{
+ "static_pricing": 0,
+ "dynamic_pricing": 1,
+ }
+)
+
+func (x PricingStrategy) Enum() *PricingStrategy {
+ p := new(PricingStrategy)
+ *p = x
+ return p
+}
+
+func (x PricingStrategy) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (PricingStrategy) Descriptor() protoreflect.EnumDescriptor {
+ return file_proto_hummingbird_v1_marketplace_proto_enumTypes[0].Descriptor()
+}
+
+func (PricingStrategy) Type() protoreflect.EnumType {
+ return &file_proto_hummingbird_v1_marketplace_proto_enumTypes[0]
+}
+
+func (x PricingStrategy) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use PricingStrategy.Descriptor instead.
+func (PricingStrategy) EnumDescriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{0}
+}
+
+type UpdateAssetsRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Assets []*AssetUpdate `protobuf:"bytes,1,rep,name=assets,proto3" json:"assets,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *UpdateAssetsRequest) Reset() {
+ *x = UpdateAssetsRequest{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *UpdateAssetsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*UpdateAssetsRequest) ProtoMessage() {}
+
+func (x *UpdateAssetsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use UpdateAssetsRequest.ProtoReflect.Descriptor instead.
+func (*UpdateAssetsRequest) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *UpdateAssetsRequest) GetAssets() []*AssetUpdate {
+ if x != nil {
+ return x.Assets
+ }
+ return nil
+}
+
+type AssetUpdate struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ AssetId string `protobuf:"bytes,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"`
+ // Types that are valid to be assigned to Operation:
+ //
+ // *AssetUpdate_Update
+ // *AssetUpdate_Remove
+ Operation isAssetUpdate_Operation `protobuf_oneof:"operation"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *AssetUpdate) Reset() {
+ *x = AssetUpdate{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AssetUpdate) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AssetUpdate) ProtoMessage() {}
+
+func (x *AssetUpdate) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AssetUpdate.ProtoReflect.Descriptor instead.
+func (*AssetUpdate) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *AssetUpdate) GetAssetId() string {
+ if x != nil {
+ return x.AssetId
+ }
+ return ""
+}
+
+func (x *AssetUpdate) GetOperation() isAssetUpdate_Operation {
+ if x != nil {
+ return x.Operation
+ }
+ return nil
+}
+
+func (x *AssetUpdate) GetUpdate() *PublisherAsset {
+ if x != nil {
+ if x, ok := x.Operation.(*AssetUpdate_Update); ok {
+ return x.Update
+ }
+ }
+ return nil
+}
+
+func (x *AssetUpdate) GetRemove() *Remove {
+ if x != nil {
+ if x, ok := x.Operation.(*AssetUpdate_Remove); ok {
+ return x.Remove
+ }
+ }
+ return nil
+}
+
+type isAssetUpdate_Operation interface {
+ isAssetUpdate_Operation()
+}
+
+type AssetUpdate_Update struct {
+ Update *PublisherAsset `protobuf:"bytes,2,opt,name=update,proto3,oneof"`
+}
+
+type AssetUpdate_Remove struct {
+ Remove *Remove `protobuf:"bytes,3,opt,name=remove,proto3,oneof"`
+}
+
+func (*AssetUpdate_Update) isAssetUpdate_Operation() {}
+
+func (*AssetUpdate_Remove) isAssetUpdate_Operation() {}
+
+type Remove struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *Remove) Reset() {
+ *x = Remove{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *Remove) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Remove) ProtoMessage() {}
+
+func (x *Remove) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[2]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Remove.ProtoReflect.Descriptor instead.
+func (*Remove) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{2}
+}
+
+type UpdateAssetsResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Result []*UpdateAssetResult `protobuf:"bytes,1,rep,name=result,proto3" json:"result,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *UpdateAssetsResponse) Reset() {
+ *x = UpdateAssetsResponse{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *UpdateAssetsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*UpdateAssetsResponse) ProtoMessage() {}
+
+func (x *UpdateAssetsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[3]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use UpdateAssetsResponse.ProtoReflect.Descriptor instead.
+func (*UpdateAssetsResponse) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *UpdateAssetsResponse) GetResult() []*UpdateAssetResult {
+ if x != nil {
+ return x.Result
+ }
+ return nil
+}
+
+type UpdateAssetResult struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // Types that are valid to be assigned to ResultType:
+ //
+ // *UpdateAssetResult_NewId
+ // *UpdateAssetResult_Error
+ ResultType isUpdateAssetResult_ResultType `protobuf_oneof:"result_type"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *UpdateAssetResult) Reset() {
+ *x = UpdateAssetResult{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *UpdateAssetResult) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*UpdateAssetResult) ProtoMessage() {}
+
+func (x *UpdateAssetResult) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[4]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use UpdateAssetResult.ProtoReflect.Descriptor instead.
+func (*UpdateAssetResult) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *UpdateAssetResult) GetResultType() isUpdateAssetResult_ResultType {
+ if x != nil {
+ return x.ResultType
+ }
+ return nil
+}
+
+func (x *UpdateAssetResult) GetNewId() string {
+ if x != nil {
+ if x, ok := x.ResultType.(*UpdateAssetResult_NewId); ok {
+ return x.NewId
+ }
+ }
+ return ""
+}
+
+func (x *UpdateAssetResult) GetError() string {
+ if x != nil {
+ if x, ok := x.ResultType.(*UpdateAssetResult_Error); ok {
+ return x.Error
+ }
+ }
+ return ""
+}
+
+type isUpdateAssetResult_ResultType interface {
+ isUpdateAssetResult_ResultType()
+}
+
+type UpdateAssetResult_NewId struct {
+ NewId string `protobuf:"bytes,1,opt,name=new_id,json=newId,proto3,oneof"`
+}
+
+type UpdateAssetResult_Error struct {
+ Error string `protobuf:"bytes,2,opt,name=error,proto3,oneof"`
+}
+
+func (*UpdateAssetResult_NewId) isUpdateAssetResult_ResultType() {}
+
+func (*UpdateAssetResult_Error) isUpdateAssetResult_ResultType() {}
+
+type StatisticsRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ IfIdIngress *uint32 `protobuf:"varint,1,opt,name=if_id_ingress,json=ifIdIngress,proto3,oneof" json:"if_id_ingress,omitempty"`
+ IfIdEgress *uint32 `protobuf:"varint,2,opt,name=if_id_egress,json=ifIdEgress,proto3,oneof" json:"if_id_egress,omitempty"`
+ Start *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=start,proto3" json:"start,omitempty"`
+ End *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=end,proto3" json:"end,omitempty"`
+ Step uint32 `protobuf:"varint,5,opt,name=step,proto3" json:"step,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *StatisticsRequest) Reset() {
+ *x = StatisticsRequest{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *StatisticsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StatisticsRequest) ProtoMessage() {}
+
+func (x *StatisticsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[5]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use StatisticsRequest.ProtoReflect.Descriptor instead.
+func (*StatisticsRequest) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *StatisticsRequest) GetIfIdIngress() uint32 {
+ if x != nil && x.IfIdIngress != nil {
+ return *x.IfIdIngress
+ }
+ return 0
+}
+
+func (x *StatisticsRequest) GetIfIdEgress() uint32 {
+ if x != nil && x.IfIdEgress != nil {
+ return *x.IfIdEgress
+ }
+ return 0
+}
+
+func (x *StatisticsRequest) GetStart() *timestamppb.Timestamp {
+ if x != nil {
+ return x.Start
+ }
+ return nil
+}
+
+func (x *StatisticsRequest) GetEnd() *timestamppb.Timestamp {
+ if x != nil {
+ return x.End
+ }
+ return nil
+}
+
+func (x *StatisticsRequest) GetStep() uint32 {
+ if x != nil {
+ return x.Step
+ }
+ return 0
+}
+
+type StatisticsResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Statistics []*StatisticsResponseEntry `protobuf:"bytes,1,rep,name=statistics,proto3" json:"statistics,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *StatisticsResponse) Reset() {
+ *x = StatisticsResponse{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *StatisticsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StatisticsResponse) ProtoMessage() {}
+
+func (x *StatisticsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[6]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use StatisticsResponse.ProtoReflect.Descriptor instead.
+func (*StatisticsResponse) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *StatisticsResponse) GetStatistics() []*StatisticsResponseEntry {
+ if x != nil {
+ return x.Statistics
+ }
+ return nil
+}
+
+type StatisticsResponseEntry struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Revenue uint64 `protobuf:"varint,1,opt,name=revenue,proto3" json:"revenue,omitempty"`
+ BandwidthUtilization float64 `protobuf:"fixed64,2,opt,name=bandwidth_utilization,json=bandwidthUtilization,proto3" json:"bandwidth_utilization,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *StatisticsResponseEntry) Reset() {
+ *x = StatisticsResponseEntry{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *StatisticsResponseEntry) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StatisticsResponseEntry) ProtoMessage() {}
+
+func (x *StatisticsResponseEntry) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[7]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use StatisticsResponseEntry.ProtoReflect.Descriptor instead.
+func (*StatisticsResponseEntry) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *StatisticsResponseEntry) GetRevenue() uint64 {
+ if x != nil {
+ return x.Revenue
+ }
+ return 0
+}
+
+func (x *StatisticsResponseEntry) GetBandwidthUtilization() float64 {
+ if x != nil {
+ return x.BandwidthUtilization
+ }
+ return 0
+}
+
+type SplitAssetRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ AssetId string `protobuf:"bytes,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"`
+ // Types that are valid to be assigned to SplitOption:
+ //
+ // *SplitAssetRequest_BwSplit
+ // *SplitAssetRequest_TimeSplit
+ SplitOption isSplitAssetRequest_SplitOption `protobuf_oneof:"split_option"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SplitAssetRequest) Reset() {
+ *x = SplitAssetRequest{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[8]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SplitAssetRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SplitAssetRequest) ProtoMessage() {}
+
+func (x *SplitAssetRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[8]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SplitAssetRequest.ProtoReflect.Descriptor instead.
+func (*SplitAssetRequest) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *SplitAssetRequest) GetAssetId() string {
+ if x != nil {
+ return x.AssetId
+ }
+ return ""
+}
+
+func (x *SplitAssetRequest) GetSplitOption() isSplitAssetRequest_SplitOption {
+ if x != nil {
+ return x.SplitOption
+ }
+ return nil
+}
+
+func (x *SplitAssetRequest) GetBwSplit() uint32 {
+ if x != nil {
+ if x, ok := x.SplitOption.(*SplitAssetRequest_BwSplit); ok {
+ return x.BwSplit
+ }
+ }
+ return 0
+}
+
+func (x *SplitAssetRequest) GetTimeSplit() *timestamppb.Timestamp {
+ if x != nil {
+ if x, ok := x.SplitOption.(*SplitAssetRequest_TimeSplit); ok {
+ return x.TimeSplit
+ }
+ }
+ return nil
+}
+
+type isSplitAssetRequest_SplitOption interface {
+ isSplitAssetRequest_SplitOption()
+}
+
+type SplitAssetRequest_BwSplit struct {
+ BwSplit uint32 `protobuf:"varint,2,opt,name=bw_split,json=bwSplit,proto3,oneof"`
+}
+
+type SplitAssetRequest_TimeSplit struct {
+ TimeSplit *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=time_split,json=timeSplit,proto3,oneof"`
+}
+
+func (*SplitAssetRequest_BwSplit) isSplitAssetRequest_SplitOption() {}
+
+func (*SplitAssetRequest_TimeSplit) isSplitAssetRequest_SplitOption() {}
+
+type SplitAssetResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ AssetId_1 string `protobuf:"bytes,1,opt,name=asset_id_1,json=assetId1,proto3" json:"asset_id_1,omitempty"`
+ AssetId_2 string `protobuf:"bytes,2,opt,name=asset_id_2,json=assetId2,proto3" json:"asset_id_2,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SplitAssetResponse) Reset() {
+ *x = SplitAssetResponse{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[9]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SplitAssetResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SplitAssetResponse) ProtoMessage() {}
+
+func (x *SplitAssetResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[9]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SplitAssetResponse.ProtoReflect.Descriptor instead.
+func (*SplitAssetResponse) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *SplitAssetResponse) GetAssetId_1() string {
+ if x != nil {
+ return x.AssetId_1
+ }
+ return ""
+}
+
+func (x *SplitAssetResponse) GetAssetId_2() string {
+ if x != nil {
+ return x.AssetId_2
+ }
+ return ""
+}
+
+type CombineAssetRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ AssetId_1 string `protobuf:"bytes,1,opt,name=asset_id_1,json=assetId1,proto3" json:"asset_id_1,omitempty"`
+ AssetId_2 string `protobuf:"bytes,2,opt,name=asset_id_2,json=assetId2,proto3" json:"asset_id_2,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CombineAssetRequest) Reset() {
+ *x = CombineAssetRequest{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[10]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CombineAssetRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CombineAssetRequest) ProtoMessage() {}
+
+func (x *CombineAssetRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[10]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CombineAssetRequest.ProtoReflect.Descriptor instead.
+func (*CombineAssetRequest) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *CombineAssetRequest) GetAssetId_1() string {
+ if x != nil {
+ return x.AssetId_1
+ }
+ return ""
+}
+
+func (x *CombineAssetRequest) GetAssetId_2() string {
+ if x != nil {
+ return x.AssetId_2
+ }
+ return ""
+}
+
+type CombineAssetResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ AssetId string `protobuf:"bytes,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CombineAssetResponse) Reset() {
+ *x = CombineAssetResponse{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[11]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CombineAssetResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CombineAssetResponse) ProtoMessage() {}
+
+func (x *CombineAssetResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[11]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CombineAssetResponse.ProtoReflect.Descriptor instead.
+func (*CombineAssetResponse) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{11}
+}
+
+func (x *CombineAssetResponse) GetAssetId() string {
+ if x != nil {
+ return x.AssetId
+ }
+ return ""
+}
+
+type MarketplaceInfoRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *MarketplaceInfoRequest) Reset() {
+ *x = MarketplaceInfoRequest{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[12]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *MarketplaceInfoRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*MarketplaceInfoRequest) ProtoMessage() {}
+
+func (x *MarketplaceInfoRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[12]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use MarketplaceInfoRequest.ProtoReflect.Descriptor instead.
+func (*MarketplaceInfoRequest) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{12}
+}
+
+type MarketplaceInfoResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ ApiMajorVersion uint32 `protobuf:"varint,1,opt,name=api_major_version,json=apiMajorVersion,proto3" json:"api_major_version,omitempty"`
+ ApiMinorVersion uint32 `protobuf:"varint,2,opt,name=api_minor_version,json=apiMinorVersion,proto3" json:"api_minor_version,omitempty"`
+ Currency string `protobuf:"bytes,3,opt,name=currency,proto3" json:"currency,omitempty"`
+ CurrencyExponent uint32 `protobuf:"varint,4,opt,name=currency_exponent,json=currencyExponent,proto3" json:"currency_exponent,omitempty"`
+ MaxStatisticsGranularity uint32 `protobuf:"varint,5,opt,name=max_statistics_granularity,json=maxStatisticsGranularity,proto3" json:"max_statistics_granularity,omitempty"`
+ PricingStrategy PricingStrategy `protobuf:"varint,6,opt,name=pricing_strategy,json=pricingStrategy,proto3,enum=proto.hummingbird.v1.PricingStrategy" json:"pricing_strategy,omitempty"`
+ TransactionFeeRelative float32 `protobuf:"fixed32,7,opt,name=transaction_fee_relative,json=transactionFeeRelative,proto3" json:"transaction_fee_relative,omitempty"`
+ TransactionFeeAbsolute uint64 `protobuf:"varint,8,opt,name=transaction_fee_absolute,json=transactionFeeAbsolute,proto3" json:"transaction_fee_absolute,omitempty"`
+ SplitCombineFeeAbsolute uint64 `protobuf:"varint,9,opt,name=split_combine_fee_absolute,json=splitCombineFeeAbsolute,proto3" json:"split_combine_fee_absolute,omitempty"`
+ SupportsRedemptionDelegation bool `protobuf:"varint,10,opt,name=supports_redemption_delegation,json=supportsRedemptionDelegation,proto3" json:"supports_redemption_delegation,omitempty"`
+ DelegationHourlyFee uint64 `protobuf:"varint,11,opt,name=delegation_hourly_fee,json=delegationHourlyFee,proto3" json:"delegation_hourly_fee,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *MarketplaceInfoResponse) Reset() {
+ *x = MarketplaceInfoResponse{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[13]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *MarketplaceInfoResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*MarketplaceInfoResponse) ProtoMessage() {}
+
+func (x *MarketplaceInfoResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[13]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use MarketplaceInfoResponse.ProtoReflect.Descriptor instead.
+func (*MarketplaceInfoResponse) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{13}
+}
+
+func (x *MarketplaceInfoResponse) GetApiMajorVersion() uint32 {
+ if x != nil {
+ return x.ApiMajorVersion
+ }
+ return 0
+}
+
+func (x *MarketplaceInfoResponse) GetApiMinorVersion() uint32 {
+ if x != nil {
+ return x.ApiMinorVersion
+ }
+ return 0
+}
+
+func (x *MarketplaceInfoResponse) GetCurrency() string {
+ if x != nil {
+ return x.Currency
+ }
+ return ""
+}
+
+func (x *MarketplaceInfoResponse) GetCurrencyExponent() uint32 {
+ if x != nil {
+ return x.CurrencyExponent
+ }
+ return 0
+}
+
+func (x *MarketplaceInfoResponse) GetMaxStatisticsGranularity() uint32 {
+ if x != nil {
+ return x.MaxStatisticsGranularity
+ }
+ return 0
+}
+
+func (x *MarketplaceInfoResponse) GetPricingStrategy() PricingStrategy {
+ if x != nil {
+ return x.PricingStrategy
+ }
+ return PricingStrategy_static_pricing
+}
+
+func (x *MarketplaceInfoResponse) GetTransactionFeeRelative() float32 {
+ if x != nil {
+ return x.TransactionFeeRelative
+ }
+ return 0
+}
+
+func (x *MarketplaceInfoResponse) GetTransactionFeeAbsolute() uint64 {
+ if x != nil {
+ return x.TransactionFeeAbsolute
+ }
+ return 0
+}
+
+func (x *MarketplaceInfoResponse) GetSplitCombineFeeAbsolute() uint64 {
+ if x != nil {
+ return x.SplitCombineFeeAbsolute
+ }
+ return 0
+}
+
+func (x *MarketplaceInfoResponse) GetSupportsRedemptionDelegation() bool {
+ if x != nil {
+ return x.SupportsRedemptionDelegation
+ }
+ return false
+}
+
+func (x *MarketplaceInfoResponse) GetDelegationHourlyFee() uint64 {
+ if x != nil {
+ return x.DelegationHourlyFee
+ }
+ return 0
+}
+
+type PublishAssetRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Asset *PublisherAsset `protobuf:"bytes,1,opt,name=asset,proto3" json:"asset,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *PublishAssetRequest) Reset() {
+ *x = PublishAssetRequest{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[14]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *PublishAssetRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PublishAssetRequest) ProtoMessage() {}
+
+func (x *PublishAssetRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[14]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use PublishAssetRequest.ProtoReflect.Descriptor instead.
+func (*PublishAssetRequest) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{14}
+}
+
+func (x *PublishAssetRequest) GetAsset() *PublisherAsset {
+ if x != nil {
+ return x.Asset
+ }
+ return nil
+}
+
+type PublishAssetResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ AssetId string `protobuf:"bytes,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *PublishAssetResponse) Reset() {
+ *x = PublishAssetResponse{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[15]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *PublishAssetResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PublishAssetResponse) ProtoMessage() {}
+
+func (x *PublishAssetResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[15]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use PublishAssetResponse.ProtoReflect.Descriptor instead.
+func (*PublishAssetResponse) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{15}
+}
+
+func (x *PublishAssetResponse) GetAssetId() string {
+ if x != nil {
+ return x.AssetId
+ }
+ return ""
+}
+
+type PublisherAsset struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ IfIdIngress *uint32 `protobuf:"varint,1,opt,name=if_id_ingress,json=ifIdIngress,proto3,oneof" json:"if_id_ingress,omitempty"`
+ IfIdEgress *uint32 `protobuf:"varint,2,opt,name=if_id_egress,json=ifIdEgress,proto3,oneof" json:"if_id_egress,omitempty"`
+ Bandwidth uint32 `protobuf:"varint,3,opt,name=bandwidth,proto3" json:"bandwidth,omitempty"`
+ BandwidthMin uint32 `protobuf:"varint,4,opt,name=bandwidth_min,json=bandwidthMin,proto3" json:"bandwidth_min,omitempty"`
+ BandwidthMax uint32 `protobuf:"varint,5,opt,name=bandwidth_max,json=bandwidthMax,proto3" json:"bandwidth_max,omitempty"`
+ StartsAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=starts_at,json=startsAt,proto3" json:"starts_at,omitempty"`
+ StopsAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=stops_at,json=stopsAt,proto3" json:"stops_at,omitempty"`
+ TimeGranularity uint32 `protobuf:"varint,8,opt,name=time_granularity,json=timeGranularity,proto3" json:"time_granularity,omitempty"`
+ TimeMinDuration uint32 `protobuf:"varint,9,opt,name=time_min_duration,json=timeMinDuration,proto3" json:"time_min_duration,omitempty"`
+ Price uint32 `protobuf:"varint,10,opt,name=price,proto3" json:"price,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *PublisherAsset) Reset() {
+ *x = PublisherAsset{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[16]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *PublisherAsset) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PublisherAsset) ProtoMessage() {}
+
+func (x *PublisherAsset) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[16]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use PublisherAsset.ProtoReflect.Descriptor instead.
+func (*PublisherAsset) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{16}
+}
+
+func (x *PublisherAsset) GetIfIdIngress() uint32 {
+ if x != nil && x.IfIdIngress != nil {
+ return *x.IfIdIngress
+ }
+ return 0
+}
+
+func (x *PublisherAsset) GetIfIdEgress() uint32 {
+ if x != nil && x.IfIdEgress != nil {
+ return *x.IfIdEgress
+ }
+ return 0
+}
+
+func (x *PublisherAsset) GetBandwidth() uint32 {
+ if x != nil {
+ return x.Bandwidth
+ }
+ return 0
+}
+
+func (x *PublisherAsset) GetBandwidthMin() uint32 {
+ if x != nil {
+ return x.BandwidthMin
+ }
+ return 0
+}
+
+func (x *PublisherAsset) GetBandwidthMax() uint32 {
+ if x != nil {
+ return x.BandwidthMax
+ }
+ return 0
+}
+
+func (x *PublisherAsset) GetStartsAt() *timestamppb.Timestamp {
+ if x != nil {
+ return x.StartsAt
+ }
+ return nil
+}
+
+func (x *PublisherAsset) GetStopsAt() *timestamppb.Timestamp {
+ if x != nil {
+ return x.StopsAt
+ }
+ return nil
+}
+
+func (x *PublisherAsset) GetTimeGranularity() uint32 {
+ if x != nil {
+ return x.TimeGranularity
+ }
+ return 0
+}
+
+func (x *PublisherAsset) GetTimeMinDuration() uint32 {
+ if x != nil {
+ return x.TimeMinDuration
+ }
+ return 0
+}
+
+func (x *PublisherAsset) GetPrice() uint32 {
+ if x != nil {
+ return x.Price
+ }
+ return 0
+}
+
+type SearchAssetsRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Owned bool `protobuf:"varint,1,opt,name=owned,proto3" json:"owned,omitempty"`
+ Ia *uint64 `protobuf:"varint,2,opt,name=ia,proto3,oneof" json:"ia,omitempty"`
+ IfIdIngress *uint32 `protobuf:"varint,3,opt,name=if_id_ingress,json=ifIdIngress,proto3,oneof" json:"if_id_ingress,omitempty"`
+ IfIdEgress *uint32 `protobuf:"varint,4,opt,name=if_id_egress,json=ifIdEgress,proto3,oneof" json:"if_id_egress,omitempty"`
+ MinRequiredBw *uint32 `protobuf:"varint,5,opt,name=min_required_bw,json=minRequiredBw,proto3,oneof" json:"min_required_bw,omitempty"`
+ StartsAtLatest *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=starts_at_latest,json=startsAtLatest,proto3,oneof" json:"starts_at_latest,omitempty"`
+ StopsAtEarliest *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=stops_at_earliest,json=stopsAtEarliest,proto3,oneof" json:"stops_at_earliest,omitempty"`
+ Price *uint32 `protobuf:"varint,8,opt,name=price,proto3,oneof" json:"price,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SearchAssetsRequest) Reset() {
+ *x = SearchAssetsRequest{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[17]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SearchAssetsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SearchAssetsRequest) ProtoMessage() {}
+
+func (x *SearchAssetsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[17]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SearchAssetsRequest.ProtoReflect.Descriptor instead.
+func (*SearchAssetsRequest) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{17}
+}
+
+func (x *SearchAssetsRequest) GetOwned() bool {
+ if x != nil {
+ return x.Owned
+ }
+ return false
+}
+
+func (x *SearchAssetsRequest) GetIa() uint64 {
+ if x != nil && x.Ia != nil {
+ return *x.Ia
+ }
+ return 0
+}
+
+func (x *SearchAssetsRequest) GetIfIdIngress() uint32 {
+ if x != nil && x.IfIdIngress != nil {
+ return *x.IfIdIngress
+ }
+ return 0
+}
+
+func (x *SearchAssetsRequest) GetIfIdEgress() uint32 {
+ if x != nil && x.IfIdEgress != nil {
+ return *x.IfIdEgress
+ }
+ return 0
+}
+
+func (x *SearchAssetsRequest) GetMinRequiredBw() uint32 {
+ if x != nil && x.MinRequiredBw != nil {
+ return *x.MinRequiredBw
+ }
+ return 0
+}
+
+func (x *SearchAssetsRequest) GetStartsAtLatest() *timestamppb.Timestamp {
+ if x != nil {
+ return x.StartsAtLatest
+ }
+ return nil
+}
+
+func (x *SearchAssetsRequest) GetStopsAtEarliest() *timestamppb.Timestamp {
+ if x != nil {
+ return x.StopsAtEarliest
+ }
+ return nil
+}
+
+func (x *SearchAssetsRequest) GetPrice() uint32 {
+ if x != nil && x.Price != nil {
+ return *x.Price
+ }
+ return 0
+}
+
+type SearchAssetsResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Assets []*SearchAsset `protobuf:"bytes,1,rep,name=assets,proto3" json:"assets,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SearchAssetsResponse) Reset() {
+ *x = SearchAssetsResponse{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[18]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SearchAssetsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SearchAssetsResponse) ProtoMessage() {}
+
+func (x *SearchAssetsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[18]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SearchAssetsResponse.ProtoReflect.Descriptor instead.
+func (*SearchAssetsResponse) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{18}
+}
+
+func (x *SearchAssetsResponse) GetAssets() []*SearchAsset {
+ if x != nil {
+ return x.Assets
+ }
+ return nil
+}
+
+type SearchAsset struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ AssetId string `protobuf:"bytes,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"`
+ Ia uint64 `protobuf:"varint,2,opt,name=ia,proto3" json:"ia,omitempty"`
+ IfIdIngress *uint32 `protobuf:"varint,3,opt,name=if_id_ingress,json=ifIdIngress,proto3,oneof" json:"if_id_ingress,omitempty"`
+ IfIdEgress *uint32 `protobuf:"varint,4,opt,name=if_id_egress,json=ifIdEgress,proto3,oneof" json:"if_id_egress,omitempty"`
+ Bandwidth uint32 `protobuf:"varint,5,opt,name=bandwidth,proto3" json:"bandwidth,omitempty"`
+ BandwidthMin uint32 `protobuf:"varint,6,opt,name=bandwidth_min,json=bandwidthMin,proto3" json:"bandwidth_min,omitempty"`
+ BandwidthMax uint32 `protobuf:"varint,7,opt,name=bandwidth_max,json=bandwidthMax,proto3" json:"bandwidth_max,omitempty"`
+ StartsAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=starts_at,json=startsAt,proto3" json:"starts_at,omitempty"`
+ StopsAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=stops_at,json=stopsAt,proto3" json:"stops_at,omitempty"`
+ TimeGranularity uint32 `protobuf:"varint,10,opt,name=time_granularity,json=timeGranularity,proto3" json:"time_granularity,omitempty"`
+ TimeMinDuration uint32 `protobuf:"varint,11,opt,name=time_min_duration,json=timeMinDuration,proto3" json:"time_min_duration,omitempty"`
+ Price uint32 `protobuf:"varint,12,opt,name=price,proto3" json:"price,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SearchAsset) Reset() {
+ *x = SearchAsset{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[19]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SearchAsset) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SearchAsset) ProtoMessage() {}
+
+func (x *SearchAsset) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[19]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SearchAsset.ProtoReflect.Descriptor instead.
+func (*SearchAsset) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{19}
+}
+
+func (x *SearchAsset) GetAssetId() string {
+ if x != nil {
+ return x.AssetId
+ }
+ return ""
+}
+
+func (x *SearchAsset) GetIa() uint64 {
+ if x != nil {
+ return x.Ia
+ }
+ return 0
+}
+
+func (x *SearchAsset) GetIfIdIngress() uint32 {
+ if x != nil && x.IfIdIngress != nil {
+ return *x.IfIdIngress
+ }
+ return 0
+}
+
+func (x *SearchAsset) GetIfIdEgress() uint32 {
+ if x != nil && x.IfIdEgress != nil {
+ return *x.IfIdEgress
+ }
+ return 0
+}
+
+func (x *SearchAsset) GetBandwidth() uint32 {
+ if x != nil {
+ return x.Bandwidth
+ }
+ return 0
+}
+
+func (x *SearchAsset) GetBandwidthMin() uint32 {
+ if x != nil {
+ return x.BandwidthMin
+ }
+ return 0
+}
+
+func (x *SearchAsset) GetBandwidthMax() uint32 {
+ if x != nil {
+ return x.BandwidthMax
+ }
+ return 0
+}
+
+func (x *SearchAsset) GetStartsAt() *timestamppb.Timestamp {
+ if x != nil {
+ return x.StartsAt
+ }
+ return nil
+}
+
+func (x *SearchAsset) GetStopsAt() *timestamppb.Timestamp {
+ if x != nil {
+ return x.StopsAt
+ }
+ return nil
+}
+
+func (x *SearchAsset) GetTimeGranularity() uint32 {
+ if x != nil {
+ return x.TimeGranularity
+ }
+ return 0
+}
+
+func (x *SearchAsset) GetTimeMinDuration() uint32 {
+ if x != nil {
+ return x.TimeMinDuration
+ }
+ return 0
+}
+
+func (x *SearchAsset) GetPrice() uint32 {
+ if x != nil {
+ return x.Price
+ }
+ return 0
+}
+
+type BuyAssetsRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Assets []*BuyAsset `protobuf:"bytes,1,rep,name=assets,proto3" json:"assets,omitempty"`
+ MaxPrice uint64 `protobuf:"varint,2,opt,name=max_price,json=maxPrice,proto3" json:"max_price,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *BuyAssetsRequest) Reset() {
+ *x = BuyAssetsRequest{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[20]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *BuyAssetsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BuyAssetsRequest) ProtoMessage() {}
+
+func (x *BuyAssetsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[20]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use BuyAssetsRequest.ProtoReflect.Descriptor instead.
+func (*BuyAssetsRequest) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{20}
+}
+
+func (x *BuyAssetsRequest) GetAssets() []*BuyAsset {
+ if x != nil {
+ return x.Assets
+ }
+ return nil
+}
+
+func (x *BuyAssetsRequest) GetMaxPrice() uint64 {
+ if x != nil {
+ return x.MaxPrice
+ }
+ return 0
+}
+
+type BuyAsset struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ AssetId string `protobuf:"bytes,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"`
+ StartsAtExactly *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=starts_at_exactly,json=startsAtExactly,proto3" json:"starts_at_exactly,omitempty"`
+ StopsAtExactly *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=stops_at_exactly,json=stopsAtExactly,proto3" json:"stops_at_exactly,omitempty"`
+ BandwidthExact uint32 `protobuf:"varint,4,opt,name=bandwidth_exact,json=bandwidthExact,proto3" json:"bandwidth_exact,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *BuyAsset) Reset() {
+ *x = BuyAsset{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[21]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *BuyAsset) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BuyAsset) ProtoMessage() {}
+
+func (x *BuyAsset) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[21]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use BuyAsset.ProtoReflect.Descriptor instead.
+func (*BuyAsset) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{21}
+}
+
+func (x *BuyAsset) GetAssetId() string {
+ if x != nil {
+ return x.AssetId
+ }
+ return ""
+}
+
+func (x *BuyAsset) GetStartsAtExactly() *timestamppb.Timestamp {
+ if x != nil {
+ return x.StartsAtExactly
+ }
+ return nil
+}
+
+func (x *BuyAsset) GetStopsAtExactly() *timestamppb.Timestamp {
+ if x != nil {
+ return x.StopsAtExactly
+ }
+ return nil
+}
+
+func (x *BuyAsset) GetBandwidthExact() uint32 {
+ if x != nil {
+ return x.BandwidthExact
+ }
+ return 0
+}
+
+type BuyAssetsResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Assets []*BoughtAsset `protobuf:"bytes,1,rep,name=assets,proto3" json:"assets,omitempty"`
+ Cost uint64 `protobuf:"varint,2,opt,name=cost,proto3" json:"cost,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *BuyAssetsResponse) Reset() {
+ *x = BuyAssetsResponse{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[22]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *BuyAssetsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BuyAssetsResponse) ProtoMessage() {}
+
+func (x *BuyAssetsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[22]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use BuyAssetsResponse.ProtoReflect.Descriptor instead.
+func (*BuyAssetsResponse) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{22}
+}
+
+func (x *BuyAssetsResponse) GetAssets() []*BoughtAsset {
+ if x != nil {
+ return x.Assets
+ }
+ return nil
+}
+
+func (x *BuyAssetsResponse) GetCost() uint64 {
+ if x != nil {
+ return x.Cost
+ }
+ return 0
+}
+
+type BoughtAsset struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ AssetId string `protobuf:"bytes,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *BoughtAsset) Reset() {
+ *x = BoughtAsset{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[23]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *BoughtAsset) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BoughtAsset) ProtoMessage() {}
+
+func (x *BoughtAsset) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[23]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use BoughtAsset.ProtoReflect.Descriptor instead.
+func (*BoughtAsset) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{23}
+}
+
+func (x *BoughtAsset) GetAssetId() string {
+ if x != nil {
+ return x.AssetId
+ }
+ return ""
+}
+
+type RedeemAssetRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // Types that are valid to be assigned to Interfaces:
+ //
+ // *RedeemAssetRequest_Pair
+ // *RedeemAssetRequest_IfPairAssetId
+ Interfaces isRedeemAssetRequest_Interfaces `protobuf_oneof:"interfaces"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *RedeemAssetRequest) Reset() {
+ *x = RedeemAssetRequest{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[24]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *RedeemAssetRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RedeemAssetRequest) ProtoMessage() {}
+
+func (x *RedeemAssetRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[24]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RedeemAssetRequest.ProtoReflect.Descriptor instead.
+func (*RedeemAssetRequest) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{24}
+}
+
+func (x *RedeemAssetRequest) GetInterfaces() isRedeemAssetRequest_Interfaces {
+ if x != nil {
+ return x.Interfaces
+ }
+ return nil
+}
+
+func (x *RedeemAssetRequest) GetPair() *IngressEgressPair {
+ if x != nil {
+ if x, ok := x.Interfaces.(*RedeemAssetRequest_Pair); ok {
+ return x.Pair
+ }
+ }
+ return nil
+}
+
+func (x *RedeemAssetRequest) GetIfPairAssetId() string {
+ if x != nil {
+ if x, ok := x.Interfaces.(*RedeemAssetRequest_IfPairAssetId); ok {
+ return x.IfPairAssetId
+ }
+ }
+ return ""
+}
+
+type isRedeemAssetRequest_Interfaces interface {
+ isRedeemAssetRequest_Interfaces()
+}
+
+type RedeemAssetRequest_Pair struct {
+ Pair *IngressEgressPair `protobuf:"bytes,1,opt,name=pair,proto3,oneof"`
+}
+
+type RedeemAssetRequest_IfPairAssetId struct {
+ IfPairAssetId string `protobuf:"bytes,3,opt,name=if_pair_asset_id,json=ifPairAssetId,proto3,oneof"`
+}
+
+func (*RedeemAssetRequest_Pair) isRedeemAssetRequest_Interfaces() {}
+
+func (*RedeemAssetRequest_IfPairAssetId) isRedeemAssetRequest_Interfaces() {}
+
+type IngressEgressPair struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ IngressAssetId string `protobuf:"bytes,1,opt,name=ingress_asset_id,json=ingressAssetId,proto3" json:"ingress_asset_id,omitempty"`
+ EgressAssetId string `protobuf:"bytes,2,opt,name=egress_asset_id,json=egressAssetId,proto3" json:"egress_asset_id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *IngressEgressPair) Reset() {
+ *x = IngressEgressPair{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[25]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *IngressEgressPair) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*IngressEgressPair) ProtoMessage() {}
+
+func (x *IngressEgressPair) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[25]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use IngressEgressPair.ProtoReflect.Descriptor instead.
+func (*IngressEgressPair) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{25}
+}
+
+func (x *IngressEgressPair) GetIngressAssetId() string {
+ if x != nil {
+ return x.IngressAssetId
+ }
+ return ""
+}
+
+func (x *IngressEgressPair) GetEgressAssetId() string {
+ if x != nil {
+ return x.EgressAssetId
+ }
+ return ""
+}
+
+type RedeemAssetResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ AuthenticationKey []byte `protobuf:"bytes,1,opt,name=authentication_key,json=authenticationKey,proto3" json:"authentication_key,omitempty"`
+ ReservationId uint32 `protobuf:"varint,2,opt,name=reservation_id,json=reservationId,proto3" json:"reservation_id,omitempty"`
+ BandwidthRounded uint32 `protobuf:"varint,3,opt,name=bandwidth_rounded,json=bandwidthRounded,proto3" json:"bandwidth_rounded,omitempty"`
+ BwDataplaneEncoding uint32 `protobuf:"varint,4,opt,name=bw_dataplane_encoding,json=bwDataplaneEncoding,proto3" json:"bw_dataplane_encoding,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *RedeemAssetResponse) Reset() {
+ *x = RedeemAssetResponse{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[26]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *RedeemAssetResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RedeemAssetResponse) ProtoMessage() {}
+
+func (x *RedeemAssetResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[26]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RedeemAssetResponse.ProtoReflect.Descriptor instead.
+func (*RedeemAssetResponse) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{26}
+}
+
+func (x *RedeemAssetResponse) GetAuthenticationKey() []byte {
+ if x != nil {
+ return x.AuthenticationKey
+ }
+ return nil
+}
+
+func (x *RedeemAssetResponse) GetReservationId() uint32 {
+ if x != nil {
+ return x.ReservationId
+ }
+ return 0
+}
+
+func (x *RedeemAssetResponse) GetBandwidthRounded() uint32 {
+ if x != nil {
+ return x.BandwidthRounded
+ }
+ return 0
+}
+
+func (x *RedeemAssetResponse) GetBwDataplaneEncoding() uint32 {
+ if x != nil {
+ return x.BwDataplaneEncoding
+ }
+ return 0
+}
+
+type FetchReservationsRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Ia *uint64 `protobuf:"varint,1,opt,name=ia,proto3,oneof" json:"ia,omitempty"`
+ IngressId *uint32 `protobuf:"varint,2,opt,name=ingress_id,json=ingressId,proto3,oneof" json:"ingress_id,omitempty"`
+ EgressId *uint32 `protobuf:"varint,3,opt,name=egress_id,json=egressId,proto3,oneof" json:"egress_id,omitempty"`
+ Bandwidth *uint32 `protobuf:"varint,4,opt,name=bandwidth,proto3,oneof" json:"bandwidth,omitempty"`
+ StartsAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=starts_at,json=startsAt,proto3,oneof" json:"starts_at,omitempty"`
+ StopsAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=stops_at,json=stopsAt,proto3,oneof" json:"stops_at,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *FetchReservationsRequest) Reset() {
+ *x = FetchReservationsRequest{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[27]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *FetchReservationsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FetchReservationsRequest) ProtoMessage() {}
+
+func (x *FetchReservationsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[27]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FetchReservationsRequest.ProtoReflect.Descriptor instead.
+func (*FetchReservationsRequest) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{27}
+}
+
+func (x *FetchReservationsRequest) GetIa() uint64 {
+ if x != nil && x.Ia != nil {
+ return *x.Ia
+ }
+ return 0
+}
+
+func (x *FetchReservationsRequest) GetIngressId() uint32 {
+ if x != nil && x.IngressId != nil {
+ return *x.IngressId
+ }
+ return 0
+}
+
+func (x *FetchReservationsRequest) GetEgressId() uint32 {
+ if x != nil && x.EgressId != nil {
+ return *x.EgressId
+ }
+ return 0
+}
+
+func (x *FetchReservationsRequest) GetBandwidth() uint32 {
+ if x != nil && x.Bandwidth != nil {
+ return *x.Bandwidth
+ }
+ return 0
+}
+
+func (x *FetchReservationsRequest) GetStartsAt() *timestamppb.Timestamp {
+ if x != nil {
+ return x.StartsAt
+ }
+ return nil
+}
+
+func (x *FetchReservationsRequest) GetStopsAt() *timestamppb.Timestamp {
+ if x != nil {
+ return x.StopsAt
+ }
+ return nil
+}
+
+type FetchReservationsResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Reservations []*Reservation `protobuf:"bytes,1,rep,name=reservations,proto3" json:"reservations,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *FetchReservationsResponse) Reset() {
+ *x = FetchReservationsResponse{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[28]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *FetchReservationsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FetchReservationsResponse) ProtoMessage() {}
+
+func (x *FetchReservationsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[28]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FetchReservationsResponse.ProtoReflect.Descriptor instead.
+func (*FetchReservationsResponse) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{28}
+}
+
+func (x *FetchReservationsResponse) GetReservations() []*Reservation {
+ if x != nil {
+ return x.Reservations
+ }
+ return nil
+}
+
+type Reservation struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ ReservationId uint32 `protobuf:"varint,1,opt,name=reservation_id,json=reservationId,proto3" json:"reservation_id,omitempty"`
+ Ia uint64 `protobuf:"varint,2,opt,name=ia,proto3" json:"ia,omitempty"`
+ IngressId uint32 `protobuf:"varint,3,opt,name=ingress_id,json=ingressId,proto3" json:"ingress_id,omitempty"`
+ EgressId uint32 `protobuf:"varint,4,opt,name=egress_id,json=egressId,proto3" json:"egress_id,omitempty"`
+ Bandwidth uint32 `protobuf:"varint,5,opt,name=bandwidth,proto3" json:"bandwidth,omitempty"`
+ DataplaneEncoding uint32 `protobuf:"varint,6,opt,name=dataplane_encoding,json=dataplaneEncoding,proto3" json:"dataplane_encoding,omitempty"`
+ StartsAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=starts_at,json=startsAt,proto3" json:"starts_at,omitempty"`
+ StopsAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=stops_at,json=stopsAt,proto3" json:"stops_at,omitempty"`
+ AuthenticationKey []byte `protobuf:"bytes,9,opt,name=authentication_key,json=authenticationKey,proto3" json:"authentication_key,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *Reservation) Reset() {
+ *x = Reservation{}
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[29]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *Reservation) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Reservation) ProtoMessage() {}
+
+func (x *Reservation) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_marketplace_proto_msgTypes[29]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Reservation.ProtoReflect.Descriptor instead.
+func (*Reservation) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP(), []int{29}
+}
+
+func (x *Reservation) GetReservationId() uint32 {
+ if x != nil {
+ return x.ReservationId
+ }
+ return 0
+}
+
+func (x *Reservation) GetIa() uint64 {
+ if x != nil {
+ return x.Ia
+ }
+ return 0
+}
+
+func (x *Reservation) GetIngressId() uint32 {
+ if x != nil {
+ return x.IngressId
+ }
+ return 0
+}
+
+func (x *Reservation) GetEgressId() uint32 {
+ if x != nil {
+ return x.EgressId
+ }
+ return 0
+}
+
+func (x *Reservation) GetBandwidth() uint32 {
+ if x != nil {
+ return x.Bandwidth
+ }
+ return 0
+}
+
+func (x *Reservation) GetDataplaneEncoding() uint32 {
+ if x != nil {
+ return x.DataplaneEncoding
+ }
+ return 0
+}
+
+func (x *Reservation) GetStartsAt() *timestamppb.Timestamp {
+ if x != nil {
+ return x.StartsAt
+ }
+ return nil
+}
+
+func (x *Reservation) GetStopsAt() *timestamppb.Timestamp {
+ if x != nil {
+ return x.StopsAt
+ }
+ return nil
+}
+
+func (x *Reservation) GetAuthenticationKey() []byte {
+ if x != nil {
+ return x.AuthenticationKey
+ }
+ return nil
+}
+
+var File_proto_hummingbird_v1_marketplace_proto protoreflect.FileDescriptor
+
+const file_proto_hummingbird_v1_marketplace_proto_rawDesc = "" +
+ "\n" +
+ "&proto/hummingbird/v1/marketplace.proto\x12\x14proto.hummingbird.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"P\n" +
+ "\x13UpdateAssetsRequest\x129\n" +
+ "\x06assets\x18\x01 \x03(\v2!.proto.hummingbird.v1.AssetUpdateR\x06assets\"\xad\x01\n" +
+ "\vAssetUpdate\x12\x19\n" +
+ "\basset_id\x18\x01 \x01(\tR\aassetId\x12>\n" +
+ "\x06update\x18\x02 \x01(\v2$.proto.hummingbird.v1.PublisherAssetH\x00R\x06update\x126\n" +
+ "\x06remove\x18\x03 \x01(\v2\x1c.proto.hummingbird.v1.RemoveH\x00R\x06removeB\v\n" +
+ "\toperation\"\b\n" +
+ "\x06Remove\"W\n" +
+ "\x14UpdateAssetsResponse\x12?\n" +
+ "\x06result\x18\x01 \x03(\v2'.proto.hummingbird.v1.UpdateAssetResultR\x06result\"S\n" +
+ "\x11UpdateAssetResult\x12\x17\n" +
+ "\x06new_id\x18\x01 \x01(\tH\x00R\x05newId\x12\x16\n" +
+ "\x05error\x18\x02 \x01(\tH\x00R\x05errorB\r\n" +
+ "\vresult_type\"\xfa\x01\n" +
+ "\x11StatisticsRequest\x12'\n" +
+ "\rif_id_ingress\x18\x01 \x01(\rH\x00R\vifIdIngress\x88\x01\x01\x12%\n" +
+ "\fif_id_egress\x18\x02 \x01(\rH\x01R\n" +
+ "ifIdEgress\x88\x01\x01\x120\n" +
+ "\x05start\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x05start\x12,\n" +
+ "\x03end\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x03end\x12\x12\n" +
+ "\x04step\x18\x05 \x01(\rR\x04stepB\x10\n" +
+ "\x0e_if_id_ingressB\x0f\n" +
+ "\r_if_id_egress\"c\n" +
+ "\x12StatisticsResponse\x12M\n" +
+ "\n" +
+ "statistics\x18\x01 \x03(\v2-.proto.hummingbird.v1.StatisticsResponseEntryR\n" +
+ "statistics\"h\n" +
+ "\x17StatisticsResponseEntry\x12\x18\n" +
+ "\arevenue\x18\x01 \x01(\x04R\arevenue\x123\n" +
+ "\x15bandwidth_utilization\x18\x02 \x01(\x01R\x14bandwidthUtilization\"\x98\x01\n" +
+ "\x11SplitAssetRequest\x12\x19\n" +
+ "\basset_id\x18\x01 \x01(\tR\aassetId\x12\x1b\n" +
+ "\bbw_split\x18\x02 \x01(\rH\x00R\abwSplit\x12;\n" +
+ "\n" +
+ "time_split\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampH\x00R\ttimeSplitB\x0e\n" +
+ "\fsplit_option\"P\n" +
+ "\x12SplitAssetResponse\x12\x1c\n" +
+ "\n" +
+ "asset_id_1\x18\x01 \x01(\tR\bassetId1\x12\x1c\n" +
+ "\n" +
+ "asset_id_2\x18\x02 \x01(\tR\bassetId2\"Q\n" +
+ "\x13CombineAssetRequest\x12\x1c\n" +
+ "\n" +
+ "asset_id_1\x18\x01 \x01(\tR\bassetId1\x12\x1c\n" +
+ "\n" +
+ "asset_id_2\x18\x02 \x01(\tR\bassetId2\"1\n" +
+ "\x14CombineAssetResponse\x12\x19\n" +
+ "\basset_id\x18\x01 \x01(\tR\aassetId\"\x18\n" +
+ "\x16MarketplaceInfoRequest\"\xf5\x04\n" +
+ "\x17MarketplaceInfoResponse\x12*\n" +
+ "\x11api_major_version\x18\x01 \x01(\rR\x0fapiMajorVersion\x12*\n" +
+ "\x11api_minor_version\x18\x02 \x01(\rR\x0fapiMinorVersion\x12\x1a\n" +
+ "\bcurrency\x18\x03 \x01(\tR\bcurrency\x12+\n" +
+ "\x11currency_exponent\x18\x04 \x01(\rR\x10currencyExponent\x12<\n" +
+ "\x1amax_statistics_granularity\x18\x05 \x01(\rR\x18maxStatisticsGranularity\x12P\n" +
+ "\x10pricing_strategy\x18\x06 \x01(\x0e2%.proto.hummingbird.v1.PricingStrategyR\x0fpricingStrategy\x128\n" +
+ "\x18transaction_fee_relative\x18\a \x01(\x02R\x16transactionFeeRelative\x128\n" +
+ "\x18transaction_fee_absolute\x18\b \x01(\x04R\x16transactionFeeAbsolute\x12;\n" +
+ "\x1asplit_combine_fee_absolute\x18\t \x01(\x04R\x17splitCombineFeeAbsolute\x12D\n" +
+ "\x1esupports_redemption_delegation\x18\n" +
+ " \x01(\bR\x1csupportsRedemptionDelegation\x122\n" +
+ "\x15delegation_hourly_fee\x18\v \x01(\x04R\x13delegationHourlyFee\"Q\n" +
+ "\x13PublishAssetRequest\x12:\n" +
+ "\x05asset\x18\x01 \x01(\v2$.proto.hummingbird.v1.PublisherAssetR\x05asset\"1\n" +
+ "\x14PublishAssetResponse\x12\x19\n" +
+ "\basset_id\x18\x01 \x01(\tR\aassetId\"\xc8\x03\n" +
+ "\x0ePublisherAsset\x12'\n" +
+ "\rif_id_ingress\x18\x01 \x01(\rH\x00R\vifIdIngress\x88\x01\x01\x12%\n" +
+ "\fif_id_egress\x18\x02 \x01(\rH\x01R\n" +
+ "ifIdEgress\x88\x01\x01\x12\x1c\n" +
+ "\tbandwidth\x18\x03 \x01(\rR\tbandwidth\x12#\n" +
+ "\rbandwidth_min\x18\x04 \x01(\rR\fbandwidthMin\x12#\n" +
+ "\rbandwidth_max\x18\x05 \x01(\rR\fbandwidthMax\x127\n" +
+ "\tstarts_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\bstartsAt\x125\n" +
+ "\bstops_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\astopsAt\x12)\n" +
+ "\x10time_granularity\x18\b \x01(\rR\x0ftimeGranularity\x12*\n" +
+ "\x11time_min_duration\x18\t \x01(\rR\x0ftimeMinDuration\x12\x14\n" +
+ "\x05price\x18\n" +
+ " \x01(\rR\x05priceB\x10\n" +
+ "\x0e_if_id_ingressB\x0f\n" +
+ "\r_if_id_egress\"\xe3\x03\n" +
+ "\x13SearchAssetsRequest\x12\x14\n" +
+ "\x05owned\x18\x01 \x01(\bR\x05owned\x12\x13\n" +
+ "\x02ia\x18\x02 \x01(\x04H\x00R\x02ia\x88\x01\x01\x12'\n" +
+ "\rif_id_ingress\x18\x03 \x01(\rH\x01R\vifIdIngress\x88\x01\x01\x12%\n" +
+ "\fif_id_egress\x18\x04 \x01(\rH\x02R\n" +
+ "ifIdEgress\x88\x01\x01\x12+\n" +
+ "\x0fmin_required_bw\x18\x05 \x01(\rH\x03R\rminRequiredBw\x88\x01\x01\x12I\n" +
+ "\x10starts_at_latest\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampH\x04R\x0estartsAtLatest\x88\x01\x01\x12K\n" +
+ "\x11stops_at_earliest\x18\a \x01(\v2\x1a.google.protobuf.TimestampH\x05R\x0fstopsAtEarliest\x88\x01\x01\x12\x19\n" +
+ "\x05price\x18\b \x01(\rH\x06R\x05price\x88\x01\x01B\x05\n" +
+ "\x03_iaB\x10\n" +
+ "\x0e_if_id_ingressB\x0f\n" +
+ "\r_if_id_egressB\x12\n" +
+ "\x10_min_required_bwB\x13\n" +
+ "\x11_starts_at_latestB\x14\n" +
+ "\x12_stops_at_earliestB\b\n" +
+ "\x06_price\"Q\n" +
+ "\x14SearchAssetsResponse\x129\n" +
+ "\x06assets\x18\x01 \x03(\v2!.proto.hummingbird.v1.SearchAssetR\x06assets\"\xf0\x03\n" +
+ "\vSearchAsset\x12\x19\n" +
+ "\basset_id\x18\x01 \x01(\tR\aassetId\x12\x0e\n" +
+ "\x02ia\x18\x02 \x01(\x04R\x02ia\x12'\n" +
+ "\rif_id_ingress\x18\x03 \x01(\rH\x00R\vifIdIngress\x88\x01\x01\x12%\n" +
+ "\fif_id_egress\x18\x04 \x01(\rH\x01R\n" +
+ "ifIdEgress\x88\x01\x01\x12\x1c\n" +
+ "\tbandwidth\x18\x05 \x01(\rR\tbandwidth\x12#\n" +
+ "\rbandwidth_min\x18\x06 \x01(\rR\fbandwidthMin\x12#\n" +
+ "\rbandwidth_max\x18\a \x01(\rR\fbandwidthMax\x127\n" +
+ "\tstarts_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\bstartsAt\x125\n" +
+ "\bstops_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\astopsAt\x12)\n" +
+ "\x10time_granularity\x18\n" +
+ " \x01(\rR\x0ftimeGranularity\x12*\n" +
+ "\x11time_min_duration\x18\v \x01(\rR\x0ftimeMinDuration\x12\x14\n" +
+ "\x05price\x18\f \x01(\rR\x05priceB\x10\n" +
+ "\x0e_if_id_ingressB\x0f\n" +
+ "\r_if_id_egress\"g\n" +
+ "\x10BuyAssetsRequest\x126\n" +
+ "\x06assets\x18\x01 \x03(\v2\x1e.proto.hummingbird.v1.BuyAssetR\x06assets\x12\x1b\n" +
+ "\tmax_price\x18\x02 \x01(\x04R\bmaxPrice\"\xdc\x01\n" +
+ "\bBuyAsset\x12\x19\n" +
+ "\basset_id\x18\x01 \x01(\tR\aassetId\x12F\n" +
+ "\x11starts_at_exactly\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x0fstartsAtExactly\x12D\n" +
+ "\x10stops_at_exactly\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x0estopsAtExactly\x12'\n" +
+ "\x0fbandwidth_exact\x18\x04 \x01(\rR\x0ebandwidthExact\"b\n" +
+ "\x11BuyAssetsResponse\x129\n" +
+ "\x06assets\x18\x01 \x03(\v2!.proto.hummingbird.v1.BoughtAssetR\x06assets\x12\x12\n" +
+ "\x04cost\x18\x02 \x01(\x04R\x04cost\"(\n" +
+ "\vBoughtAsset\x12\x19\n" +
+ "\basset_id\x18\x01 \x01(\tR\aassetId\"\x8c\x01\n" +
+ "\x12RedeemAssetRequest\x12=\n" +
+ "\x04pair\x18\x01 \x01(\v2'.proto.hummingbird.v1.IngressEgressPairH\x00R\x04pair\x12)\n" +
+ "\x10if_pair_asset_id\x18\x03 \x01(\tH\x00R\rifPairAssetIdB\f\n" +
+ "\n" +
+ "interfaces\"e\n" +
+ "\x11IngressEgressPair\x12(\n" +
+ "\x10ingress_asset_id\x18\x01 \x01(\tR\x0eingressAssetId\x12&\n" +
+ "\x0fegress_asset_id\x18\x02 \x01(\tR\regressAssetId\"\xcc\x01\n" +
+ "\x13RedeemAssetResponse\x12-\n" +
+ "\x12authentication_key\x18\x01 \x01(\fR\x11authenticationKey\x12%\n" +
+ "\x0ereservation_id\x18\x02 \x01(\rR\rreservationId\x12+\n" +
+ "\x11bandwidth_rounded\x18\x03 \x01(\rR\x10bandwidthRounded\x122\n" +
+ "\x15bw_dataplane_encoding\x18\x04 \x01(\rR\x13bwDataplaneEncoding\"\xdf\x02\n" +
+ "\x18FetchReservationsRequest\x12\x13\n" +
+ "\x02ia\x18\x01 \x01(\x04H\x00R\x02ia\x88\x01\x01\x12\"\n" +
+ "\n" +
+ "ingress_id\x18\x02 \x01(\rH\x01R\tingressId\x88\x01\x01\x12 \n" +
+ "\tegress_id\x18\x03 \x01(\rH\x02R\begressId\x88\x01\x01\x12!\n" +
+ "\tbandwidth\x18\x04 \x01(\rH\x03R\tbandwidth\x88\x01\x01\x12<\n" +
+ "\tstarts_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampH\x04R\bstartsAt\x88\x01\x01\x12:\n" +
+ "\bstops_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampH\x05R\astopsAt\x88\x01\x01B\x05\n" +
+ "\x03_iaB\r\n" +
+ "\v_ingress_idB\f\n" +
+ "\n" +
+ "_egress_idB\f\n" +
+ "\n" +
+ "_bandwidthB\f\n" +
+ "\n" +
+ "_starts_atB\v\n" +
+ "\t_stops_at\"b\n" +
+ "\x19FetchReservationsResponse\x12E\n" +
+ "\freservations\x18\x01 \x03(\v2!.proto.hummingbird.v1.ReservationR\freservations\"\xec\x02\n" +
+ "\vReservation\x12%\n" +
+ "\x0ereservation_id\x18\x01 \x01(\rR\rreservationId\x12\x0e\n" +
+ "\x02ia\x18\x02 \x01(\x04R\x02ia\x12\x1d\n" +
+ "\n" +
+ "ingress_id\x18\x03 \x01(\rR\tingressId\x12\x1b\n" +
+ "\tegress_id\x18\x04 \x01(\rR\begressId\x12\x1c\n" +
+ "\tbandwidth\x18\x05 \x01(\rR\tbandwidth\x12-\n" +
+ "\x12dataplane_encoding\x18\x06 \x01(\rR\x11dataplaneEncoding\x127\n" +
+ "\tstarts_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\bstartsAt\x125\n" +
+ "\bstops_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\astopsAt\x12-\n" +
+ "\x12authentication_key\x18\t \x01(\fR\x11authenticationKey*:\n" +
+ "\x0fPricingStrategy\x12\x12\n" +
+ "\x0estatic_pricing\x10\x00\x12\x13\n" +
+ "\x0fdynamic_pricing\x10\x012\xa4\b\n" +
+ "\x12MarketplaceService\x12e\n" +
+ "\x04Info\x12,.proto.hummingbird.v1.MarketplaceInfoRequest\x1a-.proto.hummingbird.v1.MarketplaceInfoResponse\"\x00\x12g\n" +
+ "\fPublishAsset\x12).proto.hummingbird.v1.PublishAssetRequest\x1a*.proto.hummingbird.v1.PublishAssetResponse\"\x00\x12g\n" +
+ "\fSearchAssets\x12).proto.hummingbird.v1.SearchAssetsRequest\x1a*.proto.hummingbird.v1.SearchAssetsResponse\"\x00\x12^\n" +
+ "\tBuyAssets\x12&.proto.hummingbird.v1.BuyAssetsRequest\x1a'.proto.hummingbird.v1.BuyAssetsResponse\"\x00\x12d\n" +
+ "\vRedeemAsset\x12(.proto.hummingbird.v1.RedeemAssetRequest\x1a).proto.hummingbird.v1.RedeemAssetResponse\"\x00\x12v\n" +
+ "\x11FetchReservations\x12..proto.hummingbird.v1.FetchReservationsRequest\x1a/.proto.hummingbird.v1.FetchReservationsResponse\"\x00\x12a\n" +
+ "\n" +
+ "SplitAsset\x12'.proto.hummingbird.v1.SplitAssetRequest\x1a(.proto.hummingbird.v1.SplitAssetResponse\"\x00\x12h\n" +
+ "\rCombineAssets\x12).proto.hummingbird.v1.CombineAssetRequest\x1a*.proto.hummingbird.v1.CombineAssetResponse\"\x00\x12a\n" +
+ "\n" +
+ "Statistics\x12'.proto.hummingbird.v1.StatisticsRequest\x1a(.proto.hummingbird.v1.StatisticsResponse\"\x00\x12g\n" +
+ "\fUpdateAssets\x12).proto.hummingbird.v1.UpdateAssetsRequest\x1a*.proto.hummingbird.v1.UpdateAssetsResponse\"\x00B3Z1github.com/scionproto/scion/pkg/proto/hummingbirdb\x06proto3"
+
+var (
+ file_proto_hummingbird_v1_marketplace_proto_rawDescOnce sync.Once
+ file_proto_hummingbird_v1_marketplace_proto_rawDescData []byte
+)
+
+func file_proto_hummingbird_v1_marketplace_proto_rawDescGZIP() []byte {
+ file_proto_hummingbird_v1_marketplace_proto_rawDescOnce.Do(func() {
+ file_proto_hummingbird_v1_marketplace_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_hummingbird_v1_marketplace_proto_rawDesc), len(file_proto_hummingbird_v1_marketplace_proto_rawDesc)))
+ })
+ return file_proto_hummingbird_v1_marketplace_proto_rawDescData
+}
+
+var file_proto_hummingbird_v1_marketplace_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_proto_hummingbird_v1_marketplace_proto_msgTypes = make([]protoimpl.MessageInfo, 30)
+var file_proto_hummingbird_v1_marketplace_proto_goTypes = []any{
+ (PricingStrategy)(0), // 0: proto.hummingbird.v1.PricingStrategy
+ (*UpdateAssetsRequest)(nil), // 1: proto.hummingbird.v1.UpdateAssetsRequest
+ (*AssetUpdate)(nil), // 2: proto.hummingbird.v1.AssetUpdate
+ (*Remove)(nil), // 3: proto.hummingbird.v1.Remove
+ (*UpdateAssetsResponse)(nil), // 4: proto.hummingbird.v1.UpdateAssetsResponse
+ (*UpdateAssetResult)(nil), // 5: proto.hummingbird.v1.UpdateAssetResult
+ (*StatisticsRequest)(nil), // 6: proto.hummingbird.v1.StatisticsRequest
+ (*StatisticsResponse)(nil), // 7: proto.hummingbird.v1.StatisticsResponse
+ (*StatisticsResponseEntry)(nil), // 8: proto.hummingbird.v1.StatisticsResponseEntry
+ (*SplitAssetRequest)(nil), // 9: proto.hummingbird.v1.SplitAssetRequest
+ (*SplitAssetResponse)(nil), // 10: proto.hummingbird.v1.SplitAssetResponse
+ (*CombineAssetRequest)(nil), // 11: proto.hummingbird.v1.CombineAssetRequest
+ (*CombineAssetResponse)(nil), // 12: proto.hummingbird.v1.CombineAssetResponse
+ (*MarketplaceInfoRequest)(nil), // 13: proto.hummingbird.v1.MarketplaceInfoRequest
+ (*MarketplaceInfoResponse)(nil), // 14: proto.hummingbird.v1.MarketplaceInfoResponse
+ (*PublishAssetRequest)(nil), // 15: proto.hummingbird.v1.PublishAssetRequest
+ (*PublishAssetResponse)(nil), // 16: proto.hummingbird.v1.PublishAssetResponse
+ (*PublisherAsset)(nil), // 17: proto.hummingbird.v1.PublisherAsset
+ (*SearchAssetsRequest)(nil), // 18: proto.hummingbird.v1.SearchAssetsRequest
+ (*SearchAssetsResponse)(nil), // 19: proto.hummingbird.v1.SearchAssetsResponse
+ (*SearchAsset)(nil), // 20: proto.hummingbird.v1.SearchAsset
+ (*BuyAssetsRequest)(nil), // 21: proto.hummingbird.v1.BuyAssetsRequest
+ (*BuyAsset)(nil), // 22: proto.hummingbird.v1.BuyAsset
+ (*BuyAssetsResponse)(nil), // 23: proto.hummingbird.v1.BuyAssetsResponse
+ (*BoughtAsset)(nil), // 24: proto.hummingbird.v1.BoughtAsset
+ (*RedeemAssetRequest)(nil), // 25: proto.hummingbird.v1.RedeemAssetRequest
+ (*IngressEgressPair)(nil), // 26: proto.hummingbird.v1.IngressEgressPair
+ (*RedeemAssetResponse)(nil), // 27: proto.hummingbird.v1.RedeemAssetResponse
+ (*FetchReservationsRequest)(nil), // 28: proto.hummingbird.v1.FetchReservationsRequest
+ (*FetchReservationsResponse)(nil), // 29: proto.hummingbird.v1.FetchReservationsResponse
+ (*Reservation)(nil), // 30: proto.hummingbird.v1.Reservation
+ (*timestamppb.Timestamp)(nil), // 31: google.protobuf.Timestamp
+}
+var file_proto_hummingbird_v1_marketplace_proto_depIdxs = []int32{
+ 2, // 0: proto.hummingbird.v1.UpdateAssetsRequest.assets:type_name -> proto.hummingbird.v1.AssetUpdate
+ 17, // 1: proto.hummingbird.v1.AssetUpdate.update:type_name -> proto.hummingbird.v1.PublisherAsset
+ 3, // 2: proto.hummingbird.v1.AssetUpdate.remove:type_name -> proto.hummingbird.v1.Remove
+ 5, // 3: proto.hummingbird.v1.UpdateAssetsResponse.result:type_name -> proto.hummingbird.v1.UpdateAssetResult
+ 31, // 4: proto.hummingbird.v1.StatisticsRequest.start:type_name -> google.protobuf.Timestamp
+ 31, // 5: proto.hummingbird.v1.StatisticsRequest.end:type_name -> google.protobuf.Timestamp
+ 8, // 6: proto.hummingbird.v1.StatisticsResponse.statistics:type_name -> proto.hummingbird.v1.StatisticsResponseEntry
+ 31, // 7: proto.hummingbird.v1.SplitAssetRequest.time_split:type_name -> google.protobuf.Timestamp
+ 0, // 8: proto.hummingbird.v1.MarketplaceInfoResponse.pricing_strategy:type_name -> proto.hummingbird.v1.PricingStrategy
+ 17, // 9: proto.hummingbird.v1.PublishAssetRequest.asset:type_name -> proto.hummingbird.v1.PublisherAsset
+ 31, // 10: proto.hummingbird.v1.PublisherAsset.starts_at:type_name -> google.protobuf.Timestamp
+ 31, // 11: proto.hummingbird.v1.PublisherAsset.stops_at:type_name -> google.protobuf.Timestamp
+ 31, // 12: proto.hummingbird.v1.SearchAssetsRequest.starts_at_latest:type_name -> google.protobuf.Timestamp
+ 31, // 13: proto.hummingbird.v1.SearchAssetsRequest.stops_at_earliest:type_name -> google.protobuf.Timestamp
+ 20, // 14: proto.hummingbird.v1.SearchAssetsResponse.assets:type_name -> proto.hummingbird.v1.SearchAsset
+ 31, // 15: proto.hummingbird.v1.SearchAsset.starts_at:type_name -> google.protobuf.Timestamp
+ 31, // 16: proto.hummingbird.v1.SearchAsset.stops_at:type_name -> google.protobuf.Timestamp
+ 22, // 17: proto.hummingbird.v1.BuyAssetsRequest.assets:type_name -> proto.hummingbird.v1.BuyAsset
+ 31, // 18: proto.hummingbird.v1.BuyAsset.starts_at_exactly:type_name -> google.protobuf.Timestamp
+ 31, // 19: proto.hummingbird.v1.BuyAsset.stops_at_exactly:type_name -> google.protobuf.Timestamp
+ 24, // 20: proto.hummingbird.v1.BuyAssetsResponse.assets:type_name -> proto.hummingbird.v1.BoughtAsset
+ 26, // 21: proto.hummingbird.v1.RedeemAssetRequest.pair:type_name -> proto.hummingbird.v1.IngressEgressPair
+ 31, // 22: proto.hummingbird.v1.FetchReservationsRequest.starts_at:type_name -> google.protobuf.Timestamp
+ 31, // 23: proto.hummingbird.v1.FetchReservationsRequest.stops_at:type_name -> google.protobuf.Timestamp
+ 30, // 24: proto.hummingbird.v1.FetchReservationsResponse.reservations:type_name -> proto.hummingbird.v1.Reservation
+ 31, // 25: proto.hummingbird.v1.Reservation.starts_at:type_name -> google.protobuf.Timestamp
+ 31, // 26: proto.hummingbird.v1.Reservation.stops_at:type_name -> google.protobuf.Timestamp
+ 13, // 27: proto.hummingbird.v1.MarketplaceService.Info:input_type -> proto.hummingbird.v1.MarketplaceInfoRequest
+ 15, // 28: proto.hummingbird.v1.MarketplaceService.PublishAsset:input_type -> proto.hummingbird.v1.PublishAssetRequest
+ 18, // 29: proto.hummingbird.v1.MarketplaceService.SearchAssets:input_type -> proto.hummingbird.v1.SearchAssetsRequest
+ 21, // 30: proto.hummingbird.v1.MarketplaceService.BuyAssets:input_type -> proto.hummingbird.v1.BuyAssetsRequest
+ 25, // 31: proto.hummingbird.v1.MarketplaceService.RedeemAsset:input_type -> proto.hummingbird.v1.RedeemAssetRequest
+ 28, // 32: proto.hummingbird.v1.MarketplaceService.FetchReservations:input_type -> proto.hummingbird.v1.FetchReservationsRequest
+ 9, // 33: proto.hummingbird.v1.MarketplaceService.SplitAsset:input_type -> proto.hummingbird.v1.SplitAssetRequest
+ 11, // 34: proto.hummingbird.v1.MarketplaceService.CombineAssets:input_type -> proto.hummingbird.v1.CombineAssetRequest
+ 6, // 35: proto.hummingbird.v1.MarketplaceService.Statistics:input_type -> proto.hummingbird.v1.StatisticsRequest
+ 1, // 36: proto.hummingbird.v1.MarketplaceService.UpdateAssets:input_type -> proto.hummingbird.v1.UpdateAssetsRequest
+ 14, // 37: proto.hummingbird.v1.MarketplaceService.Info:output_type -> proto.hummingbird.v1.MarketplaceInfoResponse
+ 16, // 38: proto.hummingbird.v1.MarketplaceService.PublishAsset:output_type -> proto.hummingbird.v1.PublishAssetResponse
+ 19, // 39: proto.hummingbird.v1.MarketplaceService.SearchAssets:output_type -> proto.hummingbird.v1.SearchAssetsResponse
+ 23, // 40: proto.hummingbird.v1.MarketplaceService.BuyAssets:output_type -> proto.hummingbird.v1.BuyAssetsResponse
+ 27, // 41: proto.hummingbird.v1.MarketplaceService.RedeemAsset:output_type -> proto.hummingbird.v1.RedeemAssetResponse
+ 29, // 42: proto.hummingbird.v1.MarketplaceService.FetchReservations:output_type -> proto.hummingbird.v1.FetchReservationsResponse
+ 10, // 43: proto.hummingbird.v1.MarketplaceService.SplitAsset:output_type -> proto.hummingbird.v1.SplitAssetResponse
+ 12, // 44: proto.hummingbird.v1.MarketplaceService.CombineAssets:output_type -> proto.hummingbird.v1.CombineAssetResponse
+ 7, // 45: proto.hummingbird.v1.MarketplaceService.Statistics:output_type -> proto.hummingbird.v1.StatisticsResponse
+ 4, // 46: proto.hummingbird.v1.MarketplaceService.UpdateAssets:output_type -> proto.hummingbird.v1.UpdateAssetsResponse
+ 37, // [37:47] is the sub-list for method output_type
+ 27, // [27:37] is the sub-list for method input_type
+ 27, // [27:27] is the sub-list for extension type_name
+ 27, // [27:27] is the sub-list for extension extendee
+ 0, // [0:27] is the sub-list for field type_name
+}
+
+func init() { file_proto_hummingbird_v1_marketplace_proto_init() }
+func file_proto_hummingbird_v1_marketplace_proto_init() {
+ if File_proto_hummingbird_v1_marketplace_proto != nil {
+ return
+ }
+ file_proto_hummingbird_v1_marketplace_proto_msgTypes[1].OneofWrappers = []any{
+ (*AssetUpdate_Update)(nil),
+ (*AssetUpdate_Remove)(nil),
+ }
+ file_proto_hummingbird_v1_marketplace_proto_msgTypes[4].OneofWrappers = []any{
+ (*UpdateAssetResult_NewId)(nil),
+ (*UpdateAssetResult_Error)(nil),
+ }
+ file_proto_hummingbird_v1_marketplace_proto_msgTypes[5].OneofWrappers = []any{}
+ file_proto_hummingbird_v1_marketplace_proto_msgTypes[8].OneofWrappers = []any{
+ (*SplitAssetRequest_BwSplit)(nil),
+ (*SplitAssetRequest_TimeSplit)(nil),
+ }
+ file_proto_hummingbird_v1_marketplace_proto_msgTypes[16].OneofWrappers = []any{}
+ file_proto_hummingbird_v1_marketplace_proto_msgTypes[17].OneofWrappers = []any{}
+ file_proto_hummingbird_v1_marketplace_proto_msgTypes[19].OneofWrappers = []any{}
+ file_proto_hummingbird_v1_marketplace_proto_msgTypes[24].OneofWrappers = []any{
+ (*RedeemAssetRequest_Pair)(nil),
+ (*RedeemAssetRequest_IfPairAssetId)(nil),
+ }
+ file_proto_hummingbird_v1_marketplace_proto_msgTypes[27].OneofWrappers = []any{}
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_hummingbird_v1_marketplace_proto_rawDesc), len(file_proto_hummingbird_v1_marketplace_proto_rawDesc)),
+ NumEnums: 1,
+ NumMessages: 30,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_proto_hummingbird_v1_marketplace_proto_goTypes,
+ DependencyIndexes: file_proto_hummingbird_v1_marketplace_proto_depIdxs,
+ EnumInfos: file_proto_hummingbird_v1_marketplace_proto_enumTypes,
+ MessageInfos: file_proto_hummingbird_v1_marketplace_proto_msgTypes,
+ }.Build()
+ File_proto_hummingbird_v1_marketplace_proto = out.File
+ file_proto_hummingbird_v1_marketplace_proto_goTypes = nil
+ file_proto_hummingbird_v1_marketplace_proto_depIdxs = nil
+}
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ context.Context
+var _ grpc.ClientConnInterface
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+const _ = grpc.SupportPackageIsVersion6
+
+// MarketplaceServiceClient is the client API for MarketplaceService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
+type MarketplaceServiceClient interface {
+ Info(ctx context.Context, in *MarketplaceInfoRequest, opts ...grpc.CallOption) (*MarketplaceInfoResponse, error)
+ PublishAsset(ctx context.Context, in *PublishAssetRequest, opts ...grpc.CallOption) (*PublishAssetResponse, error)
+ SearchAssets(ctx context.Context, in *SearchAssetsRequest, opts ...grpc.CallOption) (*SearchAssetsResponse, error)
+ BuyAssets(ctx context.Context, in *BuyAssetsRequest, opts ...grpc.CallOption) (*BuyAssetsResponse, error)
+ RedeemAsset(ctx context.Context, in *RedeemAssetRequest, opts ...grpc.CallOption) (*RedeemAssetResponse, error)
+ FetchReservations(ctx context.Context, in *FetchReservationsRequest, opts ...grpc.CallOption) (*FetchReservationsResponse, error)
+ SplitAsset(ctx context.Context, in *SplitAssetRequest, opts ...grpc.CallOption) (*SplitAssetResponse, error)
+ CombineAssets(ctx context.Context, in *CombineAssetRequest, opts ...grpc.CallOption) (*CombineAssetResponse, error)
+ Statistics(ctx context.Context, in *StatisticsRequest, opts ...grpc.CallOption) (*StatisticsResponse, error)
+ UpdateAssets(ctx context.Context, in *UpdateAssetsRequest, opts ...grpc.CallOption) (*UpdateAssetsResponse, error)
+}
+
+type marketplaceServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewMarketplaceServiceClient(cc grpc.ClientConnInterface) MarketplaceServiceClient {
+ return &marketplaceServiceClient{cc}
+}
+
+func (c *marketplaceServiceClient) Info(ctx context.Context, in *MarketplaceInfoRequest, opts ...grpc.CallOption) (*MarketplaceInfoResponse, error) {
+ out := new(MarketplaceInfoResponse)
+ err := c.cc.Invoke(ctx, "/proto.hummingbird.v1.MarketplaceService/Info", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *marketplaceServiceClient) PublishAsset(ctx context.Context, in *PublishAssetRequest, opts ...grpc.CallOption) (*PublishAssetResponse, error) {
+ out := new(PublishAssetResponse)
+ err := c.cc.Invoke(ctx, "/proto.hummingbird.v1.MarketplaceService/PublishAsset", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *marketplaceServiceClient) SearchAssets(ctx context.Context, in *SearchAssetsRequest, opts ...grpc.CallOption) (*SearchAssetsResponse, error) {
+ out := new(SearchAssetsResponse)
+ err := c.cc.Invoke(ctx, "/proto.hummingbird.v1.MarketplaceService/SearchAssets", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *marketplaceServiceClient) BuyAssets(ctx context.Context, in *BuyAssetsRequest, opts ...grpc.CallOption) (*BuyAssetsResponse, error) {
+ out := new(BuyAssetsResponse)
+ err := c.cc.Invoke(ctx, "/proto.hummingbird.v1.MarketplaceService/BuyAssets", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *marketplaceServiceClient) RedeemAsset(ctx context.Context, in *RedeemAssetRequest, opts ...grpc.CallOption) (*RedeemAssetResponse, error) {
+ out := new(RedeemAssetResponse)
+ err := c.cc.Invoke(ctx, "/proto.hummingbird.v1.MarketplaceService/RedeemAsset", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *marketplaceServiceClient) FetchReservations(ctx context.Context, in *FetchReservationsRequest, opts ...grpc.CallOption) (*FetchReservationsResponse, error) {
+ out := new(FetchReservationsResponse)
+ err := c.cc.Invoke(ctx, "/proto.hummingbird.v1.MarketplaceService/FetchReservations", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *marketplaceServiceClient) SplitAsset(ctx context.Context, in *SplitAssetRequest, opts ...grpc.CallOption) (*SplitAssetResponse, error) {
+ out := new(SplitAssetResponse)
+ err := c.cc.Invoke(ctx, "/proto.hummingbird.v1.MarketplaceService/SplitAsset", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *marketplaceServiceClient) CombineAssets(ctx context.Context, in *CombineAssetRequest, opts ...grpc.CallOption) (*CombineAssetResponse, error) {
+ out := new(CombineAssetResponse)
+ err := c.cc.Invoke(ctx, "/proto.hummingbird.v1.MarketplaceService/CombineAssets", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *marketplaceServiceClient) Statistics(ctx context.Context, in *StatisticsRequest, opts ...grpc.CallOption) (*StatisticsResponse, error) {
+ out := new(StatisticsResponse)
+ err := c.cc.Invoke(ctx, "/proto.hummingbird.v1.MarketplaceService/Statistics", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *marketplaceServiceClient) UpdateAssets(ctx context.Context, in *UpdateAssetsRequest, opts ...grpc.CallOption) (*UpdateAssetsResponse, error) {
+ out := new(UpdateAssetsResponse)
+ err := c.cc.Invoke(ctx, "/proto.hummingbird.v1.MarketplaceService/UpdateAssets", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// MarketplaceServiceServer is the server API for MarketplaceService service.
+type MarketplaceServiceServer interface {
+ Info(context.Context, *MarketplaceInfoRequest) (*MarketplaceInfoResponse, error)
+ PublishAsset(context.Context, *PublishAssetRequest) (*PublishAssetResponse, error)
+ SearchAssets(context.Context, *SearchAssetsRequest) (*SearchAssetsResponse, error)
+ BuyAssets(context.Context, *BuyAssetsRequest) (*BuyAssetsResponse, error)
+ RedeemAsset(context.Context, *RedeemAssetRequest) (*RedeemAssetResponse, error)
+ FetchReservations(context.Context, *FetchReservationsRequest) (*FetchReservationsResponse, error)
+ SplitAsset(context.Context, *SplitAssetRequest) (*SplitAssetResponse, error)
+ CombineAssets(context.Context, *CombineAssetRequest) (*CombineAssetResponse, error)
+ Statistics(context.Context, *StatisticsRequest) (*StatisticsResponse, error)
+ UpdateAssets(context.Context, *UpdateAssetsRequest) (*UpdateAssetsResponse, error)
+}
+
+// UnimplementedMarketplaceServiceServer can be embedded to have forward compatible implementations.
+type UnimplementedMarketplaceServiceServer struct {
+}
+
+func (*UnimplementedMarketplaceServiceServer) Info(context.Context, *MarketplaceInfoRequest) (*MarketplaceInfoResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Info not implemented")
+}
+func (*UnimplementedMarketplaceServiceServer) PublishAsset(context.Context, *PublishAssetRequest) (*PublishAssetResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method PublishAsset not implemented")
+}
+func (*UnimplementedMarketplaceServiceServer) SearchAssets(context.Context, *SearchAssetsRequest) (*SearchAssetsResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method SearchAssets not implemented")
+}
+func (*UnimplementedMarketplaceServiceServer) BuyAssets(context.Context, *BuyAssetsRequest) (*BuyAssetsResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method BuyAssets not implemented")
+}
+func (*UnimplementedMarketplaceServiceServer) RedeemAsset(context.Context, *RedeemAssetRequest) (*RedeemAssetResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method RedeemAsset not implemented")
+}
+func (*UnimplementedMarketplaceServiceServer) FetchReservations(context.Context, *FetchReservationsRequest) (*FetchReservationsResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method FetchReservations not implemented")
+}
+func (*UnimplementedMarketplaceServiceServer) SplitAsset(context.Context, *SplitAssetRequest) (*SplitAssetResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method SplitAsset not implemented")
+}
+func (*UnimplementedMarketplaceServiceServer) CombineAssets(context.Context, *CombineAssetRequest) (*CombineAssetResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method CombineAssets not implemented")
+}
+func (*UnimplementedMarketplaceServiceServer) Statistics(context.Context, *StatisticsRequest) (*StatisticsResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Statistics not implemented")
+}
+func (*UnimplementedMarketplaceServiceServer) UpdateAssets(context.Context, *UpdateAssetsRequest) (*UpdateAssetsResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method UpdateAssets not implemented")
+}
+
+func RegisterMarketplaceServiceServer(s *grpc.Server, srv MarketplaceServiceServer) {
+ s.RegisterService(&_MarketplaceService_serviceDesc, srv)
+}
+
+func _MarketplaceService_Info_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(MarketplaceInfoRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(MarketplaceServiceServer).Info(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/proto.hummingbird.v1.MarketplaceService/Info",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(MarketplaceServiceServer).Info(ctx, req.(*MarketplaceInfoRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _MarketplaceService_PublishAsset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(PublishAssetRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(MarketplaceServiceServer).PublishAsset(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/proto.hummingbird.v1.MarketplaceService/PublishAsset",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(MarketplaceServiceServer).PublishAsset(ctx, req.(*PublishAssetRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _MarketplaceService_SearchAssets_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(SearchAssetsRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(MarketplaceServiceServer).SearchAssets(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/proto.hummingbird.v1.MarketplaceService/SearchAssets",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(MarketplaceServiceServer).SearchAssets(ctx, req.(*SearchAssetsRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _MarketplaceService_BuyAssets_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(BuyAssetsRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(MarketplaceServiceServer).BuyAssets(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/proto.hummingbird.v1.MarketplaceService/BuyAssets",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(MarketplaceServiceServer).BuyAssets(ctx, req.(*BuyAssetsRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _MarketplaceService_RedeemAsset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(RedeemAssetRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(MarketplaceServiceServer).RedeemAsset(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/proto.hummingbird.v1.MarketplaceService/RedeemAsset",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(MarketplaceServiceServer).RedeemAsset(ctx, req.(*RedeemAssetRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _MarketplaceService_FetchReservations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(FetchReservationsRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(MarketplaceServiceServer).FetchReservations(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/proto.hummingbird.v1.MarketplaceService/FetchReservations",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(MarketplaceServiceServer).FetchReservations(ctx, req.(*FetchReservationsRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _MarketplaceService_SplitAsset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(SplitAssetRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(MarketplaceServiceServer).SplitAsset(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/proto.hummingbird.v1.MarketplaceService/SplitAsset",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(MarketplaceServiceServer).SplitAsset(ctx, req.(*SplitAssetRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _MarketplaceService_CombineAssets_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(CombineAssetRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(MarketplaceServiceServer).CombineAssets(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/proto.hummingbird.v1.MarketplaceService/CombineAssets",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(MarketplaceServiceServer).CombineAssets(ctx, req.(*CombineAssetRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _MarketplaceService_Statistics_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(StatisticsRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(MarketplaceServiceServer).Statistics(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/proto.hummingbird.v1.MarketplaceService/Statistics",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(MarketplaceServiceServer).Statistics(ctx, req.(*StatisticsRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _MarketplaceService_UpdateAssets_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(UpdateAssetsRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(MarketplaceServiceServer).UpdateAssets(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/proto.hummingbird.v1.MarketplaceService/UpdateAssets",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(MarketplaceServiceServer).UpdateAssets(ctx, req.(*UpdateAssetsRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+var _MarketplaceService_serviceDesc = grpc.ServiceDesc{
+ ServiceName: "proto.hummingbird.v1.MarketplaceService",
+ HandlerType: (*MarketplaceServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "Info",
+ Handler: _MarketplaceService_Info_Handler,
+ },
+ {
+ MethodName: "PublishAsset",
+ Handler: _MarketplaceService_PublishAsset_Handler,
+ },
+ {
+ MethodName: "SearchAssets",
+ Handler: _MarketplaceService_SearchAssets_Handler,
+ },
+ {
+ MethodName: "BuyAssets",
+ Handler: _MarketplaceService_BuyAssets_Handler,
+ },
+ {
+ MethodName: "RedeemAsset",
+ Handler: _MarketplaceService_RedeemAsset_Handler,
+ },
+ {
+ MethodName: "FetchReservations",
+ Handler: _MarketplaceService_FetchReservations_Handler,
+ },
+ {
+ MethodName: "SplitAsset",
+ Handler: _MarketplaceService_SplitAsset_Handler,
+ },
+ {
+ MethodName: "CombineAssets",
+ Handler: _MarketplaceService_CombineAssets_Handler,
+ },
+ {
+ MethodName: "Statistics",
+ Handler: _MarketplaceService_Statistics_Handler,
+ },
+ {
+ MethodName: "UpdateAssets",
+ Handler: _MarketplaceService_UpdateAssets_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "proto/hummingbird/v1/marketplace.proto",
+}
diff --git a/pkg/proto/hummingbird/redemption.pb.go b/pkg/proto/hummingbird/redemption.pb.go
new file mode 100644
index 0000000000..b7f84667b1
--- /dev/null
+++ b/pkg/proto/hummingbird/redemption.pb.go
@@ -0,0 +1,632 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.36.10
+// protoc v6.31.1
+// source: proto/hummingbird/v1/redemption.proto
+
+package hummingbird
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+ reflect "reflect"
+ sync "sync"
+ unsafe "unsafe"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type DelegateRedemptionRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ ExpirationTime *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"`
+ ReservationIdUpperBound uint32 `protobuf:"varint,2,opt,name=reservation_id_upper_bound,json=reservationIdUpperBound,proto3" json:"reservation_id_upper_bound,omitempty"`
+ Key []byte `protobuf:"bytes,3,opt,name=Key,proto3" json:"Key,omitempty"`
+ EncodingPoints []uint32 `protobuf:"varint,4,rep,packed,name=encoding_points,json=encodingPoints,proto3" json:"encoding_points,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *DelegateRedemptionRequest) Reset() {
+ *x = DelegateRedemptionRequest{}
+ mi := &file_proto_hummingbird_v1_redemption_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *DelegateRedemptionRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DelegateRedemptionRequest) ProtoMessage() {}
+
+func (x *DelegateRedemptionRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_redemption_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use DelegateRedemptionRequest.ProtoReflect.Descriptor instead.
+func (*DelegateRedemptionRequest) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_redemption_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *DelegateRedemptionRequest) GetExpirationTime() *timestamppb.Timestamp {
+ if x != nil {
+ return x.ExpirationTime
+ }
+ return nil
+}
+
+func (x *DelegateRedemptionRequest) GetReservationIdUpperBound() uint32 {
+ if x != nil {
+ return x.ReservationIdUpperBound
+ }
+ return 0
+}
+
+func (x *DelegateRedemptionRequest) GetKey() []byte {
+ if x != nil {
+ return x.Key
+ }
+ return nil
+}
+
+func (x *DelegateRedemptionRequest) GetEncodingPoints() []uint32 {
+ if x != nil {
+ return x.EncodingPoints
+ }
+ return nil
+}
+
+type DelegateRedemptionResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ ExpirationTime *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *DelegateRedemptionResponse) Reset() {
+ *x = DelegateRedemptionResponse{}
+ mi := &file_proto_hummingbird_v1_redemption_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *DelegateRedemptionResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DelegateRedemptionResponse) ProtoMessage() {}
+
+func (x *DelegateRedemptionResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_redemption_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use DelegateRedemptionResponse.ProtoReflect.Descriptor instead.
+func (*DelegateRedemptionResponse) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_redemption_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *DelegateRedemptionResponse) GetExpirationTime() *timestamppb.Timestamp {
+ if x != nil {
+ return x.ExpirationTime
+ }
+ return nil
+}
+
+type RedeemAssetFromASRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ IngressId uint32 `protobuf:"varint,1,opt,name=ingress_id,json=ingressId,proto3" json:"ingress_id,omitempty"`
+ EgressId uint32 `protobuf:"varint,2,opt,name=egress_id,json=egressId,proto3" json:"egress_id,omitempty"`
+ Bandwidth uint32 `protobuf:"varint,3,opt,name=bandwidth,proto3" json:"bandwidth,omitempty"`
+ StartsAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=starts_at,json=startsAt,proto3" json:"starts_at,omitempty"`
+ StopsAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=stops_at,json=stopsAt,proto3" json:"stops_at,omitempty"`
+ RequestId uint64 `protobuf:"varint,6,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *RedeemAssetFromASRequest) Reset() {
+ *x = RedeemAssetFromASRequest{}
+ mi := &file_proto_hummingbird_v1_redemption_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *RedeemAssetFromASRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RedeemAssetFromASRequest) ProtoMessage() {}
+
+func (x *RedeemAssetFromASRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_redemption_proto_msgTypes[2]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RedeemAssetFromASRequest.ProtoReflect.Descriptor instead.
+func (*RedeemAssetFromASRequest) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_redemption_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *RedeemAssetFromASRequest) GetIngressId() uint32 {
+ if x != nil {
+ return x.IngressId
+ }
+ return 0
+}
+
+func (x *RedeemAssetFromASRequest) GetEgressId() uint32 {
+ if x != nil {
+ return x.EgressId
+ }
+ return 0
+}
+
+func (x *RedeemAssetFromASRequest) GetBandwidth() uint32 {
+ if x != nil {
+ return x.Bandwidth
+ }
+ return 0
+}
+
+func (x *RedeemAssetFromASRequest) GetStartsAt() *timestamppb.Timestamp {
+ if x != nil {
+ return x.StartsAt
+ }
+ return nil
+}
+
+func (x *RedeemAssetFromASRequest) GetStopsAt() *timestamppb.Timestamp {
+ if x != nil {
+ return x.StopsAt
+ }
+ return nil
+}
+
+func (x *RedeemAssetFromASRequest) GetRequestId() uint64 {
+ if x != nil {
+ return x.RequestId
+ }
+ return 0
+}
+
+type RedeemAssetFromASResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ RequestId uint64 `protobuf:"varint,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
+ // Types that are valid to be assigned to Result:
+ //
+ // *RedeemAssetFromASResponse_ResInfo
+ // *RedeemAssetFromASResponse_Error
+ Result isRedeemAssetFromASResponse_Result `protobuf_oneof:"result"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *RedeemAssetFromASResponse) Reset() {
+ *x = RedeemAssetFromASResponse{}
+ mi := &file_proto_hummingbird_v1_redemption_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *RedeemAssetFromASResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RedeemAssetFromASResponse) ProtoMessage() {}
+
+func (x *RedeemAssetFromASResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_redemption_proto_msgTypes[3]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RedeemAssetFromASResponse.ProtoReflect.Descriptor instead.
+func (*RedeemAssetFromASResponse) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_redemption_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *RedeemAssetFromASResponse) GetRequestId() uint64 {
+ if x != nil {
+ return x.RequestId
+ }
+ return 0
+}
+
+func (x *RedeemAssetFromASResponse) GetResult() isRedeemAssetFromASResponse_Result {
+ if x != nil {
+ return x.Result
+ }
+ return nil
+}
+
+func (x *RedeemAssetFromASResponse) GetResInfo() *ReservationInfo {
+ if x != nil {
+ if x, ok := x.Result.(*RedeemAssetFromASResponse_ResInfo); ok {
+ return x.ResInfo
+ }
+ }
+ return nil
+}
+
+func (x *RedeemAssetFromASResponse) GetError() string {
+ if x != nil {
+ if x, ok := x.Result.(*RedeemAssetFromASResponse_Error); ok {
+ return x.Error
+ }
+ }
+ return ""
+}
+
+type isRedeemAssetFromASResponse_Result interface {
+ isRedeemAssetFromASResponse_Result()
+}
+
+type RedeemAssetFromASResponse_ResInfo struct {
+ ResInfo *ReservationInfo `protobuf:"bytes,2,opt,name=res_info,json=resInfo,proto3,oneof"`
+}
+
+type RedeemAssetFromASResponse_Error struct {
+ Error string `protobuf:"bytes,3,opt,name=error,proto3,oneof"`
+}
+
+func (*RedeemAssetFromASResponse_ResInfo) isRedeemAssetFromASResponse_Result() {}
+
+func (*RedeemAssetFromASResponse_Error) isRedeemAssetFromASResponse_Result() {}
+
+type ReservationInfo struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ ReservationId uint32 `protobuf:"varint,1,opt,name=reservation_id,json=reservationId,proto3" json:"reservation_id,omitempty"`
+ BandwithRounded uint32 `protobuf:"varint,2,opt,name=bandwith_rounded,json=bandwithRounded,proto3" json:"bandwith_rounded,omitempty"`
+ BwDataplaneEncoding uint32 `protobuf:"varint,3,opt,name=bw_dataplane_encoding,json=bwDataplaneEncoding,proto3" json:"bw_dataplane_encoding,omitempty"`
+ AuthenticationKey []byte `protobuf:"bytes,4,opt,name=authentication_key,json=authenticationKey,proto3" json:"authentication_key,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ReservationInfo) Reset() {
+ *x = ReservationInfo{}
+ mi := &file_proto_hummingbird_v1_redemption_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ReservationInfo) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ReservationInfo) ProtoMessage() {}
+
+func (x *ReservationInfo) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_hummingbird_v1_redemption_proto_msgTypes[4]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ReservationInfo.ProtoReflect.Descriptor instead.
+func (*ReservationInfo) Descriptor() ([]byte, []int) {
+ return file_proto_hummingbird_v1_redemption_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *ReservationInfo) GetReservationId() uint32 {
+ if x != nil {
+ return x.ReservationId
+ }
+ return 0
+}
+
+func (x *ReservationInfo) GetBandwithRounded() uint32 {
+ if x != nil {
+ return x.BandwithRounded
+ }
+ return 0
+}
+
+func (x *ReservationInfo) GetBwDataplaneEncoding() uint32 {
+ if x != nil {
+ return x.BwDataplaneEncoding
+ }
+ return 0
+}
+
+func (x *ReservationInfo) GetAuthenticationKey() []byte {
+ if x != nil {
+ return x.AuthenticationKey
+ }
+ return nil
+}
+
+var File_proto_hummingbird_v1_redemption_proto protoreflect.FileDescriptor
+
+const file_proto_hummingbird_v1_redemption_proto_rawDesc = "" +
+ "\n" +
+ "%proto/hummingbird/v1/redemption.proto\x12\x14proto.hummingbird.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xd8\x01\n" +
+ "\x19DelegateRedemptionRequest\x12C\n" +
+ "\x0fexpiration_time\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\x0eexpirationTime\x12;\n" +
+ "\x1areservation_id_upper_bound\x18\x02 \x01(\rR\x17reservationIdUpperBound\x12\x10\n" +
+ "\x03Key\x18\x03 \x01(\fR\x03Key\x12'\n" +
+ "\x0fencoding_points\x18\x04 \x03(\rR\x0eencodingPoints\"a\n" +
+ "\x1aDelegateRedemptionResponse\x12C\n" +
+ "\x0fexpiration_time\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\x0eexpirationTime\"\x83\x02\n" +
+ "\x18RedeemAssetFromASRequest\x12\x1d\n" +
+ "\n" +
+ "ingress_id\x18\x01 \x01(\rR\tingressId\x12\x1b\n" +
+ "\tegress_id\x18\x02 \x01(\rR\begressId\x12\x1c\n" +
+ "\tbandwidth\x18\x03 \x01(\rR\tbandwidth\x127\n" +
+ "\tstarts_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\bstartsAt\x125\n" +
+ "\bstops_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\astopsAt\x12\x1d\n" +
+ "\n" +
+ "request_id\x18\x06 \x01(\x04R\trequestId\"\xa0\x01\n" +
+ "\x19RedeemAssetFromASResponse\x12\x1d\n" +
+ "\n" +
+ "request_id\x18\x01 \x01(\x04R\trequestId\x12B\n" +
+ "\bres_info\x18\x02 \x01(\v2%.proto.hummingbird.v1.ReservationInfoH\x00R\aresInfo\x12\x16\n" +
+ "\x05error\x18\x03 \x01(\tH\x00R\x05errorB\b\n" +
+ "\x06result\"\xc6\x01\n" +
+ "\x0fReservationInfo\x12%\n" +
+ "\x0ereservation_id\x18\x01 \x01(\rR\rreservationId\x12)\n" +
+ "\x10bandwith_rounded\x18\x02 \x01(\rR\x0fbandwithRounded\x122\n" +
+ "\x15bw_dataplane_encoding\x18\x03 \x01(\rR\x13bwDataplaneEncoding\x12-\n" +
+ "\x12authentication_key\x18\x04 \x01(\fR\x11authenticationKey2\x86\x02\n" +
+ "\x11RedemptionService\x12v\n" +
+ "\rRedeemASAsset\x12/.proto.hummingbird.v1.RedeemAssetFromASResponse\x1a..proto.hummingbird.v1.RedeemAssetFromASRequest\"\x00(\x010\x01\x12y\n" +
+ "\x12DelegateRedemption\x12/.proto.hummingbird.v1.DelegateRedemptionRequest\x1a0.proto.hummingbird.v1.DelegateRedemptionResponse\"\x00B3Z1github.com/scionproto/scion/pkg/proto/hummingbirdb\x06proto3"
+
+var (
+ file_proto_hummingbird_v1_redemption_proto_rawDescOnce sync.Once
+ file_proto_hummingbird_v1_redemption_proto_rawDescData []byte
+)
+
+func file_proto_hummingbird_v1_redemption_proto_rawDescGZIP() []byte {
+ file_proto_hummingbird_v1_redemption_proto_rawDescOnce.Do(func() {
+ file_proto_hummingbird_v1_redemption_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_hummingbird_v1_redemption_proto_rawDesc), len(file_proto_hummingbird_v1_redemption_proto_rawDesc)))
+ })
+ return file_proto_hummingbird_v1_redemption_proto_rawDescData
+}
+
+var file_proto_hummingbird_v1_redemption_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
+var file_proto_hummingbird_v1_redemption_proto_goTypes = []any{
+ (*DelegateRedemptionRequest)(nil), // 0: proto.hummingbird.v1.DelegateRedemptionRequest
+ (*DelegateRedemptionResponse)(nil), // 1: proto.hummingbird.v1.DelegateRedemptionResponse
+ (*RedeemAssetFromASRequest)(nil), // 2: proto.hummingbird.v1.RedeemAssetFromASRequest
+ (*RedeemAssetFromASResponse)(nil), // 3: proto.hummingbird.v1.RedeemAssetFromASResponse
+ (*ReservationInfo)(nil), // 4: proto.hummingbird.v1.ReservationInfo
+ (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp
+}
+var file_proto_hummingbird_v1_redemption_proto_depIdxs = []int32{
+ 5, // 0: proto.hummingbird.v1.DelegateRedemptionRequest.expiration_time:type_name -> google.protobuf.Timestamp
+ 5, // 1: proto.hummingbird.v1.DelegateRedemptionResponse.expiration_time:type_name -> google.protobuf.Timestamp
+ 5, // 2: proto.hummingbird.v1.RedeemAssetFromASRequest.starts_at:type_name -> google.protobuf.Timestamp
+ 5, // 3: proto.hummingbird.v1.RedeemAssetFromASRequest.stops_at:type_name -> google.protobuf.Timestamp
+ 4, // 4: proto.hummingbird.v1.RedeemAssetFromASResponse.res_info:type_name -> proto.hummingbird.v1.ReservationInfo
+ 3, // 5: proto.hummingbird.v1.RedemptionService.RedeemASAsset:input_type -> proto.hummingbird.v1.RedeemAssetFromASResponse
+ 0, // 6: proto.hummingbird.v1.RedemptionService.DelegateRedemption:input_type -> proto.hummingbird.v1.DelegateRedemptionRequest
+ 2, // 7: proto.hummingbird.v1.RedemptionService.RedeemASAsset:output_type -> proto.hummingbird.v1.RedeemAssetFromASRequest
+ 1, // 8: proto.hummingbird.v1.RedemptionService.DelegateRedemption:output_type -> proto.hummingbird.v1.DelegateRedemptionResponse
+ 7, // [7:9] is the sub-list for method output_type
+ 5, // [5:7] is the sub-list for method input_type
+ 5, // [5:5] is the sub-list for extension type_name
+ 5, // [5:5] is the sub-list for extension extendee
+ 0, // [0:5] is the sub-list for field type_name
+}
+
+func init() { file_proto_hummingbird_v1_redemption_proto_init() }
+func file_proto_hummingbird_v1_redemption_proto_init() {
+ if File_proto_hummingbird_v1_redemption_proto != nil {
+ return
+ }
+ file_proto_hummingbird_v1_redemption_proto_msgTypes[3].OneofWrappers = []any{
+ (*RedeemAssetFromASResponse_ResInfo)(nil),
+ (*RedeemAssetFromASResponse_Error)(nil),
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_hummingbird_v1_redemption_proto_rawDesc), len(file_proto_hummingbird_v1_redemption_proto_rawDesc)),
+ NumEnums: 0,
+ NumMessages: 5,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_proto_hummingbird_v1_redemption_proto_goTypes,
+ DependencyIndexes: file_proto_hummingbird_v1_redemption_proto_depIdxs,
+ MessageInfos: file_proto_hummingbird_v1_redemption_proto_msgTypes,
+ }.Build()
+ File_proto_hummingbird_v1_redemption_proto = out.File
+ file_proto_hummingbird_v1_redemption_proto_goTypes = nil
+ file_proto_hummingbird_v1_redemption_proto_depIdxs = nil
+}
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ context.Context
+var _ grpc.ClientConnInterface
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+const _ = grpc.SupportPackageIsVersion6
+
+// RedemptionServiceClient is the client API for RedemptionService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
+type RedemptionServiceClient interface {
+ RedeemASAsset(ctx context.Context, opts ...grpc.CallOption) (RedemptionService_RedeemASAssetClient, error)
+ DelegateRedemption(ctx context.Context, in *DelegateRedemptionRequest, opts ...grpc.CallOption) (*DelegateRedemptionResponse, error)
+}
+
+type redemptionServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewRedemptionServiceClient(cc grpc.ClientConnInterface) RedemptionServiceClient {
+ return &redemptionServiceClient{cc}
+}
+
+func (c *redemptionServiceClient) RedeemASAsset(ctx context.Context, opts ...grpc.CallOption) (RedemptionService_RedeemASAssetClient, error) {
+ stream, err := c.cc.NewStream(ctx, &_RedemptionService_serviceDesc.Streams[0], "/proto.hummingbird.v1.RedemptionService/RedeemASAsset", opts...)
+ if err != nil {
+ return nil, err
+ }
+ x := &redemptionServiceRedeemASAssetClient{stream}
+ return x, nil
+}
+
+type RedemptionService_RedeemASAssetClient interface {
+ Send(*RedeemAssetFromASResponse) error
+ Recv() (*RedeemAssetFromASRequest, error)
+ grpc.ClientStream
+}
+
+type redemptionServiceRedeemASAssetClient struct {
+ grpc.ClientStream
+}
+
+func (x *redemptionServiceRedeemASAssetClient) Send(m *RedeemAssetFromASResponse) error {
+ return x.ClientStream.SendMsg(m)
+}
+
+func (x *redemptionServiceRedeemASAssetClient) Recv() (*RedeemAssetFromASRequest, error) {
+ m := new(RedeemAssetFromASRequest)
+ if err := x.ClientStream.RecvMsg(m); err != nil {
+ return nil, err
+ }
+ return m, nil
+}
+
+func (c *redemptionServiceClient) DelegateRedemption(ctx context.Context, in *DelegateRedemptionRequest, opts ...grpc.CallOption) (*DelegateRedemptionResponse, error) {
+ out := new(DelegateRedemptionResponse)
+ err := c.cc.Invoke(ctx, "/proto.hummingbird.v1.RedemptionService/DelegateRedemption", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// RedemptionServiceServer is the server API for RedemptionService service.
+type RedemptionServiceServer interface {
+ RedeemASAsset(RedemptionService_RedeemASAssetServer) error
+ DelegateRedemption(context.Context, *DelegateRedemptionRequest) (*DelegateRedemptionResponse, error)
+}
+
+// UnimplementedRedemptionServiceServer can be embedded to have forward compatible implementations.
+type UnimplementedRedemptionServiceServer struct {
+}
+
+func (*UnimplementedRedemptionServiceServer) RedeemASAsset(RedemptionService_RedeemASAssetServer) error {
+ return status.Errorf(codes.Unimplemented, "method RedeemASAsset not implemented")
+}
+func (*UnimplementedRedemptionServiceServer) DelegateRedemption(context.Context, *DelegateRedemptionRequest) (*DelegateRedemptionResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method DelegateRedemption not implemented")
+}
+
+func RegisterRedemptionServiceServer(s *grpc.Server, srv RedemptionServiceServer) {
+ s.RegisterService(&_RedemptionService_serviceDesc, srv)
+}
+
+func _RedemptionService_RedeemASAsset_Handler(srv interface{}, stream grpc.ServerStream) error {
+ return srv.(RedemptionServiceServer).RedeemASAsset(&redemptionServiceRedeemASAssetServer{stream})
+}
+
+type RedemptionService_RedeemASAssetServer interface {
+ Send(*RedeemAssetFromASRequest) error
+ Recv() (*RedeemAssetFromASResponse, error)
+ grpc.ServerStream
+}
+
+type redemptionServiceRedeemASAssetServer struct {
+ grpc.ServerStream
+}
+
+func (x *redemptionServiceRedeemASAssetServer) Send(m *RedeemAssetFromASRequest) error {
+ return x.ServerStream.SendMsg(m)
+}
+
+func (x *redemptionServiceRedeemASAssetServer) Recv() (*RedeemAssetFromASResponse, error) {
+ m := new(RedeemAssetFromASResponse)
+ if err := x.ServerStream.RecvMsg(m); err != nil {
+ return nil, err
+ }
+ return m, nil
+}
+
+func _RedemptionService_DelegateRedemption_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(DelegateRedemptionRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(RedemptionServiceServer).DelegateRedemption(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/proto.hummingbird.v1.RedemptionService/DelegateRedemption",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(RedemptionServiceServer).DelegateRedemption(ctx, req.(*DelegateRedemptionRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+var _RedemptionService_serviceDesc = grpc.ServiceDesc{
+ ServiceName: "proto.hummingbird.v1.RedemptionService",
+ HandlerType: (*RedemptionServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "DelegateRedemption",
+ Handler: _RedemptionService_DelegateRedemption_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{
+ {
+ StreamName: "RedeemASAsset",
+ Handler: _RedemptionService_RedeemASAsset_Handler,
+ ServerStreams: true,
+ ClientStreams: true,
+ },
+ },
+ Metadata: "proto/hummingbird/v1/redemption.proto",
+}
diff --git a/pkg/proto/hummingbird/v1/hummingbirdconnect/BUILD.bazel b/pkg/proto/hummingbird/v1/hummingbirdconnect/BUILD.bazel
new file mode 100644
index 0000000000..875b578499
--- /dev/null
+++ b/pkg/proto/hummingbird/v1/hummingbirdconnect/BUILD.bazel
@@ -0,0 +1,11 @@
+load("//tools/proto:connect.bzl", "go_connect_library")
+
+go_connect_library(
+ name = "go_default_library",
+ files = [
+ "account.connect.go",
+ "marketplace.connect.go",
+ "redemption.connect.go",
+ ],
+ proto = "hummingbird",
+)
diff --git a/pkg/proto/hummingbird/v1/hummingbirdconnect/account.connect.go b/pkg/proto/hummingbird/v1/hummingbirdconnect/account.connect.go
new file mode 100644
index 0000000000..9a51d8ae01
--- /dev/null
+++ b/pkg/proto/hummingbird/v1/hummingbirdconnect/account.connect.go
@@ -0,0 +1,195 @@
+// Code generated by protoc-gen-connect-go. DO NOT EDIT.
+//
+// Source: proto/hummingbird/v1/account.proto
+
+package hummingbirdconnect
+
+import (
+ connect "connectrpc.com/connect"
+ context "context"
+ errors "errors"
+ hummingbird "github.com/scionproto/scion/pkg/proto/hummingbird"
+ http "net/http"
+ strings "strings"
+)
+
+// This is a compile-time assertion to ensure that this generated file and the connect package are
+// compatible. If you get a compiler error that this constant is not defined, this code was
+// generated with a version of connect newer than the one compiled into your binary. You can fix the
+// problem by either regenerating this code with an older version of connect or updating the connect
+// version compiled into your binary.
+const _ = connect.IsAtLeastVersion1_13_0
+
+const (
+ // AccountServiceName is the fully-qualified name of the AccountService service.
+ AccountServiceName = "proto.hummingbird.v1.AccountService"
+)
+
+// These constants are the fully-qualified names of the RPCs defined in this package. They're
+// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
+//
+// Note that these are different from the fully-qualified method names used by
+// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
+// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
+// period.
+const (
+ // AccountServiceResetJWTProcedure is the fully-qualified name of the AccountService's ResetJWT RPC.
+ AccountServiceResetJWTProcedure = "/proto.hummingbird.v1.AccountService/ResetJWT"
+ // AccountServiceCreateChallengeProcedure is the fully-qualified name of the AccountService's
+ // CreateChallenge RPC.
+ AccountServiceCreateChallengeProcedure = "/proto.hummingbird.v1.AccountService/CreateChallenge"
+ // AccountServiceRegisterASProcedure is the fully-qualified name of the AccountService's RegisterAS
+ // RPC.
+ AccountServiceRegisterASProcedure = "/proto.hummingbird.v1.AccountService/RegisterAS"
+ // AccountServiceSetPasswordProcedure is the fully-qualified name of the AccountService's
+ // SetPassword RPC.
+ AccountServiceSetPasswordProcedure = "/proto.hummingbird.v1.AccountService/SetPassword"
+)
+
+// AccountServiceClient is a client for the proto.hummingbird.v1.AccountService service.
+type AccountServiceClient interface {
+ ResetJWT(context.Context, *connect.Request[hummingbird.JWTResetRequest]) (*connect.Response[hummingbird.JWTResetResponse], error)
+ CreateChallenge(context.Context, *connect.Request[hummingbird.CreateChallengeRequest]) (*connect.Response[hummingbird.CreateChallengeResponse], error)
+ RegisterAS(context.Context, *connect.Request[hummingbird.RegisterASRequest]) (*connect.Response[hummingbird.RegisterASResponse], error)
+ SetPassword(context.Context, *connect.Request[hummingbird.SetPasswordRequest]) (*connect.Response[hummingbird.SetPasswordResponse], error)
+}
+
+// NewAccountServiceClient constructs a client for the proto.hummingbird.v1.AccountService service.
+// By default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped
+// responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the
+// connect.WithGRPC() or connect.WithGRPCWeb() options.
+//
+// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
+// http://api.acme.com or https://acme.com/grpc).
+func NewAccountServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) AccountServiceClient {
+ baseURL = strings.TrimRight(baseURL, "/")
+ accountServiceMethods := hummingbird.File_proto_hummingbird_v1_account_proto.Services().ByName("AccountService").Methods()
+ return &accountServiceClient{
+ resetJWT: connect.NewClient[hummingbird.JWTResetRequest, hummingbird.JWTResetResponse](
+ httpClient,
+ baseURL+AccountServiceResetJWTProcedure,
+ connect.WithSchema(accountServiceMethods.ByName("ResetJWT")),
+ connect.WithClientOptions(opts...),
+ ),
+ createChallenge: connect.NewClient[hummingbird.CreateChallengeRequest, hummingbird.CreateChallengeResponse](
+ httpClient,
+ baseURL+AccountServiceCreateChallengeProcedure,
+ connect.WithSchema(accountServiceMethods.ByName("CreateChallenge")),
+ connect.WithClientOptions(opts...),
+ ),
+ registerAS: connect.NewClient[hummingbird.RegisterASRequest, hummingbird.RegisterASResponse](
+ httpClient,
+ baseURL+AccountServiceRegisterASProcedure,
+ connect.WithSchema(accountServiceMethods.ByName("RegisterAS")),
+ connect.WithClientOptions(opts...),
+ ),
+ setPassword: connect.NewClient[hummingbird.SetPasswordRequest, hummingbird.SetPasswordResponse](
+ httpClient,
+ baseURL+AccountServiceSetPasswordProcedure,
+ connect.WithSchema(accountServiceMethods.ByName("SetPassword")),
+ connect.WithClientOptions(opts...),
+ ),
+ }
+}
+
+// accountServiceClient implements AccountServiceClient.
+type accountServiceClient struct {
+ resetJWT *connect.Client[hummingbird.JWTResetRequest, hummingbird.JWTResetResponse]
+ createChallenge *connect.Client[hummingbird.CreateChallengeRequest, hummingbird.CreateChallengeResponse]
+ registerAS *connect.Client[hummingbird.RegisterASRequest, hummingbird.RegisterASResponse]
+ setPassword *connect.Client[hummingbird.SetPasswordRequest, hummingbird.SetPasswordResponse]
+}
+
+// ResetJWT calls proto.hummingbird.v1.AccountService.ResetJWT.
+func (c *accountServiceClient) ResetJWT(ctx context.Context, req *connect.Request[hummingbird.JWTResetRequest]) (*connect.Response[hummingbird.JWTResetResponse], error) {
+ return c.resetJWT.CallUnary(ctx, req)
+}
+
+// CreateChallenge calls proto.hummingbird.v1.AccountService.CreateChallenge.
+func (c *accountServiceClient) CreateChallenge(ctx context.Context, req *connect.Request[hummingbird.CreateChallengeRequest]) (*connect.Response[hummingbird.CreateChallengeResponse], error) {
+ return c.createChallenge.CallUnary(ctx, req)
+}
+
+// RegisterAS calls proto.hummingbird.v1.AccountService.RegisterAS.
+func (c *accountServiceClient) RegisterAS(ctx context.Context, req *connect.Request[hummingbird.RegisterASRequest]) (*connect.Response[hummingbird.RegisterASResponse], error) {
+ return c.registerAS.CallUnary(ctx, req)
+}
+
+// SetPassword calls proto.hummingbird.v1.AccountService.SetPassword.
+func (c *accountServiceClient) SetPassword(ctx context.Context, req *connect.Request[hummingbird.SetPasswordRequest]) (*connect.Response[hummingbird.SetPasswordResponse], error) {
+ return c.setPassword.CallUnary(ctx, req)
+}
+
+// AccountServiceHandler is an implementation of the proto.hummingbird.v1.AccountService service.
+type AccountServiceHandler interface {
+ ResetJWT(context.Context, *connect.Request[hummingbird.JWTResetRequest]) (*connect.Response[hummingbird.JWTResetResponse], error)
+ CreateChallenge(context.Context, *connect.Request[hummingbird.CreateChallengeRequest]) (*connect.Response[hummingbird.CreateChallengeResponse], error)
+ RegisterAS(context.Context, *connect.Request[hummingbird.RegisterASRequest]) (*connect.Response[hummingbird.RegisterASResponse], error)
+ SetPassword(context.Context, *connect.Request[hummingbird.SetPasswordRequest]) (*connect.Response[hummingbird.SetPasswordResponse], error)
+}
+
+// NewAccountServiceHandler builds an HTTP handler from the service implementation. It returns the
+// path on which to mount the handler and the handler itself.
+//
+// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
+// and JSON codecs. They also support gzip compression.
+func NewAccountServiceHandler(svc AccountServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
+ accountServiceMethods := hummingbird.File_proto_hummingbird_v1_account_proto.Services().ByName("AccountService").Methods()
+ accountServiceResetJWTHandler := connect.NewUnaryHandler(
+ AccountServiceResetJWTProcedure,
+ svc.ResetJWT,
+ connect.WithSchema(accountServiceMethods.ByName("ResetJWT")),
+ connect.WithHandlerOptions(opts...),
+ )
+ accountServiceCreateChallengeHandler := connect.NewUnaryHandler(
+ AccountServiceCreateChallengeProcedure,
+ svc.CreateChallenge,
+ connect.WithSchema(accountServiceMethods.ByName("CreateChallenge")),
+ connect.WithHandlerOptions(opts...),
+ )
+ accountServiceRegisterASHandler := connect.NewUnaryHandler(
+ AccountServiceRegisterASProcedure,
+ svc.RegisterAS,
+ connect.WithSchema(accountServiceMethods.ByName("RegisterAS")),
+ connect.WithHandlerOptions(opts...),
+ )
+ accountServiceSetPasswordHandler := connect.NewUnaryHandler(
+ AccountServiceSetPasswordProcedure,
+ svc.SetPassword,
+ connect.WithSchema(accountServiceMethods.ByName("SetPassword")),
+ connect.WithHandlerOptions(opts...),
+ )
+ return "/proto.hummingbird.v1.AccountService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case AccountServiceResetJWTProcedure:
+ accountServiceResetJWTHandler.ServeHTTP(w, r)
+ case AccountServiceCreateChallengeProcedure:
+ accountServiceCreateChallengeHandler.ServeHTTP(w, r)
+ case AccountServiceRegisterASProcedure:
+ accountServiceRegisterASHandler.ServeHTTP(w, r)
+ case AccountServiceSetPasswordProcedure:
+ accountServiceSetPasswordHandler.ServeHTTP(w, r)
+ default:
+ http.NotFound(w, r)
+ }
+ })
+}
+
+// UnimplementedAccountServiceHandler returns CodeUnimplemented from all methods.
+type UnimplementedAccountServiceHandler struct{}
+
+func (UnimplementedAccountServiceHandler) ResetJWT(context.Context, *connect.Request[hummingbird.JWTResetRequest]) (*connect.Response[hummingbird.JWTResetResponse], error) {
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("proto.hummingbird.v1.AccountService.ResetJWT is not implemented"))
+}
+
+func (UnimplementedAccountServiceHandler) CreateChallenge(context.Context, *connect.Request[hummingbird.CreateChallengeRequest]) (*connect.Response[hummingbird.CreateChallengeResponse], error) {
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("proto.hummingbird.v1.AccountService.CreateChallenge is not implemented"))
+}
+
+func (UnimplementedAccountServiceHandler) RegisterAS(context.Context, *connect.Request[hummingbird.RegisterASRequest]) (*connect.Response[hummingbird.RegisterASResponse], error) {
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("proto.hummingbird.v1.AccountService.RegisterAS is not implemented"))
+}
+
+func (UnimplementedAccountServiceHandler) SetPassword(context.Context, *connect.Request[hummingbird.SetPasswordRequest]) (*connect.Response[hummingbird.SetPasswordResponse], error) {
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("proto.hummingbird.v1.AccountService.SetPassword is not implemented"))
+}
diff --git a/pkg/proto/hummingbird/v1/hummingbirdconnect/marketplace.connect.go b/pkg/proto/hummingbird/v1/hummingbirdconnect/marketplace.connect.go
new file mode 100644
index 0000000000..d419fd5879
--- /dev/null
+++ b/pkg/proto/hummingbird/v1/hummingbirdconnect/marketplace.connect.go
@@ -0,0 +1,370 @@
+// Code generated by protoc-gen-connect-go. DO NOT EDIT.
+//
+// Source: proto/hummingbird/v1/marketplace.proto
+
+package hummingbirdconnect
+
+import (
+ connect "connectrpc.com/connect"
+ context "context"
+ errors "errors"
+ hummingbird "github.com/scionproto/scion/pkg/proto/hummingbird"
+ http "net/http"
+ strings "strings"
+)
+
+// This is a compile-time assertion to ensure that this generated file and the connect package are
+// compatible. If you get a compiler error that this constant is not defined, this code was
+// generated with a version of connect newer than the one compiled into your binary. You can fix the
+// problem by either regenerating this code with an older version of connect or updating the connect
+// version compiled into your binary.
+const _ = connect.IsAtLeastVersion1_13_0
+
+const (
+ // MarketplaceServiceName is the fully-qualified name of the MarketplaceService service.
+ MarketplaceServiceName = "proto.hummingbird.v1.MarketplaceService"
+)
+
+// These constants are the fully-qualified names of the RPCs defined in this package. They're
+// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
+//
+// Note that these are different from the fully-qualified method names used by
+// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
+// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
+// period.
+const (
+ // MarketplaceServiceInfoProcedure is the fully-qualified name of the MarketplaceService's Info RPC.
+ MarketplaceServiceInfoProcedure = "/proto.hummingbird.v1.MarketplaceService/Info"
+ // MarketplaceServicePublishAssetProcedure is the fully-qualified name of the MarketplaceService's
+ // PublishAsset RPC.
+ MarketplaceServicePublishAssetProcedure = "/proto.hummingbird.v1.MarketplaceService/PublishAsset"
+ // MarketplaceServiceSearchAssetsProcedure is the fully-qualified name of the MarketplaceService's
+ // SearchAssets RPC.
+ MarketplaceServiceSearchAssetsProcedure = "/proto.hummingbird.v1.MarketplaceService/SearchAssets"
+ // MarketplaceServiceBuyAssetsProcedure is the fully-qualified name of the MarketplaceService's
+ // BuyAssets RPC.
+ MarketplaceServiceBuyAssetsProcedure = "/proto.hummingbird.v1.MarketplaceService/BuyAssets"
+ // MarketplaceServiceRedeemAssetProcedure is the fully-qualified name of the MarketplaceService's
+ // RedeemAsset RPC.
+ MarketplaceServiceRedeemAssetProcedure = "/proto.hummingbird.v1.MarketplaceService/RedeemAsset"
+ // MarketplaceServiceFetchReservationsProcedure is the fully-qualified name of the
+ // MarketplaceService's FetchReservations RPC.
+ MarketplaceServiceFetchReservationsProcedure = "/proto.hummingbird.v1.MarketplaceService/FetchReservations"
+ // MarketplaceServiceSplitAssetProcedure is the fully-qualified name of the MarketplaceService's
+ // SplitAsset RPC.
+ MarketplaceServiceSplitAssetProcedure = "/proto.hummingbird.v1.MarketplaceService/SplitAsset"
+ // MarketplaceServiceCombineAssetsProcedure is the fully-qualified name of the MarketplaceService's
+ // CombineAssets RPC.
+ MarketplaceServiceCombineAssetsProcedure = "/proto.hummingbird.v1.MarketplaceService/CombineAssets"
+ // MarketplaceServiceStatisticsProcedure is the fully-qualified name of the MarketplaceService's
+ // Statistics RPC.
+ MarketplaceServiceStatisticsProcedure = "/proto.hummingbird.v1.MarketplaceService/Statistics"
+ // MarketplaceServiceUpdateAssetsProcedure is the fully-qualified name of the MarketplaceService's
+ // UpdateAssets RPC.
+ MarketplaceServiceUpdateAssetsProcedure = "/proto.hummingbird.v1.MarketplaceService/UpdateAssets"
+)
+
+// MarketplaceServiceClient is a client for the proto.hummingbird.v1.MarketplaceService service.
+type MarketplaceServiceClient interface {
+ Info(context.Context, *connect.Request[hummingbird.MarketplaceInfoRequest]) (*connect.Response[hummingbird.MarketplaceInfoResponse], error)
+ PublishAsset(context.Context, *connect.Request[hummingbird.PublishAssetRequest]) (*connect.Response[hummingbird.PublishAssetResponse], error)
+ SearchAssets(context.Context, *connect.Request[hummingbird.SearchAssetsRequest]) (*connect.Response[hummingbird.SearchAssetsResponse], error)
+ BuyAssets(context.Context, *connect.Request[hummingbird.BuyAssetsRequest]) (*connect.Response[hummingbird.BuyAssetsResponse], error)
+ RedeemAsset(context.Context, *connect.Request[hummingbird.RedeemAssetRequest]) (*connect.Response[hummingbird.RedeemAssetResponse], error)
+ FetchReservations(context.Context, *connect.Request[hummingbird.FetchReservationsRequest]) (*connect.Response[hummingbird.FetchReservationsResponse], error)
+ SplitAsset(context.Context, *connect.Request[hummingbird.SplitAssetRequest]) (*connect.Response[hummingbird.SplitAssetResponse], error)
+ CombineAssets(context.Context, *connect.Request[hummingbird.CombineAssetRequest]) (*connect.Response[hummingbird.CombineAssetResponse], error)
+ Statistics(context.Context, *connect.Request[hummingbird.StatisticsRequest]) (*connect.Response[hummingbird.StatisticsResponse], error)
+ UpdateAssets(context.Context, *connect.Request[hummingbird.UpdateAssetsRequest]) (*connect.Response[hummingbird.UpdateAssetsResponse], error)
+}
+
+// NewMarketplaceServiceClient constructs a client for the proto.hummingbird.v1.MarketplaceService
+// service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for
+// gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply
+// the connect.WithGRPC() or connect.WithGRPCWeb() options.
+//
+// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
+// http://api.acme.com or https://acme.com/grpc).
+func NewMarketplaceServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) MarketplaceServiceClient {
+ baseURL = strings.TrimRight(baseURL, "/")
+ marketplaceServiceMethods := hummingbird.File_proto_hummingbird_v1_marketplace_proto.Services().ByName("MarketplaceService").Methods()
+ return &marketplaceServiceClient{
+ info: connect.NewClient[hummingbird.MarketplaceInfoRequest, hummingbird.MarketplaceInfoResponse](
+ httpClient,
+ baseURL+MarketplaceServiceInfoProcedure,
+ connect.WithSchema(marketplaceServiceMethods.ByName("Info")),
+ connect.WithClientOptions(opts...),
+ ),
+ publishAsset: connect.NewClient[hummingbird.PublishAssetRequest, hummingbird.PublishAssetResponse](
+ httpClient,
+ baseURL+MarketplaceServicePublishAssetProcedure,
+ connect.WithSchema(marketplaceServiceMethods.ByName("PublishAsset")),
+ connect.WithClientOptions(opts...),
+ ),
+ searchAssets: connect.NewClient[hummingbird.SearchAssetsRequest, hummingbird.SearchAssetsResponse](
+ httpClient,
+ baseURL+MarketplaceServiceSearchAssetsProcedure,
+ connect.WithSchema(marketplaceServiceMethods.ByName("SearchAssets")),
+ connect.WithClientOptions(opts...),
+ ),
+ buyAssets: connect.NewClient[hummingbird.BuyAssetsRequest, hummingbird.BuyAssetsResponse](
+ httpClient,
+ baseURL+MarketplaceServiceBuyAssetsProcedure,
+ connect.WithSchema(marketplaceServiceMethods.ByName("BuyAssets")),
+ connect.WithClientOptions(opts...),
+ ),
+ redeemAsset: connect.NewClient[hummingbird.RedeemAssetRequest, hummingbird.RedeemAssetResponse](
+ httpClient,
+ baseURL+MarketplaceServiceRedeemAssetProcedure,
+ connect.WithSchema(marketplaceServiceMethods.ByName("RedeemAsset")),
+ connect.WithClientOptions(opts...),
+ ),
+ fetchReservations: connect.NewClient[hummingbird.FetchReservationsRequest, hummingbird.FetchReservationsResponse](
+ httpClient,
+ baseURL+MarketplaceServiceFetchReservationsProcedure,
+ connect.WithSchema(marketplaceServiceMethods.ByName("FetchReservations")),
+ connect.WithClientOptions(opts...),
+ ),
+ splitAsset: connect.NewClient[hummingbird.SplitAssetRequest, hummingbird.SplitAssetResponse](
+ httpClient,
+ baseURL+MarketplaceServiceSplitAssetProcedure,
+ connect.WithSchema(marketplaceServiceMethods.ByName("SplitAsset")),
+ connect.WithClientOptions(opts...),
+ ),
+ combineAssets: connect.NewClient[hummingbird.CombineAssetRequest, hummingbird.CombineAssetResponse](
+ httpClient,
+ baseURL+MarketplaceServiceCombineAssetsProcedure,
+ connect.WithSchema(marketplaceServiceMethods.ByName("CombineAssets")),
+ connect.WithClientOptions(opts...),
+ ),
+ statistics: connect.NewClient[hummingbird.StatisticsRequest, hummingbird.StatisticsResponse](
+ httpClient,
+ baseURL+MarketplaceServiceStatisticsProcedure,
+ connect.WithSchema(marketplaceServiceMethods.ByName("Statistics")),
+ connect.WithClientOptions(opts...),
+ ),
+ updateAssets: connect.NewClient[hummingbird.UpdateAssetsRequest, hummingbird.UpdateAssetsResponse](
+ httpClient,
+ baseURL+MarketplaceServiceUpdateAssetsProcedure,
+ connect.WithSchema(marketplaceServiceMethods.ByName("UpdateAssets")),
+ connect.WithClientOptions(opts...),
+ ),
+ }
+}
+
+// marketplaceServiceClient implements MarketplaceServiceClient.
+type marketplaceServiceClient struct {
+ info *connect.Client[hummingbird.MarketplaceInfoRequest, hummingbird.MarketplaceInfoResponse]
+ publishAsset *connect.Client[hummingbird.PublishAssetRequest, hummingbird.PublishAssetResponse]
+ searchAssets *connect.Client[hummingbird.SearchAssetsRequest, hummingbird.SearchAssetsResponse]
+ buyAssets *connect.Client[hummingbird.BuyAssetsRequest, hummingbird.BuyAssetsResponse]
+ redeemAsset *connect.Client[hummingbird.RedeemAssetRequest, hummingbird.RedeemAssetResponse]
+ fetchReservations *connect.Client[hummingbird.FetchReservationsRequest, hummingbird.FetchReservationsResponse]
+ splitAsset *connect.Client[hummingbird.SplitAssetRequest, hummingbird.SplitAssetResponse]
+ combineAssets *connect.Client[hummingbird.CombineAssetRequest, hummingbird.CombineAssetResponse]
+ statistics *connect.Client[hummingbird.StatisticsRequest, hummingbird.StatisticsResponse]
+ updateAssets *connect.Client[hummingbird.UpdateAssetsRequest, hummingbird.UpdateAssetsResponse]
+}
+
+// Info calls proto.hummingbird.v1.MarketplaceService.Info.
+func (c *marketplaceServiceClient) Info(ctx context.Context, req *connect.Request[hummingbird.MarketplaceInfoRequest]) (*connect.Response[hummingbird.MarketplaceInfoResponse], error) {
+ return c.info.CallUnary(ctx, req)
+}
+
+// PublishAsset calls proto.hummingbird.v1.MarketplaceService.PublishAsset.
+func (c *marketplaceServiceClient) PublishAsset(ctx context.Context, req *connect.Request[hummingbird.PublishAssetRequest]) (*connect.Response[hummingbird.PublishAssetResponse], error) {
+ return c.publishAsset.CallUnary(ctx, req)
+}
+
+// SearchAssets calls proto.hummingbird.v1.MarketplaceService.SearchAssets.
+func (c *marketplaceServiceClient) SearchAssets(ctx context.Context, req *connect.Request[hummingbird.SearchAssetsRequest]) (*connect.Response[hummingbird.SearchAssetsResponse], error) {
+ return c.searchAssets.CallUnary(ctx, req)
+}
+
+// BuyAssets calls proto.hummingbird.v1.MarketplaceService.BuyAssets.
+func (c *marketplaceServiceClient) BuyAssets(ctx context.Context, req *connect.Request[hummingbird.BuyAssetsRequest]) (*connect.Response[hummingbird.BuyAssetsResponse], error) {
+ return c.buyAssets.CallUnary(ctx, req)
+}
+
+// RedeemAsset calls proto.hummingbird.v1.MarketplaceService.RedeemAsset.
+func (c *marketplaceServiceClient) RedeemAsset(ctx context.Context, req *connect.Request[hummingbird.RedeemAssetRequest]) (*connect.Response[hummingbird.RedeemAssetResponse], error) {
+ return c.redeemAsset.CallUnary(ctx, req)
+}
+
+// FetchReservations calls proto.hummingbird.v1.MarketplaceService.FetchReservations.
+func (c *marketplaceServiceClient) FetchReservations(ctx context.Context, req *connect.Request[hummingbird.FetchReservationsRequest]) (*connect.Response[hummingbird.FetchReservationsResponse], error) {
+ return c.fetchReservations.CallUnary(ctx, req)
+}
+
+// SplitAsset calls proto.hummingbird.v1.MarketplaceService.SplitAsset.
+func (c *marketplaceServiceClient) SplitAsset(ctx context.Context, req *connect.Request[hummingbird.SplitAssetRequest]) (*connect.Response[hummingbird.SplitAssetResponse], error) {
+ return c.splitAsset.CallUnary(ctx, req)
+}
+
+// CombineAssets calls proto.hummingbird.v1.MarketplaceService.CombineAssets.
+func (c *marketplaceServiceClient) CombineAssets(ctx context.Context, req *connect.Request[hummingbird.CombineAssetRequest]) (*connect.Response[hummingbird.CombineAssetResponse], error) {
+ return c.combineAssets.CallUnary(ctx, req)
+}
+
+// Statistics calls proto.hummingbird.v1.MarketplaceService.Statistics.
+func (c *marketplaceServiceClient) Statistics(ctx context.Context, req *connect.Request[hummingbird.StatisticsRequest]) (*connect.Response[hummingbird.StatisticsResponse], error) {
+ return c.statistics.CallUnary(ctx, req)
+}
+
+// UpdateAssets calls proto.hummingbird.v1.MarketplaceService.UpdateAssets.
+func (c *marketplaceServiceClient) UpdateAssets(ctx context.Context, req *connect.Request[hummingbird.UpdateAssetsRequest]) (*connect.Response[hummingbird.UpdateAssetsResponse], error) {
+ return c.updateAssets.CallUnary(ctx, req)
+}
+
+// MarketplaceServiceHandler is an implementation of the proto.hummingbird.v1.MarketplaceService
+// service.
+type MarketplaceServiceHandler interface {
+ Info(context.Context, *connect.Request[hummingbird.MarketplaceInfoRequest]) (*connect.Response[hummingbird.MarketplaceInfoResponse], error)
+ PublishAsset(context.Context, *connect.Request[hummingbird.PublishAssetRequest]) (*connect.Response[hummingbird.PublishAssetResponse], error)
+ SearchAssets(context.Context, *connect.Request[hummingbird.SearchAssetsRequest]) (*connect.Response[hummingbird.SearchAssetsResponse], error)
+ BuyAssets(context.Context, *connect.Request[hummingbird.BuyAssetsRequest]) (*connect.Response[hummingbird.BuyAssetsResponse], error)
+ RedeemAsset(context.Context, *connect.Request[hummingbird.RedeemAssetRequest]) (*connect.Response[hummingbird.RedeemAssetResponse], error)
+ FetchReservations(context.Context, *connect.Request[hummingbird.FetchReservationsRequest]) (*connect.Response[hummingbird.FetchReservationsResponse], error)
+ SplitAsset(context.Context, *connect.Request[hummingbird.SplitAssetRequest]) (*connect.Response[hummingbird.SplitAssetResponse], error)
+ CombineAssets(context.Context, *connect.Request[hummingbird.CombineAssetRequest]) (*connect.Response[hummingbird.CombineAssetResponse], error)
+ Statistics(context.Context, *connect.Request[hummingbird.StatisticsRequest]) (*connect.Response[hummingbird.StatisticsResponse], error)
+ UpdateAssets(context.Context, *connect.Request[hummingbird.UpdateAssetsRequest]) (*connect.Response[hummingbird.UpdateAssetsResponse], error)
+}
+
+// NewMarketplaceServiceHandler builds an HTTP handler from the service implementation. It returns
+// the path on which to mount the handler and the handler itself.
+//
+// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
+// and JSON codecs. They also support gzip compression.
+func NewMarketplaceServiceHandler(svc MarketplaceServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
+ marketplaceServiceMethods := hummingbird.File_proto_hummingbird_v1_marketplace_proto.Services().ByName("MarketplaceService").Methods()
+ marketplaceServiceInfoHandler := connect.NewUnaryHandler(
+ MarketplaceServiceInfoProcedure,
+ svc.Info,
+ connect.WithSchema(marketplaceServiceMethods.ByName("Info")),
+ connect.WithHandlerOptions(opts...),
+ )
+ marketplaceServicePublishAssetHandler := connect.NewUnaryHandler(
+ MarketplaceServicePublishAssetProcedure,
+ svc.PublishAsset,
+ connect.WithSchema(marketplaceServiceMethods.ByName("PublishAsset")),
+ connect.WithHandlerOptions(opts...),
+ )
+ marketplaceServiceSearchAssetsHandler := connect.NewUnaryHandler(
+ MarketplaceServiceSearchAssetsProcedure,
+ svc.SearchAssets,
+ connect.WithSchema(marketplaceServiceMethods.ByName("SearchAssets")),
+ connect.WithHandlerOptions(opts...),
+ )
+ marketplaceServiceBuyAssetsHandler := connect.NewUnaryHandler(
+ MarketplaceServiceBuyAssetsProcedure,
+ svc.BuyAssets,
+ connect.WithSchema(marketplaceServiceMethods.ByName("BuyAssets")),
+ connect.WithHandlerOptions(opts...),
+ )
+ marketplaceServiceRedeemAssetHandler := connect.NewUnaryHandler(
+ MarketplaceServiceRedeemAssetProcedure,
+ svc.RedeemAsset,
+ connect.WithSchema(marketplaceServiceMethods.ByName("RedeemAsset")),
+ connect.WithHandlerOptions(opts...),
+ )
+ marketplaceServiceFetchReservationsHandler := connect.NewUnaryHandler(
+ MarketplaceServiceFetchReservationsProcedure,
+ svc.FetchReservations,
+ connect.WithSchema(marketplaceServiceMethods.ByName("FetchReservations")),
+ connect.WithHandlerOptions(opts...),
+ )
+ marketplaceServiceSplitAssetHandler := connect.NewUnaryHandler(
+ MarketplaceServiceSplitAssetProcedure,
+ svc.SplitAsset,
+ connect.WithSchema(marketplaceServiceMethods.ByName("SplitAsset")),
+ connect.WithHandlerOptions(opts...),
+ )
+ marketplaceServiceCombineAssetsHandler := connect.NewUnaryHandler(
+ MarketplaceServiceCombineAssetsProcedure,
+ svc.CombineAssets,
+ connect.WithSchema(marketplaceServiceMethods.ByName("CombineAssets")),
+ connect.WithHandlerOptions(opts...),
+ )
+ marketplaceServiceStatisticsHandler := connect.NewUnaryHandler(
+ MarketplaceServiceStatisticsProcedure,
+ svc.Statistics,
+ connect.WithSchema(marketplaceServiceMethods.ByName("Statistics")),
+ connect.WithHandlerOptions(opts...),
+ )
+ marketplaceServiceUpdateAssetsHandler := connect.NewUnaryHandler(
+ MarketplaceServiceUpdateAssetsProcedure,
+ svc.UpdateAssets,
+ connect.WithSchema(marketplaceServiceMethods.ByName("UpdateAssets")),
+ connect.WithHandlerOptions(opts...),
+ )
+ return "/proto.hummingbird.v1.MarketplaceService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case MarketplaceServiceInfoProcedure:
+ marketplaceServiceInfoHandler.ServeHTTP(w, r)
+ case MarketplaceServicePublishAssetProcedure:
+ marketplaceServicePublishAssetHandler.ServeHTTP(w, r)
+ case MarketplaceServiceSearchAssetsProcedure:
+ marketplaceServiceSearchAssetsHandler.ServeHTTP(w, r)
+ case MarketplaceServiceBuyAssetsProcedure:
+ marketplaceServiceBuyAssetsHandler.ServeHTTP(w, r)
+ case MarketplaceServiceRedeemAssetProcedure:
+ marketplaceServiceRedeemAssetHandler.ServeHTTP(w, r)
+ case MarketplaceServiceFetchReservationsProcedure:
+ marketplaceServiceFetchReservationsHandler.ServeHTTP(w, r)
+ case MarketplaceServiceSplitAssetProcedure:
+ marketplaceServiceSplitAssetHandler.ServeHTTP(w, r)
+ case MarketplaceServiceCombineAssetsProcedure:
+ marketplaceServiceCombineAssetsHandler.ServeHTTP(w, r)
+ case MarketplaceServiceStatisticsProcedure:
+ marketplaceServiceStatisticsHandler.ServeHTTP(w, r)
+ case MarketplaceServiceUpdateAssetsProcedure:
+ marketplaceServiceUpdateAssetsHandler.ServeHTTP(w, r)
+ default:
+ http.NotFound(w, r)
+ }
+ })
+}
+
+// UnimplementedMarketplaceServiceHandler returns CodeUnimplemented from all methods.
+type UnimplementedMarketplaceServiceHandler struct{}
+
+func (UnimplementedMarketplaceServiceHandler) Info(context.Context, *connect.Request[hummingbird.MarketplaceInfoRequest]) (*connect.Response[hummingbird.MarketplaceInfoResponse], error) {
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("proto.hummingbird.v1.MarketplaceService.Info is not implemented"))
+}
+
+func (UnimplementedMarketplaceServiceHandler) PublishAsset(context.Context, *connect.Request[hummingbird.PublishAssetRequest]) (*connect.Response[hummingbird.PublishAssetResponse], error) {
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("proto.hummingbird.v1.MarketplaceService.PublishAsset is not implemented"))
+}
+
+func (UnimplementedMarketplaceServiceHandler) SearchAssets(context.Context, *connect.Request[hummingbird.SearchAssetsRequest]) (*connect.Response[hummingbird.SearchAssetsResponse], error) {
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("proto.hummingbird.v1.MarketplaceService.SearchAssets is not implemented"))
+}
+
+func (UnimplementedMarketplaceServiceHandler) BuyAssets(context.Context, *connect.Request[hummingbird.BuyAssetsRequest]) (*connect.Response[hummingbird.BuyAssetsResponse], error) {
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("proto.hummingbird.v1.MarketplaceService.BuyAssets is not implemented"))
+}
+
+func (UnimplementedMarketplaceServiceHandler) RedeemAsset(context.Context, *connect.Request[hummingbird.RedeemAssetRequest]) (*connect.Response[hummingbird.RedeemAssetResponse], error) {
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("proto.hummingbird.v1.MarketplaceService.RedeemAsset is not implemented"))
+}
+
+func (UnimplementedMarketplaceServiceHandler) FetchReservations(context.Context, *connect.Request[hummingbird.FetchReservationsRequest]) (*connect.Response[hummingbird.FetchReservationsResponse], error) {
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("proto.hummingbird.v1.MarketplaceService.FetchReservations is not implemented"))
+}
+
+func (UnimplementedMarketplaceServiceHandler) SplitAsset(context.Context, *connect.Request[hummingbird.SplitAssetRequest]) (*connect.Response[hummingbird.SplitAssetResponse], error) {
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("proto.hummingbird.v1.MarketplaceService.SplitAsset is not implemented"))
+}
+
+func (UnimplementedMarketplaceServiceHandler) CombineAssets(context.Context, *connect.Request[hummingbird.CombineAssetRequest]) (*connect.Response[hummingbird.CombineAssetResponse], error) {
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("proto.hummingbird.v1.MarketplaceService.CombineAssets is not implemented"))
+}
+
+func (UnimplementedMarketplaceServiceHandler) Statistics(context.Context, *connect.Request[hummingbird.StatisticsRequest]) (*connect.Response[hummingbird.StatisticsResponse], error) {
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("proto.hummingbird.v1.MarketplaceService.Statistics is not implemented"))
+}
+
+func (UnimplementedMarketplaceServiceHandler) UpdateAssets(context.Context, *connect.Request[hummingbird.UpdateAssetsRequest]) (*connect.Response[hummingbird.UpdateAssetsResponse], error) {
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("proto.hummingbird.v1.MarketplaceService.UpdateAssets is not implemented"))
+}
diff --git a/pkg/proto/hummingbird/v1/hummingbirdconnect/redemption.connect.go b/pkg/proto/hummingbird/v1/hummingbirdconnect/redemption.connect.go
new file mode 100644
index 0000000000..aca8feb255
--- /dev/null
+++ b/pkg/proto/hummingbird/v1/hummingbirdconnect/redemption.connect.go
@@ -0,0 +1,139 @@
+// Code generated by protoc-gen-connect-go. DO NOT EDIT.
+//
+// Source: proto/hummingbird/v1/redemption.proto
+
+package hummingbirdconnect
+
+import (
+ connect "connectrpc.com/connect"
+ context "context"
+ errors "errors"
+ hummingbird "github.com/scionproto/scion/pkg/proto/hummingbird"
+ http "net/http"
+ strings "strings"
+)
+
+// This is a compile-time assertion to ensure that this generated file and the connect package are
+// compatible. If you get a compiler error that this constant is not defined, this code was
+// generated with a version of connect newer than the one compiled into your binary. You can fix the
+// problem by either regenerating this code with an older version of connect or updating the connect
+// version compiled into your binary.
+const _ = connect.IsAtLeastVersion1_13_0
+
+const (
+ // RedemptionServiceName is the fully-qualified name of the RedemptionService service.
+ RedemptionServiceName = "proto.hummingbird.v1.RedemptionService"
+)
+
+// These constants are the fully-qualified names of the RPCs defined in this package. They're
+// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
+//
+// Note that these are different from the fully-qualified method names used by
+// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
+// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
+// period.
+const (
+ // RedemptionServiceRedeemASAssetProcedure is the fully-qualified name of the RedemptionService's
+ // RedeemASAsset RPC.
+ RedemptionServiceRedeemASAssetProcedure = "/proto.hummingbird.v1.RedemptionService/RedeemASAsset"
+ // RedemptionServiceDelegateRedemptionProcedure is the fully-qualified name of the
+ // RedemptionService's DelegateRedemption RPC.
+ RedemptionServiceDelegateRedemptionProcedure = "/proto.hummingbird.v1.RedemptionService/DelegateRedemption"
+)
+
+// RedemptionServiceClient is a client for the proto.hummingbird.v1.RedemptionService service.
+type RedemptionServiceClient interface {
+ RedeemASAsset(context.Context) *connect.BidiStreamForClient[hummingbird.RedeemAssetFromASResponse, hummingbird.RedeemAssetFromASRequest]
+ DelegateRedemption(context.Context, *connect.Request[hummingbird.DelegateRedemptionRequest]) (*connect.Response[hummingbird.DelegateRedemptionResponse], error)
+}
+
+// NewRedemptionServiceClient constructs a client for the proto.hummingbird.v1.RedemptionService
+// service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for
+// gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply
+// the connect.WithGRPC() or connect.WithGRPCWeb() options.
+//
+// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
+// http://api.acme.com or https://acme.com/grpc).
+func NewRedemptionServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) RedemptionServiceClient {
+ baseURL = strings.TrimRight(baseURL, "/")
+ redemptionServiceMethods := hummingbird.File_proto_hummingbird_v1_redemption_proto.Services().ByName("RedemptionService").Methods()
+ return &redemptionServiceClient{
+ redeemASAsset: connect.NewClient[hummingbird.RedeemAssetFromASResponse, hummingbird.RedeemAssetFromASRequest](
+ httpClient,
+ baseURL+RedemptionServiceRedeemASAssetProcedure,
+ connect.WithSchema(redemptionServiceMethods.ByName("RedeemASAsset")),
+ connect.WithClientOptions(opts...),
+ ),
+ delegateRedemption: connect.NewClient[hummingbird.DelegateRedemptionRequest, hummingbird.DelegateRedemptionResponse](
+ httpClient,
+ baseURL+RedemptionServiceDelegateRedemptionProcedure,
+ connect.WithSchema(redemptionServiceMethods.ByName("DelegateRedemption")),
+ connect.WithClientOptions(opts...),
+ ),
+ }
+}
+
+// redemptionServiceClient implements RedemptionServiceClient.
+type redemptionServiceClient struct {
+ redeemASAsset *connect.Client[hummingbird.RedeemAssetFromASResponse, hummingbird.RedeemAssetFromASRequest]
+ delegateRedemption *connect.Client[hummingbird.DelegateRedemptionRequest, hummingbird.DelegateRedemptionResponse]
+}
+
+// RedeemASAsset calls proto.hummingbird.v1.RedemptionService.RedeemASAsset.
+func (c *redemptionServiceClient) RedeemASAsset(ctx context.Context) *connect.BidiStreamForClient[hummingbird.RedeemAssetFromASResponse, hummingbird.RedeemAssetFromASRequest] {
+ return c.redeemASAsset.CallBidiStream(ctx)
+}
+
+// DelegateRedemption calls proto.hummingbird.v1.RedemptionService.DelegateRedemption.
+func (c *redemptionServiceClient) DelegateRedemption(ctx context.Context, req *connect.Request[hummingbird.DelegateRedemptionRequest]) (*connect.Response[hummingbird.DelegateRedemptionResponse], error) {
+ return c.delegateRedemption.CallUnary(ctx, req)
+}
+
+// RedemptionServiceHandler is an implementation of the proto.hummingbird.v1.RedemptionService
+// service.
+type RedemptionServiceHandler interface {
+ RedeemASAsset(context.Context, *connect.BidiStream[hummingbird.RedeemAssetFromASResponse, hummingbird.RedeemAssetFromASRequest]) error
+ DelegateRedemption(context.Context, *connect.Request[hummingbird.DelegateRedemptionRequest]) (*connect.Response[hummingbird.DelegateRedemptionResponse], error)
+}
+
+// NewRedemptionServiceHandler builds an HTTP handler from the service implementation. It returns
+// the path on which to mount the handler and the handler itself.
+//
+// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
+// and JSON codecs. They also support gzip compression.
+func NewRedemptionServiceHandler(svc RedemptionServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
+ redemptionServiceMethods := hummingbird.File_proto_hummingbird_v1_redemption_proto.Services().ByName("RedemptionService").Methods()
+ redemptionServiceRedeemASAssetHandler := connect.NewBidiStreamHandler(
+ RedemptionServiceRedeemASAssetProcedure,
+ svc.RedeemASAsset,
+ connect.WithSchema(redemptionServiceMethods.ByName("RedeemASAsset")),
+ connect.WithHandlerOptions(opts...),
+ )
+ redemptionServiceDelegateRedemptionHandler := connect.NewUnaryHandler(
+ RedemptionServiceDelegateRedemptionProcedure,
+ svc.DelegateRedemption,
+ connect.WithSchema(redemptionServiceMethods.ByName("DelegateRedemption")),
+ connect.WithHandlerOptions(opts...),
+ )
+ return "/proto.hummingbird.v1.RedemptionService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case RedemptionServiceRedeemASAssetProcedure:
+ redemptionServiceRedeemASAssetHandler.ServeHTTP(w, r)
+ case RedemptionServiceDelegateRedemptionProcedure:
+ redemptionServiceDelegateRedemptionHandler.ServeHTTP(w, r)
+ default:
+ http.NotFound(w, r)
+ }
+ })
+}
+
+// UnimplementedRedemptionServiceHandler returns CodeUnimplemented from all methods.
+type UnimplementedRedemptionServiceHandler struct{}
+
+func (UnimplementedRedemptionServiceHandler) RedeemASAsset(context.Context, *connect.BidiStream[hummingbird.RedeemAssetFromASResponse, hummingbird.RedeemAssetFromASRequest]) error {
+ return connect.NewError(connect.CodeUnimplemented, errors.New("proto.hummingbird.v1.RedemptionService.RedeemASAsset is not implemented"))
+}
+
+func (UnimplementedRedemptionServiceHandler) DelegateRedemption(context.Context, *connect.Request[hummingbird.DelegateRedemptionRequest]) (*connect.Response[hummingbird.DelegateRedemptionResponse], error) {
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("proto.hummingbird.v1.RedemptionService.DelegateRedemption is not implemented"))
+}
diff --git a/populate_marketplace.py b/populate_marketplace.py
new file mode 100644
index 0000000000..cb5bd04979
--- /dev/null
+++ b/populate_marketplace.py
@@ -0,0 +1,406 @@
+import json
+import sqlite3
+import hashlib
+import bcrypt
+import argparse
+import re
+
+"""
+Example JSON file:
+{
+ "version": "4abdd9041b766012ed2b58222505e0a8aeead3208b7dab513bc338eacb6eb11f",
+ "users": [
+ {
+ "name": "Alice",
+ "password": "1234"
+ }
+ ],
+ "accounts": [
+ {
+ "user": "Alice",
+ "balance": 1000000000
+ },
+ {
+ "user": "Alice",
+ "scope": "bandwidth-tester",
+ "balance": 100000
+ }
+ ],
+ "ases": [
+ {
+ "ia": "1-ff00:0:110"
+ }
+ ],
+ "assets": [
+ {
+ "ia": "1-ff00:0:110",
+ "bandwidth": 1000,
+ "bandwidth_min": 10,
+ "bandwidth_max": 1000,
+ "price": 1,
+ "time_granularity": 5,
+ "time_min_duration": 5,
+ "starts_at": "2026-01-01T00:00:00Z",
+ "stops_at": "2027-01-01T00:00:00Z",
+ "ingress": 1
+ },
+ {
+ "ia": "1-ff00:0:110",
+ "bandwidth": 1000,
+ "bandwidth_min": 10,
+ "bandwidth_max": 1000,
+ "price": 1,
+ "time_granularity": 5,
+ "time_min_duration": 5,
+ "starts_at": "2026-01-01T00:00:00Z",
+ "stops_at": "2027-01-01T00:00:00Z",
+ "ingress": 2
+ },
+ {
+ "ia": "1-ff00:0:110",
+ "bandwidth": 1000,
+ "bandwidth_min": 10,
+ "bandwidth_max": 1000,
+ "price": 1,
+ "time_granularity": 5,
+ "time_min_duration": 5,
+ "starts_at": "2026-01-01T00:00:00Z",
+ "stops_at": "2027-01-01T00:00:00Z",
+ "egress": 1
+ },
+ {
+ "ia": "1-ff00:0:110",
+ "bandwidth": 1000,
+ "bandwidth_min": 10,
+ "bandwidth_max": 1000,
+ "price": 1,
+ "time_granularity": 5,
+ "time_min_duration": 5,
+ "starts_at": "2026-01-01T00:00:00Z",
+ "stops_at": "2027-01-01T00:00:00Z",
+ "egress": 2
+ },
+ {
+ "ia": "1-ff00:0:110",
+ "bandwidth": 1000,
+ "bandwidth_min": 10,
+ "bandwidth_max": 1000,
+ "price": 1,
+ "time_granularity": 5,
+ "time_min_duration": 5,
+ "starts_at": "2026-01-01T00:00:00Z",
+ "stops_at": "2027-01-01T00:00:00Z",
+ "ingress": 1,
+ "egress": 0
+ },
+ {
+ "ia": "1-ff00:0:110",
+ "bandwidth": 1000,
+ "bandwidth_min": 10,
+ "bandwidth_max": 1000,
+ "price": 1,
+ "time_granularity": 5,
+ "time_min_duration": 5,
+ "starts_at": "2026-01-01T00:00:00Z",
+ "stops_at": "2027-01-01T00:00:00Z",
+ "ingress": 2,
+ "egress": 0
+ },
+ {
+ "ia": "1-ff00:0:110",
+ "bandwidth": 1000,
+ "bandwidth_min": 10,
+ "bandwidth_max": 1000,
+ "price": 1,
+ "time_granularity": 5,
+ "time_min_duration": 5,
+ "starts_at": "2026-01-01T00:00:00Z",
+ "stops_at": "2027-01-01T00:00:00Z",
+ "ingress": 0,
+ "egress": 1
+ },
+ {
+ "ia": "1-ff00:0:110",
+ "bandwidth": 1000,
+ "bandwidth_min": 10,
+ "bandwidth_max": 1000,
+ "price": 1,
+ "time_granularity": 5,
+ "time_min_duration": 5,
+ "starts_at": "2026-01-01T00:00:00Z",
+ "stops_at": "2027-01-01T00:00:00Z",
+ "ingress": 0,
+ "egress": 2
+ }
+ ],
+ "reservations": [
+ {
+ "id": 1,
+ "ia": "1-ff00:0:110",
+ "owner": "Alice",
+ "ingress": 1,
+ "egress": 2,
+ "bandwidth": 100,
+ "bw_encoded": 20,
+ "starts_at": "2026-08-01T00:00:00Z",
+ "stops_at": "2026-08-02T00:00:00Z",
+ "key": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
+ }
+ ],
+ "delegations": [
+ {
+ "ia": "1-ff00:0:110",
+ "res_id_limit": 100000,
+ "expiration": "2027-01-01T00:00:00Z",
+ "paid_until": "2027-01-01T00:00:00Z",
+ "key": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3",
+ "encodings": [
+ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100
+ ]
+ }
+ ]
+}
+"""
+
+def loadJson(path):
+ with open(path, "r", encoding="utf-8") as file:
+ data = json.load(file)
+ return data
+
+def insertAll(db, data) -> bool:
+ if not insertUsers(db, data.get("users")):
+ return False
+ if not insertAccounts(db, data.get("accounts")):
+ return False
+ if not insertASes(db, data.get("ases")):
+ return False
+ if not insertAssets(db, data.get("assets")):
+ return False
+ if not insertReservations(db, data.get("reservations")):
+ return False
+ return insertDelegations(db, data.get("delegations"))
+
+def hash_file(filename):
+ with open(filename, "r", encoding="utf-8") as f:
+ sql = f.read()
+ normalized = re.sub(r"\s+", " ", sql).strip()
+ return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
+
+def verifyScheme(schemePath, version):
+ actualVersion = hash_file(schemePath)
+ if version != actualVersion:
+ print("scheme version mismatch. Expected: ", actualVersion)
+ return False
+ return True
+
+def hash_password(password: str) -> str:
+ if password is None:
+ return None
+ hashed = bcrypt.hashpw(
+ password.encode("utf-8"),
+ bcrypt.gensalt(rounds=12)
+ )
+ return hashed.decode("utf-8")
+
+def insertAccounts(db, accounts) -> bool:
+ if accounts is None:
+ return True
+ db.executemany(
+ """
+ INSERT OR REPLACE INTO Accounts (scope, balance, user_id)
+ VALUES (?, ?, (
+ SELECT id
+ FROM Users
+ WHERE name = ?
+ LIMIT 1
+ ))
+ """,
+ [
+ (u.get("scope"), u.get("balance"), u.get("user"))
+ for u in accounts
+ ],
+ )
+ return True
+
+def insertUsers(db,users) -> bool:
+ if users is None:
+ return True
+ for u in users:
+ u["pw_hash"] = hash_password(u.get("password"))
+ db.executemany(
+ """
+ INSERT OR REPLACE INTO Users (name, pw_hash)
+ VALUES (?, ?)
+ """,
+ [
+ (u.get("name"), u.get("pw_hash"))
+ for u in users
+ ],
+ )
+ return True
+
+def parseIA(ia_string):
+ parts = ia_string.split("-")
+ isd_id = int(parts[0])
+ as_string = parts[1]
+ as_parts = as_string.split(":")
+ if len(as_parts) == 1:
+ return isd_id, int(as_string)
+ if len(as_parts) != 3:
+ print("invalid IA", ia_string)
+ return 0,0
+ as_id = 0
+ for i in range(3):
+ as_id <<= 16
+ as_id |= int(as_parts[i], 16)
+ return isd_id, as_id
+
+def insertAssets(db, assets) -> bool:
+ if assets is None:
+ return True
+ for a in assets:
+ isd_id, as_id = parseIA(a.get("ia"))
+ if isd_id == 0 and as_id == 0:
+ return False
+ a["isd_id"] = isd_id
+ a["as_id"] = as_id
+ db.executemany(
+ """
+ INSERT INTO Assets (isd_id, as_id, bandwidth, bandwidth_min, bandwidth_max, price, time_granularity, time_min_duration, starts_at, stops_at, ingress, egress, account_id)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, (
+ SELECT a.id
+ FROM Accounts a
+ JOIN Users u ON u.ID = a.user_id
+ WHERE u.name = ? AND a.scope IS NULL
+ LIMIT 1
+ ))
+ """,
+ [
+ (u.get("isd_id"), u.get("as_id"), u.get("bandwidth"), u.get("bandwidth_min"), u.get("bandwidth_max"), u.get("price"), u.get("time_granularity"), u.get("time_min_duration"), u.get("starts_at"), u.get("stops_at"), u.get("ingress"), u.get("egress"), u.get("owner"),)
+ for u in assets
+ ],
+ )
+ return True
+
+def insertASes(db, ases) -> bool:
+ if ases is None:
+ return True
+ for a in ases:
+ isd_id, as_id = parseIA(a.get("ia"))
+ if isd_id == 0 and as_id == 0:
+ return False
+ a["isd_id"] = isd_id
+ a["as_id"] = as_id
+ a["pw_hash"] = hash_password(a.get("password"))
+ if a.get("pw_hash") == None:
+ a["pw_hash"] = ""
+ if a.get("jwt_version") == None:
+ a["jwt_version"] = 0
+ if a.get("balance") == None:
+ a["balance"] = 0
+ db.executemany(
+ """
+ INSERT OR REPLACE INTO Ases (isd_id, as_id, pw_hash, jwt_version, balance)
+ VALUES (?, ?, ?, ?, ?)
+ """,
+ [
+ (u.get("isd_id"), u.get("as_id"), u.get("pw_hash"), u.get("jwt_version"), u.get("balance"))
+ for u in ases
+ ],
+ )
+ return True
+
+def insertReservations(db, reservations):
+ if reservations is None:
+ return True
+ for a in reservations:
+ isd_id, as_id = parseIA(a.get("ia"))
+ if isd_id == 0 and as_id == 0:
+ return False
+ a["isd_id"] = isd_id
+ a["as_id"] = as_id
+ a["key_bytes"] = bytes.fromhex(a.get("key"))
+ for u in reservations:
+ db.execute(
+ """
+ INSERT OR REPLACE INTO Reservations (reservation_id, isd_id, as_id, ingress, egress, bandwidth, bw_encoded, starts_at, stops_at, key, account_id)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, (
+ SELECT id
+ FROM Users
+ WHERE name = ?
+ LIMIT 1
+ ))
+ """, (u.get("id") , u.get("isd_id"), u.get("as_id"), u.get("ingress"), u.get("egress"), u.get("bandwidth"), u.get("bw_encoded"), u.get("starts_at"), u.get("stops_at"), u.get("key_bytes"),u.get("owner"))
+ )
+ return True
+
+def insertDelegations(db, delegations):
+ if delegations is None:
+ return True
+ for a in delegations:
+ isd_id, as_id = parseIA(a.get("ia"))
+ if isd_id == 0 and as_id == 0:
+ return False
+ a["isd_id"] = isd_id
+ a["as_id"] = as_id
+ a["key_bytes"] = bytes.fromhex(a.get("key"))
+ a["encoding_bytes"] = b"".join(
+ i.to_bytes(4, byteorder="little")
+ for i in a.get("encodings")
+ )
+ db.executemany(
+ """
+ INSERT OR REPLACE INTO Redemption_Delegations (isd_id, as_id, res_id_limit, expiration, paid_until, key, encodings)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ """,
+ [
+ (u.get("isd_id"), u.get("as_id"), u.get("res_id_limit"), u.get("expiration"), u.get("paid_until"), u.get("key_bytes"),u.get("encoding_bytes"))
+ for u in delegations
+ ],
+ )
+ return True
+
+def applyScheme(db, schemePath):
+ with open(schemePath, "r", encoding="utf-8") as f:
+ db.executescript(f.read())
+
+def main(args):
+ data = loadJson(args.file)
+ if not verifyScheme(args.schema, data.get("version")):
+ return
+ conn = sqlite3.connect(args.db)
+ cursor = conn.cursor()
+ try:
+ applyScheme(cursor, args.schema)
+ if insertAll(cursor, data):
+ conn.commit()
+ print("stored in database")
+ else:
+ conn.rollback()
+ print("changes rolled back")
+ except Exception as e:
+ print("error", e)
+ conn.rollback()
+ finally:
+ conn.close()
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description="Loads assets, users, reservations and delegations into marketplace db")
+ parser.add_argument(
+ "--db",
+ default="gen-cache/marketplace.db",
+ help="Path to the SQLite database."
+ )
+ parser.add_argument(
+ "--schema",
+ default="marketplace/db/schema.sql",
+ help="Path to the schema.sql file."
+ )
+ parser.add_argument(
+ "--file",
+ required=True,
+ help="Path to the JSON file to import."
+ )
+
+ args = parser.parse_args()
+ main(args)
\ No newline at end of file
diff --git a/proto/hummingbird/v1/BUILD.bazel b/proto/hummingbird/v1/BUILD.bazel
new file mode 100644
index 0000000000..3149d931bc
--- /dev/null
+++ b/proto/hummingbird/v1/BUILD.bazel
@@ -0,0 +1,15 @@
+load("@rules_proto//proto:defs.bzl", "proto_library")
+
+proto_library(
+ name = "hummingbird",
+ srcs = [
+ "marketplace.proto",
+ "redemption.proto",
+ "account.proto",
+ ],
+ visibility = ["//visibility:public"],
+ deps = [
+ "@protobuf//:timestamp_proto",
+ "//proto/crypto/v1:crypto",
+ ],
+)
diff --git a/proto/hummingbird/v1/account.proto b/proto/hummingbird/v1/account.proto
new file mode 100644
index 0000000000..c9a420ac2b
--- /dev/null
+++ b/proto/hummingbird/v1/account.proto
@@ -0,0 +1,57 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+option go_package = "github.com/scionproto/scion/pkg/proto/hummingbird";
+
+package proto.hummingbird.v1;
+
+import "proto/crypto/v1/signed.proto";
+
+service AccountService {
+ rpc ResetJWT(JWTResetRequest) returns (JWTResetResponse) {}
+ rpc CreateChallenge(CreateChallengeRequest) returns (CreateChallengeResponse) {}
+ rpc RegisterAS(RegisterASRequest) returns (RegisterASResponse) {}
+ rpc SetPassword(SetPasswordRequest) returns (SetPasswordResponse) {}
+}
+
+message SetPasswordRequest {
+ string password = 1;
+}
+message SetPasswordResponse {
+
+}
+
+message RegisterASRequest {
+ string id = 1;
+ proto.crypto.v1.SignedMessage signed_challenge = 2;
+}
+message RegisterASResponse {
+ string jwt_publisher = 1;
+ string jwt_redemption = 2;
+}
+
+message CreateChallengeRequest {
+ uint64 ia = 1;
+}
+
+message CreateChallengeResponse {
+ string id = 1;
+ bytes value = 2;
+}
+
+message JWTResetRequest {}
+
+message JWTResetResponse {}
\ No newline at end of file
diff --git a/proto/hummingbird/v1/marketplace.proto b/proto/hummingbird/v1/marketplace.proto
new file mode 100644
index 0000000000..d2ea08443a
--- /dev/null
+++ b/proto/hummingbird/v1/marketplace.proto
@@ -0,0 +1,234 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+option go_package = "github.com/scionproto/scion/pkg/proto/hummingbird";
+
+package proto.hummingbird.v1;
+
+import "google/protobuf/timestamp.proto";
+
+service MarketplaceService {
+ rpc Info(MarketplaceInfoRequest) returns (MarketplaceInfoResponse) {}
+ rpc PublishAsset(PublishAssetRequest) returns (PublishAssetResponse) {}
+ rpc SearchAssets(SearchAssetsRequest) returns (SearchAssetsResponse) {}
+ rpc BuyAssets(BuyAssetsRequest) returns (BuyAssetsResponse) {}
+ rpc RedeemAsset(RedeemAssetRequest) returns (RedeemAssetResponse) {}
+ rpc FetchReservations(FetchReservationsRequest) returns (FetchReservationsResponse) {}
+ rpc SplitAsset(SplitAssetRequest) returns (SplitAssetResponse) {}
+ rpc CombineAssets(CombineAssetRequest) returns (CombineAssetResponse) {}
+ rpc Statistics(StatisticsRequest) returns (StatisticsResponse) {}
+ rpc UpdateAssets(UpdateAssetsRequest) returns (UpdateAssetsResponse) {}
+}
+
+message UpdateAssetsRequest {
+ repeated AssetUpdate assets = 1;
+}
+message AssetUpdate {
+ string asset_id = 1;
+ oneof operation {
+ PublisherAsset update = 2;
+ Remove remove = 3;
+ }
+}
+message Remove {}
+message UpdateAssetsResponse {
+ repeated UpdateAssetResult result = 1;
+}
+message UpdateAssetResult {
+ oneof result_type {
+ string new_id = 1;
+ string error = 2;
+ }
+}
+
+message StatisticsRequest {
+ optional uint32 if_id_ingress = 1;
+ optional uint32 if_id_egress = 2;
+ google.protobuf.Timestamp start = 3;
+ google.protobuf.Timestamp end = 4;
+ uint32 step = 5;
+}
+
+message StatisticsResponse {
+ repeated StatisticsResponseEntry statistics = 1;
+}
+
+message StatisticsResponseEntry {
+ uint64 revenue = 1;
+ double bandwidth_utilization = 2;
+}
+
+// Split assets splits an asset either by defining the bandwidth to split, or the time it should split.
+message SplitAssetRequest {
+ string asset_id = 1;
+ oneof split_option {
+ uint32 bw_split = 2;
+ google.protobuf.Timestamp time_split = 3;
+ }
+}
+// Returns the two asset IDs created from the split.
+message SplitAssetResponse {
+ string asset_id_1 = 1;
+ string asset_id_2 = 2;
+}
+// When combining assets, both assets must have the same IA, asset_type, ingress and egress.enum
+// Additionally, both assets must either:
+// a) have adjacent validity with same bandwidth, or
+// b) same validity with different bandwidth
+message CombineAssetRequest {
+ string asset_id_1 = 1;
+ string asset_id_2 = 2;
+}
+// Returns the the asset ID created from combining the two assets.
+message CombineAssetResponse {
+ string asset_id = 1;
+}
+
+message MarketplaceInfoRequest {
+
+}
+
+message MarketplaceInfoResponse {
+ uint32 api_major_version = 1;
+ uint32 api_minor_version = 2;
+ string currency = 3;
+ uint32 currency_exponent = 4;
+ uint32 max_statistics_granularity = 5;
+ PricingStrategy pricing_strategy = 6;
+ float transaction_fee_relative = 7;
+ uint64 transaction_fee_absolute = 8;
+ uint64 split_combine_fee_absolute = 9;
+ bool supports_redemption_delegation = 10;
+ uint64 delegation_hourly_fee = 11;
+}
+
+enum PricingStrategy {
+ static_pricing = 0;
+ dynamic_pricing = 1;
+}
+
+message PublishAssetRequest {
+ PublisherAsset asset = 1;
+}
+
+message PublishAssetResponse {
+ string asset_id = 1;
+}
+message PublisherAsset {
+ optional uint32 if_id_ingress = 1;
+ optional uint32 if_id_egress = 2;
+ uint32 bandwidth = 3;
+ uint32 bandwidth_min = 4;
+ uint32 bandwidth_max = 5;
+ google.protobuf.Timestamp starts_at = 6;
+ google.protobuf.Timestamp stops_at = 7;
+ uint32 time_granularity = 8;
+ uint32 time_min_duration = 9;
+ uint32 price = 10;
+}
+
+message SearchAssetsRequest {
+ bool owned = 1;
+ optional uint64 ia = 2;
+ optional uint32 if_id_ingress = 3;
+ optional uint32 if_id_egress = 4;
+ optional uint32 min_required_bw = 5;
+ optional google.protobuf.Timestamp starts_at_latest = 6;
+ optional google.protobuf.Timestamp stops_at_earliest = 7;
+ optional uint32 price = 8;
+}
+
+message SearchAssetsResponse {
+ repeated SearchAsset assets = 1;
+}
+message SearchAsset {
+ string asset_id = 1;
+ uint64 ia = 2;
+ optional uint32 if_id_ingress = 3;
+ optional uint32 if_id_egress = 4;
+ uint32 bandwidth = 5;
+ uint32 bandwidth_min = 6;
+ uint32 bandwidth_max = 7;
+ google.protobuf.Timestamp starts_at = 8;
+ google.protobuf.Timestamp stops_at = 9;
+ uint32 time_granularity = 10;
+ uint32 time_min_duration = 11;
+ uint32 price = 12;
+}
+
+message BuyAssetsRequest {
+ repeated BuyAsset assets = 1;
+ uint64 max_price = 2;
+}
+
+message BuyAsset {
+ string asset_id = 1;
+ google.protobuf.Timestamp starts_at_exactly = 2;
+ google.protobuf.Timestamp stops_at_exactly = 3;
+ uint32 bandwidth_exact = 4;
+}
+
+message BuyAssetsResponse {
+ repeated BoughtAsset assets = 1;
+ uint64 cost = 2;
+}
+
+message BoughtAsset {
+ string asset_id = 1;
+}
+
+message RedeemAssetRequest {
+ oneof interfaces {
+ IngressEgressPair pair = 1;
+ string if_pair_asset_id = 3;
+ }
+}
+message IngressEgressPair {
+ string ingress_asset_id = 1;
+ string egress_asset_id = 2;
+}
+
+message RedeemAssetResponse {
+ bytes authentication_key = 1;
+ uint32 reservation_id = 2;
+ uint32 bandwidth_rounded = 3;
+ uint32 bw_dataplane_encoding = 4;
+}
+
+message FetchReservationsRequest {
+ optional uint64 ia = 1;
+ optional uint32 ingress_id = 2;
+ optional uint32 egress_id = 3;
+ optional uint32 bandwidth = 4;
+ optional google.protobuf.Timestamp starts_at = 5;
+ optional google.protobuf.Timestamp stops_at = 6;
+}
+
+message FetchReservationsResponse {
+ repeated Reservation reservations = 1;
+}
+
+message Reservation {
+ uint32 reservation_id = 1;
+ uint64 ia = 2;
+ uint32 ingress_id = 3;
+ uint32 egress_id = 4;
+ uint32 bandwidth = 5;
+ uint32 dataplane_encoding = 6;
+ google.protobuf.Timestamp starts_at = 7;
+ google.protobuf.Timestamp stops_at = 8;
+ bytes authentication_key = 9;
+}
\ No newline at end of file
diff --git a/proto/hummingbird/v1/redemption.proto b/proto/hummingbird/v1/redemption.proto
new file mode 100644
index 0000000000..544e1ab29c
--- /dev/null
+++ b/proto/hummingbird/v1/redemption.proto
@@ -0,0 +1,66 @@
+// Copyright 2026 ETH Zurich
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+option go_package = "github.com/scionproto/scion/pkg/proto/hummingbird";
+
+package proto.hummingbird.v1;
+
+import "google/protobuf/timestamp.proto";
+
+service RedemptionService {
+ rpc RedeemASAsset(stream RedeemAssetFromASResponse) returns (stream RedeemAssetFromASRequest) {}
+ rpc DelegateRedemption(DelegateRedemptionRequest) returns (DelegateRedemptionResponse) {}
+}
+
+message DelegateRedemptionRequest {
+ // delegate request valid from now until expiration_time
+ google.protobuf.Timestamp expiration_time = 1;
+ // the exclusive end of the reservation ID interval
+ uint32 reservation_id_upper_bound = 2;
+ // the key used for the derival of the authentication keys
+ bytes Key = 3;
+ // up to 1024 sorted values that indicate the dataplane encoding points
+ repeated uint32 encoding_points = 4;
+}
+message DelegateRedemptionResponse {
+ google.protobuf.Timestamp expiration_time = 1;
+}
+
+message RedeemAssetFromASRequest {
+ uint32 ingress_id = 1;
+ uint32 egress_id = 2;
+ uint32 bandwidth = 3;
+ google.protobuf.Timestamp starts_at = 4;
+ google.protobuf.Timestamp stops_at = 5;
+ // to match request with response
+ uint64 request_id = 6;
+}
+
+message RedeemAssetFromASResponse {
+ // to match request with response
+ uint64 request_id = 1;
+ oneof result {
+ ReservationInfo res_info = 2;
+ string error = 3;
+ }
+}
+
+message ReservationInfo {
+ uint32 reservation_id = 1;
+ uint32 bandwith_rounded = 2;
+ uint32 bw_dataplane_encoding = 3;
+ bytes authentication_key = 4;
+}
\ No newline at end of file
diff --git a/setup_marketplace.py b/setup_marketplace.py
new file mode 100644
index 0000000000..85fb4fd612
--- /dev/null
+++ b/setup_marketplace.py
@@ -0,0 +1,130 @@
+#!/usr/bin/env python3
+from pathlib import Path
+import re
+import sys
+import json
+
+
+def main() -> None:
+ if len(sys.argv) != 2:
+ print("Usage: python3 setup_marketplace.py ")
+ print("Example: python3 setup_marketplace.py 1-ff00_0_111")
+ sys.exit(1)
+
+ ia_id = sys.argv[1] # e.g. ff00_0_111
+ as_id = ia_id.split("-")[1]
+ as_dir = f"AS{as_id}" # e.g. ASff00_0_111
+ group_name = f"as1-{as_id}" # e.g. as1-ff00_0_111
+
+ # Paths
+ gen_dir = Path("gen")
+ marketplace_dir = gen_dir / as_dir
+ certs_dir = marketplace_dir / "certs"
+ marketplace_toml = marketplace_dir / "marketplace.toml"
+ static_info_json = marketplace_dir / "staticInfoConfig.json"
+ supervisord_conf = gen_dir / "supervisord.conf"
+
+ # 1 & 2. Create directories
+ certs_dir.mkdir(parents=True, exist_ok=True)
+
+ # 3. Write marketplace.toml
+ marketplace_toml.write_text(
+ f"""[general]
+id = "marketplace"
+config_dir = "gen/{as_dir}"
+
+[log.console]
+level = "debug"
+
+[marketplace]
+api_addr = "localhost:8888"
+scion_api_addr = "localhost:8888"
+currency = "CHF"
+currency_exponent = 2
+supports_redemption_delegation = true
+delegation_hourly_fee = 10000
+transaction_fee_relative = 0.01
+transaction_fee_absolute = 10
+split_combine_fee_absolute = 10
+
+[marketplace_db]
+connection = "gen-cache/marketplace.db"
+""",
+ encoding="utf-8",
+ )
+
+ # 4. Prepend marketplace block
+ marketplace_block = f"""[program:marketplace]
+autostart = false
+autorestart = false
+environment = TZ=UTC,GODEBUG="cgocheck=0"
+stdout_logfile = logs/marketplace.log
+redirect_stderr = True
+startretries = 0
+startsecs = 5
+priority = 100
+command = bin/marketplace --config gen/{as_dir}/marketplace.toml
+
+"""
+
+ if supervisord_conf.exists():
+ text = supervisord_conf.read_text(encoding="utf-8")
+ else:
+ text = ""
+
+ if "[program:marketplace]" not in text:
+ text = marketplace_block + text
+
+ # 5. Append marketplace to [group:as1-]
+ pattern = re.compile(
+ rf"(\[group:{re.escape(group_name)}\]\nprograms\s*=\s*)([^\n]+)",
+ re.MULTILINE,
+ )
+
+ def add_marketplace(match: re.Match) -> str:
+ prefix = match.group(1)
+ programs = [p.strip() for p in match.group(2).split(",")]
+
+ if "marketplace" not in programs:
+ programs.append("marketplace")
+
+ return prefix + ",".join(programs)
+
+ text = pattern.sub(add_marketplace, text, count=1)
+
+ supervisord_conf.write_text(text, encoding="utf-8")
+
+ # Build inner JSON object
+ ia_id_colons = ia_id.replace("_",":")
+ note_payload = {
+ "hummingbird": [
+ {
+ "name": "Test Market",
+ "protocol": "connectrpc/TLS/QUIC/SCION",
+ "api": f"[{ia_id_colons},127.0.0.1]:9888",
+ "website": "https://127.0.0.1:8889"
+ },
+ {
+ "name": "Test Market",
+ "protocol": "connectrpc/TLS/TCP",
+ "api": "https://127.0.0.1:8888",
+ "website": "https://127.0.0.1:8889"
+ }
+ ]
+ }
+
+ # Outer object: note contains JSON string
+ static_info_payload = {
+ "note": json.dumps(note_payload)
+ }
+
+ # Write file
+ static_info_json.write_text(
+ json.dumps(static_info_payload, indent=2),
+ encoding="utf-8",
+ )
+
+ print(f"Marketplace config added for {ia_id}")
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file