diff --git a/README.md b/README.md index 450e542..710af1d 100755 --- a/README.md +++ b/README.md @@ -128,8 +128,27 @@ All API methods accept a `context.Context` as their first parameter for timeout - `Create(ctx, req)` - Create a new transaction - `CreateBulk(ctx, req)` - Create multiple transactions - `Get(ctx, transactionID)` - Get transaction by ID +- `GetInterchain(ctx, transactionID, opts...)` - Trace a transaction's interchain anchors - `List(ctx)` - List all transactions +#### Interchain trace options + +`Transaction.GetInterchain` and `Block.GetInterchain` trace a prime block to the +public-chain anchors covering it. By default they return the **first anchor per +chain** (anchor proofs are chained, so the earliest per chain is the meaningful +one). Tune with functional options: + +```go +// Default: first anchor per chain (ETH, BTC, …) +trace, _ := client.Block.GetInterchain(ctx, "42") + +// Up to 3 anchors per chain +trace, _ = client.Block.GetInterchain(ctx, "42", sdk.WithPerChain(3)) + +// All anchors, only the ETH-mainnet chain ("1"); "0" = BTC +trace, _ = client.Block.GetInterchain(ctx, "42", sdk.WithPerChain(0), sdk.WithChains("1")) +``` + ### Transaction Type - `Create(ctx, req)` - Create a new transaction type - `Get(ctx, txnType)` - Get transaction type by name diff --git a/block/block.go b/block/block.go index f3e15e1..494aaa3 100755 --- a/block/block.go +++ b/block/block.go @@ -29,9 +29,14 @@ func (bc *BlockClient) Get(ctx context.Context, blockID string) (*models.Block, // GetInterchain traces a block to the validator (verification) blocks that // validated it and the public-chain interchain anchors those validator blocks // were bundled into. -func (bc *BlockClient) GetInterchain(ctx context.Context, blockID string) (*models.InterchainTrace, error) { +// +// By default it returns the first anchor per public chain (anchor proofs are +// chained, so the earliest per chain is the meaningful one). Use +// client.WithPerChain / client.WithChains to return more anchors per chain or +// restrict to specific chains. +func (bc *BlockClient) GetInterchain(ctx context.Context, blockID string, opts ...client.InterchainOption) (*models.InterchainTrace, error) { var resp models.InterchainTrace - path := fmt.Sprintf("/api/v1/block/%s/interchain", blockID) + path := fmt.Sprintf("/api/v1/block/%s/interchain%s", blockID, client.InterchainQuery(opts...)) err := bc.client.Get(ctx, path, &resp) if err != nil { return nil, err diff --git a/client/interchain.go b/client/interchain.go new file mode 100644 index 0000000..6752ef9 --- /dev/null +++ b/client/interchain.go @@ -0,0 +1,57 @@ +package client + +import ( + "net/url" + "strconv" + "strings" +) + +// InterchainOption configures an interchain-trace request (Transaction.GetInterchain +// / Block.GetInterchain). Anchor proofs are chained, so by default the trace +// returns the first anchor per public chain; these options change how many and +// which chains are returned. +type InterchainOption func(*interchainConfig) + +type interchainConfig struct { + perChain *int + chains []string +} + +// WithPerChain caps how many interchain anchors are returned per public chain, +// earliest-first: 1 is the first anchor per chain (the service default), 0 +// returns all anchors for each chain. +func WithPerChain(n int) InterchainOption { + return func(c *interchainConfig) { c.perChain = &n } +} + +// WithChains restricts the trace to these interchain chain ids ("1" = ETH +// mainnet, "0" = BTC; testnet ids differ). +func WithChains(chains ...string) InterchainOption { + return func(c *interchainConfig) { c.chains = chains } +} + +// InterchainQuery builds the "?perChain=...&chains=..." suffix for the interchain +// trace endpoints. Returns "" when no options are set (the server then applies +// its defaults: one anchor per chain, all chains). +func InterchainQuery(opts ...InterchainOption) string { + cfg := &interchainConfig{} + for _, o := range opts { + o(cfg) + } + + var parts []string + if cfg.perChain != nil { + parts = append(parts, "perChain="+strconv.Itoa(*cfg.perChain)) + } + if len(cfg.chains) > 0 { + escaped := make([]string, len(cfg.chains)) + for i, c := range cfg.chains { + escaped[i] = url.QueryEscape(c) + } + parts = append(parts, "chains="+strings.Join(escaped, ",")) + } + if len(parts) == 0 { + return "" + } + return "?" + strings.Join(parts, "&") +} diff --git a/client/interchain_test.go b/client/interchain_test.go new file mode 100644 index 0000000..a1a9301 --- /dev/null +++ b/client/interchain_test.go @@ -0,0 +1,22 @@ +package client + +import "testing" + +func TestInterchainQuery(t *testing.T) { + cases := []struct { + name string + opts []InterchainOption + want string + }{ + {"none", nil, ""}, + {"perChain1", []InterchainOption{WithPerChain(1)}, "?perChain=1"}, + {"perChain0 (all)", []InterchainOption{WithPerChain(0)}, "?perChain=0"}, + {"chains", []InterchainOption{WithChains("1", "0")}, "?chains=1,0"}, + {"both", []InterchainOption{WithPerChain(2), WithChains("1", "0")}, "?perChain=2&chains=1,0"}, + } + for _, c := range cases { + if got := InterchainQuery(c.opts...); got != c.want { + t.Errorf("%s: InterchainQuery = %q, want %q", c.name, got, c.want) + } + } +} diff --git a/dragonchain.go b/dragonchain.go index 729674a..6512224 100755 --- a/dragonchain.go +++ b/dragonchain.go @@ -113,3 +113,15 @@ func NewDragonchainSDKWithHTTPClient(publicID, authKeyID, authKey, baseURL strin func (sdk *DragonchainSDK) GetClient() *client.Client { return sdk.client } + +// InterchainOption configures Transaction.GetInterchain / Block.GetInterchain. +// Re-exported from the client package for convenience (e.g. sdk.WithPerChain). +type InterchainOption = client.InterchainOption + +// WithPerChain caps interchain anchors returned per public chain (1 = first per +// chain, the default; 0 = all). See client.WithPerChain. +var WithPerChain = client.WithPerChain + +// WithChains restricts an interchain trace to specific chain ids ("1" = ETH +// mainnet, "0" = BTC). See client.WithChains. +var WithChains = client.WithChains diff --git a/transaction/interchain_test.go b/transaction/interchain_test.go new file mode 100644 index 0000000..99d0674 --- /dev/null +++ b/transaction/interchain_test.go @@ -0,0 +1,38 @@ +package transaction + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + + "git.dragonchain.com/dragonchain/prime-sdk-go/client" +) + +// TestGetInterchainQuery confirms GetInterchain builds the request URI (path + +// the perChain/chains query) end-to-end through the client. +func TestGetInterchainQuery(t *testing.T) { + var gotURI string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotURI = r.URL.RequestURI() + _, _ = io.WriteString(w, `{"blockId":"42","validatorBlocks":[],"interchainTransactions":[]}`) + })) + defer srv.Close() + + tc := NewTransactionClient(client.NewClient("pid", "kid", "key", srv.URL)) + + if _, err := tc.GetInterchain(context.Background(), "tx1", client.WithPerChain(2), client.WithChains("1", "0")); err != nil { + t.Fatalf("GetInterchain with opts: %v", err) + } + if want := "/api/v1/transaction/tx1/interchain?perChain=2&chains=1,0"; gotURI != want { + t.Errorf("with opts: URI = %q, want %q", gotURI, want) + } + + if _, err := tc.GetInterchain(context.Background(), "tx1"); err != nil { + t.Fatalf("GetInterchain default: %v", err) + } + if want := "/api/v1/transaction/tx1/interchain"; gotURI != want { + t.Errorf("default (no opts): URI = %q, want %q", gotURI, want) + } +} diff --git a/transaction/transaction.go b/transaction/transaction.go index 90375dd..c36bcf3 100755 --- a/transaction/transaction.go +++ b/transaction/transaction.go @@ -56,9 +56,14 @@ func (tc *TransactionClient) Get(ctx context.Context, transactionID string) (*mo // validated its prime block and the public-chain interchain anchors those // validator blocks were bundled into. If the transaction is still pending (not // yet in a block) the trace's slices are empty. -func (tc *TransactionClient) GetInterchain(ctx context.Context, transactionID string) (*models.InterchainTrace, error) { +// +// By default it returns the first anchor per public chain (anchor proofs are +// chained, so the earliest per chain is the meaningful one). Use +// client.WithPerChain / client.WithChains to return more anchors per chain or +// restrict to specific chains. +func (tc *TransactionClient) GetInterchain(ctx context.Context, transactionID string, opts ...client.InterchainOption) (*models.InterchainTrace, error) { var resp models.InterchainTrace - path := fmt.Sprintf("/api/v1/transaction/%s/interchain", transactionID) + path := fmt.Sprintf("/api/v1/transaction/%s/interchain%s", transactionID, client.InterchainQuery(opts...)) err := tc.client.Get(ctx, path, &resp) if err != nil { return nil, err