From f691775e80a77d0a1da5e8e0a50749467208b4b7 Mon Sep 17 00:00:00 2001 From: Chris Gianelloni Date: Wed, 22 Jan 2025 17:22:24 -0500 Subject: [PATCH] refactor: create client receiver funcs and helpers Create helper functions at the top to reduce the amount of UTxORPC specific objects users will need to encounter. Signed-off-by: Chris Gianelloni --- examples/query/main.go | 153 ++++++++++++++++++---------------------- examples/submit/main.go | 105 +++++++++------------------ examples/sync/main.go | 145 +++++++++---------------------------- main.go | 70 +++++++++++------- query.go | 98 +++++++++++++++++++++++++ submit.go | 116 ++++++++++++++++++++++++++++++ sync.go | 82 +++++++++++++++++++++ watch.go | 64 +++++++++++++++++ 8 files changed, 541 insertions(+), 292 deletions(-) create mode 100644 query.go create mode 100644 submit.go create mode 100644 sync.go create mode 100644 watch.go diff --git a/examples/query/main.go b/examples/query/main.go index 92d2e1b..252dc9b 100644 --- a/examples/query/main.go +++ b/examples/query/main.go @@ -1,13 +1,11 @@ package main import ( - "context" - "encoding/base64" "encoding/hex" "fmt" "log" + "os" - "connectrpc.com/connect" "github.com/blinklabs-io/gouroboros/ledger/common" "github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano" "github.com/utxorpc/go-codegen/utxorpc/v1alpha/query" @@ -16,96 +14,75 @@ import ( ) func main() { - - ctx := context.Background() - baseUrl := "https://preview.utxorpc-v0.demeter.run" + baseUrl := os.Getenv("UTXORPC_URL") + if baseUrl == "" { + baseUrl = "https://preview.utxorpc-v0.demeter.run" + } + client := utxorpc.NewClient(utxorpc.WithBaseUrl(baseUrl)) + dmtrApiKey := os.Getenv("DMTR_API_KEY") // set API key for demeter - client := utxorpc.CreateUtxoRPCClient(baseUrl, - // set API key for demeter - utxorpc.WithHeaders(map[string]string{ - "dmtr-api-key": "dmtr_apikey...", - }), - ) - - // Set mode to "readParams", "readUtxos", "searchUtxos" to select the desired example. - var mode string = "searchUtxos" - - switch mode { - case "readParams": - readParams(ctx, client) - case "readUtxos": - // readUtxos(ctx, client, "71a7498f086d378ec5e558581286629b678be1dd65d5d4e2a5d634ba6fdf8299", 0) - // readUtxos(ctx, client, "791309b6b0facc80b3fa896e830999e7bef321ea5279f6bedbe1279ee1e9d4ae", 1) - readUtxos(ctx, client, "24efe5f12d1d93bb419cfb84338d6602dfe78c614b489edb72df0594a077431c", 0) - case "searchUtxos": - // searchUtxos(ctx, client, "addr_test1qzrkvcfvd7k5jx54xxkz87p8xn88304jd2g4jsa0hwwmg20k3c7k36lsg8rdupz6e36j5ctzs6lzjymc9vw7djrmgdnqff9z6j", "", "") - // https://preprod.cexplorer.io/asset/asset1tvkt35str8aeepuflxmnjzcdj87em8xrlx4ehz - // Use policy ID and asset name in hex format (https://cips.cardano.org/cip/CIP-68/) - // Hunt - searchUtxos(ctx, client, "addr_test1qptfy9zhaeuqfptcu79q6gm9l3r6cfp5gnlqc7m42qwln0lsvex239qmryg4yh3pda3rh3rnce4wd46gdyqlscrq7s4shekqrt", "63f9a5fc96d4f87026e97af4569975016b50eef092a46859b61898e5", "0014df1048554e54") - // Dedi - searchUtxos(ctx, client, "addr_test1qptfy9zhaeuqfptcu79q6gm9l3r6cfp5gnlqc7m42qwln0lsvex239qmryg4yh3pda3rh3rnce4wd46gdyqlscrq7s4shekqrt", "63f9a5fc96d4f87026e97af4569975016b50eef092a46859b61898e5", "0014df1044454449") - // No assets - searchUtxos(ctx, client, "addr_test1qzrkvcfvd7k5jx54xxkz87p8xn88304jd2g4jsa0hwwmg20k3c7k36lsg8rdupz6e36j5ctzs6lzjymc9vw7djrmgdnqff9z6j", "63f9a5fc96d4f87026e97af4569975016b50eef092a46859b61898e5", "0014df1044454449") - default: - fmt.Println("Unknown mode:", mode) + if dmtrApiKey != "" { + client.SetHeader("dmtr-api-key", "dmtr_apikey...") } -} -func readParams(ctx context.Context, client *utxorpc.UtxorpcClient) { - req := connect.NewRequest(&query.ReadParamsRequest{}) - client.AddHeadersToRequest(req) + // Run them all + readParams(client) + readUtxo( + client, + "24efe5f12d1d93bb419cfb84338d6602dfe78c614b489edb72df0594a077431c", + 0, + ) + // https://preprod.cexplorer.io/asset/asset1tvkt35str8aeepuflxmnjzcdj87em8xrlx4ehz + // Use policy ID and asset name in hex format (https://cips.cardano.org/cip/CIP-68/) + // Hunt + searchUtxos( + client, + "addr_test1qptfy9zhaeuqfptcu79q6gm9l3r6cfp5gnlqc7m42qwln0lsvex239qmryg4yh3pda3rh3rnce4wd46gdyqlscrq7s4shekqrt", + "63f9a5fc96d4f87026e97af4569975016b50eef092a46859b61898e5", + "0014df1048554e54", + ) + // Dedi + searchUtxos( + client, + "addr_test1qptfy9zhaeuqfptcu79q6gm9l3r6cfp5gnlqc7m42qwln0lsvex239qmryg4yh3pda3rh3rnce4wd46gdyqlscrq7s4shekqrt", + "63f9a5fc96d4f87026e97af4569975016b50eef092a46859b61898e5", + "0014df1044454449", + ) + // No assets + searchUtxos( + client, + "addr_test1qzrkvcfvd7k5jx54xxkz87p8xn88304jd2g4jsa0hwwmg20k3c7k36lsg8rdupz6e36j5ctzs6lzjymc9vw7djrmgdnqff9z6j", + "63f9a5fc96d4f87026e97af4569975016b50eef092a46859b61898e5", + "0014df1044454449", + ) +} +func readParams(client *utxorpc.UtxorpcClient) { fmt.Println("Connecting to utxorpc host:", client.URL()) - resp, err := client.Query.ReadParams(ctx, req) + resp, err := client.ReadParams() if err != nil { utxorpc.HandleError(err) } fmt.Printf("Response: %+v\n", resp) if resp.Msg.LedgerTip != nil { - fmt.Printf("Ledger Tip: Slot: %d, Hash: %x\n", resp.Msg.LedgerTip.Slot, resp.Msg.LedgerTip.Hash) + fmt.Printf( + "Ledger Tip: Slot: %d, Hash: %x\n", + resp.Msg.LedgerTip.Slot, + resp.Msg.LedgerTip.Hash, + ) } if resp.Msg.Values != nil { fmt.Printf("Cardano: %+v\n", resp.Msg.Values) } } -func readUtxos(ctx context.Context, client *utxorpc.UtxorpcClient, txHashStr string, txIndex uint32) { - var txHashBytes []byte - var err error - - // Attempt to decode the input as hex - txHashBytes, err = hex.DecodeString(txHashStr) - if err == nil { - log.Printf("Input txHashStr decoded from hex.") - } else { - // If not hex, attempt to decode as Base64 - txHashBytes, err = base64.StdEncoding.DecodeString(txHashStr) - if err == nil { - log.Printf("Input txHashStr decoded from Base64.") - } else { - log.Printf("Input txHashStr is neither valid hex nor Base64.") - fmt.Println("Error: txHashStr must be a valid hexadecimal or Base64 string.") - return - } - } - - // Create TxoRef with the decoded hash bytes - txoRef := &query.TxoRef{ - Hash: txHashBytes, // Use the decoded []byte - Index: txIndex, - } - - // Prepare the request - req := connect.NewRequest(&query.ReadUtxosRequest{ - Keys: []*query.TxoRef{txoRef}, - }) - client.AddHeadersToRequest(req) - fmt.Println("Connecting to utxorpc host:", client.URL()) - - // Send the request - resp, err := client.Query.ReadUtxos(ctx, req) +func readUtxo( + client *utxorpc.UtxorpcClient, + txHashStr string, + txIndex uint32, +) { + resp, err := client.ReadUtxo(txHashStr, txIndex) if err != nil { utxorpc.HandleError(err) return @@ -115,7 +92,11 @@ func readUtxos(ctx context.Context, client *utxorpc.UtxorpcClient, txHashStr str fmt.Printf("Response: %+v\n", resp) if resp.Msg.LedgerTip != nil { - fmt.Printf("Ledger Tip:\n Slot: %d\n Hash: %x\n", resp.Msg.LedgerTip.Slot, resp.Msg.LedgerTip.Hash) + fmt.Printf( + "Ledger Tip:\n Slot: %d\n Hash: %x\n", + resp.Msg.LedgerTip.Slot, + resp.Msg.LedgerTip.Hash, + ) } for _, item := range resp.Msg.Items { @@ -134,7 +115,12 @@ func readUtxos(ctx context.Context, client *utxorpc.UtxorpcClient, txHashStr str } } -func searchUtxos(ctx context.Context, client *utxorpc.UtxorpcClient, rawAddress string, policyID string, assetName string) { +func searchUtxos( + client *utxorpc.UtxorpcClient, + rawAddress string, + policyID string, + assetName string, +) { // Use to support bech32/base58 addresses addr, err := common.NewAddress(rawAddress) if err != nil { @@ -208,11 +194,8 @@ func searchUtxos(ctx context.Context, client *utxorpc.UtxorpcClient, rawAddress StartToken: "", // For pagination; empty for the first page } - req := connect.NewRequest(searchRequest) - client.AddHeadersToRequest(req) - fmt.Println("connecting to utxorpc host:", client.URL()) - resp, err := client.Query.SearchUtxos(ctx, req) + resp, err := client.SearchUtxos(searchRequest) if err != nil { utxorpc.HandleError(err) } @@ -221,7 +204,11 @@ func searchUtxos(ctx context.Context, client *utxorpc.UtxorpcClient, rawAddress // fmt.Printf("Response: %+v\n", resp) if resp.Msg.LedgerTip != nil { - fmt.Printf("Ledger Tip:\n Slot: %d\n Hash: %x\n", resp.Msg.LedgerTip.Slot, resp.Msg.LedgerTip.Hash) + fmt.Printf( + "Ledger Tip:\n Slot: %d\n Hash: %x\n", + resp.Msg.LedgerTip.Slot, + resp.Msg.LedgerTip.Hash, + ) } for _, item := range resp.Msg.Items { diff --git a/examples/submit/main.go b/examples/submit/main.go index b6624b6..0c70542 100644 --- a/examples/submit/main.go +++ b/examples/submit/main.go @@ -1,9 +1,9 @@ package main import ( - "context" "encoding/hex" "fmt" + "os" "connectrpc.com/connect" "github.com/utxorpc/go-codegen/utxorpc/v1alpha/submit" @@ -11,82 +11,64 @@ import ( ) func main() { - ctx := context.Background() - baseUrl := "https://preview.utxorpc-v0.demeter.run" + baseUrl := os.Getenv("UTXORPC_URL") + if baseUrl == "" { + baseUrl = "https://preview.utxorpc-v0.demeter.run" + } + client := utxorpc.NewClient(utxorpc.WithBaseUrl(baseUrl)) + dmtrApiKey := os.Getenv("DMTR_API_KEY") // set API key for demeter - client := utxorpc.CreateUtxoRPCClient(baseUrl, - // set API key for demeter - utxorpc.WithHeaders(map[string]string{ - "dmtr-api-key": "dmtr_utxorpc1...", - }), - ) + if dmtrApiKey != "" { + client.SetHeader("dmtr-api-key", "dmtr_apikey...") + } // Set mode to "submitTx", "readMempool", "waitForTx", or "watchMempool" to select the desired example. - var mode string = "submitTx" + var mode string = "readMempool" switch mode { case "submitTx": // Submit a transaction txCbor := "Replace this with signed CBOR transaction" - txRefs, err := submitTx(ctx, client, txCbor) + txRef, err := submitTx(client, txCbor) if err != nil { fmt.Printf("Error submitting transaction: %v\n", err) return } // Immediately wait for the transaction confirmation - if err := waitForTx(ctx, client, txRefs); err != nil { + if err := waitForTx(client, txRef); err != nil { fmt.Printf("Error waiting for transaction: %v\n", err) } case "readMempool": - readMempool(ctx, client) + readMempool(client) case "waitForTx": - if err := waitForTx(ctx, client, []string{"31bffedd962f4a6f5e85620985ccdf71f7b78988a6483e090f42d1e8badcebc8"}); err != nil { + if err := waitForTx(client, "31bffedd962f4a6f5e85620985ccdf71f7b78988a6483e090f42d1e8badcebc8"); err != nil { fmt.Printf("Error waiting for transaction: %v\n", err) } case "watchMempool": - watchMempool(ctx, client) + watchMempool(client) default: fmt.Println("Unknown mode:", mode) } } // Modified submitTx to return transaction references -func submitTx(ctx context.Context, client *utxorpc.UtxorpcClient, txCbor string) ([]string, error) { - // Decode the transaction data from hex - txRawBytes, err := hex.DecodeString(txCbor) - if err != nil { - return nil, fmt.Errorf("failed to decode transaction hash: %w", err) - } - - // Create a SubmitTxRequest with the transaction data - tx := &submit.AnyChainTx{ - Type: &submit.AnyChainTx_Raw{ - Raw: txRawBytes, - }, - } - - // Create a list with one transaction - req := connect.NewRequest(&submit.SubmitTxRequest{ - Tx: []*submit.AnyChainTx{tx}, - }) - client.AddHeadersToRequest(req) - +func submitTx(client *utxorpc.UtxorpcClient, txCbor string) (string, error) { fmt.Println("Connecting to utxorpc host:", client.URL()) - resp, err := client.Submit.SubmitTx(ctx, req) + resp, err := client.SubmitTx(txCbor) if err != nil { if connectErr, ok := err.(*connect.Error); ok { // Extract error details errorCode := connectErr.Code() errorMessage := connectErr.Error() grpcMessage := connectErr.Meta().Get("Grpc-Message") - return nil, fmt.Errorf( + return "", fmt.Errorf( "gRPC error occurred:\n Code: %v\n Message: %s\n Details: %s", errorCode, errorMessage, grpcMessage, ) } - return nil, fmt.Errorf("unexpected error occurred: %w", err) + return "", fmt.Errorf("unexpected error occurred: %w", err) } // Extract and return transaction references @@ -98,49 +80,30 @@ func submitTx(ctx context.Context, client *utxorpc.UtxorpcClient, txCbor string) refs = append(refs, hexRef) fmt.Printf(" Ref[%d]: %s\n", i, hexRef) } - return refs, nil + return refs[0], nil } - fmt.Println("No references found in the response.") - return nil, nil + return "", fmt.Errorf("No references found in the response.") } -func readMempool(ctx context.Context, client *utxorpc.UtxorpcClient) { - req := connect.NewRequest(&submit.ReadMempoolRequest{}) - client.AddHeadersToRequest(req) - fmt.Println("Connecting to utxorpc host:", client.URL()) - resp, err := client.Submit.ReadMempool(ctx, req) +func readMempool(client *utxorpc.UtxorpcClient) { + resp, err := client.ReadMempool() if err != nil { utxorpc.HandleError(err) } fmt.Printf("Response: %+v\n", resp) } -func waitForTx(ctx context.Context, client *utxorpc.UtxorpcClient, txRefs []string) error { - fmt.Println("Waiting for the following transaction references:") - for _, ref := range txRefs { - fmt.Printf(" TxRef: %s\n", ref) - } - - // Decode the transaction references from hex - var decodedRefs [][]byte - for _, ref := range txRefs { - refBytes, err := hex.DecodeString(ref) - if err != nil { - return fmt.Errorf("failed to decode transaction reference %s: %w", ref, err) - } - decodedRefs = append(decodedRefs, refBytes) - } - - // Create a WaitForTxRequest with the decoded transaction references - req := connect.NewRequest(&submit.WaitForTxRequest{ - Ref: decodedRefs, - }) - client.AddHeadersToRequest(req) +func waitForTx( + client *utxorpc.UtxorpcClient, + txRef string, +) error { + fmt.Println("Waiting for the following transaction reference:") + fmt.Printf(" TxRef: %s\n", txRef) fmt.Println("Connecting to utxorpc host:", client.URL()) // Open a streaming connection to wait for transaction confirmation - stream, err := client.Submit.WaitForTx(ctx, req) + stream, err := client.WaitForTx(txRef) if err != nil { return fmt.Errorf("failed to open waitForTx stream: %w", err) } @@ -169,11 +132,9 @@ func waitForTx(ctx context.Context, client *utxorpc.UtxorpcClient, txRefs []stri return nil } -func watchMempool(ctx context.Context, client *utxorpc.UtxorpcClient) { - req := connect.NewRequest(&submit.WatchMempoolRequest{}) - client.AddHeadersToRequest(req) +func watchMempool(client *utxorpc.UtxorpcClient) { fmt.Println("Connecting to utxorpc host:", client.URL()) - stream, err := client.Submit.WatchMempool(ctx, req) + stream, err := client.WatchMempool() if err != nil { utxorpc.HandleError(err) } diff --git a/examples/sync/main.go b/examples/sync/main.go index 8b60a15..bbecb86 100644 --- a/examples/sync/main.go +++ b/examples/sync/main.go @@ -1,85 +1,46 @@ package main import ( - "context" "encoding/hex" "fmt" - "log" + "os" - "connectrpc.com/connect" sync "github.com/utxorpc/go-codegen/utxorpc/v1alpha/sync" utxorpc "github.com/utxorpc/go-sdk" - "google.golang.org/protobuf/types/known/fieldmaskpb" ) func main() { - ctx := context.Background() - baseUrl := "https://preview.utxorpc-v0.demeter.run" - client := utxorpc.CreateUtxoRPCClient(baseUrl, - // set API key for demeter - utxorpc.WithHeaders(map[string]string{ - "dmtr-api-key": "dmtr_utxorpc1...", - }), + baseUrl := os.Getenv("UTXORPC_URL") + if baseUrl == "" { + baseUrl = "https://preview.utxorpc-v0.demeter.run" + } + client := utxorpc.NewClient(utxorpc.WithBaseUrl(baseUrl)) + dmtrApiKey := os.Getenv("DMTR_API_KEY") + // set API key for demeter + if dmtrApiKey != "" { + client.SetHeader("dmtr-api-key", "dmtr_apikey...") + } + + // Run them all + fetchBlock( + client, + "235f9a217b826276d6cdfbb05c11572a06aef092535b6df8c682d501af59c230", + 65017558, + ) + followTip( + client, + "235f9a217b826276d6cdfbb05c11572a06aef092535b6df8c682d501af59c230", + 65017558, ) - - // Set mode to "fetchBlock" or "followTip" to select the desired example. - var mode string = "followTip" - - switch mode { - case "fetchBlock": - fetchBlock(ctx, client, "235f9a217b826276d6cdfbb05c11572a06aef092535b6df8c682d501af59c230", 65017558, nil) - case "followTip": - followTip(ctx, client, "235f9a217b826276d6cdfbb05c11572a06aef092535b6df8c682d501af59c230", 65017558, nil) - default: - fmt.Println("Unknown mode:", mode) - } } -func fetchBlock(ctx context.Context, client *utxorpc.UtxorpcClient, blockHash string, blockIndex int64, fieldMaskPaths []string) { - var req *connect.Request[sync.FetchBlockRequest] - var intersect []*sync.BlockRef - var fieldMask *fieldmaskpb.FieldMask - - // Construct the BlockRef based on the provided parameters - blockRef := &sync.BlockRef{} - if blockHash != "" { - hash, err := hex.DecodeString(blockHash) - if err != nil { - log.Fatalf("failed to decode hex string: %v", err) - } - blockRef.Hash = hash - } - // We assume blockIndex can be 0 or any positive number - if blockIndex > -1 { - blockRef.Index = uint64(blockIndex) - } - - // Only add blockRef to intersect if at least one of blockHash or blockIndex is provided - if blockHash != "" || blockIndex > -1 { - intersect = []*sync.BlockRef{blockRef} - } - - // Construct the FieldMask if paths are provided - if len(fieldMaskPaths) > 0 { - fieldMask = &fieldmaskpb.FieldMask{ - Paths: fieldMaskPaths, - } - } - - // Create the FetchBlockRequest - req = connect.NewRequest(&sync.FetchBlockRequest{ - Ref: intersect, - FieldMask: fieldMask, - }) - - // Print BlockRef details if intersect is provided - if len(intersect) > 0 { - fmt.Printf("Blockref: %d, %x\n", req.Msg.Ref[0].Index, req.Msg.Ref[0].Hash) - } - - client.AddHeadersToRequest(req) +func fetchBlock( + client *utxorpc.UtxorpcClient, + blockHash string, + blockIndex int64, +) { fmt.Println("connecting to utxorpc host:", client.URL()) - resp, err := client.Sync.FetchBlock(ctx, req) + resp, err := client.FetchBlock(blockHash, blockIndex) if err != nil { utxorpc.HandleError(err) } @@ -91,51 +52,13 @@ func fetchBlock(ctx context.Context, client *utxorpc.UtxorpcClient, blockHash st } } -func followTip(ctx context.Context, client *utxorpc.UtxorpcClient, blockHash string, blockIndex int64, fieldMaskPaths []string) { - var req *connect.Request[sync.FollowTipRequest] - var intersect []*sync.BlockRef - var fieldMask *fieldmaskpb.FieldMask - - // Construct the BlockRef based on the provided parameters - blockRef := &sync.BlockRef{} - if blockHash != "" { - hash, err := hex.DecodeString(blockHash) - if err != nil { - log.Fatalf("failed to decode hex string: %v", err) - } - blockRef.Hash = hash - } - // We assume blockIndex can be 0 or any positive number - if blockIndex > -1 { - blockRef.Index = uint64(blockIndex) - } - - // Only add blockRef to intersect if at least one of blockHash or blockIndex is provided - if blockHash != "" || blockIndex > -1 { - intersect = []*sync.BlockRef{blockRef} - } - - // Construct the FieldMask if paths are provided - if len(fieldMaskPaths) > 0 { - fieldMask = &fieldmaskpb.FieldMask{ - Paths: fieldMaskPaths, - } - } - - // Create the FollowTipRequest - req = connect.NewRequest(&sync.FollowTipRequest{ - Intersect: intersect, - FieldMask: fieldMask, - }) - - // Print BlockRef details if intersect is provided - if len(intersect) > 0 { - fmt.Printf("Blockref: %d, %x\n", req.Msg.Intersect[0].Index, req.Msg.Intersect[0].Hash) - } - - client.AddHeadersToRequest(req) +func followTip( + client *utxorpc.UtxorpcClient, + blockHash string, + blockIndex int64, +) { fmt.Println("connecting to utxorpc host:", client.URL()) - stream, err := client.Sync.FollowTip(ctx, req) + stream, err := client.FollowTip(blockHash, blockIndex) if err != nil { utxorpc.HandleError(err) return diff --git a/main.go b/main.go index 2a979c0..482f747 100644 --- a/main.go +++ b/main.go @@ -8,10 +8,6 @@ import ( "net/http" "connectrpc.com/connect" - "github.com/utxorpc/go-codegen/utxorpc/v1alpha/query/queryconnect" - "github.com/utxorpc/go-codegen/utxorpc/v1alpha/submit/submitconnect" - "github.com/utxorpc/go-codegen/utxorpc/v1alpha/sync/syncconnect" - "github.com/utxorpc/go-codegen/utxorpc/v1alpha/watch/watchconnect" "golang.org/x/net/http2" ) @@ -19,40 +15,64 @@ type UtxorpcClient struct { httpClient connect.HTTPClient baseUrl string headers map[string]string - Query queryconnect.QueryServiceClient - Submit submitconnect.SubmitServiceClient - Sync syncconnect.SyncServiceClient - Watch watchconnect.WatchServiceClient + Query QueryServiceClient + Submit SubmitServiceClient + Sync SyncServiceClient + Watch WatchServiceClient } type ClientOption func(*UtxorpcClient) +func WithBaseUrl(baseUrl string) ClientOption { + return func(u *UtxorpcClient) { + u.baseUrl = baseUrl + } +} + func WithHeaders(headers map[string]string) ClientOption { - return func(client *UtxorpcClient) { - client.headers = headers + return func(u *UtxorpcClient) { + u.headers = headers } } -func NewClient(httpClient *http.Client, baseUrl string, options ...ClientOption) *UtxorpcClient { - client := &UtxorpcClient{ - httpClient: httpClient, - baseUrl: baseUrl, - Query: queryconnect.NewQueryServiceClient(httpClient, baseUrl, connect.WithGRPC()), - Submit: submitconnect.NewSubmitServiceClient(httpClient, baseUrl, connect.WithGRPC()), - Sync: syncconnect.NewSyncServiceClient(httpClient, baseUrl, connect.WithGRPC()), - Watch: watchconnect.NewWatchServiceClient(httpClient, baseUrl, connect.WithGRPC()), +func WithHttpClient(httpClient connect.HTTPClient) ClientOption { + return func(u *UtxorpcClient) { + u.httpClient = httpClient } +} + +func NewClient(options ...ClientOption) *UtxorpcClient { + u := &UtxorpcClient{} for _, option := range options { - option(client) + option(u) + } + if u.httpClient == nil { + u.httpClient = createHttpClient() } - return client + u.Query = u.NewQueryServiceClient() + u.Submit = u.NewSubmitServiceClient() + u.Sync = u.NewSyncServiceClient() + u.Watch = u.NewWatchServiceClient() + return u +} + +func (u *UtxorpcClient) reset() { + u.Query = u.NewQueryServiceClient() + u.Submit = u.NewSubmitServiceClient() + u.Sync = u.NewSyncServiceClient() + u.Watch = u.NewWatchServiceClient() } func (u *UtxorpcClient) HTTPClient() connect.HTTPClient { return u.httpClient } +func (u *UtxorpcClient) SetURL(baseUrl string) { + u.baseUrl = baseUrl + u.reset() +} + func (u *UtxorpcClient) URL() string { return u.baseUrl } @@ -73,7 +93,10 @@ func createHttpClient() *http.Client { // Establish a TLS connection using the custom TLS configuration conn, err := tls.Dial(network, addr, tlsConfig) if err != nil { - return nil, fmt.Errorf("failed to establish TLS connection: %w", err) + return nil, fmt.Errorf( + "failed to establish TLS connection: %w", + err, + ) } return conn, nil }, @@ -81,11 +104,6 @@ func createHttpClient() *http.Client { } } -func CreateUtxoRPCClient(baseUrl string, options ...ClientOption) *UtxorpcClient { - httpClient := createHttpClient() - return NewClient(httpClient, baseUrl, options...) -} - func (u *UtxorpcClient) Headers() map[string]string { headers := u.headers if headers == nil { diff --git a/query.go b/query.go new file mode 100644 index 0000000..9a3b32e --- /dev/null +++ b/query.go @@ -0,0 +1,98 @@ +package sdk + +import ( + "context" + "encoding/base64" + "encoding/hex" + + "connectrpc.com/connect" + "github.com/utxorpc/go-codegen/utxorpc/v1alpha/query" + "github.com/utxorpc/go-codegen/utxorpc/v1alpha/query/queryconnect" +) + +type QueryServiceClient queryconnect.QueryServiceClient + +func NewQueryServiceClient(u *UtxorpcClient) QueryServiceClient { + return u.NewQueryServiceClient() +} + +func (u *UtxorpcClient) NewQueryServiceClient() QueryServiceClient { + return queryconnect.NewQueryServiceClient( + u.httpClient, + u.baseUrl, + connect.WithGRPC(), + ) +} + +func (u *UtxorpcClient) QueryService() QueryServiceClient { + return u.Query +} + +func (u *UtxorpcClient) ReadParams() (*connect.Response[query.ReadParamsResponse], error) { + ctx := context.Background() + return u.ReadParamsWithContext(ctx) +} + +func (u *UtxorpcClient) ReadParamsWithContext( + ctx context.Context, +) (*connect.Response[query.ReadParamsResponse], error) { + req := connect.NewRequest(&query.ReadParamsRequest{}) + u.AddHeadersToRequest(req) + return u.Query.ReadParams(ctx, req) +} + +func (u *UtxorpcClient) ReadUtxo( + txHashStr string, + txIndex uint32, +) (*connect.Response[query.ReadUtxosResponse], error) { + var txHashBytes []byte + var err error + // Attempt to decode the input as hex + txHashBytes, hexErr := hex.DecodeString(txHashStr) + if hexErr != nil { + // If not hex, attempt to decode as Base64 + txHashBytes, err = base64.StdEncoding.DecodeString(txHashStr) + if err != nil { + return nil, err + } + } + // Create TxoRef with the decoded hash bytes + txoRef := &query.TxoRef{ + Hash: txHashBytes, // Use the decoded []byte + Index: txIndex, + } + req := &query.ReadUtxosRequest{Keys: []*query.TxoRef{txoRef}} + return u.ReadUtxos(req) +} + +func (u *UtxorpcClient) ReadUtxos( + req *query.ReadUtxosRequest, +) (*connect.Response[query.ReadUtxosResponse], error) { + ctx := context.Background() + return u.ReadUtxosWithContext(ctx, req) +} + +func (u *UtxorpcClient) ReadUtxosWithContext( + ctx context.Context, + queryReq *query.ReadUtxosRequest, +) (*connect.Response[query.ReadUtxosResponse], error) { + req := connect.NewRequest(queryReq) + u.AddHeadersToRequest(req) + return u.Query.ReadUtxos(ctx, req) +} + +func (u *UtxorpcClient) SearchUtxos( + req *query.SearchUtxosRequest, +) (*connect.Response[query.SearchUtxosResponse], error) { + ctx := context.Background() + return u.SearchUtxosWithContext(ctx, req) +} + +func (u *UtxorpcClient) SearchUtxosWithContext( + ctx context.Context, + queryReq *query.SearchUtxosRequest, +) (*connect.Response[query.SearchUtxosResponse], error) { + req := connect.NewRequest(queryReq) + u.AddHeadersToRequest(req) + return u.Query.SearchUtxos(ctx, req) +} diff --git a/submit.go b/submit.go new file mode 100644 index 0000000..70d9543 --- /dev/null +++ b/submit.go @@ -0,0 +1,116 @@ +package sdk + +import ( + "context" + "encoding/hex" + "fmt" + + "connectrpc.com/connect" + "github.com/utxorpc/go-codegen/utxorpc/v1alpha/submit" + "github.com/utxorpc/go-codegen/utxorpc/v1alpha/submit/submitconnect" +) + +type SubmitServiceClient submitconnect.SubmitServiceClient + +func (u *UtxorpcClient) NewSubmitServiceClient() SubmitServiceClient { + return submitconnect.NewSubmitServiceClient( + u.httpClient, + u.baseUrl, + connect.WithGRPC(), + ) +} + +func (u *UtxorpcClient) ReadMempool() (*connect.Response[submit.ReadMempoolResponse], error) { + ctx := context.Background() + return u.ReadMempoolWithContext(ctx) +} + +func (u *UtxorpcClient) ReadMempoolWithContext( + ctx context.Context, +) (*connect.Response[submit.ReadMempoolResponse], error) { + req := connect.NewRequest(&submit.ReadMempoolRequest{}) + u.AddHeadersToRequest(req) + return u.Submit.ReadMempool(ctx, req) +} + +func (u *UtxorpcClient) SubmitTx( + txCbor string, +) (*connect.Response[submit.SubmitTxResponse], error) { + ctx := context.Background() + // Decode the transaction data from hex + txRawBytes, err := hex.DecodeString(txCbor) + if err != nil { + return nil, fmt.Errorf("failed to decode transaction hash: %w", err) + } + + // Create a SubmitTxRequest with the transaction data + tx := &submit.AnyChainTx{ + Type: &submit.AnyChainTx_Raw{ + Raw: txRawBytes, + }, + } + + // Create a list with one transaction + req := &submit.SubmitTxRequest{ + Tx: []*submit.AnyChainTx{tx}, + } + return u.SubmitTxWithContext(ctx, req) +} + +func (u *UtxorpcClient) SubmitTxWithContext( + ctx context.Context, + txReq *submit.SubmitTxRequest, +) (*connect.Response[submit.SubmitTxResponse], error) { + req := connect.NewRequest(txReq) + u.AddHeadersToRequest(req) + return u.Submit.SubmitTx(ctx, req) +} + +func (u *UtxorpcClient) WaitForTx( + txRef string, +) (*connect.ServerStreamForClient[submit.WaitForTxResponse], error) { + ctx := context.Background() + // Decode the transaction references from hex + var decodedRefs [][]byte + refBytes, err := hex.DecodeString(txRef) + if err != nil { + return nil, fmt.Errorf( + "failed to decode transaction reference %s: %w", + txRef, + err, + ) + } + decodedRefs = append(decodedRefs, refBytes) + + // Create a WaitForTxRequest with the decoded transaction references + req := &submit.WaitForTxRequest{ + Ref: decodedRefs, + } + return u.WaitForTxWithContext(ctx, req) +} + +func (u *UtxorpcClient) WaitForTxWithContext( + ctx context.Context, + txReq *submit.WaitForTxRequest, +) (*connect.ServerStreamForClient[submit.WaitForTxResponse], error) { + req := connect.NewRequest(txReq) + u.AddHeadersToRequest(req) + return u.Submit.WaitForTx(ctx, req) +} + +func (u *UtxorpcClient) WatchMempool() ( + *connect.ServerStreamForClient[submit.WatchMempoolResponse], + error, +) { + ctx := context.Background() + return u.WatchMempoolWithContext(ctx) +} + +func (u *UtxorpcClient) WatchMempoolWithContext(ctx context.Context) ( + *connect.ServerStreamForClient[submit.WatchMempoolResponse], + error, +) { + req := connect.NewRequest(&submit.WatchMempoolRequest{}) + u.AddHeadersToRequest(req) + return u.Submit.WatchMempool(ctx, req) +} diff --git a/sync.go b/sync.go new file mode 100644 index 0000000..e0e9f7b --- /dev/null +++ b/sync.go @@ -0,0 +1,82 @@ +package sdk + +import ( + "context" + "encoding/hex" + + "connectrpc.com/connect" + "github.com/utxorpc/go-codegen/utxorpc/v1alpha/sync" + "github.com/utxorpc/go-codegen/utxorpc/v1alpha/sync/syncconnect" +) + +type SyncServiceClient syncconnect.SyncServiceClient + +func NewSyncServiceClient(u *UtxorpcClient) SyncServiceClient { + return u.NewSyncServiceClient() +} + +func (u *UtxorpcClient) NewSyncServiceClient() SyncServiceClient { + return syncconnect.NewSyncServiceClient( + u.httpClient, + u.baseUrl, + connect.WithGRPC(), + ) +} + +func syncIntersect(blockHashStr string, blockIndex int64) []*sync.BlockRef { + var intersect []*sync.BlockRef + // Construct the BlockRef based on the provided parameters + blockRef := &sync.BlockRef{} + if blockHashStr != "" { + hash, err := hex.DecodeString(blockHashStr) + if err != nil { + return nil + } + blockRef.Hash = hash + } + // We assume blockIndex can be 0 or any positive number + if blockIndex > -1 { + blockRef.Index = uint64(blockIndex) + } + // Only add blockRef to intersect if at least one of blockHashStr or blockIndex is provided + if blockHashStr != "" || blockIndex > -1 { + intersect = []*sync.BlockRef{blockRef} + } + return intersect +} + +func (u *UtxorpcClient) FetchBlock( + blockHashStr string, + blockIndex int64, +) (*connect.Response[sync.FetchBlockResponse], error) { + ctx := context.Background() + req := &sync.FetchBlockRequest{Ref: syncIntersect(blockHashStr, blockIndex)} + return u.FetchBlockWithContext(ctx, req) +} + +func (u *UtxorpcClient) FetchBlockWithContext( + ctx context.Context, + blockReq *sync.FetchBlockRequest, +) (*connect.Response[sync.FetchBlockResponse], error) { + req := connect.NewRequest(blockReq) + u.AddHeadersToRequest(req) + return u.Sync.FetchBlock(ctx, req) +} + +func (u *UtxorpcClient) FollowTip( + blockHashStr string, + blockIndex int64, +) (*connect.ServerStreamForClient[sync.FollowTipResponse], error) { + ctx := context.Background() + req := &sync.FollowTipRequest{Intersect: syncIntersect(blockHashStr, blockIndex)} + return u.FollowTipWithContext(ctx, req) +} + +func (u *UtxorpcClient) FollowTipWithContext( + ctx context.Context, + blockReq *sync.FollowTipRequest, +) (*connect.ServerStreamForClient[sync.FollowTipResponse], error) { + req := connect.NewRequest(blockReq) + u.AddHeadersToRequest(req) + return u.Sync.FollowTip(ctx, req) +} diff --git a/watch.go b/watch.go new file mode 100644 index 0000000..84da4d4 --- /dev/null +++ b/watch.go @@ -0,0 +1,64 @@ +package sdk + +import ( + "context" + "encoding/hex" + + "connectrpc.com/connect" + "github.com/utxorpc/go-codegen/utxorpc/v1alpha/watch" + "github.com/utxorpc/go-codegen/utxorpc/v1alpha/watch/watchconnect" +) + +type WatchServiceClient watchconnect.WatchServiceClient + +func NewWatchServiceClient(u *UtxorpcClient) WatchServiceClient { + return u.NewWatchServiceClient() +} + +func (u *UtxorpcClient) NewWatchServiceClient() WatchServiceClient { + return watchconnect.NewWatchServiceClient( + u.httpClient, + u.baseUrl, + connect.WithGRPC(), + ) +} + +func watchIntersect(blockHashStr string, blockIndex int64) []*watch.BlockRef { + var intersect []*watch.BlockRef + // Construct the BlockRef based on the provided parameters + blockRef := &watch.BlockRef{} + if blockHashStr != "" { + hash, err := hex.DecodeString(blockHashStr) + if err != nil { + return nil + } + blockRef.Hash = hash + } + // We assume blockIndex can be 0 or any positive number + if blockIndex > -1 { + blockRef.Index = uint64(blockIndex) + } + // Only add blockRef to intersect if at least one of blockHashStr or blockIndex is provided + if blockHashStr != "" || blockIndex > -1 { + intersect = []*watch.BlockRef{blockRef} + } + return intersect +} + +func (u *UtxorpcClient) WatchTx( + blockHashStr string, + blockIndex int64, +) (*connect.ServerStreamForClient[watch.WatchTxResponse], error) { + ctx := context.Background() + req := &watch.WatchTxRequest{Intersect: watchIntersect(blockHashStr, blockIndex)} + return u.WatchTxWithContext(ctx, req) +} + +func (u *UtxorpcClient) WatchTxWithContext( + ctx context.Context, + watchReq *watch.WatchTxRequest, +) (*connect.ServerStreamForClient[watch.WatchTxResponse], error) { + req := connect.NewRequest(watchReq) + u.AddHeadersToRequest(req) + return u.Watch.WatchTx(ctx, req) +}