Skip to content

Commit 5b50c6e

Browse files
authored
feat: localstatequery UTxOsByAddress and UTxOsByTxIn support (#554)
This adds support for the UTxOs by Address and UTxOs by TxIn queries. Fixes #315
1 parent c5a7fdf commit 5b50c6e

File tree

3 files changed

+229
-11
lines changed

3 files changed

+229
-11
lines changed

cmd/gouroboros/query.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@
1515
package main
1616

1717
import (
18+
"encoding/hex"
19+
"encoding/json"
1820
"flag"
1921
"fmt"
2022
"os"
23+
"strconv"
24+
"strings"
2125

2226
ouroboros "github.com/blinklabs-io/gouroboros"
27+
"github.com/blinklabs-io/gouroboros/ledger"
2328
"github.com/blinklabs-io/gouroboros/protocol/localstatequery"
2429
)
2530

@@ -165,6 +170,83 @@ func testQuery(f *globalFlags) {
165170
os.Exit(1)
166171
}
167172
fmt.Printf("genesis-config: %#v\n", *genesisConfig)
173+
case "utxos-by-address":
174+
var tmpAddrs []ledger.Address
175+
if len(queryFlags.flagset.Args()) <= 1 {
176+
fmt.Println("No addresses specified")
177+
os.Exit(1)
178+
}
179+
for _, addr := range queryFlags.flagset.Args()[1:] {
180+
tmpAddr, err := ledger.NewAddress(addr)
181+
if err != nil {
182+
fmt.Printf("Invalid address %q: %s", addr, err)
183+
os.Exit(1)
184+
}
185+
tmpAddrs = append(tmpAddrs, tmpAddr)
186+
}
187+
utxos, err := o.LocalStateQuery().Client.GetUTxOByAddress(tmpAddrs)
188+
if err != nil {
189+
fmt.Printf("ERROR: failure querying UTxOs by address: %s\n", err)
190+
os.Exit(1)
191+
}
192+
for utxoId, utxo := range utxos.Results {
193+
fmt.Println("---")
194+
fmt.Printf("UTxO ID: %s#%d\n", utxoId.Hash.String(), utxoId.Idx)
195+
fmt.Printf("Amount: %d\n", utxo.OutputAmount.Amount)
196+
if utxo.OutputAmount.Assets != nil {
197+
assetsJson, err := json.Marshal(utxo.OutputAmount.Assets)
198+
if err != nil {
199+
fmt.Printf("ERROR: failed to marshal asset JSON: %s\n", err)
200+
os.Exit(1)
201+
}
202+
fmt.Printf("Assets: %s\n", assetsJson)
203+
}
204+
}
205+
case "utxos-by-txin":
206+
var tmpTxIns []ledger.TransactionInput
207+
if len(queryFlags.flagset.Args()) <= 1 {
208+
fmt.Println("No UTxO IDs specified")
209+
os.Exit(1)
210+
}
211+
for _, txIn := range queryFlags.flagset.Args()[1:] {
212+
txInParts := strings.SplitN(txIn, `#`, 2)
213+
if len(txInParts) != 2 {
214+
fmt.Printf("Invalid UTxO ID %q", txIn)
215+
os.Exit(1)
216+
}
217+
txIdHex, err := hex.DecodeString(txInParts[0])
218+
if err != nil {
219+
fmt.Printf("Invalid UTxO ID %q: %s", txIn, err)
220+
os.Exit(1)
221+
}
222+
txOutputIdx, err := strconv.ParseUint(txInParts[1], 10, 32)
223+
if err != nil {
224+
fmt.Printf("Invalid UTxO ID %q: %s", txIn, err)
225+
}
226+
tmpTxIn := ledger.ShelleyTransactionInput{
227+
TxId: ledger.Blake2b256(txIdHex),
228+
OutputIndex: uint32(txOutputIdx),
229+
}
230+
tmpTxIns = append(tmpTxIns, tmpTxIn)
231+
}
232+
utxos, err := o.LocalStateQuery().Client.GetUTxOByTxIn(tmpTxIns)
233+
if err != nil {
234+
fmt.Printf("ERROR: failure querying UTxOs by TxIn: %s\n", err)
235+
os.Exit(1)
236+
}
237+
for utxoId, utxo := range utxos.Results {
238+
fmt.Println("---")
239+
fmt.Printf("UTxO ID: %s#%d\n", utxoId.Hash.String(), utxoId.Idx)
240+
fmt.Printf("Amount: %d\n", utxo.OutputAmount.Amount)
241+
if utxo.OutputAmount.Assets != nil {
242+
assetsJson, err := json.Marshal(utxo.OutputAmount.Assets)
243+
if err != nil {
244+
fmt.Printf("ERROR: failed to marshal asset JSON: %s\n", err)
245+
os.Exit(1)
246+
}
247+
fmt.Printf("Assets: %s\n", assetsJson)
248+
}
249+
}
168250
default:
169251
fmt.Printf("ERROR: unknown query: %s\n", queryFlags.flagset.Args()[0])
170252
os.Exit(1)

protocol/localstatequery/client.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,9 @@ func (c *Client) GetEpochNo() (int, error) {
309309
}
310310

311311
// TODO
312+
/*
313+
query [2 #6.258([*[0 int]]) int is the stake the user intends to delegate, the array must be sorted
314+
*/
312315
func (c *Client) GetNonMyopicMemberRewards() (*NonMyopicMemberRewardsResult, error) {
313316
c.busyMutex.Lock()
314317
defer c.busyMutex.Unlock()
@@ -355,7 +358,6 @@ func (c *Client) GetCurrentProtocolParams() (CurrentProtocolParamsResult, error)
355358
}
356359
}
357360

358-
// TODO
359361
func (c *Client) GetProposedProtocolParamsUpdates() (*ProposedProtocolParamsUpdatesResult, error) {
360362
c.busyMutex.Lock()
361363
defer c.busyMutex.Unlock()
@@ -394,9 +396,8 @@ func (c *Client) GetStakeDistribution() (*StakeDistributionResult, error) {
394396
return &result, nil
395397
}
396398

397-
// TODO
398399
func (c *Client) GetUTxOByAddress(
399-
addrs []interface{},
400+
addrs []ledger.Address,
400401
) (*UTxOByAddressResult, error) {
401402
c.busyMutex.Lock()
402403
defer c.busyMutex.Unlock()
@@ -407,6 +408,7 @@ func (c *Client) GetUTxOByAddress(
407408
query := buildShelleyQuery(
408409
currentEra,
409410
QueryTypeShelleyUtxoByAddress,
411+
addrs,
410412
)
411413
var result UTxOByAddressResult
412414
if err := c.runQuery(query, &result); err != nil {
@@ -415,7 +417,6 @@ func (c *Client) GetUTxOByAddress(
415417
return &result, nil
416418
}
417419

418-
// TODO
419420
func (c *Client) GetUTxOWhole() (*UTxOWholeResult, error) {
420421
c.busyMutex.Lock()
421422
defer c.busyMutex.Unlock()
@@ -454,6 +455,9 @@ func (c *Client) DebugEpochState() (*DebugEpochStateResult, error) {
454455
}
455456

456457
// TODO
458+
/*
459+
query [10 #6.258([ *rwdr ])]
460+
*/
457461
func (c *Client) GetFilteredDelegationsAndRewardAccounts(
458462
creds []interface{},
459463
) (*FilteredDelegationsAndRewardAccountsResult, error) {
@@ -466,6 +470,7 @@ func (c *Client) GetFilteredDelegationsAndRewardAccounts(
466470
query := buildShelleyQuery(
467471
currentEra,
468472
QueryTypeShelleyFilteredDelegationAndRewardAccounts,
473+
// TODO: add params
469474
)
470475
var result FilteredDelegationsAndRewardAccountsResult
471476
if err := c.runQuery(query, &result); err != nil {
@@ -474,7 +479,6 @@ func (c *Client) GetFilteredDelegationsAndRewardAccounts(
474479
return &result, nil
475480
}
476481

477-
// TODO
478482
func (c *Client) GetGenesisConfig() (*GenesisConfigResult, error) {
479483
c.busyMutex.Lock()
480484
defer c.busyMutex.Unlock()
@@ -531,7 +535,6 @@ func (c *Client) DebugChainDepState() (*DebugChainDepStateResult, error) {
531535
return &result, nil
532536
}
533537

534-
// TODO
535538
func (c *Client) GetRewardProvenance() (*RewardProvenanceResult, error) {
536539
c.busyMutex.Lock()
537540
defer c.busyMutex.Unlock()
@@ -550,8 +553,7 @@ func (c *Client) GetRewardProvenance() (*RewardProvenanceResult, error) {
550553
return &result, nil
551554
}
552555

553-
// TODO
554-
func (c *Client) GetUTxOByTxIn(txins []interface{}) (*UTxOByTxInResult, error) {
556+
func (c *Client) GetUTxOByTxIn(txIns []ledger.TransactionInput) (*UTxOByTxInResult, error) {
555557
c.busyMutex.Lock()
556558
defer c.busyMutex.Unlock()
557559
currentEra, err := c.getCurrentEra()
@@ -561,6 +563,7 @@ func (c *Client) GetUTxOByTxIn(txins []interface{}) (*UTxOByTxInResult, error) {
561563
query := buildShelleyQuery(
562564
currentEra,
563565
QueryTypeShelleyUtxoByTxin,
566+
txIns,
564567
)
565568
var result UTxOByTxInResult
566569
if err := c.runQuery(query, &result); err != nil {
@@ -569,7 +572,6 @@ func (c *Client) GetUTxOByTxIn(txins []interface{}) (*UTxOByTxInResult, error) {
569572
return &result, nil
570573
}
571574

572-
// TODO
573575
func (c *Client) GetStakePools() (*StakePoolsResult, error) {
574576
c.busyMutex.Lock()
575577
defer c.busyMutex.Unlock()
@@ -589,6 +591,9 @@ func (c *Client) GetStakePools() (*StakePoolsResult, error) {
589591
}
590592

591593
// TODO
594+
/*
595+
query [17 #6.258([*poolid])
596+
*/
592597
func (c *Client) GetStakePoolParams(
593598
poolIds []interface{},
594599
) (*StakePoolParamsResult, error) {
@@ -601,6 +606,7 @@ func (c *Client) GetStakePoolParams(
601606
query := buildShelleyQuery(
602607
currentEra,
603608
QueryTypeShelleyStakePoolParams,
609+
// TODO: add params
604610
)
605611
var result StakePoolParamsResult
606612
if err := c.runQuery(query, &result); err != nil {

protocol/localstatequery/queries.go

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
package localstatequery
1616

1717
import (
18+
"fmt"
19+
1820
"github.com/blinklabs-io/gouroboros/cbor"
1921
"github.com/blinklabs-io/gouroboros/ledger"
2022
)
@@ -141,6 +143,10 @@ type eraHistoryResultParams struct {
141143
}
142144

143145
// TODO
146+
/*
147+
result [{ *[0 int] => non_myopic_rewards }] for each stake display reward
148+
non_myopic_rewards { *poolid => int } int is the amount of lovelaces each pool would reward
149+
*/
144150
type NonMyopicMemberRewardsResult interface{}
145151

146152
type CurrentProtocolParamsResult interface {
@@ -149,10 +155,68 @@ type CurrentProtocolParamsResult interface {
149155

150156
// TODO
151157
type ProposedProtocolParamsUpdatesResult interface{}
158+
159+
// TODO
160+
/*
161+
result [{ *poolid => [[num den] vrf-hash]}] num/den is the quotient representing the stake fractions
162+
*/
152163
type StakeDistributionResult interface{}
153-
type UTxOByAddressResult interface{}
164+
165+
type UTxOByAddressResult struct {
166+
cbor.StructAsArray
167+
Results map[UtxoId]ledger.BabbageTransactionOutput
168+
}
169+
170+
type UtxoId struct {
171+
cbor.StructAsArray
172+
Hash ledger.Blake2b256
173+
Idx int
174+
DatumHash ledger.Blake2b256
175+
}
176+
177+
func (u *UtxoId) UnmarshalCBOR(data []byte) error {
178+
listLen, err := cbor.ListLength(data)
179+
if err != nil {
180+
return err
181+
}
182+
switch listLen {
183+
case 2:
184+
var tmpData struct {
185+
cbor.StructAsArray
186+
Hash ledger.Blake2b256
187+
Idx int
188+
}
189+
if _, err := cbor.Decode(data, &tmpData); err != nil {
190+
return err
191+
}
192+
u.Hash = tmpData.Hash
193+
u.Idx = tmpData.Idx
194+
case 3:
195+
return cbor.DecodeGeneric(data, u)
196+
default:
197+
return fmt.Errorf("invalid list length: %d", listLen)
198+
}
199+
return nil
200+
}
201+
202+
// TODO
203+
/*
204+
result [{* utxo => value }]
205+
*/
154206
type UTxOWholeResult interface{}
207+
208+
// TODO
155209
type DebugEpochStateResult interface{}
210+
211+
// TODO
212+
/*
213+
rwdr [flag bytestring] bytestring is the keyhash of the staking vkey
214+
flag 0/1 0=keyhash 1=scripthash
215+
result [[ delegation rewards] ]
216+
delegation { * rwdr => poolid } poolid is a bytestring
217+
rewards { * rwdr => int }
218+
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.
219+
*/
156220
type FilteredDelegationsAndRewardAccountsResult interface{}
157221

158222
type GenesisConfigResult struct {
@@ -199,12 +263,78 @@ type GenesisConfigResult struct {
199263

200264
// TODO
201265
type DebugNewEpochStateResult interface{}
266+
267+
// TODO
202268
type DebugChainDepStateResult interface{}
269+
270+
// TODO
271+
/*
272+
result [ *Element ] Expanded in order on the next rows.
273+
Element CDDL Comment
274+
epochLength
275+
poolMints { *poolid => block-count }
276+
maxLovelaceSupply
277+
NA
278+
NA
279+
NA
280+
?circulatingsupply?
281+
total-blocks
282+
?decentralization? [num den]
283+
?available block entries
284+
success-rate [num den]
285+
NA
286+
NA ??treasuryCut
287+
activeStakeGo
288+
nil
289+
nil
290+
*/
203291
type RewardProvenanceResult interface{}
204-
type UTxOByTxInResult interface{}
292+
293+
type UTxOByTxInResult struct {
294+
cbor.StructAsArray
295+
Results map[UtxoId]ledger.BabbageTransactionOutput
296+
}
297+
298+
// TODO
299+
/*
300+
result [#6.258([ *poolid ])]
301+
*/
205302
type StakePoolsResult interface{}
303+
304+
// TODO
305+
/*
306+
result [{ *poolid => [ *pool_param ] }]
307+
pool_param CDDL Comment
308+
operator keyhash
309+
vrf_keyhash keyhash
310+
pledge coin
311+
margin #6.30([num den])
312+
reward_account
313+
pool_owners set<addr_keyhash>
314+
relays [ *relay ]
315+
pool_metadata pool_metadata/null
316+
relay CDDL Comment
317+
single_host_addr [0 port/null ipv4/null ipv6/null]
318+
single_host_name [1 port/null dns_name] An A or AAAA DNS
319+
multi_host_name [2 dns_name] A SRV DNS record
320+
Type CDDL Comment
321+
port uint .le 65535
322+
ipv4 bytes .size 4
323+
ipv6 bytes .size 16
324+
dns_name tstr .size (0..64)
325+
pool_metadata [url metadata_hash]
326+
url tstr .size (0..64)
327+
*/
206328
type StakePoolParamsResult interface{}
329+
330+
// TODO
207331
type RewardInfoPoolsResult interface{}
332+
333+
// TODO
208334
type PoolStateResult interface{}
335+
336+
// TODO
209337
type StakeSnapshotsResult interface{}
338+
339+
// TODO
210340
type PoolDistrResult interface{}

0 commit comments

Comments
 (0)