From b4912815dc813286e3cf9e1d9196597f5063b84b Mon Sep 17 00:00:00 2001 From: 0xluk Date: Tue, 11 Aug 2026 11:57:22 +0300 Subject: [PATCH 1/3] chore: update go-node-connector to v2.3.0 v2.3.0 adds PrefetchOwnedAndPossessedAssets, which pipelines the owned and possessed asset requests for several identities over a single connection. The only changed file upstream is qubic.go; NewClient gained variadic options and stays source compatible, so nothing here needed adapting. Also pass the protobuf include path to the openapi-v3 target. It was only applied to the Go codegen target, so on macOS `make all` always failed to find the well known types and never regenerated the OpenAPI document. Co-Authored-By: Claude Opus 5 (1M context) --- go.mod | 2 +- go.sum | 4 ++-- protobuff/Makefile | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 2d90ffc..3978dc0 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/google/gnostic v0.7.1 github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 github.com/pkg/errors v0.9.1 - github.com/qubic/go-node-connector/v2 v2.2.1 + github.com/qubic/go-node-connector/v2 v2.3.0 github.com/qubic/go-schnorrq v1.1.3 github.com/stretchr/testify v1.11.1 google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101 diff --git a/go.sum b/go.sum index 7018550..627cfbb 100644 --- a/go.sum +++ b/go.sum @@ -43,8 +43,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/qubic/go-node-connector/v2 v2.2.1 h1:77Gp6jRcZJwUtoylCtDjscfmlLvRAzLzBX+2XVX0uCQ= -github.com/qubic/go-node-connector/v2 v2.2.1/go.mod h1:E7SSfGWbHl/twyAROXYV78Ss3k2g9VWtzoqx21Rbnik= +github.com/qubic/go-node-connector/v2 v2.3.0 h1:h3ZCtpbuVRMPshO9bV9ziU5Rih1M/bzHSFHcCvmMA3M= +github.com/qubic/go-node-connector/v2 v2.3.0/go.mod h1:E7SSfGWbHl/twyAROXYV78Ss3k2g9VWtzoqx21Rbnik= github.com/qubic/go-schnorrq v1.1.3 h1:xJGrFb02GHUGi2PHpGBaPwBU+1SnOwZCujXF8QTlD0o= github.com/qubic/go-schnorrq v1.1.3/go.mod h1:AXdwMtrAL2zLFanUbZCS4WudjyB44dNrHMQQe7TtK/8= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= diff --git a/protobuff/Makefile b/protobuff/Makefile index bee159e..c3b0b92 100644 --- a/protobuff/Makefile +++ b/protobuff/Makefile @@ -32,7 +32,7 @@ all: $(GO) openapi-v3 --go_out=paths=source_relative:. *.proto openapi-v3: - protoc -I=. --openapi_out=output_mode=source_relative,default_response=false:. *.proto + protoc -I=. $(OPT_ARGS) --openapi_out=output_mode=source_relative,default_response=false:. *.proto clean: rm -f *.pb.go From e293049735a076278a15b77cc0f9d53bff68517e Mon Sep 17 00:00:00 2001 From: 0xluk Date: Tue, 11 Aug 2026 11:57:37 +0300 Subject: [PATCH 2/3] feat(proto): add batch asset endpoint definition Adds GetAssetsForIdentities, bound to POST /assets, returning the owned and possessed assets of each requested identity. The request takes a list rather than a path parameter, and is a POST so the identities travel in the body: an identity is 60 characters, so a batch of any useful size would push a GET URL past common proxy limits. This follows the existing body: "*" precedent set by BroadcastTransaction and QuerySmartContract. The response reuses the existing OwnedAsset and PossessedAsset messages, so a client that already parses /assets/{identity}/owned needs no new parsing code. Generated files are produced by protobuff/make.sh. Co-Authored-By: Claude Opus 5 (1M context) --- protobuff/qubic.openapi.yaml | 51 ++++ protobuff/qubic.pb.go | 495 ++++++++++++++++++++++++----------- protobuff/qubic.pb.gw.go | 109 +++++++- protobuff/qubic.proto | 29 ++ protobuff/qubic_grpc.pb.go | 42 ++- 5 files changed, 566 insertions(+), 160 deletions(-) diff --git a/protobuff/qubic.openapi.yaml b/protobuff/qubic.openapi.yaml index a7926f6..b927ce1 100644 --- a/protobuff/qubic.openapi.yaml +++ b/protobuff/qubic.openapi.yaml @@ -9,6 +9,27 @@ info: servers: - url: https://rpc.qubic.org/live/v1 paths: + /assets: + post: + tags: + - QubicLiveService + - Assets + summary: List Owned And Possessed Assets For Multiple Identities + description: Gets the assets owned and possessed by each of the given identities. The requests are pipelined over a single node connection, so one call costs roughly one round trip regardless of how many identities are requested. Results are returned in the order the identities were given. + operationId: QubicLiveService_GetAssetsForIdentities + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GetAssetsForIdentitiesRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GetAssetsForIdentitiesResponse' /assets/issuances: get: tags: @@ -599,6 +620,22 @@ components: items: $ref: '#/components/schemas/Ipo' description: GetActiveIposResponse + GetAssetsForIdentitiesRequest: + type: object + properties: + identities: + type: array + items: + type: string + description: GetAssetsForIdentitiesRequest + GetAssetsForIdentitiesResponse: + type: object + properties: + assets: + type: array + items: + $ref: '#/components/schemas/IdentityAssets' + description: GetAssetsForIdentitiesResponse GetBalanceResponse: type: object properties: @@ -629,6 +666,20 @@ components: type: string description: Health status information. description: HealthResponse + IdentityAssets: + type: object + properties: + identity: + type: string + ownedAssets: + type: array + items: + $ref: '#/components/schemas/OwnedAsset' + possessedAssets: + type: array + items: + $ref: '#/components/schemas/PossessedAsset' + description: IdentityAssets holds the assets owned and possessed by a single identity. Ipo: type: object properties: diff --git a/protobuff/qubic.pb.go b/protobuff/qubic.pb.go index def55c0..075144e 100644 --- a/protobuff/qubic.pb.go +++ b/protobuff/qubic.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.11 -// protoc v3.21.12 +// protoc-gen-go v1.36.10 +// protoc v4.23.4 // source: qubic.proto package protobuff @@ -225,8 +225,8 @@ func (x *GetBalanceResponse) GetBalance() *Balance { // BroadcastTransactionRequest type BroadcastTransactionRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - EncodedTransaction string `protobuf:"bytes,1,opt,name=encoded_transaction,json=encodedTransaction,proto3" json:"encoded_transaction,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + EncodedTransaction string `protobuf:"bytes,1,opt,name=encoded_transaction,json=encodedTransaction,proto3" json:"encoded_transaction,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1236,6 +1236,157 @@ func (x *PossessedAssetsResponse) GetPossessedAssets() []*PossessedAsset { return nil } +// GetAssetsForIdentitiesRequest +type GetAssetsForIdentitiesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Identities []string `protobuf:"bytes,1,rep,name=identities,proto3" json:"identities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAssetsForIdentitiesRequest) Reset() { + *x = GetAssetsForIdentitiesRequest{} + mi := &file_qubic_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAssetsForIdentitiesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAssetsForIdentitiesRequest) ProtoMessage() {} + +func (x *GetAssetsForIdentitiesRequest) ProtoReflect() protoreflect.Message { + mi := &file_qubic_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 GetAssetsForIdentitiesRequest.ProtoReflect.Descriptor instead. +func (*GetAssetsForIdentitiesRequest) Descriptor() ([]byte, []int) { + return file_qubic_proto_rawDescGZIP(), []int{21} +} + +func (x *GetAssetsForIdentitiesRequest) GetIdentities() []string { + if x != nil { + return x.Identities + } + return nil +} + +// IdentityAssets holds the assets owned and possessed by a single identity. +type IdentityAssets struct { + state protoimpl.MessageState `protogen:"open.v1"` + Identity string `protobuf:"bytes,1,opt,name=identity,proto3" json:"identity,omitempty"` + OwnedAssets []*OwnedAsset `protobuf:"bytes,2,rep,name=owned_assets,json=ownedAssets,proto3" json:"owned_assets,omitempty"` + PossessedAssets []*PossessedAsset `protobuf:"bytes,3,rep,name=possessed_assets,json=possessedAssets,proto3" json:"possessed_assets,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IdentityAssets) Reset() { + *x = IdentityAssets{} + mi := &file_qubic_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IdentityAssets) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IdentityAssets) ProtoMessage() {} + +func (x *IdentityAssets) ProtoReflect() protoreflect.Message { + mi := &file_qubic_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 IdentityAssets.ProtoReflect.Descriptor instead. +func (*IdentityAssets) Descriptor() ([]byte, []int) { + return file_qubic_proto_rawDescGZIP(), []int{22} +} + +func (x *IdentityAssets) GetIdentity() string { + if x != nil { + return x.Identity + } + return "" +} + +func (x *IdentityAssets) GetOwnedAssets() []*OwnedAsset { + if x != nil { + return x.OwnedAssets + } + return nil +} + +func (x *IdentityAssets) GetPossessedAssets() []*PossessedAsset { + if x != nil { + return x.PossessedAssets + } + return nil +} + +// GetAssetsForIdentitiesResponse +type GetAssetsForIdentitiesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Assets []*IdentityAssets `protobuf:"bytes,1,rep,name=assets,proto3" json:"assets,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAssetsForIdentitiesResponse) Reset() { + *x = GetAssetsForIdentitiesResponse{} + mi := &file_qubic_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAssetsForIdentitiesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAssetsForIdentitiesResponse) ProtoMessage() {} + +func (x *GetAssetsForIdentitiesResponse) ProtoReflect() protoreflect.Message { + mi := &file_qubic_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 GetAssetsForIdentitiesResponse.ProtoReflect.Descriptor instead. +func (*GetAssetsForIdentitiesResponse) Descriptor() ([]byte, []int) { + return file_qubic_proto_rawDescGZIP(), []int{23} +} + +func (x *GetAssetsForIdentitiesResponse) GetAssets() []*IdentityAssets { + if x != nil { + return x.Assets + } + return nil +} + // QuerySmartContractRequest type QuerySmartContractRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1253,7 +1404,7 @@ type QuerySmartContractRequest struct { func (x *QuerySmartContractRequest) Reset() { *x = QuerySmartContractRequest{} - mi := &file_qubic_proto_msgTypes[21] + mi := &file_qubic_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1265,7 +1416,7 @@ func (x *QuerySmartContractRequest) String() string { func (*QuerySmartContractRequest) ProtoMessage() {} func (x *QuerySmartContractRequest) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[21] + mi := &file_qubic_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1278,7 +1429,7 @@ func (x *QuerySmartContractRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use QuerySmartContractRequest.ProtoReflect.Descriptor instead. func (*QuerySmartContractRequest) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{21} + return file_qubic_proto_rawDescGZIP(), []int{24} } func (x *QuerySmartContractRequest) GetContractIndex() uint32 { @@ -1319,7 +1470,7 @@ type QuerySmartContractResponse struct { func (x *QuerySmartContractResponse) Reset() { *x = QuerySmartContractResponse{} - mi := &file_qubic_proto_msgTypes[22] + mi := &file_qubic_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1331,7 +1482,7 @@ func (x *QuerySmartContractResponse) String() string { func (*QuerySmartContractResponse) ProtoMessage() {} func (x *QuerySmartContractResponse) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[22] + mi := &file_qubic_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1344,7 +1495,7 @@ func (x *QuerySmartContractResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use QuerySmartContractResponse.ProtoReflect.Descriptor instead. func (*QuerySmartContractResponse) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{22} + return file_qubic_proto_rawDescGZIP(), []int{25} } func (x *QuerySmartContractResponse) GetResponseData() string { @@ -1368,7 +1519,7 @@ type AssetIssuanceData struct { func (x *AssetIssuanceData) Reset() { *x = AssetIssuanceData{} - mi := &file_qubic_proto_msgTypes[23] + mi := &file_qubic_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1380,7 +1531,7 @@ func (x *AssetIssuanceData) String() string { func (*AssetIssuanceData) ProtoMessage() {} func (x *AssetIssuanceData) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[23] + mi := &file_qubic_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1393,7 +1544,7 @@ func (x *AssetIssuanceData) ProtoReflect() protoreflect.Message { // Deprecated: Use AssetIssuanceData.ProtoReflect.Descriptor instead. func (*AssetIssuanceData) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{23} + return file_qubic_proto_rawDescGZIP(), []int{26} } func (x *AssetIssuanceData) GetIssuerIdentity() string { @@ -1443,7 +1594,7 @@ type AssetIssuance struct { func (x *AssetIssuance) Reset() { *x = AssetIssuance{} - mi := &file_qubic_proto_msgTypes[24] + mi := &file_qubic_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1455,7 +1606,7 @@ func (x *AssetIssuance) String() string { func (*AssetIssuance) ProtoMessage() {} func (x *AssetIssuance) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[24] + mi := &file_qubic_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1468,7 +1619,7 @@ func (x *AssetIssuance) ProtoReflect() protoreflect.Message { // Deprecated: Use AssetIssuance.ProtoReflect.Descriptor instead. func (*AssetIssuance) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{24} + return file_qubic_proto_rawDescGZIP(), []int{27} } func (x *AssetIssuance) GetData() *AssetIssuanceData { @@ -1502,7 +1653,7 @@ type AssetIssuances struct { func (x *AssetIssuances) Reset() { *x = AssetIssuances{} - mi := &file_qubic_proto_msgTypes[25] + mi := &file_qubic_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1514,7 +1665,7 @@ func (x *AssetIssuances) String() string { func (*AssetIssuances) ProtoMessage() {} func (x *AssetIssuances) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[25] + mi := &file_qubic_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1527,7 +1678,7 @@ func (x *AssetIssuances) ProtoReflect() protoreflect.Message { // Deprecated: Use AssetIssuances.ProtoReflect.Descriptor instead. func (*AssetIssuances) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{25} + return file_qubic_proto_rawDescGZIP(), []int{28} } func (x *AssetIssuances) GetAssets() []*AssetIssuance { @@ -1551,7 +1702,7 @@ type AssetOwnershipData struct { func (x *AssetOwnershipData) Reset() { *x = AssetOwnershipData{} - mi := &file_qubic_proto_msgTypes[26] + mi := &file_qubic_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1563,7 +1714,7 @@ func (x *AssetOwnershipData) String() string { func (*AssetOwnershipData) ProtoMessage() {} func (x *AssetOwnershipData) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[26] + mi := &file_qubic_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1576,7 +1727,7 @@ func (x *AssetOwnershipData) ProtoReflect() protoreflect.Message { // Deprecated: Use AssetOwnershipData.ProtoReflect.Descriptor instead. func (*AssetOwnershipData) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{26} + return file_qubic_proto_rawDescGZIP(), []int{29} } func (x *AssetOwnershipData) GetOwnerIdentity() string { @@ -1626,7 +1777,7 @@ type AssetOwnership struct { func (x *AssetOwnership) Reset() { *x = AssetOwnership{} - mi := &file_qubic_proto_msgTypes[27] + mi := &file_qubic_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1638,7 +1789,7 @@ func (x *AssetOwnership) String() string { func (*AssetOwnership) ProtoMessage() {} func (x *AssetOwnership) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[27] + mi := &file_qubic_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1651,7 +1802,7 @@ func (x *AssetOwnership) ProtoReflect() protoreflect.Message { // Deprecated: Use AssetOwnership.ProtoReflect.Descriptor instead. func (*AssetOwnership) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{27} + return file_qubic_proto_rawDescGZIP(), []int{30} } func (x *AssetOwnership) GetData() *AssetOwnershipData { @@ -1685,7 +1836,7 @@ type AssetOwnerships struct { func (x *AssetOwnerships) Reset() { *x = AssetOwnerships{} - mi := &file_qubic_proto_msgTypes[28] + mi := &file_qubic_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1697,7 +1848,7 @@ func (x *AssetOwnerships) String() string { func (*AssetOwnerships) ProtoMessage() {} func (x *AssetOwnerships) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[28] + mi := &file_qubic_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1710,7 +1861,7 @@ func (x *AssetOwnerships) ProtoReflect() protoreflect.Message { // Deprecated: Use AssetOwnerships.ProtoReflect.Descriptor instead. func (*AssetOwnerships) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{28} + return file_qubic_proto_rawDescGZIP(), []int{31} } func (x *AssetOwnerships) GetAssets() []*AssetOwnership { @@ -1734,7 +1885,7 @@ type AssetPossessionData struct { func (x *AssetPossessionData) Reset() { *x = AssetPossessionData{} - mi := &file_qubic_proto_msgTypes[29] + mi := &file_qubic_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1746,7 +1897,7 @@ func (x *AssetPossessionData) String() string { func (*AssetPossessionData) ProtoMessage() {} func (x *AssetPossessionData) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[29] + mi := &file_qubic_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1759,7 +1910,7 @@ func (x *AssetPossessionData) ProtoReflect() protoreflect.Message { // Deprecated: Use AssetPossessionData.ProtoReflect.Descriptor instead. func (*AssetPossessionData) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{29} + return file_qubic_proto_rawDescGZIP(), []int{32} } func (x *AssetPossessionData) GetPossessorIdentity() string { @@ -1809,7 +1960,7 @@ type AssetPossession struct { func (x *AssetPossession) Reset() { *x = AssetPossession{} - mi := &file_qubic_proto_msgTypes[30] + mi := &file_qubic_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1821,7 +1972,7 @@ func (x *AssetPossession) String() string { func (*AssetPossession) ProtoMessage() {} func (x *AssetPossession) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[30] + mi := &file_qubic_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1834,7 +1985,7 @@ func (x *AssetPossession) ProtoReflect() protoreflect.Message { // Deprecated: Use AssetPossession.ProtoReflect.Descriptor instead. func (*AssetPossession) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{30} + return file_qubic_proto_rawDescGZIP(), []int{33} } func (x *AssetPossession) GetData() *AssetPossessionData { @@ -1868,7 +2019,7 @@ type AssetPossessions struct { func (x *AssetPossessions) Reset() { *x = AssetPossessions{} - mi := &file_qubic_proto_msgTypes[31] + mi := &file_qubic_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1880,7 +2031,7 @@ func (x *AssetPossessions) String() string { func (*AssetPossessions) ProtoMessage() {} func (x *AssetPossessions) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[31] + mi := &file_qubic_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1893,7 +2044,7 @@ func (x *AssetPossessions) ProtoReflect() protoreflect.Message { // Deprecated: Use AssetPossessions.ProtoReflect.Descriptor instead. func (*AssetPossessions) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{31} + return file_qubic_proto_rawDescGZIP(), []int{34} } func (x *AssetPossessions) GetAssets() []*AssetPossession { @@ -1913,7 +2064,7 @@ type GetByUniverseIndexRequest struct { func (x *GetByUniverseIndexRequest) Reset() { *x = GetByUniverseIndexRequest{} - mi := &file_qubic_proto_msgTypes[32] + mi := &file_qubic_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1925,7 +2076,7 @@ func (x *GetByUniverseIndexRequest) String() string { func (*GetByUniverseIndexRequest) ProtoMessage() {} func (x *GetByUniverseIndexRequest) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[32] + mi := &file_qubic_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1938,7 +2089,7 @@ func (x *GetByUniverseIndexRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetByUniverseIndexRequest.ProtoReflect.Descriptor instead. func (*GetByUniverseIndexRequest) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{32} + return file_qubic_proto_rawDescGZIP(), []int{35} } func (x *GetByUniverseIndexRequest) GetIndex() uint32 { @@ -1959,7 +2110,7 @@ type GetIssuedAssetsByFilterRequest struct { func (x *GetIssuedAssetsByFilterRequest) Reset() { *x = GetIssuedAssetsByFilterRequest{} - mi := &file_qubic_proto_msgTypes[33] + mi := &file_qubic_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1971,7 +2122,7 @@ func (x *GetIssuedAssetsByFilterRequest) String() string { func (*GetIssuedAssetsByFilterRequest) ProtoMessage() {} func (x *GetIssuedAssetsByFilterRequest) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[33] + mi := &file_qubic_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1984,7 +2135,7 @@ func (x *GetIssuedAssetsByFilterRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetIssuedAssetsByFilterRequest.ProtoReflect.Descriptor instead. func (*GetIssuedAssetsByFilterRequest) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{33} + return file_qubic_proto_rawDescGZIP(), []int{36} } func (x *GetIssuedAssetsByFilterRequest) GetIssuerIdentity() string { @@ -2018,7 +2169,7 @@ type GetOwnedAssetsByFilterRequest struct { func (x *GetOwnedAssetsByFilterRequest) Reset() { *x = GetOwnedAssetsByFilterRequest{} - mi := &file_qubic_proto_msgTypes[34] + mi := &file_qubic_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2030,7 +2181,7 @@ func (x *GetOwnedAssetsByFilterRequest) String() string { func (*GetOwnedAssetsByFilterRequest) ProtoMessage() {} func (x *GetOwnedAssetsByFilterRequest) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[34] + mi := &file_qubic_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2043,7 +2194,7 @@ func (x *GetOwnedAssetsByFilterRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetOwnedAssetsByFilterRequest.ProtoReflect.Descriptor instead. func (*GetOwnedAssetsByFilterRequest) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{34} + return file_qubic_proto_rawDescGZIP(), []int{37} } func (x *GetOwnedAssetsByFilterRequest) GetIssuerIdentity() string { @@ -2095,7 +2246,7 @@ type GetPossessedAssetsByFilterRequest struct { func (x *GetPossessedAssetsByFilterRequest) Reset() { *x = GetPossessedAssetsByFilterRequest{} - mi := &file_qubic_proto_msgTypes[35] + mi := &file_qubic_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2107,7 +2258,7 @@ func (x *GetPossessedAssetsByFilterRequest) String() string { func (*GetPossessedAssetsByFilterRequest) ProtoMessage() {} func (x *GetPossessedAssetsByFilterRequest) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[35] + mi := &file_qubic_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2120,7 +2271,7 @@ func (x *GetPossessedAssetsByFilterRequest) ProtoReflect() protoreflect.Message // Deprecated: Use GetPossessedAssetsByFilterRequest.ProtoReflect.Descriptor instead. func (*GetPossessedAssetsByFilterRequest) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{35} + return file_qubic_proto_rawDescGZIP(), []int{38} } func (x *GetPossessedAssetsByFilterRequest) GetIssuerIdentity() string { @@ -2178,7 +2329,7 @@ type Ipo struct { func (x *Ipo) Reset() { *x = Ipo{} - mi := &file_qubic_proto_msgTypes[36] + mi := &file_qubic_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2190,7 +2341,7 @@ func (x *Ipo) String() string { func (*Ipo) ProtoMessage() {} func (x *Ipo) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[36] + mi := &file_qubic_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2203,7 +2354,7 @@ func (x *Ipo) ProtoReflect() protoreflect.Message { // Deprecated: Use Ipo.ProtoReflect.Descriptor instead. func (*Ipo) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{36} + return file_qubic_proto_rawDescGZIP(), []int{39} } func (x *Ipo) GetContractIndex() uint32 { @@ -2230,7 +2381,7 @@ type GetActiveIposResponse struct { func (x *GetActiveIposResponse) Reset() { *x = GetActiveIposResponse{} - mi := &file_qubic_proto_msgTypes[37] + mi := &file_qubic_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2242,7 +2393,7 @@ func (x *GetActiveIposResponse) String() string { func (*GetActiveIposResponse) ProtoMessage() {} func (x *GetActiveIposResponse) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[37] + mi := &file_qubic_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2255,7 +2406,7 @@ func (x *GetActiveIposResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActiveIposResponse.ProtoReflect.Descriptor instead. func (*GetActiveIposResponse) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{37} + return file_qubic_proto_rawDescGZIP(), []int{40} } func (x *GetActiveIposResponse) GetIpos() []*Ipo { @@ -2275,7 +2426,7 @@ type IpoBid struct { func (x *IpoBid) Reset() { *x = IpoBid{} - mi := &file_qubic_proto_msgTypes[38] + mi := &file_qubic_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2287,7 +2438,7 @@ func (x *IpoBid) String() string { func (*IpoBid) ProtoMessage() {} func (x *IpoBid) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[38] + mi := &file_qubic_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2300,7 +2451,7 @@ func (x *IpoBid) ProtoReflect() protoreflect.Message { // Deprecated: Use IpoBid.ProtoReflect.Descriptor instead. func (*IpoBid) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{38} + return file_qubic_proto_rawDescGZIP(), []int{41} } func (x *IpoBid) GetIdentity() string { @@ -2328,7 +2479,7 @@ type IpoBidData struct { func (x *IpoBidData) Reset() { *x = IpoBidData{} - mi := &file_qubic_proto_msgTypes[39] + mi := &file_qubic_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2340,7 +2491,7 @@ func (x *IpoBidData) String() string { func (*IpoBidData) ProtoMessage() {} func (x *IpoBidData) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[39] + mi := &file_qubic_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2353,7 +2504,7 @@ func (x *IpoBidData) ProtoReflect() protoreflect.Message { // Deprecated: Use IpoBidData.ProtoReflect.Descriptor instead. func (*IpoBidData) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{39} + return file_qubic_proto_rawDescGZIP(), []int{42} } func (x *IpoBidData) GetContractIndex() uint32 { @@ -2386,7 +2537,7 @@ type GetContractIpoBidsRequest struct { func (x *GetContractIpoBidsRequest) Reset() { *x = GetContractIpoBidsRequest{} - mi := &file_qubic_proto_msgTypes[40] + mi := &file_qubic_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2398,7 +2549,7 @@ func (x *GetContractIpoBidsRequest) String() string { func (*GetContractIpoBidsRequest) ProtoMessage() {} func (x *GetContractIpoBidsRequest) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[40] + mi := &file_qubic_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2411,7 +2562,7 @@ func (x *GetContractIpoBidsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetContractIpoBidsRequest.ProtoReflect.Descriptor instead. func (*GetContractIpoBidsRequest) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{40} + return file_qubic_proto_rawDescGZIP(), []int{43} } func (x *GetContractIpoBidsRequest) GetContractIndex() uint32 { @@ -2430,7 +2581,7 @@ type GetContractIpoBidsResponse struct { func (x *GetContractIpoBidsResponse) Reset() { *x = GetContractIpoBidsResponse{} - mi := &file_qubic_proto_msgTypes[41] + mi := &file_qubic_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2442,7 +2593,7 @@ func (x *GetContractIpoBidsResponse) String() string { func (*GetContractIpoBidsResponse) ProtoMessage() {} func (x *GetContractIpoBidsResponse) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[41] + mi := &file_qubic_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2455,7 +2606,7 @@ func (x *GetContractIpoBidsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetContractIpoBidsResponse.ProtoReflect.Descriptor instead. func (*GetContractIpoBidsResponse) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{41} + return file_qubic_proto_rawDescGZIP(), []int{44} } func (x *GetContractIpoBidsResponse) GetBidData() *IpoBidData { @@ -2475,7 +2626,7 @@ type HealthResponse struct { func (x *HealthResponse) Reset() { *x = HealthResponse{} - mi := &file_qubic_proto_msgTypes[42] + mi := &file_qubic_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2487,7 +2638,7 @@ func (x *HealthResponse) String() string { func (*HealthResponse) ProtoMessage() {} func (x *HealthResponse) ProtoReflect() protoreflect.Message { - mi := &file_qubic_proto_msgTypes[42] + mi := &file_qubic_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2500,7 +2651,7 @@ func (x *HealthResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use HealthResponse.ProtoReflect.Descriptor instead. func (*HealthResponse) Descriptor() ([]byte, []int) { - return file_qubic_proto_rawDescGZIP(), []int{42} + return file_qubic_proto_rawDescGZIP(), []int{45} } func (x *HealthResponse) GetStatus() string { @@ -2530,9 +2681,9 @@ const file_qubic_proto_rawDesc = "" + "\x12GetBalanceResponse\x126\n" + "\abalance\x18\x01 \x01(\v2\x1c.qubic.http.qubic.pb.BalanceR\abalance\"}\n" + "\x1bBroadcastTransactionRequest\x12^\n" + - "\x13encoded_transaction\x18\x01 \x01(\tB-\xbaG*\x92\x02'Base64 encoded binary transaction data.R\x12encodedTransaction\"\xdb\x02\n" + - "\x1cBroadcastTransactionResponse\x12z\n" + - "\x11peers_broadcasted\x18\x01 \x01(\x05BM\xbaGJ\x92\x02GThe number of Qubic node peers this transactions has been broadcast to.R\x10peersBroadcasted\x12o\n" + + "\x13encoded_transaction\x18\x01 \x01(\tB-\xbaG*\x92\x02'Base64 encoded binary transaction data.R\x12encodedTransaction\"\xe2\x03\n" + + "\x1cBroadcastTransactionResponse\x12\x80\x02\n" + + "\x11peers_broadcasted\x18\x01 \x01(\x05B\xd2\x01\xbaG\xce\x01\x92\x02\xca\x01The number of Qubic node peers this transaction has been broadcast to. A value greater than zero only means the transaction was relayed; it does not mean the transaction was added to a tick or executed.R\x10peersBroadcasted\x12o\n" + "\x13encoded_transaction\x18\x02 \x01(\tB>\xbaG;\x92\x028The Base 64 encoded binary transaction from the request.R\x12encodedTransaction\x12N\n" + "\x0etransaction_id\x18\x03 \x01(\tB'\xbaG$\x92\x02!The id / hash of the transaction.R\rtransactionId\"s\n" + "\bTickInfo\x12\x12\n" + @@ -2591,7 +2742,17 @@ const file_qubic_proto_rawDesc = "" + "\x16PossessedAssetsRequest\x12\x1a\n" + "\bidentity\x18\x01 \x01(\tR\bidentity\"i\n" + "\x17PossessedAssetsResponse\x12N\n" + - "\x10possessed_assets\x18\x01 \x03(\v2#.qubic.http.qubic.pb.PossessedAssetR\x0fpossessedAssets\"\xa3\x01\n" + + "\x10possessed_assets\x18\x01 \x03(\v2#.qubic.http.qubic.pb.PossessedAssetR\x0fpossessedAssets\"?\n" + + "\x1dGetAssetsForIdentitiesRequest\x12\x1e\n" + + "\n" + + "identities\x18\x01 \x03(\tR\n" + + "identities\"\xc0\x01\n" + + "\x0eIdentityAssets\x12\x1a\n" + + "\bidentity\x18\x01 \x01(\tR\bidentity\x12B\n" + + "\fowned_assets\x18\x02 \x03(\v2\x1f.qubic.http.qubic.pb.OwnedAssetR\vownedAssets\x12N\n" + + "\x10possessed_assets\x18\x03 \x03(\v2#.qubic.http.qubic.pb.PossessedAssetR\x0fpossessedAssets\"]\n" + + "\x1eGetAssetsForIdentitiesResponse\x12;\n" + + "\x06assets\x18\x01 \x03(\v2#.qubic.http.qubic.pb.IdentityAssetsR\x06assets\"\xa3\x01\n" + "\x19QuerySmartContractRequest\x12%\n" + "\x0econtract_index\x18\x01 \x01(\rR\rcontractIndex\x12\x1d\n" + "\n" + @@ -2680,15 +2841,31 @@ const file_qubic_proto_rawDesc = "" + "\x1aGetContractIpoBidsResponse\x12:\n" + "\bbid_data\x18\x01 \x01(\v2\x1f.qubic.http.qubic.pb.IpoBidDataR\abidData\"J\n" + "\x0eHealthResponse\x128\n" + - "\x06status\x18\x01 \x01(\tB \xbaG\x1d\x92\x02\x1aHealth status information.R\x06status2\x92\x1f\n" + + "\x06status\x18\x01 \x01(\tB \xbaG\x1d\x92\x02\x1aHealth status information.R\x06status2\xe4>\n" + "\x10QubicLiveService\x12\xbc\x01\n" + "\n" + "GetBalance\x12&.qubic.http.qubic.pb.GetBalanceRequest\x1a'.qubic.http.qubic.pb.GetBalanceResponse\"]\xbaGD\n" + "\bAccounts\x12\vGet Balance\x1a+Gets the balance of the specified identity.\x82\xd3\xe4\x93\x02\x10\x12\x0e/balances/{id}\x12\xe3\x01\n" + "\x12QuerySmartContract\x12..qubic.http.qubic.pb.QuerySmartContractRequest\x1a/.qubic.http.qubic.pb.QuerySmartContractResponse\"l\xbaGK\n" + - "\x0fSmart Contracts\x12\x14Query Smart Contract\x1a\"Queries a smart contract function.\x82\xd3\xe4\x93\x02\x18:\x01*\"\x13/querySmartContract\x12\xf0\x01\n" + - "\x14BroadcastTransaction\x120.qubic.http.qubic.pb.BroadcastTransactionRequest\x1a1.qubic.http.qubic.pb.BroadcastTransactionResponse\"s\xbaGO\n" + - "\fTransactions\x12\x15Broadcast Transaction\x1a(Broadcasts a transaction to the network.\x82\xd3\xe4\x93\x02\x1b:\x01*\"\x16/broadcast-transaction\x12\xa2\x01\n" + + "\x0fSmart Contracts\x12\x14Query Smart Contract\x1a\"Queries a smart contract function.\x82\xd3\xe4\x93\x02\x18:\x01*\"\x13/querySmartContract\x12\xcc\x1d\n" + + "\x14BroadcastTransaction\x120.qubic.http.qubic.pb.BroadcastTransactionRequest\x1a1.qubic.http.qubic.pb.BroadcastTransactionResponse\"\xce\x1c\xbaG\xa9\x1c\n" + + "\fTransactions\x12\x15Broadcast Transaction\x1a\x81\x1cBroadcasts a transaction to the network.\n" + + "\n" + + "**A successful response does NOT mean the transaction was added to the blockchain.** It only confirms the transaction was relayed to `peers_broadcasted` node peers. The transaction still has to reach the computors, be selected into its target tick, and be executed. None of that is reported here.\n" + + "\n" + + "Common reasons a broadcast transaction is never added to a tick (no error is returned in any of these cases):\n" + + "- **Insufficient balance:** a validly signed transaction can be included in a tick but do nothing, or be dropped entirely, if the source lacks the funds. The balance simply does not change in the Spectrum ledger.\n" + + "- **Tick congestion / low priority:** each tick holds a limited number of transactions. When more are scheduled than fit, the lowest-priority ones are discarded. Priority favors higher balances and addresses that have not recently transacted.\n" + + "- **No quorum / empty tick:** if fewer than the required computors agree on the target tick, it becomes an empty tick and every transaction scheduled for it is discarded.\n" + + "- **Target tick too close:** the target tick must be far enough in the future for the transaction to propagate (a margin of ~10 ticks is recommended). If it is processed before the transaction reaches enough computors, it is missed and not retried automatically.\n" + + "- **Did not reach a computor in time:** broadcasting relays to node peers via gossip, not directly to computors. If peers are lagging or out of sync the transaction may never reach a computor for the target tick.\n" + + "\n" + + "To confirm a transaction, do not rely on this response — the Live API does not track transaction outcomes. Use the Qubic public Query API (`https://rpc.qubic.org`, see the [Query API documentation](https://docs.qubic.org/apis/query)). Because every transaction targets a specific future tick, you can only check it once that target tick has been processed:\n" + + "- **Has the target tick been processed yet?** Compare the target tick against the last processed tick — see [getLastProcessedTick](https://docs.qubic.org/apis/query#tag/archive/GET/getLastProcessedTick) in the Query API. The checks below are only conclusive once the last processed tick is greater than or equal to the target tick. Some ticks are skipped (empty); a transaction whose target tick was skipped will never appear.\n" + + "- **Was it included?** Look the transaction up by its `transactionId` — see [getTransactionByHash](https://docs.qubic.org/apis/query#tag/transactions/POST/getTransactionByHash) in the Query API. This is only conclusive once the target tick has been processed (the check above): if the transaction is still not found at that point, it was not included (dropped, congestion, or empty tick). Before the target tick has been processed, 'not found' only means 'not processed yet', not 'not included'.\n" + + "- **Did it take effect?** Being included in a tick does not mean the transaction did anything. The [getTransactionByHash](https://docs.qubic.org/apis/query#tag/transactions/POST/getTransactionByHash) response includes a `moneyFlew` flag: for value transfers (simple transfers and SendMany multi-transfers) it tells you whether funds actually moved (`moneyFlew = false` means no money moved, e.g. insufficient balance). For other transaction types (e.g. smart contract calls), `moneyFlew` does not indicate success, and there is no generic way to tell whether the call did what it was meant to: it depends on the specific contract and what its procedure does.\n" + + "\n" + + "Resubmit with a new target tick if the transaction was not included or had no effect.\x82\xd3\xe4\x93\x02\x1b:\x01*\"\x16/broadcast-transaction\x12\xa2\x01\n" + "\vGetTickInfo\x12\x16.google.protobuf.Empty\x1a(.qubic.http.qubic.pb.GetTickInfoResponse\"Q\xbaG<\n" + "\aNetwork\x12\rGet Tick Info\x1a\"Gets the current tick information.\x82\xd3\xe4\x93\x02\f\x12\n" + "/tick-info\x12\xb1\x01\n" + @@ -2699,7 +2876,9 @@ const file_qubic_proto_rawDesc = "" + "\x0eGetOwnedAssets\x12'.qubic.http.qubic.pb.OwnedAssetsRequest\x1a(.qubic.http.qubic.pb.OwnedAssetsResponse\"u\xbaGR\n" + "\x06Assets\x12\x11List Owned Assets\x1a5Gets assets that are owned by the specified identity.\x82\xd3\xe4\x93\x02\x1a\x12\x18/assets/{identity}/owned\x12\xf3\x01\n" + "\x12GetPossessedAssets\x12+.qubic.http.qubic.pb.PossessedAssetsRequest\x1a,.qubic.http.qubic.pb.PossessedAssetsResponse\"\x81\x01\xbaGZ\n" + - "\x06Assets\x12\x15List Possessed Assets\x1a9Gets assets that are possessed by the specified identity.\x82\xd3\xe4\x93\x02\x1e\x12\x1c/assets/{identity}/possessed\x12\xff\x01\n" + + "\x06Assets\x12\x15List Possessed Assets\x1a9Gets assets that are possessed by the specified identity.\x82\xd3\xe4\x93\x02\x1e\x12\x1c/assets/{identity}/possessed\x12\xf3\x03\n" + + "\x16GetAssetsForIdentities\x122.qubic.http.qubic.pb.GetAssetsForIdentitiesRequest\x1a3.qubic.http.qubic.pb.GetAssetsForIdentitiesResponse\"\xef\x02\xbaG\xd9\x02\n" + + "\x06Assets\x127List Owned And Possessed Assets For Multiple Identities\x1a\x95\x02Gets the assets owned and possessed by each of the given identities. The requests are pipelined over a single node connection, so one call costs roughly one round trip regardless of how many identities are requested. Results are returned in the order the identities were given.\x82\xd3\xe4\x93\x02\f:\x01*\"\a/assets\x12\xff\x01\n" + "\x17GetIssuedAssetsByFilter\x123.qubic.http.qubic.pb.GetIssuedAssetsByFilterRequest\x1a#.qubic.http.qubic.pb.AssetIssuances\"\x89\x01\xbaGm\n" + "\x06Assets\x12\x16Search Asset Issuances\x1aKReturns a list of issued assets filtered by issuer identity and asset name.\x82\xd3\xe4\x93\x02\x13\x12\x11/assets/issuances\x12\xec\x01\n" + "\x1dGetIssuedAssetByUniverseIndex\x12..qubic.http.qubic.pb.GetByUniverseIndexRequest\x1a\".qubic.http.qubic.pb.AssetIssuance\"w\xbaGS\n" + @@ -2739,7 +2918,7 @@ func file_qubic_proto_rawDescGZIP() []byte { return file_qubic_proto_rawDescData } -var file_qubic_proto_msgTypes = make([]protoimpl.MessageInfo, 44) +var file_qubic_proto_msgTypes = make([]protoimpl.MessageInfo, 47) var file_qubic_proto_goTypes = []any{ (*Balance)(nil), // 0: qubic.http.qubic.pb.Balance (*GetBalanceRequest)(nil), // 1: qubic.http.qubic.pb.GetBalanceRequest @@ -2762,30 +2941,33 @@ var file_qubic_proto_goTypes = []any{ (*PossessedAsset)(nil), // 18: qubic.http.qubic.pb.PossessedAsset (*PossessedAssetsRequest)(nil), // 19: qubic.http.qubic.pb.PossessedAssetsRequest (*PossessedAssetsResponse)(nil), // 20: qubic.http.qubic.pb.PossessedAssetsResponse - (*QuerySmartContractRequest)(nil), // 21: qubic.http.qubic.pb.QuerySmartContractRequest - (*QuerySmartContractResponse)(nil), // 22: qubic.http.qubic.pb.QuerySmartContractResponse - (*AssetIssuanceData)(nil), // 23: qubic.http.qubic.pb.AssetIssuanceData - (*AssetIssuance)(nil), // 24: qubic.http.qubic.pb.AssetIssuance - (*AssetIssuances)(nil), // 25: qubic.http.qubic.pb.AssetIssuances - (*AssetOwnershipData)(nil), // 26: qubic.http.qubic.pb.AssetOwnershipData - (*AssetOwnership)(nil), // 27: qubic.http.qubic.pb.AssetOwnership - (*AssetOwnerships)(nil), // 28: qubic.http.qubic.pb.AssetOwnerships - (*AssetPossessionData)(nil), // 29: qubic.http.qubic.pb.AssetPossessionData - (*AssetPossession)(nil), // 30: qubic.http.qubic.pb.AssetPossession - (*AssetPossessions)(nil), // 31: qubic.http.qubic.pb.AssetPossessions - (*GetByUniverseIndexRequest)(nil), // 32: qubic.http.qubic.pb.GetByUniverseIndexRequest - (*GetIssuedAssetsByFilterRequest)(nil), // 33: qubic.http.qubic.pb.GetIssuedAssetsByFilterRequest - (*GetOwnedAssetsByFilterRequest)(nil), // 34: qubic.http.qubic.pb.GetOwnedAssetsByFilterRequest - (*GetPossessedAssetsByFilterRequest)(nil), // 35: qubic.http.qubic.pb.GetPossessedAssetsByFilterRequest - (*Ipo)(nil), // 36: qubic.http.qubic.pb.Ipo - (*GetActiveIposResponse)(nil), // 37: qubic.http.qubic.pb.GetActiveIposResponse - (*IpoBid)(nil), // 38: qubic.http.qubic.pb.IpoBid - (*IpoBidData)(nil), // 39: qubic.http.qubic.pb.IpoBidData - (*GetContractIpoBidsRequest)(nil), // 40: qubic.http.qubic.pb.GetContractIpoBidsRequest - (*GetContractIpoBidsResponse)(nil), // 41: qubic.http.qubic.pb.GetContractIpoBidsResponse - (*HealthResponse)(nil), // 42: qubic.http.qubic.pb.HealthResponse - nil, // 43: qubic.http.qubic.pb.IpoBidData.BidsEntry - (*emptypb.Empty)(nil), // 44: google.protobuf.Empty + (*GetAssetsForIdentitiesRequest)(nil), // 21: qubic.http.qubic.pb.GetAssetsForIdentitiesRequest + (*IdentityAssets)(nil), // 22: qubic.http.qubic.pb.IdentityAssets + (*GetAssetsForIdentitiesResponse)(nil), // 23: qubic.http.qubic.pb.GetAssetsForIdentitiesResponse + (*QuerySmartContractRequest)(nil), // 24: qubic.http.qubic.pb.QuerySmartContractRequest + (*QuerySmartContractResponse)(nil), // 25: qubic.http.qubic.pb.QuerySmartContractResponse + (*AssetIssuanceData)(nil), // 26: qubic.http.qubic.pb.AssetIssuanceData + (*AssetIssuance)(nil), // 27: qubic.http.qubic.pb.AssetIssuance + (*AssetIssuances)(nil), // 28: qubic.http.qubic.pb.AssetIssuances + (*AssetOwnershipData)(nil), // 29: qubic.http.qubic.pb.AssetOwnershipData + (*AssetOwnership)(nil), // 30: qubic.http.qubic.pb.AssetOwnership + (*AssetOwnerships)(nil), // 31: qubic.http.qubic.pb.AssetOwnerships + (*AssetPossessionData)(nil), // 32: qubic.http.qubic.pb.AssetPossessionData + (*AssetPossession)(nil), // 33: qubic.http.qubic.pb.AssetPossession + (*AssetPossessions)(nil), // 34: qubic.http.qubic.pb.AssetPossessions + (*GetByUniverseIndexRequest)(nil), // 35: qubic.http.qubic.pb.GetByUniverseIndexRequest + (*GetIssuedAssetsByFilterRequest)(nil), // 36: qubic.http.qubic.pb.GetIssuedAssetsByFilterRequest + (*GetOwnedAssetsByFilterRequest)(nil), // 37: qubic.http.qubic.pb.GetOwnedAssetsByFilterRequest + (*GetPossessedAssetsByFilterRequest)(nil), // 38: qubic.http.qubic.pb.GetPossessedAssetsByFilterRequest + (*Ipo)(nil), // 39: qubic.http.qubic.pb.Ipo + (*GetActiveIposResponse)(nil), // 40: qubic.http.qubic.pb.GetActiveIposResponse + (*IpoBid)(nil), // 41: qubic.http.qubic.pb.IpoBid + (*IpoBidData)(nil), // 42: qubic.http.qubic.pb.IpoBidData + (*GetContractIpoBidsRequest)(nil), // 43: qubic.http.qubic.pb.GetContractIpoBidsRequest + (*GetContractIpoBidsResponse)(nil), // 44: qubic.http.qubic.pb.GetContractIpoBidsResponse + (*HealthResponse)(nil), // 45: qubic.http.qubic.pb.HealthResponse + nil, // 46: qubic.http.qubic.pb.IpoBidData.BidsEntry + (*emptypb.Empty)(nil), // 47: google.protobuf.Empty } var file_qubic_proto_depIdxs = []int32{ 0, // 0: qubic.http.qubic.pb.GetBalanceResponse.balance:type_name -> qubic.http.qubic.pb.Balance @@ -2802,55 +2984,60 @@ var file_qubic_proto_depIdxs = []int32{ 17, // 11: qubic.http.qubic.pb.PossessedAsset.data:type_name -> qubic.http.qubic.pb.PossessedAssetData 8, // 12: qubic.http.qubic.pb.PossessedAsset.info:type_name -> qubic.http.qubic.pb.AssetInfo 18, // 13: qubic.http.qubic.pb.PossessedAssetsResponse.possessed_assets:type_name -> qubic.http.qubic.pb.PossessedAsset - 23, // 14: qubic.http.qubic.pb.AssetIssuance.data:type_name -> qubic.http.qubic.pb.AssetIssuanceData - 24, // 15: qubic.http.qubic.pb.AssetIssuances.assets:type_name -> qubic.http.qubic.pb.AssetIssuance - 26, // 16: qubic.http.qubic.pb.AssetOwnership.data:type_name -> qubic.http.qubic.pb.AssetOwnershipData - 27, // 17: qubic.http.qubic.pb.AssetOwnerships.assets:type_name -> qubic.http.qubic.pb.AssetOwnership - 29, // 18: qubic.http.qubic.pb.AssetPossession.data:type_name -> qubic.http.qubic.pb.AssetPossessionData - 30, // 19: qubic.http.qubic.pb.AssetPossessions.assets:type_name -> qubic.http.qubic.pb.AssetPossession - 36, // 20: qubic.http.qubic.pb.GetActiveIposResponse.ipos:type_name -> qubic.http.qubic.pb.Ipo - 43, // 21: qubic.http.qubic.pb.IpoBidData.bids:type_name -> qubic.http.qubic.pb.IpoBidData.BidsEntry - 39, // 22: qubic.http.qubic.pb.GetContractIpoBidsResponse.bid_data:type_name -> qubic.http.qubic.pb.IpoBidData - 38, // 23: qubic.http.qubic.pb.IpoBidData.BidsEntry.value:type_name -> qubic.http.qubic.pb.IpoBid - 1, // 24: qubic.http.qubic.pb.QubicLiveService.GetBalance:input_type -> qubic.http.qubic.pb.GetBalanceRequest - 21, // 25: qubic.http.qubic.pb.QubicLiveService.QuerySmartContract:input_type -> qubic.http.qubic.pb.QuerySmartContractRequest - 3, // 26: qubic.http.qubic.pb.QubicLiveService.BroadcastTransaction:input_type -> qubic.http.qubic.pb.BroadcastTransactionRequest - 44, // 27: qubic.http.qubic.pb.QubicLiveService.GetTickInfo:input_type -> google.protobuf.Empty - 44, // 28: qubic.http.qubic.pb.QubicLiveService.GetBlockHeight:input_type -> google.protobuf.Empty - 11, // 29: qubic.http.qubic.pb.QubicLiveService.GetIssuedAssets:input_type -> qubic.http.qubic.pb.IssuedAssetsRequest - 15, // 30: qubic.http.qubic.pb.QubicLiveService.GetOwnedAssets:input_type -> qubic.http.qubic.pb.OwnedAssetsRequest - 19, // 31: qubic.http.qubic.pb.QubicLiveService.GetPossessedAssets:input_type -> qubic.http.qubic.pb.PossessedAssetsRequest - 33, // 32: qubic.http.qubic.pb.QubicLiveService.GetIssuedAssetsByFilter:input_type -> qubic.http.qubic.pb.GetIssuedAssetsByFilterRequest - 32, // 33: qubic.http.qubic.pb.QubicLiveService.GetIssuedAssetByUniverseIndex:input_type -> qubic.http.qubic.pb.GetByUniverseIndexRequest - 34, // 34: qubic.http.qubic.pb.QubicLiveService.GetOwnedAssetsByFilter:input_type -> qubic.http.qubic.pb.GetOwnedAssetsByFilterRequest - 32, // 35: qubic.http.qubic.pb.QubicLiveService.GetOwnedAssetByUniverseIndex:input_type -> qubic.http.qubic.pb.GetByUniverseIndexRequest - 35, // 36: qubic.http.qubic.pb.QubicLiveService.GetPossessedAssetsByFilter:input_type -> qubic.http.qubic.pb.GetPossessedAssetsByFilterRequest - 32, // 37: qubic.http.qubic.pb.QubicLiveService.GetPossessedAssetByUniverseIndex:input_type -> qubic.http.qubic.pb.GetByUniverseIndexRequest - 44, // 38: qubic.http.qubic.pb.QubicLiveService.GetActiveIpos:input_type -> google.protobuf.Empty - 40, // 39: qubic.http.qubic.pb.QubicLiveService.GetContractIpoBids:input_type -> qubic.http.qubic.pb.GetContractIpoBidsRequest - 44, // 40: qubic.http.qubic.pb.QubicLiveService.GetHealth:input_type -> google.protobuf.Empty - 2, // 41: qubic.http.qubic.pb.QubicLiveService.GetBalance:output_type -> qubic.http.qubic.pb.GetBalanceResponse - 22, // 42: qubic.http.qubic.pb.QubicLiveService.QuerySmartContract:output_type -> qubic.http.qubic.pb.QuerySmartContractResponse - 4, // 43: qubic.http.qubic.pb.QubicLiveService.BroadcastTransaction:output_type -> qubic.http.qubic.pb.BroadcastTransactionResponse - 6, // 44: qubic.http.qubic.pb.QubicLiveService.GetTickInfo:output_type -> qubic.http.qubic.pb.GetTickInfoResponse - 7, // 45: qubic.http.qubic.pb.QubicLiveService.GetBlockHeight:output_type -> qubic.http.qubic.pb.GetBlockHeightResponse - 12, // 46: qubic.http.qubic.pb.QubicLiveService.GetIssuedAssets:output_type -> qubic.http.qubic.pb.IssuedAssetsResponse - 16, // 47: qubic.http.qubic.pb.QubicLiveService.GetOwnedAssets:output_type -> qubic.http.qubic.pb.OwnedAssetsResponse - 20, // 48: qubic.http.qubic.pb.QubicLiveService.GetPossessedAssets:output_type -> qubic.http.qubic.pb.PossessedAssetsResponse - 25, // 49: qubic.http.qubic.pb.QubicLiveService.GetIssuedAssetsByFilter:output_type -> qubic.http.qubic.pb.AssetIssuances - 24, // 50: qubic.http.qubic.pb.QubicLiveService.GetIssuedAssetByUniverseIndex:output_type -> qubic.http.qubic.pb.AssetIssuance - 28, // 51: qubic.http.qubic.pb.QubicLiveService.GetOwnedAssetsByFilter:output_type -> qubic.http.qubic.pb.AssetOwnerships - 27, // 52: qubic.http.qubic.pb.QubicLiveService.GetOwnedAssetByUniverseIndex:output_type -> qubic.http.qubic.pb.AssetOwnership - 31, // 53: qubic.http.qubic.pb.QubicLiveService.GetPossessedAssetsByFilter:output_type -> qubic.http.qubic.pb.AssetPossessions - 30, // 54: qubic.http.qubic.pb.QubicLiveService.GetPossessedAssetByUniverseIndex:output_type -> qubic.http.qubic.pb.AssetPossession - 37, // 55: qubic.http.qubic.pb.QubicLiveService.GetActiveIpos:output_type -> qubic.http.qubic.pb.GetActiveIposResponse - 41, // 56: qubic.http.qubic.pb.QubicLiveService.GetContractIpoBids:output_type -> qubic.http.qubic.pb.GetContractIpoBidsResponse - 42, // 57: qubic.http.qubic.pb.QubicLiveService.GetHealth:output_type -> qubic.http.qubic.pb.HealthResponse - 41, // [41:58] is the sub-list for method output_type - 24, // [24:41] is the sub-list for method input_type - 24, // [24:24] is the sub-list for extension type_name - 24, // [24:24] is the sub-list for extension extendee - 0, // [0:24] is the sub-list for field type_name + 14, // 14: qubic.http.qubic.pb.IdentityAssets.owned_assets:type_name -> qubic.http.qubic.pb.OwnedAsset + 18, // 15: qubic.http.qubic.pb.IdentityAssets.possessed_assets:type_name -> qubic.http.qubic.pb.PossessedAsset + 22, // 16: qubic.http.qubic.pb.GetAssetsForIdentitiesResponse.assets:type_name -> qubic.http.qubic.pb.IdentityAssets + 26, // 17: qubic.http.qubic.pb.AssetIssuance.data:type_name -> qubic.http.qubic.pb.AssetIssuanceData + 27, // 18: qubic.http.qubic.pb.AssetIssuances.assets:type_name -> qubic.http.qubic.pb.AssetIssuance + 29, // 19: qubic.http.qubic.pb.AssetOwnership.data:type_name -> qubic.http.qubic.pb.AssetOwnershipData + 30, // 20: qubic.http.qubic.pb.AssetOwnerships.assets:type_name -> qubic.http.qubic.pb.AssetOwnership + 32, // 21: qubic.http.qubic.pb.AssetPossession.data:type_name -> qubic.http.qubic.pb.AssetPossessionData + 33, // 22: qubic.http.qubic.pb.AssetPossessions.assets:type_name -> qubic.http.qubic.pb.AssetPossession + 39, // 23: qubic.http.qubic.pb.GetActiveIposResponse.ipos:type_name -> qubic.http.qubic.pb.Ipo + 46, // 24: qubic.http.qubic.pb.IpoBidData.bids:type_name -> qubic.http.qubic.pb.IpoBidData.BidsEntry + 42, // 25: qubic.http.qubic.pb.GetContractIpoBidsResponse.bid_data:type_name -> qubic.http.qubic.pb.IpoBidData + 41, // 26: qubic.http.qubic.pb.IpoBidData.BidsEntry.value:type_name -> qubic.http.qubic.pb.IpoBid + 1, // 27: qubic.http.qubic.pb.QubicLiveService.GetBalance:input_type -> qubic.http.qubic.pb.GetBalanceRequest + 24, // 28: qubic.http.qubic.pb.QubicLiveService.QuerySmartContract:input_type -> qubic.http.qubic.pb.QuerySmartContractRequest + 3, // 29: qubic.http.qubic.pb.QubicLiveService.BroadcastTransaction:input_type -> qubic.http.qubic.pb.BroadcastTransactionRequest + 47, // 30: qubic.http.qubic.pb.QubicLiveService.GetTickInfo:input_type -> google.protobuf.Empty + 47, // 31: qubic.http.qubic.pb.QubicLiveService.GetBlockHeight:input_type -> google.protobuf.Empty + 11, // 32: qubic.http.qubic.pb.QubicLiveService.GetIssuedAssets:input_type -> qubic.http.qubic.pb.IssuedAssetsRequest + 15, // 33: qubic.http.qubic.pb.QubicLiveService.GetOwnedAssets:input_type -> qubic.http.qubic.pb.OwnedAssetsRequest + 19, // 34: qubic.http.qubic.pb.QubicLiveService.GetPossessedAssets:input_type -> qubic.http.qubic.pb.PossessedAssetsRequest + 21, // 35: qubic.http.qubic.pb.QubicLiveService.GetAssetsForIdentities:input_type -> qubic.http.qubic.pb.GetAssetsForIdentitiesRequest + 36, // 36: qubic.http.qubic.pb.QubicLiveService.GetIssuedAssetsByFilter:input_type -> qubic.http.qubic.pb.GetIssuedAssetsByFilterRequest + 35, // 37: qubic.http.qubic.pb.QubicLiveService.GetIssuedAssetByUniverseIndex:input_type -> qubic.http.qubic.pb.GetByUniverseIndexRequest + 37, // 38: qubic.http.qubic.pb.QubicLiveService.GetOwnedAssetsByFilter:input_type -> qubic.http.qubic.pb.GetOwnedAssetsByFilterRequest + 35, // 39: qubic.http.qubic.pb.QubicLiveService.GetOwnedAssetByUniverseIndex:input_type -> qubic.http.qubic.pb.GetByUniverseIndexRequest + 38, // 40: qubic.http.qubic.pb.QubicLiveService.GetPossessedAssetsByFilter:input_type -> qubic.http.qubic.pb.GetPossessedAssetsByFilterRequest + 35, // 41: qubic.http.qubic.pb.QubicLiveService.GetPossessedAssetByUniverseIndex:input_type -> qubic.http.qubic.pb.GetByUniverseIndexRequest + 47, // 42: qubic.http.qubic.pb.QubicLiveService.GetActiveIpos:input_type -> google.protobuf.Empty + 43, // 43: qubic.http.qubic.pb.QubicLiveService.GetContractIpoBids:input_type -> qubic.http.qubic.pb.GetContractIpoBidsRequest + 47, // 44: qubic.http.qubic.pb.QubicLiveService.GetHealth:input_type -> google.protobuf.Empty + 2, // 45: qubic.http.qubic.pb.QubicLiveService.GetBalance:output_type -> qubic.http.qubic.pb.GetBalanceResponse + 25, // 46: qubic.http.qubic.pb.QubicLiveService.QuerySmartContract:output_type -> qubic.http.qubic.pb.QuerySmartContractResponse + 4, // 47: qubic.http.qubic.pb.QubicLiveService.BroadcastTransaction:output_type -> qubic.http.qubic.pb.BroadcastTransactionResponse + 6, // 48: qubic.http.qubic.pb.QubicLiveService.GetTickInfo:output_type -> qubic.http.qubic.pb.GetTickInfoResponse + 7, // 49: qubic.http.qubic.pb.QubicLiveService.GetBlockHeight:output_type -> qubic.http.qubic.pb.GetBlockHeightResponse + 12, // 50: qubic.http.qubic.pb.QubicLiveService.GetIssuedAssets:output_type -> qubic.http.qubic.pb.IssuedAssetsResponse + 16, // 51: qubic.http.qubic.pb.QubicLiveService.GetOwnedAssets:output_type -> qubic.http.qubic.pb.OwnedAssetsResponse + 20, // 52: qubic.http.qubic.pb.QubicLiveService.GetPossessedAssets:output_type -> qubic.http.qubic.pb.PossessedAssetsResponse + 23, // 53: qubic.http.qubic.pb.QubicLiveService.GetAssetsForIdentities:output_type -> qubic.http.qubic.pb.GetAssetsForIdentitiesResponse + 28, // 54: qubic.http.qubic.pb.QubicLiveService.GetIssuedAssetsByFilter:output_type -> qubic.http.qubic.pb.AssetIssuances + 27, // 55: qubic.http.qubic.pb.QubicLiveService.GetIssuedAssetByUniverseIndex:output_type -> qubic.http.qubic.pb.AssetIssuance + 31, // 56: qubic.http.qubic.pb.QubicLiveService.GetOwnedAssetsByFilter:output_type -> qubic.http.qubic.pb.AssetOwnerships + 30, // 57: qubic.http.qubic.pb.QubicLiveService.GetOwnedAssetByUniverseIndex:output_type -> qubic.http.qubic.pb.AssetOwnership + 34, // 58: qubic.http.qubic.pb.QubicLiveService.GetPossessedAssetsByFilter:output_type -> qubic.http.qubic.pb.AssetPossessions + 33, // 59: qubic.http.qubic.pb.QubicLiveService.GetPossessedAssetByUniverseIndex:output_type -> qubic.http.qubic.pb.AssetPossession + 40, // 60: qubic.http.qubic.pb.QubicLiveService.GetActiveIpos:output_type -> qubic.http.qubic.pb.GetActiveIposResponse + 44, // 61: qubic.http.qubic.pb.QubicLiveService.GetContractIpoBids:output_type -> qubic.http.qubic.pb.GetContractIpoBidsResponse + 45, // 62: qubic.http.qubic.pb.QubicLiveService.GetHealth:output_type -> qubic.http.qubic.pb.HealthResponse + 45, // [45:63] is the sub-list for method output_type + 27, // [27:45] 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_qubic_proto_init() } @@ -2864,7 +3051,7 @@ func file_qubic_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_qubic_proto_rawDesc), len(file_qubic_proto_rawDesc)), NumEnums: 0, - NumMessages: 44, + NumMessages: 47, NumExtensions: 0, NumServices: 1, }, diff --git a/protobuff/qubic.pb.gw.go b/protobuff/qubic.pb.gw.go index 04adc43..0b08a7a 100644 --- a/protobuff/qubic.pb.gw.go +++ b/protobuff/qubic.pb.gw.go @@ -88,7 +88,11 @@ func request_QubicLiveService_QuerySmartContract_0(ctx context.Context, marshale var protoReq QuerySmartContractRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -101,7 +105,11 @@ func local_request_QubicLiveService_QuerySmartContract_0(ctx context.Context, ma var protoReq QuerySmartContractRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -114,7 +122,11 @@ func request_QubicLiveService_BroadcastTransaction_0(ctx context.Context, marsha var protoReq BroadcastTransactionRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -127,7 +139,11 @@ func local_request_QubicLiveService_BroadcastTransaction_0(ctx context.Context, var protoReq BroadcastTransactionRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -328,6 +344,40 @@ func local_request_QubicLiveService_GetPossessedAssets_0(ctx context.Context, ma } +func request_QubicLiveService_GetAssetsForIdentities_0(ctx context.Context, marshaler runtime.Marshaler, client QubicLiveServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetAssetsForIdentitiesRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.GetAssetsForIdentities(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_QubicLiveService_GetAssetsForIdentities_0(ctx context.Context, marshaler runtime.Marshaler, server QubicLiveServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetAssetsForIdentitiesRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.GetAssetsForIdentities(ctx, &protoReq) + return msg, metadata, err + +} + var ( filter_QubicLiveService_GetIssuedAssetsByFilter_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} ) @@ -886,6 +936,31 @@ func RegisterQubicLiveServiceHandlerServer(ctx context.Context, mux *runtime.Ser }) + mux.Handle("POST", pattern_QubicLiveService_GetAssetsForIdentities_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/qubic.http.qubic.pb.QubicLiveService/GetAssetsForIdentities", runtime.WithHTTPPathPattern("/assets")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_QubicLiveService_GetAssetsForIdentities_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_QubicLiveService_GetAssetsForIdentities_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_QubicLiveService_GetIssuedAssetsByFilter_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -1328,6 +1403,28 @@ func RegisterQubicLiveServiceHandlerClient(ctx context.Context, mux *runtime.Ser }) + mux.Handle("POST", pattern_QubicLiveService_GetAssetsForIdentities_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/qubic.http.qubic.pb.QubicLiveService/GetAssetsForIdentities", runtime.WithHTTPPathPattern("/assets")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_QubicLiveService_GetAssetsForIdentities_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_QubicLiveService_GetAssetsForIdentities_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_QubicLiveService_GetIssuedAssetsByFilter_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -1546,6 +1643,8 @@ var ( pattern_QubicLiveService_GetPossessedAssets_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1, 2, 2}, []string{"assets", "identity", "possessed"}, "")) + pattern_QubicLiveService_GetAssetsForIdentities_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"assets"}, "")) + pattern_QubicLiveService_GetIssuedAssetsByFilter_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"assets", "issuances"}, "")) pattern_QubicLiveService_GetIssuedAssetByUniverseIndex_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"assets", "issuances", "index"}, "")) @@ -1582,6 +1681,8 @@ var ( forward_QubicLiveService_GetPossessedAssets_0 = runtime.ForwardResponseMessage + forward_QubicLiveService_GetAssetsForIdentities_0 = runtime.ForwardResponseMessage + forward_QubicLiveService_GetIssuedAssetsByFilter_0 = runtime.ForwardResponseMessage forward_QubicLiveService_GetIssuedAssetByUniverseIndex_0 = runtime.ForwardResponseMessage diff --git a/protobuff/qubic.proto b/protobuff/qubic.proto index cb057df..979cda7 100644 --- a/protobuff/qubic.proto +++ b/protobuff/qubic.proto @@ -190,6 +190,23 @@ message PossessedAssetsResponse { repeated PossessedAsset possessed_assets = 1; } +// GetAssetsForIdentitiesRequest +message GetAssetsForIdentitiesRequest { + repeated string identities = 1; +} + +// IdentityAssets holds the assets owned and possessed by a single identity. +message IdentityAssets { + string identity = 1; + repeated OwnedAsset owned_assets = 2; + repeated PossessedAsset possessed_assets = 3; +} + +// GetAssetsForIdentitiesResponse +message GetAssetsForIdentitiesResponse { + repeated IdentityAssets assets = 1; +} + // QuerySmartContractRequest message QuerySmartContractRequest { // identifies the smart contract @@ -440,6 +457,18 @@ service QubicLiveService { }; }; + rpc GetAssetsForIdentities(GetAssetsForIdentitiesRequest) returns (GetAssetsForIdentitiesResponse) { + option (openapi.v3.operation) = { + tags: ["Assets"] + summary: "List Owned And Possessed Assets For Multiple Identities" + description: "Gets the assets owned and possessed by each of the given identities. The requests are pipelined over a single node connection, so one call costs roughly one round trip regardless of how many identities are requested. Results are returned in the order the identities were given." + }; + option (google.api.http) = { + post: "/assets" + body: "*" + }; + }; + rpc GetIssuedAssetsByFilter(GetIssuedAssetsByFilterRequest) returns (AssetIssuances) { option (openapi.v3.operation) = { tags: ["Assets"] diff --git a/protobuff/qubic_grpc.pb.go b/protobuff/qubic_grpc.pb.go index 6c183e6..be0fa75 100644 --- a/protobuff/qubic_grpc.pb.go +++ b/protobuff/qubic_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.6.1 -// - protoc v3.21.12 +// - protoc-gen-go-grpc v1.6.0 +// - protoc v4.23.4 // source: qubic.proto package protobuff @@ -28,6 +28,7 @@ const ( QubicLiveService_GetIssuedAssets_FullMethodName = "/qubic.http.qubic.pb.QubicLiveService/GetIssuedAssets" QubicLiveService_GetOwnedAssets_FullMethodName = "/qubic.http.qubic.pb.QubicLiveService/GetOwnedAssets" QubicLiveService_GetPossessedAssets_FullMethodName = "/qubic.http.qubic.pb.QubicLiveService/GetPossessedAssets" + QubicLiveService_GetAssetsForIdentities_FullMethodName = "/qubic.http.qubic.pb.QubicLiveService/GetAssetsForIdentities" QubicLiveService_GetIssuedAssetsByFilter_FullMethodName = "/qubic.http.qubic.pb.QubicLiveService/GetIssuedAssetsByFilter" QubicLiveService_GetIssuedAssetByUniverseIndex_FullMethodName = "/qubic.http.qubic.pb.QubicLiveService/GetIssuedAssetByUniverseIndex" QubicLiveService_GetOwnedAssetsByFilter_FullMethodName = "/qubic.http.qubic.pb.QubicLiveService/GetOwnedAssetsByFilter" @@ -51,6 +52,7 @@ type QubicLiveServiceClient interface { GetIssuedAssets(ctx context.Context, in *IssuedAssetsRequest, opts ...grpc.CallOption) (*IssuedAssetsResponse, error) GetOwnedAssets(ctx context.Context, in *OwnedAssetsRequest, opts ...grpc.CallOption) (*OwnedAssetsResponse, error) GetPossessedAssets(ctx context.Context, in *PossessedAssetsRequest, opts ...grpc.CallOption) (*PossessedAssetsResponse, error) + GetAssetsForIdentities(ctx context.Context, in *GetAssetsForIdentitiesRequest, opts ...grpc.CallOption) (*GetAssetsForIdentitiesResponse, error) GetIssuedAssetsByFilter(ctx context.Context, in *GetIssuedAssetsByFilterRequest, opts ...grpc.CallOption) (*AssetIssuances, error) GetIssuedAssetByUniverseIndex(ctx context.Context, in *GetByUniverseIndexRequest, opts ...grpc.CallOption) (*AssetIssuance, error) GetOwnedAssetsByFilter(ctx context.Context, in *GetOwnedAssetsByFilterRequest, opts ...grpc.CallOption) (*AssetOwnerships, error) @@ -150,6 +152,16 @@ func (c *qubicLiveServiceClient) GetPossessedAssets(ctx context.Context, in *Pos return out, nil } +func (c *qubicLiveServiceClient) GetAssetsForIdentities(ctx context.Context, in *GetAssetsForIdentitiesRequest, opts ...grpc.CallOption) (*GetAssetsForIdentitiesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetAssetsForIdentitiesResponse) + err := c.cc.Invoke(ctx, QubicLiveService_GetAssetsForIdentities_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *qubicLiveServiceClient) GetIssuedAssetsByFilter(ctx context.Context, in *GetIssuedAssetsByFilterRequest, opts ...grpc.CallOption) (*AssetIssuances, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(AssetIssuances) @@ -252,6 +264,7 @@ type QubicLiveServiceServer interface { GetIssuedAssets(context.Context, *IssuedAssetsRequest) (*IssuedAssetsResponse, error) GetOwnedAssets(context.Context, *OwnedAssetsRequest) (*OwnedAssetsResponse, error) GetPossessedAssets(context.Context, *PossessedAssetsRequest) (*PossessedAssetsResponse, error) + GetAssetsForIdentities(context.Context, *GetAssetsForIdentitiesRequest) (*GetAssetsForIdentitiesResponse, error) GetIssuedAssetsByFilter(context.Context, *GetIssuedAssetsByFilterRequest) (*AssetIssuances, error) GetIssuedAssetByUniverseIndex(context.Context, *GetByUniverseIndexRequest) (*AssetIssuance, error) GetOwnedAssetsByFilter(context.Context, *GetOwnedAssetsByFilterRequest) (*AssetOwnerships, error) @@ -295,6 +308,9 @@ func (UnimplementedQubicLiveServiceServer) GetOwnedAssets(context.Context, *Owne func (UnimplementedQubicLiveServiceServer) GetPossessedAssets(context.Context, *PossessedAssetsRequest) (*PossessedAssetsResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetPossessedAssets not implemented") } +func (UnimplementedQubicLiveServiceServer) GetAssetsForIdentities(context.Context, *GetAssetsForIdentitiesRequest) (*GetAssetsForIdentitiesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetAssetsForIdentities not implemented") +} func (UnimplementedQubicLiveServiceServer) GetIssuedAssetsByFilter(context.Context, *GetIssuedAssetsByFilterRequest) (*AssetIssuances, error) { return nil, status.Error(codes.Unimplemented, "method GetIssuedAssetsByFilter not implemented") } @@ -487,6 +503,24 @@ func _QubicLiveService_GetPossessedAssets_Handler(srv interface{}, ctx context.C return interceptor(ctx, in, info, handler) } +func _QubicLiveService_GetAssetsForIdentities_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetAssetsForIdentitiesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QubicLiveServiceServer).GetAssetsForIdentities(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: QubicLiveService_GetAssetsForIdentities_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QubicLiveServiceServer).GetAssetsForIdentities(ctx, req.(*GetAssetsForIdentitiesRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _QubicLiveService_GetIssuedAssetsByFilter_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetIssuedAssetsByFilterRequest) if err := dec(in); err != nil { @@ -688,6 +722,10 @@ var QubicLiveService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetPossessedAssets", Handler: _QubicLiveService_GetPossessedAssets_Handler, }, + { + MethodName: "GetAssetsForIdentities", + Handler: _QubicLiveService_GetAssetsForIdentities_Handler, + }, { MethodName: "GetIssuedAssetsByFilter", Handler: _QubicLiveService_GetIssuedAssetsByFilter_Handler, From ce449562df21b20be4c0297192c793df0a808be1 Mon Sep 17 00:00:00 2001 From: 0xluk Date: Tue, 11 Aug 2026 11:58:00 +0300 Subject: [PATCH 3/3] feat: fetch assets for multiple identities in one pipelined batch Implements GetAssetsForIdentities on top of PrefetchOwnedAndPossessedAssets, so fetching assets for N identities costs roughly one round trip instead of 2N. Callers previously had to fan out N*2 requests, which saturated the connection pool. Identities are validated before a connection is taken, so malformed input never costs a pooled connection. Empty lists, batches over the configured limit, malformed identities and duplicates are all rejected with InvalidArgument. The limit defaults to 15 and is configurable via QUBIC_API_SIDECAR_SERVER_MAX_BATCH_IDENTITIES. A failed batch leaves the connection byte unaligned, so the handler closes it rather than returning it to the pool, and a test pins that. To make the handler testable, the pool is reached through a nodePool/nodeClient seam instead of the concrete *qubic.Pool, whose Get returns a client holding a real socket. The seam covers every client method the handlers already use, so no existing handler body changed, and a compile time assertion keeps the interface honest as the client evolves. BEHAVIOR CHANGE: the nested owned_asset of /assets/{identity}/possessed was built from the possession record's own fields instead of the ownership record it wraps, so it reported the possession type (3) rather than the ownership type (2), and the possession's index and unit count. Extracting the duplicated conversion into shared converters fixes this by construction, since the converter reads only the ownership record it is given. README samples updated to match. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 92 ++++++- app/grpc_server/main.go | 4 +- foundation/rpc_server/asset_converter.go | 163 +++++++++++++ .../identity_assets_converter_test.go | 217 +++++++++++++++++ .../identity_assets_handler_test.go | 173 ++++++++++++++ foundation/rpc_server/node.go | 78 ++++++ foundation/rpc_server/node_test.go | 59 +++++ foundation/rpc_server/rpc_server.go | 224 +++++++----------- 8 files changed, 860 insertions(+), 150 deletions(-) create mode 100644 foundation/rpc_server/identity_assets_converter_test.go create mode 100644 foundation/rpc_server/identity_assets_handler_test.go create mode 100644 foundation/rpc_server/node.go create mode 100644 foundation/rpc_server/node_test.go diff --git a/README.md b/README.md index 4e1431d..fbc1d09 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ QUBIC_API_SIDECAR_POOL_NODE_FETCHER_URL: "http://127.0.0.1:8080/status" QUBIC_API_SIDECAR_SERVER_READ_TIMEOUT: (default: 5s) QUBIC_API_SIDECAR_SERVER_WRITE_TIMEOUT: (default: 5s) QUBIC_API_SIDECAR_SERVER_SHUTDOWN_TIMEOUT: (default: 5s) +QUBIC_API_SIDECAR_SERVER_MAX_BATCH_IDENTITIES: (default: 15) QUBIC_API_SIDECAR_POOL_NODE_FETCHER_TIMEOUT: (default: 2s) QUBIC_API_SIDECAR_POOL_INITIAL_CAP: (default: 5) @@ -316,7 +317,7 @@ curl localhost:8000/assets/IGJQYTMFLVNIMEAKLANHKGNGZPFCFJGSMVOWMNGLWCZWKFHANHGCB "numberOfUnits": "2", "ownedAsset": { "ownerIdentity": "IGJQYTMFLVNIMEAKLANHKGNGZPFCFJGSMVOWMNGLWCZWKFHANHGCBYODMKBC", - "type": 3, + "type": 2, "padding": 0, "managingContractIndex": 1, "issuanceIndex": 9707976, @@ -345,7 +346,7 @@ curl localhost:8000/assets/IGJQYTMFLVNIMEAKLANHKGNGZPFCFJGSMVOWMNGLWCZWKFHANHGCB "numberOfUnits": "1", "ownedAsset": { "ownerIdentity": "IGJQYTMFLVNIMEAKLANHKGNGZPFCFJGSMVOWMNGLWCZWKFHANHGCBYODMKBC", - "type": 3, + "type": 2, "padding": 0, "managingContractIndex": 1, "issuanceIndex": 9707978, @@ -374,7 +375,7 @@ curl localhost:8000/assets/IGJQYTMFLVNIMEAKLANHKGNGZPFCFJGSMVOWMNGLWCZWKFHANHGCB "numberOfUnits": "186685601", "ownedAsset": { "ownerIdentity": "IGJQYTMFLVNIMEAKLANHKGNGZPFCFJGSMVOWMNGLWCZWKFHANHGCBYODMKBC", - "type": 3, + "type": 2, "padding": 0, "managingContractIndex": 1, "issuanceIndex": 9707980, @@ -403,7 +404,7 @@ curl localhost:8000/assets/IGJQYTMFLVNIMEAKLANHKGNGZPFCFJGSMVOWMNGLWCZWKFHANHGCB "numberOfUnits": "10", "ownedAsset": { "ownerIdentity": "IGJQYTMFLVNIMEAKLANHKGNGZPFCFJGSMVOWMNGLWCZWKFHANHGCBYODMKBC", - "type": 3, + "type": 2, "padding": 0, "managingContractIndex": 1, "issuanceIndex": 9707982, @@ -424,4 +425,85 @@ curl localhost:8000/assets/IGJQYTMFLVNIMEAKLANHKGNGZPFCFJGSMVOWMNGLWCZWKFHANHGCB } ] } -``` \ No newline at end of file +``` +#### /assets (batch) + +Fetches the assets owned and possessed by several identities at once. The requests are +pipelined over a single node connection, so one call costs roughly one round trip for the +whole set instead of one per identity. Prefer this over calling +`/assets/{identity}/owned` and `/assets/{identity}/possessed` in a loop. + +Results are returned in the order the identities were given, and every requested identity +appears in the response, with empty lists if it holds no assets. The list must contain +between 1 and `QUBIC_API_SIDECAR_SERVER_MAX_BATCH_IDENTITIES` (default 15) identities, +with no duplicates. All identities are validated before any node call, so a single +malformed entry fails the whole request without fetching anything. + +```shell +curl -X POST localhost:8000/assets \ + -H 'Content-Type: application/json' \ + -d '{"identities":["IGJQYTMFLVNIMEAKLANHKGNGZPFCFJGSMVOWMNGLWCZWKFHANHGCBYODMKBC"]}' +``` +```json +{ + "assets": [ + { + "identity": "IGJQYTMFLVNIMEAKLANHKGNGZPFCFJGSMVOWMNGLWCZWKFHANHGCBYODMKBC", + "ownedAssets": [ + { + "data": { + "ownerIdentity": "IGJQYTMFLVNIMEAKLANHKGNGZPFCFJGSMVOWMNGLWCZWKFHANHGCBYODMKBC", + "type": 2, + "padding": 0, + "managingContractIndex": 1, + "issuanceIndex": 9707976, + "numberOfUnits": "2", + "issuedAsset": { + "issuerIdentity": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFXIB", + "type": 1, + "name": "RANDOM", + "numberOfDecimalPlaces": 0, + "unitOfMeasurement": [0, 0, 0, 0, 0, 0, 0] + } + }, + "info": { + "tick": 14057759, + "universeIndex": 0 + } + } + ], + "possessedAssets": [ + { + "data": { + "possessorIdentity": "IGJQYTMFLVNIMEAKLANHKGNGZPFCFJGSMVOWMNGLWCZWKFHANHGCBYODMKBC", + "type": 3, + "padding": 0, + "managingContractIndex": 1, + "issuanceIndex": 9707976, + "numberOfUnits": "2", + "ownedAsset": { + "ownerIdentity": "IGJQYTMFLVNIMEAKLANHKGNGZPFCFJGSMVOWMNGLWCZWKFHANHGCBYODMKBC", + "type": 2, + "padding": 0, + "managingContractIndex": 1, + "issuanceIndex": 9707976, + "numberOfUnits": "2", + "issuedAsset": { + "issuerIdentity": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFXIB", + "type": 1, + "name": "RANDOM", + "numberOfDecimalPlaces": 0, + "unitOfMeasurement": [0, 0, 0, 0, 0, 0, 0] + } + } + }, + "info": { + "tick": 14057759, + "universeIndex": 0 + } + } + ] + } + ] +} +``` diff --git a/app/grpc_server/main.go b/app/grpc_server/main.go index 83c79c9..4dee1e1 100644 --- a/app/grpc_server/main.go +++ b/app/grpc_server/main.go @@ -33,6 +33,8 @@ func run(logger *log.Logger) error { HttpHost string `conf:"default:0.0.0.0:8000"` GrpcHost string `conf:"default:0.0.0.0:8001"` MaxTickFetchUrl string `conf:"default:http://localhost:8080/max-tick"` + // MaxBatchIdentities caps how many identities one batch asset request may ask for. + MaxBatchIdentities int `conf:"default:15"` } Pool struct { NodeFetcherUrl string `conf:"default:http://localhost:8080/status"` @@ -84,7 +86,7 @@ func run(logger *log.Logger) error { return errors.Wrap(err, "creating qubic pool") } - rpcServer := rpc.NewServer(cfg.Server.GrpcHost, cfg.Server.HttpHost, logger, pool, cfg.Server.MaxTickFetchUrl) + rpcServer := rpc.NewServer(cfg.Server.GrpcHost, cfg.Server.HttpHost, logger, pool, cfg.Server.MaxTickFetchUrl, cfg.Server.MaxBatchIdentities) err = rpcServer.Start() if err != nil { return errors.Wrap(err, "starting rpc server") diff --git a/foundation/rpc_server/asset_converter.go b/foundation/rpc_server/asset_converter.go index 106f7c5..a4f112d 100644 --- a/foundation/rpc_server/asset_converter.go +++ b/foundation/rpc_server/asset_converter.go @@ -4,10 +4,173 @@ import ( "fmt" "github.com/pkg/errors" + qubic "github.com/qubic/go-node-connector/v2" "github.com/qubic/go-node-connector/v2/types" "github.com/qubic/qubic-http/protobuff" ) +func convertAssetInfo(source types.AssetInfo) *protobuff.AssetInfo { + return &protobuff.AssetInfo{ + Tick: source.Tick, + UniverseIndex: source.UniverseIndex, + } +} + +func convertIssuedAssetData(source types.IssuedAssetData) (*protobuff.IssuedAssetData, error) { + var issuer types.Identity + issuer, err := issuer.FromPubKey(source.PublicKey, false) + if err != nil { + return nil, errors.Wrap(err, "failed to get identity for issued asset public key") + } + + return &protobuff.IssuedAssetData{ + IssuerIdentity: issuer.String(), + Type: uint32(source.Type), + Name: int8ArrayToString(source.Name[:]), + NumberOfDecimalPlaces: int32(source.NumberOfDecimalPlaces), + UnitOfMeasurement: int8ArrayToInt32Array(source.UnitOfMeasurement[:]), + }, nil +} + +func convertOwnedAssetData(source types.OwnedAssetData) (*protobuff.OwnedAssetData, error) { + var owner types.Identity + owner, err := owner.FromPubKey(source.PublicKey, false) + if err != nil { + return nil, errors.Wrap(err, "failed to get identity for owned asset public key") + } + + issuedAsset, err := convertIssuedAssetData(source.IssuedAsset) + if err != nil { + return nil, errors.Wrap(err, "converting issued asset") + } + + return &protobuff.OwnedAssetData{ + OwnerIdentity: owner.String(), + Type: uint32(source.Type), + Padding: int32(source.Padding[0]), + ManagingContractIndex: uint32(source.ManagingContractIndex), + IssuanceIndex: source.IssuanceIndex, + NumberOfUnits: source.NumberOfUnits, + IssuedAsset: issuedAsset, + }, nil +} + +func convertPossessedAssetData(source types.PossessedAssetData) (*protobuff.PossessedAssetData, error) { + var possessor types.Identity + possessor, err := possessor.FromPubKey(source.PublicKey, false) + if err != nil { + return nil, errors.Wrap(err, "failed to get identity for possessed asset public key") + } + + // the nested ownership record is converted from its own data, not from the + // possession record that contains it + ownedAsset, err := convertOwnedAssetData(source.OwnedAsset) + if err != nil { + return nil, errors.Wrap(err, "converting owned asset") + } + + return &protobuff.PossessedAssetData{ + PossessorIdentity: possessor.String(), + Type: uint32(source.Type), + Padding: int32(source.Padding[0]), + ManagingContractIndex: uint32(source.ManagingContractIndex), + IssuanceIndex: source.IssuanceIndex, + NumberOfUnits: source.NumberOfUnits, + OwnedAsset: ownedAsset, + }, nil +} + +func convertIssuedAsset(source types.IssuedAsset) (*protobuff.IssuedAsset, error) { + data, err := convertIssuedAssetData(source.Data) + if err != nil { + return nil, err + } + + return &protobuff.IssuedAsset{Data: data, Info: convertAssetInfo(source.Info)}, nil +} + +func convertOwnedAsset(source types.OwnedAsset) (*protobuff.OwnedAsset, error) { + data, err := convertOwnedAssetData(source.Data) + if err != nil { + return nil, err + } + + return &protobuff.OwnedAsset{Data: data, Info: convertAssetInfo(source.Info)}, nil +} + +func convertPossessedAsset(source types.PossessedAsset) (*protobuff.PossessedAsset, error) { + data, err := convertPossessedAssetData(source.Data) + if err != nil { + return nil, err + } + + return &protobuff.PossessedAsset{Data: data, Info: convertAssetInfo(source.Info)}, nil +} + +func convertIssuedAssets(source types.IssuedAssets) ([]*protobuff.IssuedAsset, error) { + converted := make([]*protobuff.IssuedAsset, 0, len(source)) + for _, asset := range source { + issuedAsset, err := convertIssuedAsset(asset) + if err != nil { + return nil, err + } + converted = append(converted, issuedAsset) + } + + return converted, nil +} + +func convertOwnedAssets(source types.OwnedAssets) ([]*protobuff.OwnedAsset, error) { + converted := make([]*protobuff.OwnedAsset, 0, len(source)) + for _, asset := range source { + ownedAsset, err := convertOwnedAsset(asset) + if err != nil { + return nil, err + } + converted = append(converted, ownedAsset) + } + + return converted, nil +} + +func convertPossessedAssets(source types.PossessedAssets) ([]*protobuff.PossessedAsset, error) { + converted := make([]*protobuff.PossessedAsset, 0, len(source)) + for _, asset := range source { + possessedAsset, err := convertPossessedAsset(asset) + if err != nil { + return nil, err + } + converted = append(converted, possessedAsset) + } + + return converted, nil +} + +// convertAddressAssets maps the batched node response into the proto response, keeping +// the order the identities were requested in. +func convertAddressAssets(source []qubic.AddressAssets) ([]*protobuff.IdentityAssets, error) { + converted := make([]*protobuff.IdentityAssets, 0, len(source)) + for _, addressAssets := range source { + owned, err := convertOwnedAssets(addressAssets.Owned) + if err != nil { + return nil, errors.Wrapf(err, "converting owned assets for identity %s", addressAssets.Identity) + } + + possessed, err := convertPossessedAssets(addressAssets.Possessed) + if err != nil { + return nil, errors.Wrapf(err, "converting possessed assets for identity %s", addressAssets.Identity) + } + + converted = append(converted, &protobuff.IdentityAssets{ + Identity: addressAssets.Identity, + OwnedAssets: owned, + PossessedAssets: possessed, + }) + } + + return converted, nil +} + func convertAssetIssuance(source types.AssetIssuance) (*protobuff.AssetIssuance, error) { var issuerIdentity types.Identity issuerIdentity, err := issuerIdentity.FromPubKey(source.Asset.PublicKey, false) diff --git a/foundation/rpc_server/identity_assets_converter_test.go b/foundation/rpc_server/identity_assets_converter_test.go new file mode 100644 index 0000000..1abc30d --- /dev/null +++ b/foundation/rpc_server/identity_assets_converter_test.go @@ -0,0 +1,217 @@ +package rpc + +import ( + "testing" + + qubic "github.com/qubic/go-node-connector/v2" + "github.com/qubic/go-node-connector/v2/types" + "github.com/qubic/qubic-http/protobuff" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + issuerIdentity = "TESTIOGXQKYYZEQXOXFSWWAJNYLCDBWFAPNBLNBUZFHDVFMYPJZXGMEEJEGI" + ownerIdentity = "AFZPUAIYVPNUYGJRQVLUKOPPVLHAZQTGLYAAUUNBXFTVTAMSBKQBLEIEPCVJ" + possessorIdentity = "TLIDHAWGQTCELDIPZFHAKTNFFWMEAZKZALRUFSJUDALPRMVBHXCQNVYEEBDH" +) + +func pubKeyOf(t *testing.T, identity string) [32]byte { + t.Helper() + + id := types.Identity(identity) + pubKey, err := id.ToPubKey(false) + require.NoError(t, err) + + return pubKey +} + +func testIssuedAssetData(t *testing.T) types.IssuedAssetData { + t.Helper() + + return types.IssuedAssetData{ + PublicKey: pubKeyOf(t, issuerIdentity), + Type: 1, + Name: [7]int8{82, 65, 78, 68, 79, 77, 0}, + NumberOfDecimalPlaces: 2, + UnitOfMeasurement: [7]int8{1, 2, 3, 4, 5, 6, 7}, + } +} + +func expectedIssuedAssetData() *protobuff.IssuedAssetData { + return &protobuff.IssuedAssetData{ + IssuerIdentity: issuerIdentity, + Type: 1, + Name: "RANDOM", + NumberOfDecimalPlaces: 2, + UnitOfMeasurement: []int32{1, 2, 3, 4, 5, 6, 7}, + } +} + +// testOwnedAssetData uses values that differ from the possession record wrapping it in +// TestAssetConverter_convertPossessedAsset, so that mixing the two up is detectable. +func testOwnedAssetData(t *testing.T) types.OwnedAssetData { + t.Helper() + + return types.OwnedAssetData{ + PublicKey: pubKeyOf(t, ownerIdentity), + Type: 2, + Padding: [1]int8{7}, + ManagingContractIndex: 1, + IssuanceIndex: 99, + NumberOfUnits: 42, + IssuedAsset: testIssuedAssetData(t), + } +} + +func expectedOwnedAssetData() *protobuff.OwnedAssetData { + return &protobuff.OwnedAssetData{ + OwnerIdentity: ownerIdentity, + Type: 2, + Padding: 7, + ManagingContractIndex: 1, + IssuanceIndex: 99, + NumberOfUnits: 42, + IssuedAsset: expectedIssuedAssetData(), + } +} + +func TestAssetConverter_convertIssuedAsset(t *testing.T) { + source := types.IssuedAsset{ + Data: testIssuedAssetData(t), + Info: types.AssetInfo{Tick: 42, UniverseIndex: 43}, + } + + converted, err := convertIssuedAsset(source) + assert.NoError(t, err) + + assert.Equal(t, &protobuff.IssuedAsset{ + Data: expectedIssuedAssetData(), + Info: &protobuff.AssetInfo{Tick: 42, UniverseIndex: 43}, + }, converted) +} + +func TestAssetConverter_convertOwnedAsset(t *testing.T) { + source := types.OwnedAsset{ + Data: testOwnedAssetData(t), + Info: types.AssetInfo{Tick: 42, UniverseIndex: 43}, + } + + converted, err := convertOwnedAsset(source) + assert.NoError(t, err) + + assert.Equal(t, &protobuff.OwnedAsset{ + Data: expectedOwnedAssetData(), + Info: &protobuff.AssetInfo{Tick: 42, UniverseIndex: 43}, + }, converted) +} + +// TestAssetConverter_convertPossessedAsset pins that the nested ownership record is +// converted from its own data. Every field of the possession record deliberately differs +// from the ownership record it wraps, so copying one into the other would fail here. +func TestAssetConverter_convertPossessedAsset(t *testing.T) { + source := types.PossessedAsset{ + Data: types.PossessedAssetData{ + PublicKey: pubKeyOf(t, possessorIdentity), + Type: 3, + Padding: [1]int8{5}, + ManagingContractIndex: 8, + IssuanceIndex: 11, + NumberOfUnits: 7, + OwnedAsset: testOwnedAssetData(t), + }, + Info: types.AssetInfo{Tick: 42, UniverseIndex: 43}, + } + + converted, err := convertPossessedAsset(source) + assert.NoError(t, err) + + assert.Equal(t, &protobuff.PossessedAsset{ + Data: &protobuff.PossessedAssetData{ + PossessorIdentity: possessorIdentity, + Type: 3, + Padding: 5, + ManagingContractIndex: 8, + IssuanceIndex: 11, + NumberOfUnits: 7, + OwnedAsset: expectedOwnedAssetData(), + }, + Info: &protobuff.AssetInfo{Tick: 42, UniverseIndex: 43}, + }, converted) +} + +func TestAssetConverter_convertOwnedAssets_empty(t *testing.T) { + converted, err := convertOwnedAssets(types.OwnedAssets{}) + assert.NoError(t, err) + + assert.NotNil(t, converted) + assert.Empty(t, converted) +} + +func TestAssetConverter_convertPossessedAssets_empty(t *testing.T) { + converted, err := convertPossessedAssets(types.PossessedAssets{}) + assert.NoError(t, err) + + assert.NotNil(t, converted) + assert.Empty(t, converted) +} + +func TestAssetConverter_convertOwnedAssets_preservesOrder(t *testing.T) { + first := testOwnedAssetData(t) + first.NumberOfUnits = 1 + second := testOwnedAssetData(t) + second.NumberOfUnits = 2 + + converted, err := convertOwnedAssets(types.OwnedAssets{ + {Data: first, Info: types.AssetInfo{Tick: 1}}, + {Data: second, Info: types.AssetInfo{Tick: 2}}, + }) + assert.NoError(t, err) + + require.Len(t, converted, 2) + assert.Equal(t, int64(1), converted[0].Data.NumberOfUnits) + assert.Equal(t, int64(2), converted[1].Data.NumberOfUnits) +} + +func TestAssetConverter_convertAddressAssets(t *testing.T) { + source := []qubic.AddressAssets{ + { + Identity: ownerIdentity, + Owned: types.OwnedAssets{ + {Data: testOwnedAssetData(t), Info: types.AssetInfo{Tick: 42, UniverseIndex: 43}}, + }, + Possessed: types.PossessedAssets{}, + }, + { + Identity: possessorIdentity, + Owned: types.OwnedAssets{}, + Possessed: types.PossessedAssets{}, + }, + } + + converted, err := convertAddressAssets(source) + assert.NoError(t, err) + + require.Len(t, converted, 2) + + assert.Equal(t, ownerIdentity, converted[0].Identity) + require.Len(t, converted[0].OwnedAssets, 1) + assert.Equal(t, expectedOwnedAssetData(), converted[0].OwnedAssets[0].Data) + assert.Empty(t, converted[0].PossessedAssets) + + // an identity holding nothing still gets an entry, with empty lists rather than + // being dropped from the response + assert.Equal(t, possessorIdentity, converted[1].Identity) + assert.NotNil(t, converted[1].OwnedAssets) + assert.Empty(t, converted[1].OwnedAssets) + assert.NotNil(t, converted[1].PossessedAssets) + assert.Empty(t, converted[1].PossessedAssets) +} + +func TestAssetConverter_convertAddressAssets_empty(t *testing.T) { + converted, err := convertAddressAssets(nil) + assert.NoError(t, err) + + assert.NotNil(t, converted) + assert.Empty(t, converted) +} diff --git a/foundation/rpc_server/identity_assets_handler_test.go b/foundation/rpc_server/identity_assets_handler_test.go new file mode 100644 index 0000000..5ea4338 --- /dev/null +++ b/foundation/rpc_server/identity_assets_handler_test.go @@ -0,0 +1,173 @@ +package rpc + +import ( + "context" + "testing" + + "github.com/pkg/errors" + qubic "github.com/qubic/go-node-connector/v2" + "github.com/qubic/go-node-connector/v2/types" + "github.com/qubic/qubic-http/protobuff" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const defaultTestBatchLimit = 15 + +func assetsResponse(t *testing.T, identities ...string) []qubic.AddressAssets { + t.Helper() + + response := make([]qubic.AddressAssets, 0, len(identities)) + for _, identity := range identities { + response = append(response, qubic.AddressAssets{ + Identity: identity, + Owned: types.OwnedAssets{ + {Data: testOwnedAssetData(t), Info: types.AssetInfo{Tick: 42, UniverseIndex: 43}}, + }, + Possessed: types.PossessedAssets{}, + }) + } + + return response +} + +// requireInvalidArgument asserts the request was rejected before any node connection was +// taken, so bad input never costs a pooled connection. +func requireInvalidArgument(t *testing.T, pool *fakeNodePool, err error) { + t.Helper() + + st, ok := status.FromError(err) + require.True(t, ok, "expected a gRPC status error, got %v", err) + assert.Equal(t, codes.InvalidArgument, st.Code()) + assert.Zero(t, pool.gets, "no pool connection should be taken for an invalid request") +} + +func TestServer_GetAssetsForIdentities_success(t *testing.T) { + identities := []string{ownerIdentity, possessorIdentity} + + client := &fakeNodeClient{ + prefetch: func(_ context.Context, ids []string) ([]qubic.AddressAssets, error) { + return assetsResponse(t, ids...), nil + }, + } + pool := &fakeNodePool{client: client} + + response, err := newTestServer(pool, defaultTestBatchLimit). + GetAssetsForIdentities(context.Background(), &protobuff.GetAssetsForIdentitiesRequest{Identities: identities}) + require.NoError(t, err) + + assert.Equal(t, identities, client.prefetchedIdentities, "identities should be forwarded verbatim") + + require.Len(t, response.Assets, 2) + assert.Equal(t, ownerIdentity, response.Assets[0].Identity) + assert.Equal(t, possessorIdentity, response.Assets[1].Identity) + require.Len(t, response.Assets[0].OwnedAssets, 1) + assert.Equal(t, expectedOwnedAssetData(), response.Assets[0].OwnedAssets[0].Data) + + // one batch means exactly one connection, returned to the pool for reuse + assert.Equal(t, 1, pool.gets) + assert.Equal(t, 1, pool.puts) + assert.Zero(t, pool.closes) +} + +func TestServer_GetAssetsForIdentities_identityWithoutAssets(t *testing.T) { + client := &fakeNodeClient{ + prefetch: func(_ context.Context, ids []string) ([]qubic.AddressAssets, error) { + return []qubic.AddressAssets{{ + Identity: ids[0], + Owned: types.OwnedAssets{}, + Possessed: types.PossessedAssets{}, + }}, nil + }, + } + pool := &fakeNodePool{client: client} + + response, err := newTestServer(pool, defaultTestBatchLimit). + GetAssetsForIdentities(context.Background(), &protobuff.GetAssetsForIdentitiesRequest{Identities: []string{ownerIdentity}}) + require.NoError(t, err) + + // the identity is still represented, with empty lists rather than being omitted + require.Len(t, response.Assets, 1) + assert.Equal(t, ownerIdentity, response.Assets[0].Identity) + assert.Empty(t, response.Assets[0].OwnedAssets) + assert.Empty(t, response.Assets[0].PossessedAssets) +} + +func TestServer_GetAssetsForIdentities_emptyIdentities(t *testing.T) { + pool := &fakeNodePool{client: &fakeNodeClient{}} + + _, err := newTestServer(pool, defaultTestBatchLimit). + GetAssetsForIdentities(context.Background(), &protobuff.GetAssetsForIdentitiesRequest{Identities: nil}) + + requireInvalidArgument(t, pool, err) +} + +func TestServer_GetAssetsForIdentities_tooManyIdentities(t *testing.T) { + pool := &fakeNodePool{client: &fakeNodeClient{}} + + _, err := newTestServer(pool, 1). + GetAssetsForIdentities(context.Background(), &protobuff.GetAssetsForIdentitiesRequest{ + Identities: []string{ownerIdentity, possessorIdentity}, + }) + + requireInvalidArgument(t, pool, err) + assert.Contains(t, status.Convert(err).Message(), "maximum is 1") +} + +func TestServer_GetAssetsForIdentities_invalidIdentity(t *testing.T) { + pool := &fakeNodePool{client: &fakeNodeClient{}} + + // a single malformed entry rejects the whole batch, so nothing is fetched + _, err := newTestServer(pool, defaultTestBatchLimit). + GetAssetsForIdentities(context.Background(), &protobuff.GetAssetsForIdentitiesRequest{ + Identities: []string{ownerIdentity, "not-an-identity"}, + }) + + requireInvalidArgument(t, pool, err) + assert.Contains(t, status.Convert(err).Message(), "not-an-identity") +} + +func TestServer_GetAssetsForIdentities_duplicateIdentities(t *testing.T) { + pool := &fakeNodePool{client: &fakeNodeClient{}} + + _, err := newTestServer(pool, defaultTestBatchLimit). + GetAssetsForIdentities(context.Background(), &protobuff.GetAssetsForIdentitiesRequest{ + Identities: []string{ownerIdentity, ownerIdentity}, + }) + + requireInvalidArgument(t, pool, err) + assert.Contains(t, status.Convert(err).Message(), "duplicate identity") +} + +func TestServer_GetAssetsForIdentities_poolGetError(t *testing.T) { + pool := &fakeNodePool{getErr: errors.New("pool exhausted")} + + _, err := newTestServer(pool, defaultTestBatchLimit). + GetAssetsForIdentities(context.Background(), &protobuff.GetAssetsForIdentitiesRequest{Identities: []string{ownerIdentity}}) + + assert.Equal(t, codes.Internal, status.Code(err)) + // nothing was handed out, so nothing may be returned or discarded + assert.Zero(t, pool.puts) + assert.Zero(t, pool.closes) +} + +// TestServer_GetAssetsForIdentities_prefetchError pins the pool safety rule: a failed +// pipelined batch leaves the connection byte-unaligned, so it must be discarded rather +// than put back where another request would pick it up. +func TestServer_GetAssetsForIdentities_prefetchError(t *testing.T) { + client := &fakeNodeClient{ + prefetch: func(context.Context, []string) ([]qubic.AddressAssets, error) { + return nil, errors.New("connection reset") + }, + } + pool := &fakeNodePool{client: client} + + _, err := newTestServer(pool, defaultTestBatchLimit). + GetAssetsForIdentities(context.Background(), &protobuff.GetAssetsForIdentitiesRequest{Identities: []string{ownerIdentity}}) + + assert.Equal(t, codes.Internal, status.Code(err)) + assert.Equal(t, 1, pool.closes, "a failed batch must close the connection") + assert.Zero(t, pool.puts, "a failed batch must never return the connection to the pool") +} diff --git a/foundation/rpc_server/node.go b/foundation/rpc_server/node.go new file mode 100644 index 0000000..1a8ca39 --- /dev/null +++ b/foundation/rpc_server/node.go @@ -0,0 +1,78 @@ +package rpc + +import ( + "context" + + "github.com/pkg/errors" + qubic "github.com/qubic/go-node-connector/v2" + "github.com/qubic/go-node-connector/v2/types" +) + +// nodeClient is the subset of *qubic.Client used by the handlers. It exists so that +// handlers can be unit tested against a fake instead of a live node connection. +type nodeClient interface { + GetIdentity(ctx context.Context, id string) (types.AddressInfo, error) + GetTickInfo(ctx context.Context) (types.TickInfo, error) + QuerySmartContract(ctx context.Context, rcf qubic.RequestContractFunction, requestData []byte) (types.SmartContractData, error) + SendRawTransaction(ctx context.Context, rawTx []byte) error + GetIssuedAssets(ctx context.Context, id string) (types.IssuedAssets, error) + GetOwnedAssets(ctx context.Context, id string) (types.OwnedAssets, error) + GetPossessedAssets(ctx context.Context, id string) (types.PossessedAssets, error) + PrefetchOwnedAndPossessedAssets(ctx context.Context, ids []string) ([]qubic.AddressAssets, error) + GetAssetIssuancesByUniverseIndex(ctx context.Context, index uint32) (types.AssetIssuances, error) + GetAssetOwnershipsByUniverseIndex(ctx context.Context, index uint32) (types.AssetOwnerships, error) + GetAssetPossessionsByUniverseIndex(ctx context.Context, index uint32) (types.AssetPossessions, error) + GetAssetIssuancesByFilter(ctx context.Context, issuerIdentity, assetName string) (types.AssetIssuances, error) + GetAssetOwnershipsByFilter(ctx context.Context, issuerIdentity, assetName, ownerIdentity string, ownerContract uint16) (types.AssetOwnerships, error) + GetAssetPossessionsByFilter(ctx context.Context, issuerIdentity, assetName, ownerIdentity, possessorIdentity string, ownerContract, possessorContract uint16) (types.AssetPossessions, error) + GetActiveIpos(ctx context.Context) (types.Ipos, error) + GetContractIpo(ctx context.Context, contractIndex uint32) (types.ContractIpo, error) +} + +// compile time check that the real client still satisfies the seam +var _ nodeClient = (*qubic.Client)(nil) + +// nodePool mirrors the Get/Put/Close contract of *qubic.Pool. Put returns a client for +// reuse, Close discards it. A client whose request failed must always be closed and +// never put back, since its connection may be left unusable. +type nodePool interface { + Get() (nodeClient, error) + Put(client nodeClient) error + Close(client nodeClient) error +} + +// qubicNodePool adapts *qubic.Pool, whose Get returns the concrete *qubic.Client, to +// the nodePool interface. +type qubicNodePool struct { + pool *qubic.Pool +} + +func newQubicNodePool(pool *qubic.Pool) *qubicNodePool { + return &qubicNodePool{pool: pool} +} + +func (p *qubicNodePool) Get() (nodeClient, error) { + client, err := p.pool.Get() + if err != nil { + // returning client directly would produce a non nil interface holding a nil + // *qubic.Client, which callers could not nil check + return nil, err + } + return client, nil +} + +func (p *qubicNodePool) Put(client nodeClient) error { + c, ok := client.(*qubic.Client) + if !ok { + return errors.Errorf("putting back unexpected client type %T", client) + } + return p.pool.Put(c) +} + +func (p *qubicNodePool) Close(client nodeClient) error { + c, ok := client.(*qubic.Client) + if !ok { + return errors.Errorf("closing unexpected client type %T", client) + } + return p.pool.Close(c) +} diff --git a/foundation/rpc_server/node_test.go b/foundation/rpc_server/node_test.go new file mode 100644 index 0000000..d1055a2 --- /dev/null +++ b/foundation/rpc_server/node_test.go @@ -0,0 +1,59 @@ +package rpc + +import ( + "context" + "io" + "log" + + qubic "github.com/qubic/go-node-connector/v2" +) + +// fakeNodeClient embeds nodeClient so only the methods a test actually needs have to be +// implemented. Any other call panics, which surfaces unexpected node usage loudly. +type fakeNodeClient struct { + nodeClient + prefetchedIdentities []string + prefetch func(ctx context.Context, ids []string) ([]qubic.AddressAssets, error) +} + +func (f *fakeNodeClient) PrefetchOwnedAndPossessedAssets(ctx context.Context, ids []string) ([]qubic.AddressAssets, error) { + f.prefetchedIdentities = ids + return f.prefetch(ctx, ids) +} + +// fakeNodePool records how connections were handed out and returned so tests can assert +// that a failed request closes the connection instead of putting it back in the pool. +type fakeNodePool struct { + client nodeClient + getErr error + + gets int + puts int + closes int +} + +func (p *fakeNodePool) Get() (nodeClient, error) { + p.gets++ + if p.getErr != nil { + return nil, p.getErr + } + return p.client, nil +} + +func (p *fakeNodePool) Put(nodeClient) error { + p.puts++ + return nil +} + +func (p *fakeNodePool) Close(nodeClient) error { + p.closes++ + return nil +} + +func newTestServer(pool nodePool, maxBatchIdentities int) *Server { + return &Server{ + logger: log.New(io.Discard, "", 0), + qPool: pool, + maxBatchIdentities: maxBatchIdentities, + } +} diff --git a/foundation/rpc_server/rpc_server.go b/foundation/rpc_server/rpc_server.go index b635fe3..28b1638 100644 --- a/foundation/rpc_server/rpc_server.go +++ b/foundation/rpc_server/rpc_server.go @@ -30,22 +30,32 @@ import ( var _ protobuff.QubicLiveServiceServer = &Server{} +// defaultMaxBatchIdentities caps how many identities a single batch asset request may +// ask for, used when no limit is configured. +const defaultMaxBatchIdentities = 15 + type Server struct { protobuff.UnimplementedQubicLiveServiceServer - logger *log.Logger - listenAddrGRPC string - listenAddrHTTP string - qPool *qubic.Pool - maxTickFetchUrl string + logger *log.Logger + listenAddrGRPC string + listenAddrHTTP string + qPool nodePool + maxTickFetchUrl string + maxBatchIdentities int } -func NewServer(listenAddrGRPC, listenAddrHTTP string, logger *log.Logger, qPool *qubic.Pool, maxTickFetchUrl string) *Server { +func NewServer(listenAddrGRPC, listenAddrHTTP string, logger *log.Logger, qPool *qubic.Pool, maxTickFetchUrl string, maxBatchIdentities int) *Server { + if maxBatchIdentities <= 0 { + maxBatchIdentities = defaultMaxBatchIdentities + } + return &Server{ - listenAddrGRPC: listenAddrGRPC, - listenAddrHTTP: listenAddrHTTP, - logger: logger, - qPool: qPool, - maxTickFetchUrl: maxTickFetchUrl, + listenAddrGRPC: listenAddrGRPC, + listenAddrHTTP: listenAddrHTTP, + logger: logger, + qPool: newQubicNodePool(qPool), + maxTickFetchUrl: maxTickFetchUrl, + maxBatchIdentities: maxBatchIdentities, } } @@ -244,7 +254,7 @@ func (s *Server) BroadcastTransaction(ctx context.Context, req *protobuff.Broadc }, nil } -func broadcastTxToMultiple(ctx context.Context, pool *qubic.Pool, decodedTx []byte) int { +func broadcastTxToMultiple(ctx context.Context, pool nodePool, decodedTx []byte) int { nrSuccess := 0 for i := 0; i < 3; i++ { func() { @@ -302,36 +312,9 @@ func (s *Server) GetIssuedAssets(ctx context.Context, req *protobuff.IssuedAsset s.qPool.Put(client) - issuedAssets := make([]*protobuff.IssuedAsset, 0) - - for _, asset := range assets { - - iAsset := asset.Data - var iAssetIdentity types.Identity - iAssetIdentity, err = iAssetIdentity.FromPubKey(iAsset.PublicKey, false) - if err != nil { - return nil, errors.Wrap(err, "failed to get identity for issued asset public key") - } - - data := protobuff.IssuedAssetData{ - IssuerIdentity: iAssetIdentity.String(), - Type: uint32(iAsset.Type), - Name: int8ArrayToString(iAsset.Name[:]), - NumberOfDecimalPlaces: int32(iAsset.NumberOfDecimalPlaces), - UnitOfMeasurement: int8ArrayToInt32Array(iAsset.UnitOfMeasurement[:]), - } - - info := protobuff.AssetInfo{ - Tick: asset.Info.Tick, - UniverseIndex: asset.Info.UniverseIndex, - } - - issuedAsset := protobuff.IssuedAsset{ - Data: &data, - Info: &info, - } - - issuedAssets = append(issuedAssets, &issuedAsset) + issuedAssets, err := convertIssuedAssets(assets) + if err != nil { + return nil, status.Errorf(codes.Internal, "converting node response: %v", err) } return &protobuff.IssuedAssetsResponse{IssuedAssets: issuedAssets}, nil @@ -351,54 +334,9 @@ func (s *Server) GetOwnedAssets(ctx context.Context, req *protobuff.OwnedAssetsR s.qPool.Put(client) - ownedAssets := make([]*protobuff.OwnedAsset, 0) - - for _, asset := range assets { - - iAsset := asset.Data.IssuedAsset - - var iAssetIdentity types.Identity - iAssetIdentity, err = iAssetIdentity.FromPubKey(iAsset.PublicKey, false) - if err != nil { - return nil, errors.Wrap(err, "failed to get identity for issued asset public key") - } - - issuedAsset := protobuff.IssuedAssetData{ - IssuerIdentity: iAssetIdentity.String(), - Type: uint32(iAsset.Type), - Name: int8ArrayToString(iAsset.Name[:]), - NumberOfDecimalPlaces: int32(iAsset.NumberOfDecimalPlaces), - UnitOfMeasurement: int8ArrayToInt32Array(iAsset.UnitOfMeasurement[:]), - } - - var oAssetIdentity types.Identity - oAssetIdentity, err = oAssetIdentity.FromPubKey(asset.Data.PublicKey, false) - if err != nil { - return nil, errors.Wrap(err, "failed to get identity for owned asset public key") - } - - data := protobuff.OwnedAssetData{ - OwnerIdentity: oAssetIdentity.String(), - Type: uint32(asset.Data.Type), - Padding: int32(asset.Data.Padding[0]), - ManagingContractIndex: uint32(asset.Data.ManagingContractIndex), - IssuanceIndex: asset.Data.IssuanceIndex, - NumberOfUnits: asset.Data.NumberOfUnits, - IssuedAsset: &issuedAsset, - } - - info := protobuff.AssetInfo{ - Tick: asset.Info.Tick, - UniverseIndex: asset.Info.UniverseIndex, - } - - ownedAsset := protobuff.OwnedAsset{ - Data: &data, - Info: &info, - } - - ownedAssets = append(ownedAssets, &ownedAsset) - + ownedAssets, err := convertOwnedAssets(assets) + if err != nil { + return nil, status.Errorf(codes.Internal, "converting node response: %v", err) } return &protobuff.OwnedAssetsResponse{OwnedAssets: ownedAssets}, nil @@ -418,73 +356,71 @@ func (s *Server) GetPossessedAssets(ctx context.Context, req *protobuff.Possesse s.qPool.Put(client) - possessedAssets := make([]*protobuff.PossessedAsset, 0) + possessedAssets, err := convertPossessedAssets(assets) + if err != nil { + return nil, status.Errorf(codes.Internal, "converting node response: %v", err) + } - for _, asset := range assets { + return &protobuff.PossessedAssetsResponse{PossessedAssets: possessedAssets}, nil +} - oAsset := asset.Data.OwnedAsset - var oAssetIdentity types.Identity - oAssetIdentity, err = oAssetIdentity.FromPubKey(oAsset.PublicKey, false) - if err != nil { - return nil, errors.Wrap(err, "failed to get identity for owned asset public key") - } +// GetAssetsForIdentities fetches the owned and possessed assets of several identities in +// one pipelined batch, costing roughly one round trip for the whole set instead of one +// per identity. +func (s *Server) GetAssetsForIdentities(ctx context.Context, req *protobuff.GetAssetsForIdentitiesRequest) (*protobuff.GetAssetsForIdentitiesResponse, error) { + if err := s.validateBatchIdentities(req.Identities); err != nil { + return nil, err + } - iAsset := oAsset.IssuedAsset - var iAssetIdentity types.Identity - iAssetIdentity, err = iAssetIdentity.FromPubKey(iAsset.PublicKey, false) - if err != nil { - return nil, errors.Wrap(err, "failed to get identity for issued asset public key") - } + client, err := s.qPool.Get() + if err != nil { + return nil, status.Errorf(codes.Internal, "getting pool connection :%v", err) + } - issuedAsset := protobuff.IssuedAssetData{ - IssuerIdentity: iAssetIdentity.String(), - Type: uint32(iAsset.Type), - Name: int8ArrayToString(iAsset.Name[:]), - NumberOfDecimalPlaces: int32(iAsset.NumberOfDecimalPlaces), - UnitOfMeasurement: int8ArrayToInt32Array(iAsset.UnitOfMeasurement[:]), - } + assets, err := client.PrefetchOwnedAndPossessedAssets(ctx, req.Identities) + if err != nil { + // a failed batch leaves the connection unusable, so it must be discarded + // instead of returned to the pool + s.qPool.Close(client) + return nil, status.Errorf(codes.Internal, "getting owned and possessed assets from node %v", err) + } - ownedAsset := protobuff.OwnedAssetData{ - OwnerIdentity: oAssetIdentity.String(), - Type: uint32(asset.Data.Type), - Padding: int32(asset.Data.Padding[0]), - ManagingContractIndex: uint32(asset.Data.ManagingContractIndex), - IssuanceIndex: asset.Data.IssuanceIndex, - NumberOfUnits: asset.Data.NumberOfUnits, - IssuedAsset: &issuedAsset, - } + s.qPool.Put(client) - var pAssetIdentity types.Identity - pAssetIdentity, err = pAssetIdentity.FromPubKey(asset.Data.PublicKey, false) - if err != nil { - return nil, errors.Wrap(err, "failed to get identity for possessed asset public key") - } + identityAssets, err := convertAddressAssets(assets) + if err != nil { + return nil, status.Errorf(codes.Internal, "converting node response: %v", err) + } - data := protobuff.PossessedAssetData{ - PossessorIdentity: pAssetIdentity.String(), - Type: uint32(asset.Data.Type), - Padding: int32(asset.Data.Padding[0]), - ManagingContractIndex: uint32(asset.Data.ManagingContractIndex), - IssuanceIndex: asset.Data.IssuanceIndex, - NumberOfUnits: asset.Data.NumberOfUnits, - OwnedAsset: &ownedAsset, - } + return &protobuff.GetAssetsForIdentitiesResponse{Assets: identityAssets}, nil +} - info := protobuff.AssetInfo{ - Tick: asset.Info.Tick, - UniverseIndex: asset.Info.UniverseIndex, - } +// validateBatchIdentities checks a batch request before any node connection is taken. +// Validating up front means malformed input can never fail mid batch and cost us a +// pooled connection. +func (s *Server) validateBatchIdentities(identities []string) error { + if len(identities) == 0 { + return status.Error(codes.InvalidArgument, "identities must not be empty") + } - possessedAsset := protobuff.PossessedAsset{ - Data: &data, - Info: &info, - } + if len(identities) > s.maxBatchIdentities { + return status.Errorf(codes.InvalidArgument, "too many identities: %d requested, maximum is %d", len(identities), s.maxBatchIdentities) + } - possessedAssets = append(possessedAssets, &possessedAsset) + seen := make(map[string]struct{}, len(identities)) + for _, identity := range identities { + id := types.Identity(identity) + if _, err := id.ToPubKey(false); err != nil { + return status.Errorf(codes.InvalidArgument, "invalid identity %q: %v", identity, err) + } + if _, duplicate := seen[identity]; duplicate { + return status.Errorf(codes.InvalidArgument, "duplicate identity %q", identity) + } + seen[identity] = struct{}{} } - return &protobuff.PossessedAssetsResponse{PossessedAssets: possessedAssets}, nil + return nil } const assetIssuanceType = 1