diff --git a/components/app/app.go b/components/app/app.go index c630896..ed6cec4 100644 --- a/components/app/app.go +++ b/components/app/app.go @@ -14,7 +14,7 @@ var ( Name = "inx-indexer" // Version of the app. - Version = "1.0.0-rc.3" + Version = "1.0.0-rc.4" ) func App() *app.App { diff --git a/pkg/indexer/alias.go b/pkg/indexer/alias.go index 1c09305..72ed16b 100644 --- a/pkg/indexer/alias.go +++ b/pkg/indexer/alias.go @@ -5,18 +5,20 @@ import ( "fmt" "time" + "gorm.io/gorm" + iotago "github.com/iotaledger/iota.go/v3" ) type alias struct { - AliasID aliasIDBytes `gorm:"primaryKey;notnull"` - OutputID outputIDBytes `gorm:"unique;notnull"` - NativeTokenCount uint32 `gorm:"notnull;type:integer"` - StateController addressBytes `gorm:"notnull;index:alias_state_controller"` - Governor addressBytes `gorm:"notnull;index:alias_governor"` - Issuer addressBytes `gorm:"index:alias_issuer"` - Sender addressBytes `gorm:"index:alias_sender"` - CreatedAt time.Time `gorm:"notnull;index:alias_created_at"` + AliasID []byte `gorm:"primaryKey;notnull"` + OutputID []byte `gorm:"unique;notnull"` + NativeTokenCount uint32 `gorm:"notnull;type:integer"` + StateController []byte `gorm:"notnull;index:alias_state_controller"` + Governor []byte `gorm:"notnull;index:alias_governor"` + Issuer []byte `gorm:"index:alias_issuer"` + Sender []byte `gorm:"index:alias_sender"` + CreatedAt time.Time `gorm:"notnull;index:alias_created_at"` } func (o *alias) String() string { @@ -27,6 +29,7 @@ type AliasFilterOptions struct { hasNativeTokens *bool minNativeTokenCount *uint32 maxNativeTokenCount *uint32 + unlockableByAddress *iotago.Address stateController *iotago.Address governor *iotago.Address issuer *iotago.Address @@ -57,6 +60,12 @@ func AliasMaxNativeTokenCount(value uint32) AliasFilterOption { } } +func AliasUnlockableByAddress(address iotago.Address) AliasFilterOption { + return func(args *AliasFilterOptions) { + args.unlockableByAddress = &address + } +} + func AliasStateController(address iotago.Address) AliasFilterOption { return func(args *AliasFilterOptions) { args.stateController = &address @@ -123,8 +132,7 @@ func (i *Indexer) AliasOutput(aliasID *iotago.AliasID) *IndexerResult { return i.combineOutputIDFilteredQuery(query, 0, nil) } -func (i *Indexer) AliasOutputsWithFilters(filter ...AliasFilterOption) *IndexerResult { - opts := aliasFilterOptions(filter) +func (i *Indexer) aliasQueryWithFilter(opts *AliasFilterOptions) (*gorm.DB, error) { query := i.db.Model(&alias{}) if opts.hasNativeTokens != nil { @@ -143,36 +151,44 @@ func (i *Indexer) AliasOutputsWithFilters(filter ...AliasFilterOption) *IndexerR query = query.Where("native_token_count <= ?", *opts.maxNativeTokenCount) } + if opts.unlockableByAddress != nil { + addr, err := addressBytesForAddress(*opts.unlockableByAddress) + if err != nil { + return nil, err + } + query = query.Where("(state_controller = ? OR governor = ?)", addr, addr) + } + if opts.stateController != nil { addr, err := addressBytesForAddress(*opts.stateController) if err != nil { - return errorResult(err) + return nil, err } - query = query.Where("state_controller = ?", addr[:]) + query = query.Where("state_controller = ?", addr) } if opts.governor != nil { addr, err := addressBytesForAddress(*opts.governor) if err != nil { - return errorResult(err) + return nil, err } - query = query.Where("governor = ?", addr[:]) + query = query.Where("governor = ?", addr) } if opts.sender != nil { addr, err := addressBytesForAddress(*opts.sender) if err != nil { - return errorResult(err) + return nil, err } - query = query.Where("sender = ?", addr[:]) + query = query.Where("sender = ?", addr) } if opts.issuer != nil { addr, err := addressBytesForAddress(*opts.issuer) if err != nil { - return errorResult(err) + return nil, err } - query = query.Where("issuer = ?", addr[:]) + query = query.Where("issuer = ?", addr) } if opts.createdBefore != nil { @@ -183,5 +199,15 @@ func (i *Indexer) AliasOutputsWithFilters(filter ...AliasFilterOption) *IndexerR query = query.Where("created_at > ?", *opts.createdAfter) } + return query, nil +} + +func (i *Indexer) AliasOutputsWithFilters(filter ...AliasFilterOption) *IndexerResult { + opts := aliasFilterOptions(filter) + query, err := i.aliasQueryWithFilter(opts) + if err != nil { + return errorResult(err) + } + return i.combineOutputIDFilteredQuery(query, opts.pageSize, opts.cursor) } diff --git a/pkg/indexer/basic_output.go b/pkg/indexer/basic_output.go index 2fe9087..846ae1e 100644 --- a/pkg/indexer/basic_output.go +++ b/pkg/indexer/basic_output.go @@ -5,21 +5,23 @@ import ( "fmt" "time" + "gorm.io/gorm" + iotago "github.com/iotaledger/iota.go/v3" ) type basicOutput struct { - OutputID outputIDBytes `gorm:"primaryKey;notnull"` - NativeTokenCount uint32 `gorm:"notnull;type:integer"` - Sender addressBytes `gorm:"index:basic_outputs_sender_tag"` - Tag []byte `gorm:"index:basic_outputs_sender_tag"` - Address addressBytes `gorm:"notnull;index:basic_outputs_address"` + OutputID []byte `gorm:"primaryKey;notnull"` + NativeTokenCount uint32 `gorm:"notnull;type:integer"` + Sender []byte `gorm:"index:basic_outputs_sender_tag"` + Tag []byte `gorm:"index:basic_outputs_sender_tag"` + Address []byte `gorm:"notnull;index:basic_outputs_address"` StorageDepositReturn *uint64 - StorageDepositReturnAddress addressBytes `gorm:"index:basic_outputs_storage_deposit_return_address"` + StorageDepositReturnAddress []byte `gorm:"index:basic_outputs_storage_deposit_return_address"` TimelockTime *time.Time ExpirationTime *time.Time - ExpirationReturnAddress addressBytes `gorm:"index:basic_outputs_expiration_return_address"` - CreatedAt time.Time `gorm:"notnull;index:basic_outputs_created_at"` + ExpirationReturnAddress []byte `gorm:"index:basic_outputs_expiration_return_address"` + CreatedAt time.Time `gorm:"notnull;index:basic_outputs_created_at"` } func (o *basicOutput) String() string { @@ -31,6 +33,7 @@ type BasicOutputFilterOptions struct { minNativeTokenCount *uint32 maxNativeTokenCount *uint32 unlockableByAddress *iotago.Address + address *iotago.Address hasStorageDepositReturnCondition *bool storageDepositReturnAddress *iotago.Address hasExpirationCondition *bool @@ -74,6 +77,12 @@ func BasicOutputUnlockableByAddress(address iotago.Address) BasicOutputFilterOpt } } +func BasicOutputUnlockAddress(address iotago.Address) BasicOutputFilterOption { + return func(args *BasicOutputFilterOptions) { + args.address = &address + } +} + func BasicOutputHasStorageDepositReturnCondition(value bool) BasicOutputFilterOption { return func(args *BasicOutputFilterOptions) { args.hasStorageDepositReturnCondition = &value @@ -174,8 +183,7 @@ func basicOutputFilterOptions(optionalOptions []BasicOutputFilterOption) *BasicO return result } -func (i *Indexer) BasicOutputsWithFilters(filters ...BasicOutputFilterOption) *IndexerResult { - opts := basicOutputFilterOptions(filters) +func (i *Indexer) basicOutputsQueryWithFilter(opts *BasicOutputFilterOptions) (*gorm.DB, error) { query := i.db.Model(&basicOutput{}) if opts.hasNativeTokens != nil { @@ -197,9 +205,17 @@ func (i *Indexer) BasicOutputsWithFilters(filters ...BasicOutputFilterOption) *I if opts.unlockableByAddress != nil { addr, err := addressBytesForAddress(*opts.unlockableByAddress) if err != nil { - return errorResult(err) + return nil, err } - query = query.Where("address = ?", addr[:]) + query = query.Where("(address = ? OR expiration_return_address = ? OR storage_deposit_return_address = ?)", addr, addr, addr) + } + + if opts.address != nil { + addr, err := addressBytesForAddress(*opts.address) + if err != nil { + return nil, err + } + query = query.Where("address = ?", addr) } if opts.hasStorageDepositReturnCondition != nil { @@ -213,9 +229,9 @@ func (i *Indexer) BasicOutputsWithFilters(filters ...BasicOutputFilterOption) *I if opts.storageDepositReturnAddress != nil { addr, err := addressBytesForAddress(*opts.storageDepositReturnAddress) if err != nil { - return errorResult(err) + return nil, err } - query = query.Where("storage_deposit_return_address = ?", addr[:]) + query = query.Where("storage_deposit_return_address = ?", addr) } if opts.hasExpirationCondition != nil { @@ -229,9 +245,9 @@ func (i *Indexer) BasicOutputsWithFilters(filters ...BasicOutputFilterOption) *I if opts.expirationReturnAddress != nil { addr, err := addressBytesForAddress(*opts.expirationReturnAddress) if err != nil { - return errorResult(err) + return nil, err } - query = query.Where("expiration_return_address = ?", addr[:]) + query = query.Where("expiration_return_address = ?", addr) } if opts.expiresBefore != nil { @@ -261,9 +277,9 @@ func (i *Indexer) BasicOutputsWithFilters(filters ...BasicOutputFilterOption) *I if opts.sender != nil { addr, err := addressBytesForAddress(*opts.sender) if err != nil { - return errorResult(err) + return nil, err } - query = query.Where("sender = ?", addr[:]) + query = query.Where("sender = ?", addr) } if opts.tag != nil && len(opts.tag) > 0 { @@ -278,5 +294,15 @@ func (i *Indexer) BasicOutputsWithFilters(filters ...BasicOutputFilterOption) *I query = query.Where("created_at > ?", *opts.createdAfter) } + return query, nil +} + +func (i *Indexer) BasicOutputsWithFilters(filters ...BasicOutputFilterOption) *IndexerResult { + opts := basicOutputFilterOptions(filters) + query, err := i.basicOutputsQueryWithFilter(opts) + if err != nil { + return errorResult(err) + } + return i.combineOutputIDFilteredQuery(query, opts.pageSize, opts.cursor) } diff --git a/pkg/indexer/combined.go b/pkg/indexer/combined.go new file mode 100644 index 0000000..60a5bc2 --- /dev/null +++ b/pkg/indexer/combined.go @@ -0,0 +1,142 @@ +package indexer + +import ( + "time" + + "gorm.io/gorm" + + iotago "github.com/iotaledger/iota.go/v3" +) + +type CombinedFilterOptions struct { + hasNativeTokens *bool + minNativeTokenCount *uint32 + maxNativeTokenCount *uint32 + unlockableByAddress *iotago.Address + pageSize uint32 + cursor *string + createdBefore *time.Time + createdAfter *time.Time +} + +type CombinedFilterOption func(*CombinedFilterOptions) + +func CombinedHasNativeTokens(value bool) CombinedFilterOption { + return func(args *CombinedFilterOptions) { + args.hasNativeTokens = &value + } +} + +func CombinedMinNativeTokenCount(value uint32) CombinedFilterOption { + return func(args *CombinedFilterOptions) { + args.minNativeTokenCount = &value + } +} + +func CombinedMaxNativeTokenCount(value uint32) CombinedFilterOption { + return func(args *CombinedFilterOptions) { + args.maxNativeTokenCount = &value + } +} + +func CombinedUnlockableByAddress(address iotago.Address) CombinedFilterOption { + return func(args *CombinedFilterOptions) { + args.unlockableByAddress = &address + } +} + +func CombinedPageSize(pageSize uint32) CombinedFilterOption { + return func(args *CombinedFilterOptions) { + args.pageSize = pageSize + } +} + +func CombinedCursor(cursor string) CombinedFilterOption { + return func(args *CombinedFilterOptions) { + args.cursor = &cursor + } +} + +func CombinedCreatedBefore(time time.Time) CombinedFilterOption { + return func(args *CombinedFilterOptions) { + args.createdBefore = &time + } +} + +func CombinedCreatedAfter(time time.Time) CombinedFilterOption { + return func(args *CombinedFilterOptions) { + args.createdAfter = &time + } +} + +func combinedFilterOptions(optionalOptions []CombinedFilterOption) *CombinedFilterOptions { + result := &CombinedFilterOptions{} + + for _, optionalOption := range optionalOptions { + optionalOption(result) + } + + return result +} + +func (o *CombinedFilterOptions) BasicFilterOptions() *BasicOutputFilterOptions { + return &BasicOutputFilterOptions{ + hasNativeTokens: o.hasNativeTokens, + minNativeTokenCount: o.minNativeTokenCount, + maxNativeTokenCount: o.maxNativeTokenCount, + unlockableByAddress: o.unlockableByAddress, + pageSize: o.pageSize, + cursor: o.cursor, + createdBefore: o.createdBefore, + createdAfter: o.createdAfter, + } +} + +func (o *CombinedFilterOptions) AliasFilterOptions() *AliasFilterOptions { + return &AliasFilterOptions{ + hasNativeTokens: o.hasNativeTokens, + minNativeTokenCount: o.minNativeTokenCount, + maxNativeTokenCount: o.maxNativeTokenCount, + unlockableByAddress: o.unlockableByAddress, + pageSize: o.pageSize, + cursor: o.cursor, + createdBefore: o.createdBefore, + createdAfter: o.createdAfter, + } +} + +func (o *CombinedFilterOptions) NFTFilterOptions() *NFTFilterOptions { + return &NFTFilterOptions{ + hasNativeTokens: o.hasNativeTokens, + minNativeTokenCount: o.minNativeTokenCount, + maxNativeTokenCount: o.maxNativeTokenCount, + unlockableByAddress: o.unlockableByAddress, + pageSize: o.pageSize, + cursor: o.cursor, + createdBefore: o.createdBefore, + createdAfter: o.createdAfter, + } +} + +func (i *Indexer) CombinedOutputsWithFilters(filters ...CombinedFilterOption) *IndexerResult { + opts := combinedFilterOptions(filters) + + basicQuery, err := i.basicOutputsQueryWithFilter(opts.BasicFilterOptions()) + if err != nil { + return errorResult(err) + } + + aliasQuery, err := i.aliasQueryWithFilter(opts.AliasFilterOptions()) + if err != nil { + return errorResult(err) + } + + nftQuery, err := i.nftOutputsQueryWithFilter(opts.NFTFilterOptions()) + if err != nil { + return errorResult(err) + } + + queries := []*gorm.DB{basicQuery, aliasQuery, nftQuery} + + return i.combineOutputIDFilteredQueries(queries, opts.pageSize, opts.cursor) +} diff --git a/pkg/indexer/foundry.go b/pkg/indexer/foundry.go index 65474d8..52a5737 100644 --- a/pkg/indexer/foundry.go +++ b/pkg/indexer/foundry.go @@ -5,15 +5,17 @@ import ( "fmt" "time" + "gorm.io/gorm" + iotago "github.com/iotaledger/iota.go/v3" ) type foundry struct { - FoundryID foundryIDBytes `gorm:"primaryKey;notnull"` - OutputID outputIDBytes `gorm:"unique;notnull"` - NativeTokenCount uint32 `gorm:"notnull;type:integer"` - AliasAddress addressBytes `gorm:"notnull;index:foundries_alias_address"` - CreatedAt time.Time `gorm:"notnull;index:foundries_created_at"` + FoundryID []byte `gorm:"primaryKey;notnull"` + OutputID []byte `gorm:"unique;notnull"` + NativeTokenCount uint32 `gorm:"notnull;type:integer"` + AliasAddress []byte `gorm:"notnull;index:foundries_alias_address"` + CreatedAt time.Time `gorm:"notnull;index:foundries_created_at"` } func (o *foundry) String() string { @@ -99,8 +101,7 @@ func (i *Indexer) FoundryOutput(foundryID *iotago.FoundryID) *IndexerResult { return i.combineOutputIDFilteredQuery(query, 0, nil) } -func (i *Indexer) FoundryOutputsWithFilters(filters ...FoundryFilterOption) *IndexerResult { - opts := foundryFilterOptions(filters) +func (i *Indexer) foundryOutputsQueryWithFilter(opts *FoundryFilterOptions) (*gorm.DB, error) { query := i.db.Model(&foundry{}) if opts.hasNativeTokens != nil { @@ -122,9 +123,9 @@ func (i *Indexer) FoundryOutputsWithFilters(filters ...FoundryFilterOption) *Ind if opts.aliasAddress != nil { addr, err := addressBytesForAddress(opts.aliasAddress) if err != nil { - return errorResult(err) + return nil, err } - query = query.Where("alias_address = ?", addr[:]) + query = query.Where("alias_address = ?", addr) } if opts.createdBefore != nil { @@ -135,5 +136,15 @@ func (i *Indexer) FoundryOutputsWithFilters(filters ...FoundryFilterOption) *Ind query = query.Where("created_at > ?", *opts.createdAfter) } + return query, nil +} + +func (i *Indexer) FoundryOutputsWithFilters(filters ...FoundryFilterOption) *IndexerResult { + opts := foundryFilterOptions(filters) + query, err := i.foundryOutputsQueryWithFilter(opts) + if err != nil { + return errorResult(err) + } + return i.combineOutputIDFilteredQuery(query, opts.pageSize, opts.cursor) } diff --git a/pkg/indexer/indexer.go b/pkg/indexer/indexer.go index cee97a3..1c8957c 100644 --- a/pkg/indexer/indexer.go +++ b/pkg/indexer/indexer.go @@ -90,7 +90,7 @@ func entryForOutput(outputID iotago.OutputID, output iotago.Output, timestampBoo conditions := iotaOutput.UnlockConditionSet() basic := &basicOutput{ - OutputID: make(outputIDBytes, iotago.OutputIDLength), + OutputID: make([]byte, iotago.OutputIDLength), NativeTokenCount: uint32(len(iotaOutput.NativeTokens)), CreatedAt: unixTime(timestampBooked), } @@ -151,8 +151,8 @@ func entryForOutput(outputID iotago.OutputID, output iotago.Output, timestampBoo conditions := iotaOutput.UnlockConditionSet() alias := &alias{ - AliasID: make(aliasIDBytes, iotago.AliasIDLength), - OutputID: make(outputIDBytes, iotago.OutputIDLength), + AliasID: make([]byte, iotago.AliasIDLength), + OutputID: make([]byte, iotago.OutputIDLength), NativeTokenCount: uint32(len(iotaOutput.NativeTokens)), CreatedAt: unixTime(timestampBooked), } @@ -202,8 +202,8 @@ func entryForOutput(outputID iotago.OutputID, output iotago.Output, timestampBoo } nft := &nft{ - NFTID: make(nftIDBytes, iotago.NFTIDLength), - OutputID: make(outputIDBytes, iotago.OutputIDLength), + NFTID: make([]byte, iotago.NFTIDLength), + OutputID: make([]byte, iotago.OutputIDLength), NativeTokenCount: uint32(len(iotaOutput.NativeTokens)), CreatedAt: unixTime(timestampBooked), } @@ -270,7 +270,7 @@ func entryForOutput(outputID iotago.OutputID, output iotago.Output, timestampBoo foundry := &foundry{ FoundryID: foundryID[:], - OutputID: make(outputIDBytes, iotago.OutputIDLength), + OutputID: make([]byte, iotago.OutputIDLength), NativeTokenCount: uint32(len(iotaOutput.NativeTokens)), CreatedAt: unixTime(timestampBooked), } diff --git a/pkg/indexer/nft.go b/pkg/indexer/nft.go index 08fd970..caa71f2 100644 --- a/pkg/indexer/nft.go +++ b/pkg/indexer/nft.go @@ -5,23 +5,25 @@ import ( "fmt" "time" + "gorm.io/gorm" + iotago "github.com/iotaledger/iota.go/v3" ) type nft struct { - NFTID nftIDBytes `gorm:"primaryKey;notnull"` - OutputID outputIDBytes `gorm:"unique;notnull"` - NativeTokenCount uint32 `gorm:"notnull;type:integer"` - Issuer addressBytes `gorm:"index:nfts_issuer"` - Sender addressBytes `gorm:"index:nfts_sender_tag"` - Tag []byte `gorm:"index:nfts_sender_tag"` - Address addressBytes `gorm:"notnull;index:nfts_address"` + NFTID []byte `gorm:"primaryKey;notnull"` + OutputID []byte `gorm:"unique;notnull"` + NativeTokenCount uint32 `gorm:"notnull;type:integer"` + Issuer []byte `gorm:"index:nfts_issuer"` + Sender []byte `gorm:"index:nfts_sender_tag"` + Tag []byte `gorm:"index:nfts_sender_tag"` + Address []byte `gorm:"notnull;index:nfts_address"` StorageDepositReturn *uint64 - StorageDepositReturnAddress addressBytes `gorm:"index:nfts_storage_deposit_return_address"` + StorageDepositReturnAddress []byte `gorm:"index:nfts_storage_deposit_return_address"` TimelockTime *time.Time ExpirationTime *time.Time - ExpirationReturnAddress addressBytes `gorm:"index:nfts_expiration_return_address"` - CreatedAt time.Time `gorm:"notnull;index:nfts_created_at"` + ExpirationReturnAddress []byte `gorm:"index:nfts_expiration_return_address"` + CreatedAt time.Time `gorm:"notnull;index:nfts_created_at"` } func (o *nft) String() string { @@ -33,6 +35,7 @@ type NFTFilterOptions struct { minNativeTokenCount *uint32 maxNativeTokenCount *uint32 unlockableByAddress *iotago.Address + address *iotago.Address hasStorageDepositReturnCondition *bool storageDepositReturnAddress *iotago.Address hasExpirationCondition *bool @@ -77,6 +80,12 @@ func NFTUnlockableByAddress(address iotago.Address) NFTFilterOption { } } +func NFTUnlockAddress(address iotago.Address) NFTFilterOption { + return func(args *NFTFilterOptions) { + args.address = &address + } +} + func NFTHasStorageDepositReturnCondition(value bool) NFTFilterOption { return func(args *NFTFilterOptions) { args.hasStorageDepositReturnCondition = &value @@ -191,8 +200,7 @@ func (i *Indexer) NFTOutput(nftID *iotago.NFTID) *IndexerResult { return i.combineOutputIDFilteredQuery(query, 0, nil) } -func (i *Indexer) NFTOutputsWithFilters(filters ...NFTFilterOption) *IndexerResult { - opts := nftFilterOptions(filters) +func (i *Indexer) nftOutputsQueryWithFilter(opts *NFTFilterOptions) (*gorm.DB, error) { query := i.db.Model(&nft{}) if opts.hasNativeTokens != nil { @@ -214,9 +222,17 @@ func (i *Indexer) NFTOutputsWithFilters(filters ...NFTFilterOption) *IndexerResu if opts.unlockableByAddress != nil { addr, err := addressBytesForAddress(*opts.unlockableByAddress) if err != nil { - return errorResult(err) + return nil, err } - query = query.Where("address = ?", addr[:]) + query = query.Where("(address = ? OR expiration_return_address = ? OR storage_deposit_return_address = ?)", addr, addr, addr) + } + + if opts.address != nil { + addr, err := addressBytesForAddress(*opts.address) + if err != nil { + return nil, err + } + query = query.Where("address = ?", addr) } if opts.hasStorageDepositReturnCondition != nil { @@ -230,9 +246,9 @@ func (i *Indexer) NFTOutputsWithFilters(filters ...NFTFilterOption) *IndexerResu if opts.storageDepositReturnAddress != nil { addr, err := addressBytesForAddress(*opts.storageDepositReturnAddress) if err != nil { - return errorResult(err) + return nil, err } - query = query.Where("storage_deposit_return_address = ?", addr[:]) + query = query.Where("storage_deposit_return_address = ?", addr) } if opts.hasExpirationCondition != nil { @@ -246,9 +262,9 @@ func (i *Indexer) NFTOutputsWithFilters(filters ...NFTFilterOption) *IndexerResu if opts.expirationReturnAddress != nil { addr, err := addressBytesForAddress(*opts.expirationReturnAddress) if err != nil { - return errorResult(err) + return nil, err } - query = query.Where("expiration_return_address = ?", addr[:]) + query = query.Where("expiration_return_address = ?", addr) } if opts.expiresBefore != nil { @@ -278,17 +294,17 @@ func (i *Indexer) NFTOutputsWithFilters(filters ...NFTFilterOption) *IndexerResu if opts.issuer != nil { addr, err := addressBytesForAddress(*opts.issuer) if err != nil { - return errorResult(err) + return nil, err } - query = query.Where("issuer = ?", addr[:]) + query = query.Where("issuer = ?", addr) } if opts.sender != nil { addr, err := addressBytesForAddress(*opts.sender) if err != nil { - return errorResult(err) + return nil, err } - query = query.Where("sender = ?", addr[:]) + query = query.Where("sender = ?", addr) } if opts.tag != nil && len(opts.tag) > 0 { @@ -303,5 +319,15 @@ func (i *Indexer) NFTOutputsWithFilters(filters ...NFTFilterOption) *IndexerResu query = query.Where("created_at > ?", *opts.createdAfter) } + return query, nil +} + +func (i *Indexer) NFTOutputsWithFilters(filters ...NFTFilterOption) *IndexerResult { + opts := nftFilterOptions(filters) + query, err := i.nftOutputsQueryWithFilter(opts) + if err != nil { + return errorResult(err) + } + return i.combineOutputIDFilteredQuery(query, opts.pageSize, opts.cursor) } diff --git a/pkg/indexer/types.go b/pkg/indexer/types.go index 2300daa..3e22739 100644 --- a/pkg/indexer/types.go +++ b/pkg/indexer/types.go @@ -1,6 +1,7 @@ package indexer import ( + "fmt" "strings" "time" @@ -20,12 +21,6 @@ var ( NullOutputID = iotago.OutputID{} ) -type outputIDBytes []byte -type addressBytes []byte -type nftIDBytes []byte -type aliasIDBytes []byte -type foundryIDBytes []byte - type Status struct { ID uint `gorm:"primaryKey;notnull"` LedgerIndex uint32 @@ -35,30 +30,23 @@ type Status struct { } type queryResult struct { - OutputID outputIDBytes + OutputID []byte Cursor string LedgerIndex uint32 } -func (o outputIDBytes) ID() iotago.OutputID { - id := iotago.OutputID{} - copy(id[:], o) - - return id -} - type queryResults []queryResult func (q queryResults) IDs() iotago.OutputIDs { outputIDs := iotago.OutputIDs{} for _, r := range q { - outputIDs = append(outputIDs, r.OutputID.ID()) + outputIDs = append(outputIDs, iotago.OutputID(r.OutputID)) } return outputIDs } -func addressBytesForAddress(addr iotago.Address) (addressBytes, error) { +func addressBytesForAddress(addr iotago.Address) ([]byte, error) { return addr.Serialize(serializer.DeSeriModeNoValidation, nil) } @@ -81,9 +69,8 @@ func unixTime(fromValue uint32) time.Time { return time.Unix(int64(fromValue), 0) } -func (i *Indexer) combineOutputIDFilteredQuery(query *gorm.DB, pageSize uint32, cursor *string) *IndexerResult { - - query = query.Select("output_id").Order("created_at asc, output_id asc") +func (i *Indexer) filteredQuery(query *gorm.DB, pageSize uint32, cursor *string) (*gorm.DB, error) { + query = query.Select("output_id", "created_at").Order("created_at asc, output_id asc") if pageSize > 0 { var cursorQuery string //nolint:exhaustive // we have a default case. @@ -96,11 +83,12 @@ func (i *Indexer) combineOutputIDFilteredQuery(query *gorm.DB, pageSize uint32, i.LogErrorfAndExit("Unsupported db engine pagination queries: %s", i.engine) } - query = query.Select("output_id", cursorQuery).Limit(int(pageSize + 1)) + // We use pageSize + 1 to load the next item to use as the cursor + query = query.Select("output_id", "created_at", cursorQuery).Limit(int(pageSize + 1)) if cursor != nil { if len(*cursor) != CursorLength { - return errorResult(errors.Errorf("Invalid cursor length: %d", len(*cursor))) + return nil, errors.Errorf("Invalid cursor length: %d", len(*cursor)) } //nolint:exhaustive // we have a default case. switch i.engine { @@ -114,9 +102,49 @@ func (i *Indexer) combineOutputIDFilteredQuery(query *gorm.DB, pageSize uint32, } } + return query, nil +} + +func (i *Indexer) combineOutputIDFilteredQuery(query *gorm.DB, pageSize uint32, cursor *string) *IndexerResult { + var err error + query, err = i.filteredQuery(query, pageSize, cursor) + if err != nil { + return errorResult(err) + } + + return i.resultsForQuery(query, pageSize) +} + +func (i *Indexer) combineOutputIDFilteredQueries(queries []*gorm.DB, pageSize uint32, cursor *string) *IndexerResult { + // Cast to []interface{} so that we can pass them to i.db.Raw as parameters + filteredQueries := make([]interface{}, len(queries)) + for q, query := range queries { + filtered, err := i.filteredQuery(query, pageSize, cursor) + if err != nil { + return errorResult(err) + } + filteredQueries[q] = filtered + } + + unionQueryItem := "SELECT output_id, created_at FROM (?) as temp;" + if pageSize > 0 { + unionQueryItem = "SELECT output_id, created_at, cursor FROM (?) as temp;" + } + repeatedUnionQueryItem := strings.Split(strings.Repeat(unionQueryItem, len(queries)), ";") + unionQuery := strings.Join(repeatedUnionQueryItem[:len(repeatedUnionQueryItem)-1], " UNION ") + + // We use pageSize + 1 to load the next item to use as the cursor + unionQuery = fmt.Sprintf("%s ORDER BY created_at asc, output_id asc LIMIT %d", unionQuery, pageSize+1) + + rawQuery := i.db.Raw(unionQuery, filteredQueries...) + rawQuery = rawQuery.Order("created_at asc, output_id asc") + + return i.resultsForQuery(rawQuery, pageSize) +} + +func (i *Indexer) resultsForQuery(query *gorm.DB, pageSize uint32) *IndexerResult { // This combines the query with a second query that checks for the current ledger_index. // This way we do not need to lock anything and we know the index matches the results. - //TODO: measure performance for big datasets ledgerIndexQuery := i.db.Model(&Status{}).Select("ledger_index") joinedQuery := i.db.Table("(?) as results, (?) as status", query, ledgerIndexQuery) diff --git a/pkg/server/restapi.go b/pkg/server/restapi.go index 330a808..4d6439b 100644 --- a/pkg/server/restapi.go +++ b/pkg/server/restapi.go @@ -10,6 +10,9 @@ const ( // ParameterNFTID is used to identify a nft by its ID. ParameterNFTID = "nftID" + // QueryParameterUnlockableByAddress is used to filter for all unlock conditions regarding a certain address. + QueryParameterUnlockableByAddress = "unlockableByAddress" + // QueryParameterAddress is used to filter for a certain address. QueryParameterAddress = "address" diff --git a/pkg/server/routes.go b/pkg/server/routes.go index 92ddf3b..510d3f5 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -15,6 +15,12 @@ import ( ) const ( + // RouteOutputs is the route for getting basic, alias and nft outputs filtered by the given parameters. + // GET with query parameter returns all outputIDs that fit these filter criteria. + // Query parameters: "hasNativeTokens", "minNativeTokenCount", "maxNativeTokenCount", + // "unlockableByAddress", "createdBefore", "createdAfter" + // Returns an empty list if no results are found. + RouteOutputs = "/outputs" // RouteOutputsBasic is the route for getting basic outputs filtered by the given parameters. // GET with query parameter returns all outputIDs that fit these filter criteria. @@ -65,6 +71,15 @@ const ( func (s *IndexerServer) configureRoutes(routeGroup *echo.Group) { + routeGroup.GET(RouteOutputs, func(c echo.Context) error { + resp, err := s.combinedOutputsWithFilter(c) + if err != nil { + return err + } + + return c.JSON(http.StatusOK, resp) + }) + routeGroup.GET(RouteOutputsBasic, func(c echo.Context) error { resp, err := s.basicOutputsWithFilter(c) if err != nil { @@ -129,6 +144,68 @@ func (s *IndexerServer) configureRoutes(routeGroup *echo.Group) { }) } +func (s *IndexerServer) combinedOutputsWithFilter(c echo.Context) (*outputsResponse, error) { + filters := []indexer.CombinedFilterOption{indexer.CombinedPageSize(s.pageSizeFromContext(c))} + + if len(c.QueryParam(QueryParameterHasNativeTokens)) > 0 { + value, err := httpserver.ParseBoolQueryParam(c, QueryParameterHasNativeTokens) + if err != nil { + return nil, err + } + filters = append(filters, indexer.CombinedHasNativeTokens(value)) + } + + if len(c.QueryParam(QueryParameterMinNativeTokenCount)) > 0 { + value, err := httpserver.ParseUint32QueryParam(c, QueryParameterMinNativeTokenCount, iotago.MaxNativeTokenCountPerOutput) // Use the iotago.MaxNativeTokenCountPerOutput as an upper bound check + if err != nil { + return nil, err + } + filters = append(filters, indexer.CombinedMinNativeTokenCount(value)) + } + + if len(c.QueryParam(QueryParameterMaxNativeTokenCount)) > 0 { + value, err := httpserver.ParseUint32QueryParam(c, QueryParameterMaxNativeTokenCount, iotago.MaxNativeTokenCountPerOutput) // Use the iotago.MaxNativeTokenCountPerOutput as an upper bound check + if err != nil { + return nil, err + } + filters = append(filters, indexer.CombinedMaxNativeTokenCount(value)) + } + + if len(c.QueryParam(QueryParameterUnlockableByAddress)) > 0 { + addr, err := httpserver.ParseBech32AddressQueryParam(c, s.Bech32HRP, QueryParameterUnlockableByAddress) + if err != nil { + return nil, err + } + filters = append(filters, indexer.CombinedUnlockableByAddress(addr)) + } + + if len(c.QueryParam(QueryParameterCursor)) > 0 { + cursor, pageSize, err := s.parseCursorQueryParameter(c) + if err != nil { + return nil, err + } + filters = append(filters, indexer.CombinedCursor(cursor), indexer.CombinedPageSize(pageSize)) + } + + if len(c.QueryParam(QueryParameterCreatedBefore)) > 0 { + timestamp, err := httpserver.ParseUnixTimestampQueryParam(c, QueryParameterCreatedBefore) + if err != nil { + return nil, err + } + filters = append(filters, indexer.CombinedCreatedBefore(timestamp)) + } + + if len(c.QueryParam(QueryParameterCreatedAfter)) > 0 { + timestamp, err := httpserver.ParseUnixTimestampQueryParam(c, QueryParameterCreatedAfter) + if err != nil { + return nil, err + } + filters = append(filters, indexer.CombinedCreatedAfter(timestamp)) + } + + return outputsResponseFromResult(s.Indexer.CombinedOutputsWithFilters(filters...)) +} + func (s *IndexerServer) basicOutputsWithFilter(c echo.Context) (*outputsResponse, error) { filters := []indexer.BasicOutputFilterOption{indexer.BasicOutputPageSize(s.pageSizeFromContext(c))} @@ -141,7 +218,7 @@ func (s *IndexerServer) basicOutputsWithFilter(c echo.Context) (*outputsResponse } if len(c.QueryParam(QueryParameterMinNativeTokenCount)) > 0 { - value, err := httpserver.ParseUint32QueryParam(c, QueryParameterMinNativeTokenCount, iotago.MaxNativeTokenCountPerOutput) + value, err := httpserver.ParseUint32QueryParam(c, QueryParameterMinNativeTokenCount, iotago.MaxNativeTokenCountPerOutput) // Use the iotago.MaxNativeTokenCountPerOutput as an upper bound check if err != nil { return nil, err } @@ -149,19 +226,27 @@ func (s *IndexerServer) basicOutputsWithFilter(c echo.Context) (*outputsResponse } if len(c.QueryParam(QueryParameterMaxNativeTokenCount)) > 0 { - value, err := httpserver.ParseUint32QueryParam(c, QueryParameterMaxNativeTokenCount, iotago.MaxNativeTokenCountPerOutput) + value, err := httpserver.ParseUint32QueryParam(c, QueryParameterMaxNativeTokenCount, iotago.MaxNativeTokenCountPerOutput) // Use the iotago.MaxNativeTokenCountPerOutput as an upper bound check if err != nil { return nil, err } filters = append(filters, indexer.BasicOutputMaxNativeTokenCount(value)) } + if len(c.QueryParam(QueryParameterUnlockableByAddress)) > 0 { + addr, err := httpserver.ParseBech32AddressQueryParam(c, s.Bech32HRP, QueryParameterUnlockableByAddress) + if err != nil { + return nil, err + } + filters = append(filters, indexer.BasicOutputUnlockableByAddress(addr)) + } + if len(c.QueryParam(QueryParameterAddress)) > 0 { addr, err := httpserver.ParseBech32AddressQueryParam(c, s.Bech32HRP, QueryParameterAddress) if err != nil { return nil, err } - filters = append(filters, indexer.BasicOutputUnlockableByAddress(addr)) + filters = append(filters, indexer.BasicOutputUnlockAddress(addr)) } if len(c.QueryParam(QueryParameterHasStorageDepositReturn)) > 0 { @@ -300,7 +385,7 @@ func (s *IndexerServer) aliasesWithFilter(c echo.Context) (*outputsResponse, err } if len(c.QueryParam(QueryParameterMinNativeTokenCount)) > 0 { - value, err := httpserver.ParseUint32QueryParam(c, QueryParameterMinNativeTokenCount, iotago.MaxNativeTokenCountPerOutput) + value, err := httpserver.ParseUint32QueryParam(c, QueryParameterMinNativeTokenCount, iotago.MaxNativeTokenCountPerOutput) // Use the iotago.MaxNativeTokenCountPerOutput as an upper bound check if err != nil { return nil, err } @@ -308,13 +393,21 @@ func (s *IndexerServer) aliasesWithFilter(c echo.Context) (*outputsResponse, err } if len(c.QueryParam(QueryParameterMaxNativeTokenCount)) > 0 { - value, err := httpserver.ParseUint32QueryParam(c, QueryParameterMaxNativeTokenCount, iotago.MaxNativeTokenCountPerOutput) + value, err := httpserver.ParseUint32QueryParam(c, QueryParameterMaxNativeTokenCount, iotago.MaxNativeTokenCountPerOutput) // Use the iotago.MaxNativeTokenCountPerOutput as an upper bound check if err != nil { return nil, err } filters = append(filters, indexer.AliasMaxNativeTokenCount(value)) } + if len(c.QueryParam(QueryParameterUnlockableByAddress)) > 0 { + addr, err := httpserver.ParseBech32AddressQueryParam(c, s.Bech32HRP, QueryParameterUnlockableByAddress) + if err != nil { + return nil, err + } + filters = append(filters, indexer.AliasUnlockableByAddress(addr)) + } + if len(c.QueryParam(QueryParameterStateController)) > 0 { stateController, err := httpserver.ParseBech32AddressQueryParam(c, s.Bech32HRP, QueryParameterStateController) if err != nil { @@ -395,7 +488,7 @@ func (s *IndexerServer) nftsWithFilter(c echo.Context) (*outputsResponse, error) } if len(c.QueryParam(QueryParameterMinNativeTokenCount)) > 0 { - value, err := httpserver.ParseUint32QueryParam(c, QueryParameterMinNativeTokenCount, iotago.MaxNativeTokenCountPerOutput) + value, err := httpserver.ParseUint32QueryParam(c, QueryParameterMinNativeTokenCount, iotago.MaxNativeTokenCountPerOutput) // Use the iotago.MaxNativeTokenCountPerOutput as an upper bound check if err != nil { return nil, err } @@ -403,19 +496,27 @@ func (s *IndexerServer) nftsWithFilter(c echo.Context) (*outputsResponse, error) } if len(c.QueryParam(QueryParameterMaxNativeTokenCount)) > 0 { - value, err := httpserver.ParseUint32QueryParam(c, QueryParameterMaxNativeTokenCount, iotago.MaxNativeTokenCountPerOutput) + value, err := httpserver.ParseUint32QueryParam(c, QueryParameterMaxNativeTokenCount, iotago.MaxNativeTokenCountPerOutput) // Use the iotago.MaxNativeTokenCountPerOutput as an upper bound check if err != nil { return nil, err } filters = append(filters, indexer.NFTMaxNativeTokenCount(value)) } + if len(c.QueryParam(QueryParameterUnlockableByAddress)) > 0 { + addr, err := httpserver.ParseBech32AddressQueryParam(c, s.Bech32HRP, QueryParameterUnlockableByAddress) + if err != nil { + return nil, err + } + filters = append(filters, indexer.NFTUnlockableByAddress(addr)) + } + if len(c.QueryParam(QueryParameterAddress)) > 0 { addr, err := httpserver.ParseBech32AddressQueryParam(c, s.Bech32HRP, QueryParameterAddress) if err != nil { return nil, err } - filters = append(filters, indexer.NFTUnlockableByAddress(addr)) + filters = append(filters, indexer.NFTUnlockAddress(addr)) } if len(c.QueryParam(QueryParameterHasStorageDepositReturn)) > 0 { @@ -562,7 +663,7 @@ func (s *IndexerServer) foundriesWithFilter(c echo.Context) (*outputsResponse, e } if len(c.QueryParam(QueryParameterMinNativeTokenCount)) > 0 { - value, err := httpserver.ParseUint32QueryParam(c, QueryParameterMinNativeTokenCount, iotago.MaxNativeTokenCountPerOutput) + value, err := httpserver.ParseUint32QueryParam(c, QueryParameterMinNativeTokenCount, iotago.MaxNativeTokenCountPerOutput) // Use the iotago.MaxNativeTokenCountPerOutput as an upper bound check if err != nil { return nil, err } @@ -570,7 +671,7 @@ func (s *IndexerServer) foundriesWithFilter(c echo.Context) (*outputsResponse, e } if len(c.QueryParam(QueryParameterMaxNativeTokenCount)) > 0 { - value, err := httpserver.ParseUint32QueryParam(c, QueryParameterMaxNativeTokenCount, iotago.MaxNativeTokenCountPerOutput) + value, err := httpserver.ParseUint32QueryParam(c, QueryParameterMaxNativeTokenCount, iotago.MaxNativeTokenCountPerOutput) // Use the iotago.MaxNativeTokenCountPerOutput as an upper bound check if err != nil { return nil, err }