Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions database/plugin/metadata/sqlite/asset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2025 Blink Labs Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package sqlite

import (
"errors"

"github.com/blinklabs-io/dingo/database/plugin/metadata/sqlite/models"
lcommon "github.com/blinklabs-io/gouroboros/ledger/common"
"gorm.io/gorm"
)

// GetAssetByPolicyAndName returns an asset by policy ID and asset name
func (d *MetadataStoreSqlite) GetAssetByPolicyAndName(
policyId lcommon.Blake2b224,
assetName []byte,
txn *gorm.DB,
) (models.Asset, error) {
var asset models.Asset
var result *gorm.DB

query := d.DB()
if txn != nil {
query = txn
}

result = query.Where("policy_id = ? AND name = ?", policyId[:], assetName).First(&asset)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return models.Asset{}, nil
}
return models.Asset{}, result.Error
}
return asset, nil
}

// GetAssetsByPolicy returns all assets for a given policy ID
func (d *MetadataStoreSqlite) GetAssetsByPolicy(
policyId lcommon.Blake2b224,
txn *gorm.DB,
) ([]models.Asset, error) {
var assets []models.Asset
var result *gorm.DB

query := d.DB()
if txn != nil {
query = txn
}

result = query.Where("policy_id = ?", policyId[:]).Find(&assets)
if result.Error != nil {
return nil, result.Error
}
return assets, nil
}

// GetAssetsByUTxO returns all assets for a given UTxO using transaction ID and output index
func (d *MetadataStoreSqlite) GetAssetsByUTxO(
txId []byte,
idx uint32,
txn *gorm.DB,
) ([]models.Asset, error) {
var assets []models.Asset
var result *gorm.DB

query := d.DB()
if txn != nil {
query = txn
}

// Join with UTxO table to find assets by transaction ID and output index
result = query.Joins("INNER JOIN utxos ON assets.utxo_id = utxos.id").
Where("utxos.tx_id = ? AND utxos.idx = ?", txId, idx).
Find(&assets)
if result.Error != nil {
return nil, result.Error
}
return assets, nil
}
31 changes: 31 additions & 0 deletions database/plugin/metadata/sqlite/models/asset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2025 Blink Labs Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package models

import "github.com/blinklabs-io/dingo/database/types"

type Asset struct {
ID uint `gorm:"primaryKey"`
UtxoID uint
Name []byte `gorm:"index"`
NameHex []byte `gorm:"index"`
PolicyId []byte `gorm:"index"`
Fingerprint []byte `gorm:"index"`
Amount types.Uint64 `gorm:"index"`
}

func (Asset) TableName() string {
return "asset"
}
1 change: 1 addition & 0 deletions database/plugin/metadata/sqlite/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package models
// MigrateModels contains a list of model objects that should have DB migrations applied
var MigrateModels = []any{
&Account{},
&Asset{},
&AuthCommitteeHot{},
&BlockNonce{},
&Datum{},
Expand Down
1 change: 1 addition & 0 deletions database/plugin/metadata/sqlite/models/utxo.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Utxo struct {
PaymentKey []byte `gorm:"index"`
StakingKey []byte `gorm:"index"`
Amount uint64 `gorm:"index"`
Assets []Asset
Cbor []byte `gorm:"-"` // This is here for convenience but not represented in the metadata DB
}

Expand Down
63 changes: 55 additions & 8 deletions database/plugin/metadata/sqlite/utxo.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
package sqlite

import (
"encoding/hex"
"fmt"

"github.com/blinklabs-io/dingo/database/plugin/metadata/sqlite/models"
"github.com/blinklabs-io/dingo/database/types"
"github.com/blinklabs-io/gouroboros/ledger"
lcommon "github.com/blinklabs-io/gouroboros/ledger/common"
"gorm.io/gorm"
)

Expand Down Expand Up @@ -203,6 +205,7 @@ func (d *MetadataStoreSqlite) SetUtxo(
payment []byte, // payment
stake []byte, // stake
amount uint64, // amount
assets *lcommon.MultiAsset[lcommon.MultiAssetTypeOutput], // assets (multi-asset)
txn *gorm.DB,
) error {
tmpUtxo := models.Utxo{
Expand All @@ -212,16 +215,21 @@ func (d *MetadataStoreSqlite) SetUtxo(
PaymentKey: payment,
StakingKey: stake,
}

// Handle assets if present
if assets != nil {
tmpUtxo.Assets = convertMultiAssetToModels(assets)
}

var result *gorm.DB
if txn != nil {
result := txn.Create(&tmpUtxo)
if result.Error != nil {
return result.Error
}
result = txn.Create(&tmpUtxo)
} else {
result := d.DB().Create(&tmpUtxo)
if result.Error != nil {
return result.Error
}
result = d.DB().Create(&tmpUtxo)
}

if result.Error != nil {
return result.Error
}
return nil
}
Expand Down Expand Up @@ -312,5 +320,44 @@ func utxoLedgerToModel(
StakingKey: outAddr.StakeKeyHash().Bytes(),
Cbor: utxo.Output.Cbor(),
}

if multiAssetOutput, ok := utxo.Output.(interface {
MultiAsset() *lcommon.MultiAsset[lcommon.MultiAssetTypeOutput]
}); ok {
if multiAsset := multiAssetOutput.MultiAsset(); multiAsset != nil {
ret.Assets = convertMultiAssetToModels(multiAsset)
}
}

return ret
}

func convertMultiAssetToModels(multiAsset *lcommon.MultiAsset[lcommon.MultiAssetTypeOutput]) []models.Asset {
var assets []models.Asset

// Get all policy IDs
policyIds := multiAsset.Policies()
for _, policyId := range policyIds {
policyIdBytes := policyId.Bytes()

// Get asset names for this policy
assetNames := multiAsset.Assets(policyId)
for _, assetNameBytes := range assetNames {
amount := multiAsset.Asset(policyId, assetNameBytes)

// Calculate fingerprint
fingerprint := lcommon.NewAssetFingerprint(policyIdBytes, assetNameBytes)

asset := models.Asset{
Name: assetNameBytes,
NameHex: []byte(hex.EncodeToString(assetNameBytes)),
PolicyId: policyIdBytes,
Fingerprint: []byte(fingerprint.String()),
Amount: types.Uint64(amount),
}
assets = append(assets, asset)
}
}

return assets
}
1 change: 1 addition & 0 deletions database/plugin/metadata/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ type MetadataStore interface {
[]byte, // payment
[]byte, // stake
uint64, // amount
*lcommon.MultiAsset[lcommon.MultiAssetTypeOutput], // asset
*gorm.DB,
) error
SetVoteDelegation(
Expand Down
43 changes: 40 additions & 3 deletions database/utxo.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

"github.com/blinklabs-io/dingo/database/types"
"github.com/blinklabs-io/gouroboros/ledger"
lcommon "github.com/blinklabs-io/gouroboros/ledger/common"
"github.com/dgraph-io/badger/v4"
)

Expand All @@ -33,9 +34,20 @@ type Utxo struct {
PaymentKey []byte `gorm:"index"`
StakingKey []byte `gorm:"index"`
Amount uint64 `gorm:"index"`
Assets []Asset
Cbor []byte `gorm:"-"` // This is not represented in the metadata DB
}

type Asset struct {
ID uint `gorm:"primaryKey"`
UtxoID uint
Name []byte `gorm:"index"`
NameHex []byte `gorm:"index"`
PolicyId []byte `gorm:"index"`
Fingerprint []byte `gorm:"index"`
Amount types.Uint64 `gorm:"index"`
}

func (u *Utxo) TableName() string {
return "utxo"
}
Expand Down Expand Up @@ -66,6 +78,7 @@ func (d *Database) NewUtxo(
slot uint64,
paymentKey, stakeKey, cbor []byte,
amt uint64,
asset *lcommon.MultiAsset[lcommon.MultiAssetTypeOutput],
txn *Txn,
) error {
if txn == nil {
Expand All @@ -85,6 +98,7 @@ func (d *Database) NewUtxo(
paymentKey,
stakeKey,
amt,
asset,
txn.Metadata(),
)
}
Expand Down Expand Up @@ -128,7 +142,19 @@ func (d *Database) UtxoByRef(
if err != nil {
return tmpUtxo, err
}
tmpUtxo = Utxo(utxo)
tmpUtxo = Utxo{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to add assets to the Utxo struct above so this doesn't need to change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wolf31o2 what should be the type for assets in Utxo struct here ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*lcommon.MultiAsset[T]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type that we're getting from d.metadata.GetUtxo() is a models.Utxo with an embedded []models.Asset. We can create a duplicate Asset type in this package and modify the duplicate Utxo type accordingly in the short term, but we're eventually going to need to do some conversion to get it back in a lcommon.MultiAsset. It probably makes sense to just do that here, since this PR already has a conversion function for the other direction.

ID: utxo.ID,
TxId: utxo.TxId,
OutputIdx: utxo.OutputIdx,
AddedSlot: utxo.AddedSlot,
DeletedSlot: utxo.DeletedSlot,
PaymentKey: utxo.PaymentKey,
StakingKey: utxo.StakingKey,
Amount: utxo.Amount,
}
for _, asset := range utxo.Assets {
tmpUtxo.Assets = append(tmpUtxo.Assets, Asset(asset))
}
if err := tmpUtxo.loadCbor(txn); err != nil {
return tmpUtxo, err
}
Expand Down Expand Up @@ -160,9 +186,20 @@ func (d *Database) UtxosByAddress(
if err != nil {
return ret, err
}
var tmpUtxo Utxo
for _, utxo := range utxos {
tmpUtxo = Utxo(utxo)
tmpUtxo := Utxo{
ID: utxo.ID,
TxId: utxo.TxId,
OutputIdx: utxo.OutputIdx,
AddedSlot: utxo.AddedSlot,
DeletedSlot: utxo.DeletedSlot,
PaymentKey: utxo.PaymentKey,
StakingKey: utxo.StakingKey,
Amount: utxo.Amount,
}
for _, asset := range utxo.Assets {
tmpUtxo.Assets = append(tmpUtxo.Assets, Asset(asset))
}
if err := tmpUtxo.loadCbor(txn); err != nil {
return ret, err
}
Expand Down
1 change: 1 addition & 0 deletions ledger/chainsync.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ func (ls *LedgerState) createGenesisBlock() error {
outAddr.StakeKeyHash().Bytes(),
outputCbor,
utxo.Output.Amount(),
utxo.Output.Assets(),
txn,
)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions ledger/delta.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func (d *LedgerDelta) apply(ls *LedgerState, txn *database.Txn) error {
outAddr.StakeKeyHash().Bytes(),
produced.Output.Cbor(),
produced.Output.Amount(),
produced.Output.Assets(),
txn,
)
if err != nil {
Expand Down
Loading