From 5e7a8df645bb444af708946b1e947856e6c990f2 Mon Sep 17 00:00:00 2001 From: Aurora Gaffney Date: Sat, 16 Mar 2024 17:18:31 -0500 Subject: [PATCH] feat: localstatequery UTxOsByAddress and UTxOsByTxIn support This adds support for the UTxOs by Address and UTxOs by TxIn queries. Fixes #315 --- cmd/gouroboros/query.go | 82 +++++++++++++++++ protocol/localstatequery/client.go | 24 +++-- protocol/localstatequery/queries.go | 134 +++++++++++++++++++++++++++- 3 files changed, 229 insertions(+), 11 deletions(-) diff --git a/cmd/gouroboros/query.go b/cmd/gouroboros/query.go index 34539040..caa0f390 100644 --- a/cmd/gouroboros/query.go +++ b/cmd/gouroboros/query.go @@ -15,11 +15,16 @@ package main import ( + "encoding/hex" + "encoding/json" "flag" "fmt" "os" + "strconv" + "strings" ouroboros "github.com/blinklabs-io/gouroboros" + "github.com/blinklabs-io/gouroboros/ledger" "github.com/blinklabs-io/gouroboros/protocol/localstatequery" ) @@ -165,6 +170,83 @@ func testQuery(f *globalFlags) { os.Exit(1) } fmt.Printf("genesis-config: %#v\n", *genesisConfig) + case "utxos-by-address": + var tmpAddrs []ledger.Address + if len(queryFlags.flagset.Args()) <= 1 { + fmt.Println("No addresses specified") + os.Exit(1) + } + for _, addr := range queryFlags.flagset.Args()[1:] { + tmpAddr, err := ledger.NewAddress(addr) + if err != nil { + fmt.Printf("Invalid address %q: %s", addr, err) + os.Exit(1) + } + tmpAddrs = append(tmpAddrs, tmpAddr) + } + utxos, err := o.LocalStateQuery().Client.GetUTxOByAddress(tmpAddrs) + if err != nil { + fmt.Printf("ERROR: failure querying UTxOs by address: %s\n", err) + os.Exit(1) + } + for utxoId, utxo := range utxos.Results { + fmt.Println("---") + fmt.Printf("UTxO ID: %s#%d\n", utxoId.Hash.String(), utxoId.Idx) + fmt.Printf("Amount: %d\n", utxo.OutputAmount.Amount) + if utxo.OutputAmount.Assets != nil { + assetsJson, err := json.Marshal(utxo.OutputAmount.Assets) + if err != nil { + fmt.Printf("ERROR: failed to marshal asset JSON: %s\n", err) + os.Exit(1) + } + fmt.Printf("Assets: %s\n", assetsJson) + } + } + case "utxos-by-txin": + var tmpTxIns []ledger.TransactionInput + if len(queryFlags.flagset.Args()) <= 1 { + fmt.Println("No UTxO IDs specified") + os.Exit(1) + } + for _, txIn := range queryFlags.flagset.Args()[1:] { + txInParts := strings.SplitN(txIn, `#`, 2) + if len(txInParts) != 2 { + fmt.Printf("Invalid UTxO ID %q", txIn) + os.Exit(1) + } + txIdHex, err := hex.DecodeString(txInParts[0]) + if err != nil { + fmt.Printf("Invalid UTxO ID %q: %s", txIn, err) + os.Exit(1) + } + txOutputIdx, err := strconv.ParseUint(txInParts[1], 10, 32) + if err != nil { + fmt.Printf("Invalid UTxO ID %q: %s", txIn, err) + } + tmpTxIn := ledger.ShelleyTransactionInput{ + TxId: ledger.Blake2b256(txIdHex), + OutputIndex: uint32(txOutputIdx), + } + tmpTxIns = append(tmpTxIns, tmpTxIn) + } + utxos, err := o.LocalStateQuery().Client.GetUTxOByTxIn(tmpTxIns) + if err != nil { + fmt.Printf("ERROR: failure querying UTxOs by TxIn: %s\n", err) + os.Exit(1) + } + for utxoId, utxo := range utxos.Results { + fmt.Println("---") + fmt.Printf("UTxO ID: %s#%d\n", utxoId.Hash.String(), utxoId.Idx) + fmt.Printf("Amount: %d\n", utxo.OutputAmount.Amount) + if utxo.OutputAmount.Assets != nil { + assetsJson, err := json.Marshal(utxo.OutputAmount.Assets) + if err != nil { + fmt.Printf("ERROR: failed to marshal asset JSON: %s\n", err) + os.Exit(1) + } + fmt.Printf("Assets: %s\n", assetsJson) + } + } default: fmt.Printf("ERROR: unknown query: %s\n", queryFlags.flagset.Args()[0]) os.Exit(1) diff --git a/protocol/localstatequery/client.go b/protocol/localstatequery/client.go index 90c96670..b38fd7ec 100644 --- a/protocol/localstatequery/client.go +++ b/protocol/localstatequery/client.go @@ -309,6 +309,9 @@ func (c *Client) GetEpochNo() (int, error) { } // TODO +/* +query [2 #6.258([*[0 int]]) int is the stake the user intends to delegate, the array must be sorted +*/ func (c *Client) GetNonMyopicMemberRewards() (*NonMyopicMemberRewardsResult, error) { c.busyMutex.Lock() defer c.busyMutex.Unlock() @@ -355,7 +358,6 @@ func (c *Client) GetCurrentProtocolParams() (CurrentProtocolParamsResult, error) } } -// TODO func (c *Client) GetProposedProtocolParamsUpdates() (*ProposedProtocolParamsUpdatesResult, error) { c.busyMutex.Lock() defer c.busyMutex.Unlock() @@ -394,9 +396,8 @@ func (c *Client) GetStakeDistribution() (*StakeDistributionResult, error) { return &result, nil } -// TODO func (c *Client) GetUTxOByAddress( - addrs []interface{}, + addrs []ledger.Address, ) (*UTxOByAddressResult, error) { c.busyMutex.Lock() defer c.busyMutex.Unlock() @@ -407,6 +408,7 @@ func (c *Client) GetUTxOByAddress( query := buildShelleyQuery( currentEra, QueryTypeShelleyUtxoByAddress, + addrs, ) var result UTxOByAddressResult if err := c.runQuery(query, &result); err != nil { @@ -415,7 +417,6 @@ func (c *Client) GetUTxOByAddress( return &result, nil } -// TODO func (c *Client) GetUTxOWhole() (*UTxOWholeResult, error) { c.busyMutex.Lock() defer c.busyMutex.Unlock() @@ -454,6 +455,9 @@ func (c *Client) DebugEpochState() (*DebugEpochStateResult, error) { } // TODO +/* +query [10 #6.258([ *rwdr ])] +*/ func (c *Client) GetFilteredDelegationsAndRewardAccounts( creds []interface{}, ) (*FilteredDelegationsAndRewardAccountsResult, error) { @@ -466,6 +470,7 @@ func (c *Client) GetFilteredDelegationsAndRewardAccounts( query := buildShelleyQuery( currentEra, QueryTypeShelleyFilteredDelegationAndRewardAccounts, + // TODO: add params ) var result FilteredDelegationsAndRewardAccountsResult if err := c.runQuery(query, &result); err != nil { @@ -474,7 +479,6 @@ func (c *Client) GetFilteredDelegationsAndRewardAccounts( return &result, nil } -// TODO func (c *Client) GetGenesisConfig() (*GenesisConfigResult, error) { c.busyMutex.Lock() defer c.busyMutex.Unlock() @@ -531,7 +535,6 @@ func (c *Client) DebugChainDepState() (*DebugChainDepStateResult, error) { return &result, nil } -// TODO func (c *Client) GetRewardProvenance() (*RewardProvenanceResult, error) { c.busyMutex.Lock() defer c.busyMutex.Unlock() @@ -550,8 +553,7 @@ func (c *Client) GetRewardProvenance() (*RewardProvenanceResult, error) { return &result, nil } -// TODO -func (c *Client) GetUTxOByTxIn(txins []interface{}) (*UTxOByTxInResult, error) { +func (c *Client) GetUTxOByTxIn(txIns []ledger.TransactionInput) (*UTxOByTxInResult, error) { c.busyMutex.Lock() defer c.busyMutex.Unlock() currentEra, err := c.getCurrentEra() @@ -561,6 +563,7 @@ func (c *Client) GetUTxOByTxIn(txins []interface{}) (*UTxOByTxInResult, error) { query := buildShelleyQuery( currentEra, QueryTypeShelleyUtxoByTxin, + txIns, ) var result UTxOByTxInResult if err := c.runQuery(query, &result); err != nil { @@ -569,7 +572,6 @@ func (c *Client) GetUTxOByTxIn(txins []interface{}) (*UTxOByTxInResult, error) { return &result, nil } -// TODO func (c *Client) GetStakePools() (*StakePoolsResult, error) { c.busyMutex.Lock() defer c.busyMutex.Unlock() @@ -589,6 +591,9 @@ func (c *Client) GetStakePools() (*StakePoolsResult, error) { } // TODO +/* +query [17 #6.258([*poolid]) +*/ func (c *Client) GetStakePoolParams( poolIds []interface{}, ) (*StakePoolParamsResult, error) { @@ -601,6 +606,7 @@ func (c *Client) GetStakePoolParams( query := buildShelleyQuery( currentEra, QueryTypeShelleyStakePoolParams, + // TODO: add params ) var result StakePoolParamsResult if err := c.runQuery(query, &result); err != nil { diff --git a/protocol/localstatequery/queries.go b/protocol/localstatequery/queries.go index fc06b59c..8fe69c07 100644 --- a/protocol/localstatequery/queries.go +++ b/protocol/localstatequery/queries.go @@ -15,6 +15,8 @@ package localstatequery import ( + "fmt" + "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/gouroboros/ledger" ) @@ -141,6 +143,10 @@ type eraHistoryResultParams struct { } // TODO +/* +result [{ *[0 int] => non_myopic_rewards }] for each stake display reward +non_myopic_rewards { *poolid => int } int is the amount of lovelaces each pool would reward +*/ type NonMyopicMemberRewardsResult interface{} type CurrentProtocolParamsResult interface { @@ -149,10 +155,68 @@ type CurrentProtocolParamsResult interface { // TODO type ProposedProtocolParamsUpdatesResult interface{} + +// TODO +/* +result [{ *poolid => [[num den] vrf-hash]}] num/den is the quotient representing the stake fractions +*/ type StakeDistributionResult interface{} -type UTxOByAddressResult interface{} + +type UTxOByAddressResult struct { + cbor.StructAsArray + Results map[UtxoId]ledger.BabbageTransactionOutput +} + +type UtxoId struct { + cbor.StructAsArray + Hash ledger.Blake2b256 + Idx int + DatumHash ledger.Blake2b256 +} + +func (u *UtxoId) UnmarshalCBOR(data []byte) error { + listLen, err := cbor.ListLength(data) + if err != nil { + return err + } + switch listLen { + case 2: + var tmpData struct { + cbor.StructAsArray + Hash ledger.Blake2b256 + Idx int + } + if _, err := cbor.Decode(data, &tmpData); err != nil { + return err + } + u.Hash = tmpData.Hash + u.Idx = tmpData.Idx + case 3: + return cbor.DecodeGeneric(data, u) + default: + return fmt.Errorf("invalid list length: %d", listLen) + } + return nil +} + +// TODO +/* +result [{* utxo => value }] +*/ type UTxOWholeResult interface{} + +// TODO type DebugEpochStateResult interface{} + +// TODO +/* +rwdr [flag bytestring] bytestring is the keyhash of the staking vkey +flag 0/1 0=keyhash 1=scripthash +result [[ delegation rewards] ] +delegation { * rwdr => poolid } poolid is a bytestring +rewards { * rwdr => int } +It seems to be a requirement to sort the reward addresses on the query. Scripthash addresses come first, then within a group the bytestring being a network order integer sort ascending. +*/ type FilteredDelegationsAndRewardAccountsResult interface{} type GenesisConfigResult struct { @@ -199,12 +263,78 @@ type GenesisConfigResult struct { // TODO type DebugNewEpochStateResult interface{} + +// TODO type DebugChainDepStateResult interface{} + +// TODO +/* +result [ *Element ] Expanded in order on the next rows. +Element CDDL Comment +epochLength +poolMints { *poolid => block-count } +maxLovelaceSupply +NA +NA +NA +?circulatingsupply? +total-blocks +?decentralization? [num den] +?available block entries +success-rate [num den] +NA +NA ??treasuryCut +activeStakeGo +nil +nil +*/ type RewardProvenanceResult interface{} -type UTxOByTxInResult interface{} + +type UTxOByTxInResult struct { + cbor.StructAsArray + Results map[UtxoId]ledger.BabbageTransactionOutput +} + +// TODO +/* +result [#6.258([ *poolid ])] +*/ type StakePoolsResult interface{} + +// TODO +/* +result [{ *poolid => [ *pool_param ] }] +pool_param CDDL Comment +operator keyhash +vrf_keyhash keyhash +pledge coin +margin #6.30([num den]) +reward_account +pool_owners set +relays [ *relay ] +pool_metadata pool_metadata/null +relay CDDL Comment +single_host_addr [0 port/null ipv4/null ipv6/null] +single_host_name [1 port/null dns_name] An A or AAAA DNS +multi_host_name [2 dns_name] A SRV DNS record +Type CDDL Comment +port uint .le 65535 +ipv4 bytes .size 4 +ipv6 bytes .size 16 +dns_name tstr .size (0..64) +pool_metadata [url metadata_hash] +url tstr .size (0..64) +*/ type StakePoolParamsResult interface{} + +// TODO type RewardInfoPoolsResult interface{} + +// TODO type PoolStateResult interface{} + +// TODO type StakeSnapshotsResult interface{} + +// TODO type PoolDistrResult interface{}