diff --git a/access_keys_test.go b/access_keys_test.go index 9df5437..179a1b9 100644 --- a/access_keys_test.go +++ b/access_keys_test.go @@ -40,7 +40,7 @@ func TestAccessKeys(t *testing.T) { }) t.Run("GetAccessKeys", func(t *testing.T) { - accessKeys, err := client.GetAccessKeys(context.Background(), nil) + accessKeys, err := client.GetAccessKeys(context.Background(), nil, nil, nil) require.NoError(t, err) require.Equal(t, []*models.AccessKey{fixtures.AccessKey}, accessKeys) }) diff --git a/config.go b/config.go index 99f0c4b..cde6aeb 100644 --- a/config.go +++ b/config.go @@ -23,14 +23,6 @@ type Recipients struct { To string `json:"to"` } -// QueryParams object to use when limiting and sorting database query results -type QueryParams struct { - Page int `json:"page,omitempty"` - PageSize int `json:"page_size,omitempty"` - OrderByField string `json:"order_by_field,omitempty"` - SortDirection string `json:"sort_direction,omitempty"` -} - const ( // FieldMetadata is the field name for metadata FieldMetadata = "metadata" diff --git a/destinations_test.go b/destinations_test.go index dbd9483..40b294c 100644 --- a/destinations_test.go +++ b/destinations_test.go @@ -9,6 +9,7 @@ import ( "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" "github.com/bitcoin-sv/spv-wallet/models" + "github.com/bitcoin-sv/spv-wallet/models/filter" "github.com/stretchr/testify/require" ) @@ -63,7 +64,7 @@ func TestDestinations(t *testing.T) { }) t.Run("GetDestinations", func(t *testing.T) { - destinations, err := client.GetDestinations(context.Background(), fixtures.TestMetadata) + destinations, err := client.GetDestinations(context.Background(), &filter.DestinationFilter{}, nil, nil) require.NoError(t, err) require.Equal(t, []*models.Destination{fixtures.Destination}, destinations) }) diff --git a/examples/register_xpub/register_xpub.go b/examples/register_xpub/register_xpub.go index f35bb17..d63792e 100644 --- a/examples/register_xpub/register_xpub.go +++ b/examples/register_xpub/register_xpub.go @@ -4,8 +4,6 @@ import ( "context" "fmt" - "github.com/bitcoin-sv/spv-wallet/models" - walletclient "github.com/bitcoin-sv/spv-wallet-go-client" "github.com/bitcoin-sv/spv-wallet-go-client/xpriv" ) @@ -17,7 +15,7 @@ func main() { // Create a client wc := walletclient.NewWithXPriv("localhost:3003", keys.XPriv()) ctx := context.Background() - _ = wc.AdminNewXpub(ctx, keys.XPub().String(), &models.Metadata{"example_field": "example_data"}) + _ = wc.AdminNewXpub(ctx, keys.XPub().String(), map[string]any{"example_field": "example_data"}) xpubKey, err := wc.GetXPub(ctx) if err != nil { diff --git a/fixtures/fixtures.go b/fixtures/fixtures.go index fb7f464..ae6b8aa 100644 --- a/fixtures/fixtures.go +++ b/fixtures/fixtures.go @@ -37,11 +37,11 @@ func MarshallForTestHandler(object any) string { } // TestMetadata model for metadata -var TestMetadata = &models.Metadata{"test-key": "test-value"} +var TestMetadata = map[string]any{"test-key": "test-value"} // Xpub model for testing var Xpub = &models.Xpub{ - Model: common.Model{Metadata: *TestMetadata}, + Model: common.Model{Metadata: TestMetadata}, ID: "cba0be1e753a7609e1a2f792d2e80ea6fce241be86f0690ec437377477809ccc", CurrentBalance: 16680, NextInternalNum: 2, @@ -50,7 +50,7 @@ var Xpub = &models.Xpub{ // AccessKey model for testing var AccessKey = &models.AccessKey{ - Model: common.Model{Metadata: *TestMetadata}, + Model: common.Model{Metadata: TestMetadata}, ID: "access-key-id", XpubID: Xpub.ID, Key: AccessKeyString, @@ -58,7 +58,7 @@ var AccessKey = &models.AccessKey{ // Destination model for testing var Destination = &models.Destination{ - Model: common.Model{Metadata: *TestMetadata}, + Model: common.Model{Metadata: TestMetadata}, ID: "90d10acb85f37dd009238fe7ec61a1411725825c82099bd8432fcb47ad8326ce", XpubID: Xpub.ID, LockingScript: "76a9140e0eb4911d79e9b7683f268964f595b66fa3604588ac", @@ -71,7 +71,7 @@ var Destination = &models.Destination{ // Transaction model for testing var Transaction = &models.Transaction{ - Model: common.Model{Metadata: *TestMetadata}, + Model: common.Model{Metadata: TestMetadata}, ID: "caae6e799210dfea7591e3d55455437eb7e1091bb01463ae1e7ddf9e29c75eda", Hex: "0100000001cf4faa628ce1abdd2cfc641c948898bb7a3dbe043999236c3ea4436a0c79f5dc000000006a47304402206aeca14175e4477031970c1cda0af4d9d1206289212019b54f8e1c9272b5bac2022067c4d32086146ca77640f02a989f51b3c6738ebfa24683c4a923f647cf7f1c624121036295a81525ba33e22c6497c0b758e6a84b60d97c2d8905aa603dd364915c3a0effffffff023e030000000000001976a914f7fc6e0b05e91c3610efd0ce3f04f6502e2ed93d88ac99030000000000001976a914550e06a3aa71ba7414b53922c13f96a882bf027988ac00000000", XpubInIDs: []string{Xpub.ID}, @@ -91,7 +91,7 @@ var Transaction = &models.Transaction{ // DraftTx model for testing var DraftTx = &models.DraftTransaction{ - Model: common.Model{Metadata: *TestMetadata}, + Model: common.Model{Metadata: TestMetadata}, ID: "3a0e1fdd9ac6046c0c82aa36b462e477a455880ceeb08d3aabb1bf031553d1df", Hex: "010000000123462f14e60556718916a8cff9dbf2258195a928777c0373200dba1cee105bdb0100000000ffffffff020c000000000000001976a914c4b15e7f65e3e6a062c1d21b7f1d7d2cd3b18e8188ac0b000000000000001976a91455873fd2baa7b51a624f6416b1d824939d99151a88ac00000000", XpubID: Xpub.ID, diff --git a/go.mod b/go.mod index 1779f8d..4d1a68e 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/bitcoin-sv/spv-wallet-go-client go 1.21 require ( - github.com/bitcoin-sv/spv-wallet/models v1.0.0-beta.6 + github.com/bitcoin-sv/spv-wallet/models v1.0.0-beta.9 github.com/bitcoinschema/go-bitcoin/v2 v2.0.5 github.com/libsv/go-bk v0.1.6 github.com/libsv/go-bt/v2 v2.2.5 diff --git a/go.sum b/go.sum index 5aa878a..3435a03 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/bitcoin-sv/spv-wallet/models v1.0.0-beta.6 h1:KKWtlfhqQ9fdza+ZZ5YraPy3KTqJq/hRhAuJTDDTLfQ= -github.com/bitcoin-sv/spv-wallet/models v1.0.0-beta.6/go.mod h1:i3txysriHpprqYd3u97wEQsC4/jn+KHcyFOmuFYMw8M= +github.com/bitcoin-sv/spv-wallet/models v1.0.0-beta.9 h1:lPN+r10zuQ19tNMczYT1H+FcUCZMh3LAhAqngrqM1i0= +github.com/bitcoin-sv/spv-wallet/models v1.0.0-beta.9/go.mod h1:i3txysriHpprqYd3u97wEQsC4/jn+KHcyFOmuFYMw8M= github.com/bitcoinschema/go-bitcoin/v2 v2.0.5 h1:Sgh5Eb746Zck/46rFDrZZEXZWyO53fMuWYhNoZa1tck= github.com/bitcoinschema/go-bitcoin/v2 v2.0.5/go.mod h1:JjO1ivfZv6vhK0uAXzyH08AAHlzNMAfnyK1Fiv9r4ZA= github.com/bitcoinsv/bsvd v0.0.0-20190609155523-4c29707f7173 h1:2yTIV9u7H0BhRDGXH5xrAwAz7XibWJtX2dNezMeNsUo= diff --git a/http.go b/http.go index 613eaf9..edfa4cc 100644 --- a/http.go +++ b/http.go @@ -13,6 +13,7 @@ import ( "github.com/bitcoin-sv/spv-wallet-go-client/utils" "github.com/bitcoin-sv/spv-wallet/models" "github.com/bitcoin-sv/spv-wallet/models/apierrors" + "github.com/bitcoin-sv/spv-wallet/models/filter" "github.com/bitcoinschema/go-bitcoin/v2" "github.com/libsv/go-bk/bec" "github.com/libsv/go-bk/bip32" @@ -46,9 +47,9 @@ func (wc *WalletClient) GetXPub(ctx context.Context) (*models.Xpub, ResponseErro } // UpdateXPubMetadata update the metadata of the logged in xpub -func (wc *WalletClient) UpdateXPubMetadata(ctx context.Context, metadata *models.Metadata) (*models.Xpub, ResponseError) { +func (wc *WalletClient) UpdateXPubMetadata(ctx context.Context, metadata map[string]any) (*models.Xpub, ResponseError) { jsonStr, err := json.Marshal(map[string]interface{}{ - FieldMetadata: processMetadata(metadata), + FieldMetadata: metadata, }) if err != nil { return nil, WrapError(err) @@ -77,41 +78,37 @@ func (wc *WalletClient) GetAccessKey(ctx context.Context, id string) (*models.Ac } // GetAccessKeys will get all access keys matching the metadata filter -func (wc *WalletClient) GetAccessKeys(ctx context.Context, metadataConditions *models.Metadata) ([]*models.AccessKey, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldMetadata: processMetadata(metadataConditions), - }) - if err != nil { - return nil, WrapError(err) - } - var accessKey []*models.AccessKey - if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/access-key/search", jsonStr, wc.xPriv, true, &accessKey, - ); err != nil { - return nil, err - } - - return accessKey, nil +func (wc *WalletClient) GetAccessKeys( + ctx context.Context, + conditions *filter.AccessKeyFilter, + metadata map[string]any, + queryParams *filter.QueryParams, +) ([]*models.AccessKey, ResponseError) { + return Search[filter.AccessKeyFilter, []*models.AccessKey]( + ctx, http.MethodPost, + "/access-key/search", + wc.xPriv, + conditions, + metadata, + queryParams, + wc.doHTTPRequest, + ) } // GetAccessKeysCount will get the count of access keys -func (wc *WalletClient) GetAccessKeysCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata) (int64, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldConditions: conditions, - FieldMetadata: processMetadata(metadata), - }) - if err != nil { - return 0, WrapError(err) - } - - var count int64 - if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/access-key/count", jsonStr, wc.xPriv, true, &count, - ); err != nil { - return 0, err - } - - return count, nil +func (wc *WalletClient) GetAccessKeysCount( + ctx context.Context, + conditions *filter.AccessKeyFilter, + metadata map[string]any, +) (int64, ResponseError) { + return Count[filter.AccessKeyFilter]( + ctx, http.MethodPost, + "/access-key/count", + wc.xPriv, + conditions, + metadata, + wc.doHTTPRequest, + ) } // RevokeAccessKey will revoke an access key by id @@ -127,9 +124,9 @@ func (wc *WalletClient) RevokeAccessKey(ctx context.Context, id string) (*models } // CreateAccessKey will create new access key -func (wc *WalletClient) CreateAccessKey(ctx context.Context, metadata *models.Metadata) (*models.AccessKey, ResponseError) { +func (wc *WalletClient) CreateAccessKey(ctx context.Context, metadata map[string]any) (*models.AccessKey, ResponseError) { jsonStr, err := json.Marshal(map[string]interface{}{ - FieldMetadata: processMetadata(metadata), + FieldMetadata: metadata, }) if err != nil { return nil, WrapError(err) @@ -181,47 +178,35 @@ func (wc *WalletClient) GetDestinationByLockingScript(ctx context.Context, locki } // GetDestinations will get all destinations matching the metadata filter -func (wc *WalletClient) GetDestinations(ctx context.Context, metadataConditions *models.Metadata) ([]*models.Destination, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldMetadata: processMetadata(metadataConditions), - }) - if err != nil { - return nil, WrapError(err) - } - var destinations []*models.Destination - if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/destination/search", jsonStr, wc.xPriv, true, &destinations, - ); err != nil { - return nil, err - } - - return destinations, nil +func (wc *WalletClient) GetDestinations(ctx context.Context, conditions *filter.DestinationFilter, metadata map[string]any, queryParams *filter.QueryParams) ([]*models.Destination, ResponseError) { + return Search[filter.DestinationFilter, []*models.Destination]( + ctx, http.MethodPost, + "/destination/search", + wc.xPriv, + conditions, + metadata, + queryParams, + wc.doHTTPRequest, + ) } // GetDestinationsCount will get the count of destinations matching the metadata filter -func (wc *WalletClient) GetDestinationsCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata) (int64, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldConditions: conditions, - FieldMetadata: processMetadata(metadata), - }) - if err != nil { - return 0, WrapError(err) - } - - var count int64 - if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/destination/count", jsonStr, wc.xPriv, true, &count, - ); err != nil { - return 0, err - } - - return count, nil +func (wc *WalletClient) GetDestinationsCount(ctx context.Context, conditions *filter.DestinationFilter, metadata map[string]any) (int64, ResponseError) { + return Count( + ctx, + http.MethodPost, + "/destination/count", + wc.xPriv, + conditions, + metadata, + wc.doHTTPRequest, + ) } // NewDestination will create a new destination and return it -func (wc *WalletClient) NewDestination(ctx context.Context, metadata *models.Metadata) (*models.Destination, ResponseError) { +func (wc *WalletClient) NewDestination(ctx context.Context, metadata map[string]any) (*models.Destination, ResponseError) { jsonStr, err := json.Marshal(map[string]interface{}{ - FieldMetadata: processMetadata(metadata), + FieldMetadata: metadata, }) if err != nil { return nil, WrapError(err) @@ -237,12 +222,10 @@ func (wc *WalletClient) NewDestination(ctx context.Context, metadata *models.Met } // UpdateDestinationMetadataByID updates the destination metadata by id -func (wc *WalletClient) UpdateDestinationMetadataByID(ctx context.Context, id string, - metadata *models.Metadata, -) (*models.Destination, ResponseError) { +func (wc *WalletClient) UpdateDestinationMetadataByID(ctx context.Context, id string, metadata map[string]any) (*models.Destination, ResponseError) { jsonStr, err := json.Marshal(map[string]interface{}{ FieldID: id, - FieldMetadata: processMetadata(metadata), + FieldMetadata: metadata, }) if err != nil { return nil, WrapError(err) @@ -259,12 +242,10 @@ func (wc *WalletClient) UpdateDestinationMetadataByID(ctx context.Context, id st } // UpdateDestinationMetadataByAddress updates the destination metadata by address -func (wc *WalletClient) UpdateDestinationMetadataByAddress(ctx context.Context, address string, - metadata *models.Metadata, -) (*models.Destination, ResponseError) { +func (wc *WalletClient) UpdateDestinationMetadataByAddress(ctx context.Context, address string, metadata map[string]any) (*models.Destination, ResponseError) { jsonStr, err := json.Marshal(map[string]interface{}{ FieldAddress: address, - FieldMetadata: processMetadata(metadata), + FieldMetadata: metadata, }) if err != nil { return nil, WrapError(err) @@ -281,12 +262,10 @@ func (wc *WalletClient) UpdateDestinationMetadataByAddress(ctx context.Context, } // UpdateDestinationMetadataByLockingScript updates the destination metadata by locking script -func (wc *WalletClient) UpdateDestinationMetadataByLockingScript(ctx context.Context, lockingScript string, - metadata *models.Metadata, -) (*models.Destination, ResponseError) { +func (wc *WalletClient) UpdateDestinationMetadataByLockingScript(ctx context.Context, lockingScript string, metadata map[string]any) (*models.Destination, ResponseError) { jsonStr, err := json.Marshal(map[string]interface{}{ FieldLockingScript: lockingScript, - FieldMetadata: processMetadata(metadata), + FieldMetadata: metadata, }) if err != nil { return nil, WrapError(err) @@ -313,52 +292,41 @@ func (wc *WalletClient) GetTransaction(ctx context.Context, txID string) (*model } // GetTransactions will get transactions by conditions -func (wc *WalletClient) GetTransactions(ctx context.Context, conditions map[string]interface{}, metadataConditions *models.Metadata, queryParams *QueryParams) ([]*models.Transaction, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldConditions: conditions, - FieldMetadata: processMetadata(metadataConditions), - FieldQueryParams: queryParams, - }) - if err != nil { - return nil, WrapError(err) - } - - var transactions []*models.Transaction - if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/transaction/search", jsonStr, wc.xPriv, wc.signRequest, &transactions, - ); err != nil { - return nil, err - } - - return transactions, nil +func (wc *WalletClient) GetTransactions( + ctx context.Context, + conditions *filter.TransactionFilter, + metadata map[string]any, + queryParams *filter.QueryParams, +) ([]*models.Transaction, ResponseError) { + return Search[filter.TransactionFilter, []*models.Transaction]( + ctx, http.MethodPost, + "/transaction/search", + wc.xPriv, + conditions, + metadata, + queryParams, + wc.doHTTPRequest, + ) } // GetTransactionsCount get number of user transactions -func (wc *WalletClient) GetTransactionsCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, +func (wc *WalletClient) GetTransactionsCount( + ctx context.Context, + conditions *filter.TransactionFilter, + metadata map[string]any, ) (int64, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldConditions: conditions, - FieldMetadata: processMetadata(metadata), - }) - if err != nil { - return 0, WrapError(err) - } - - var count int64 - if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/transaction/count", jsonStr, wc.xPriv, wc.signRequest, &count, - ); err != nil { - return 0, err - } - - return count, nil + return Count[filter.TransactionFilter]( + ctx, http.MethodPost, + "/transaction/count", + wc.xPriv, + conditions, + metadata, + wc.doHTTPRequest, + ) } // DraftToRecipients is a draft transaction to a slice of recipients -func (wc *WalletClient) DraftToRecipients(ctx context.Context, recipients []*Recipients, - metadata *models.Metadata, -) (*models.DraftTransaction, ResponseError) { +func (wc *WalletClient) DraftToRecipients(ctx context.Context, recipients []*Recipients, metadata map[string]any) (*models.DraftTransaction, ResponseError) { outputs := make([]map[string]interface{}, 0) for _, recipient := range recipients { outputs = append(outputs, map[string]interface{}{ @@ -372,17 +340,15 @@ func (wc *WalletClient) DraftToRecipients(ctx context.Context, recipients []*Rec FieldConfig: map[string]interface{}{ FieldOutputs: outputs, }, - FieldMetadata: processMetadata(metadata), + FieldMetadata: metadata, }) } // DraftTransaction is a draft transaction -func (wc *WalletClient) DraftTransaction(ctx context.Context, transactionConfig *models.TransactionConfig, - metadata *models.Metadata, -) (*models.DraftTransaction, ResponseError) { +func (wc *WalletClient) DraftTransaction(ctx context.Context, transactionConfig *models.TransactionConfig, metadata map[string]any) (*models.DraftTransaction, ResponseError) { return wc.createDraftTransaction(ctx, map[string]interface{}{ FieldConfig: transactionConfig, - FieldMetadata: processMetadata(metadata), + FieldMetadata: metadata, }) } @@ -409,13 +375,11 @@ func (wc *WalletClient) createDraftTransaction(ctx context.Context, } // RecordTransaction will record a transaction -func (wc *WalletClient) RecordTransaction(ctx context.Context, hex, referenceID string, - metadata *models.Metadata, -) (*models.Transaction, ResponseError) { +func (wc *WalletClient) RecordTransaction(ctx context.Context, hex, referenceID string, metadata map[string]any) (*models.Transaction, ResponseError) { jsonStr, err := json.Marshal(map[string]interface{}{ FieldHex: hex, FieldReferenceID: referenceID, - FieldMetadata: processMetadata(metadata), + FieldMetadata: metadata, }) if err != nil { return nil, WrapError(err) @@ -432,12 +396,10 @@ func (wc *WalletClient) RecordTransaction(ctx context.Context, hex, referenceID } // UpdateTransactionMetadata update the metadata of a transaction -func (wc *WalletClient) UpdateTransactionMetadata(ctx context.Context, txID string, - metadata *models.Metadata, -) (*models.Transaction, ResponseError) { +func (wc *WalletClient) UpdateTransactionMetadata(ctx context.Context, txID string, metadata map[string]any) (*models.Transaction, ResponseError) { jsonStr, err := json.Marshal(map[string]interface{}{ FieldID: txID, - FieldMetadata: processMetadata(metadata), + FieldMetadata: metadata, }) if err != nil { return nil, WrapError(err) @@ -484,44 +446,28 @@ func (wc *WalletClient) GetUtxo(ctx context.Context, txID string, outputIndex ui } // GetUtxos will get a list of utxos filtered by conditions and metadata -func (wc *WalletClient) GetUtxos(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *QueryParams) ([]*models.Utxo, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldConditions: conditions, - FieldMetadata: processMetadata(metadata), - FieldQueryParams: queryParams, - }) - if err != nil { - return nil, WrapError(err) - } - - var utxos []*models.Utxo - if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/utxo/search", jsonStr, wc.xPriv, wc.signRequest, &utxos, - ); err != nil { - return nil, err - } - - return utxos, nil +func (wc *WalletClient) GetUtxos(ctx context.Context, conditions *filter.UtxoFilter, metadata map[string]any, queryParams *filter.QueryParams) ([]*models.Utxo, ResponseError) { + return Search[filter.UtxoFilter, []*models.Utxo]( + ctx, http.MethodPost, + "/utxo/search", + wc.xPriv, + conditions, + metadata, + queryParams, + wc.doHTTPRequest, + ) } // GetUtxosCount will get the count of utxos filtered by conditions and metadata -func (wc *WalletClient) GetUtxosCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata) (int64, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldConditions: conditions, - FieldMetadata: processMetadata(metadata), - }) - if err != nil { - return 0, WrapError(err) - } - - var count int64 - if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/utxo/count", jsonStr, wc.xPriv, wc.signRequest, &count, - ); err != nil { - return 0, err - } - - return count, nil +func (wc *WalletClient) GetUtxosCount(ctx context.Context, conditions *filter.UtxoFilter, metadata map[string]any) (int64, ResponseError) { + return Count[filter.UtxoFilter]( + ctx, http.MethodPost, + "/utxo/count", + wc.xPriv, + conditions, + metadata, + wc.doHTTPRequest, + ) } // createSignatureAccessKey will create a signature for the given access key & body contents @@ -664,36 +610,28 @@ func (wc *WalletClient) ConfirmContact(ctx context.Context, contact *models.Cont } // GetContacts will get contacts by conditions -func (wc *WalletClient) GetContacts(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *QueryParams) (*models.SearchContactsResponse, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldConditions: conditions, - FieldMetadata: processMetadata(metadata), - FieldQueryParams: queryParams, - }) - if err != nil { - return nil, WrapError(err) - } - - var result *models.SearchContactsResponse - if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/contact/search", jsonStr, wc.xPriv, wc.signRequest, &result, - ); err != nil { - return nil, err - } - - return result, nil +func (wc *WalletClient) GetContacts(ctx context.Context, conditions *filter.ContactFilter, metadata map[string]any, queryParams *filter.QueryParams) (*models.SearchContactsResponse, ResponseError) { + return Search[filter.ContactFilter, *models.SearchContactsResponse]( + ctx, http.MethodPost, + "/contact/search", + wc.xPriv, + conditions, + metadata, + queryParams, + wc.doHTTPRequest, + ) } // UpsertContact add or update contact. When adding a new contact, the system utilizes Paymail's PIKE capability to dispatch an invitation request, asking the counterparty to include the current user in their contacts. -func (wc *WalletClient) UpsertContact(ctx context.Context, paymail, fullName string, metadata *models.Metadata) (*models.Contact, ResponseError) { +func (wc *WalletClient) UpsertContact(ctx context.Context, paymail, fullName string, metadata map[string]any) (*models.Contact, ResponseError) { return wc.UpsertContactForPaymail(ctx, paymail, fullName, metadata, "") } // UpsertContactForPaymail add or update contact. When adding a new contact, the system utilizes Paymail's PIKE capability to dispatch an invitation request, asking the counterparty to include the current user in their contacts. -func (wc *WalletClient) UpsertContactForPaymail(ctx context.Context, paymail, fullName string, metadata *models.Metadata, requesterPaymail string) (*models.Contact, ResponseError) { +func (wc *WalletClient) UpsertContactForPaymail(ctx context.Context, paymail, fullName string, metadata map[string]any, requesterPaymail string) (*models.Contact, ResponseError) { payload := map[string]interface{}{ "fullName": fullName, - FieldMetadata: processMetadata(metadata), + FieldMetadata: metadata, } if requesterPaymail != "" { @@ -736,14 +674,14 @@ func (wc *WalletClient) GetSharedConfig(ctx context.Context) (*models.SharedConf } // AdminNewXpub will register an xPub -func (wc *WalletClient) AdminNewXpub(ctx context.Context, rawXPub string, metadata *models.Metadata) ResponseError { +func (wc *WalletClient) AdminNewXpub(ctx context.Context, rawXPub string, metadata map[string]any) ResponseError { // Adding a xpub needs to be signed by an admin key if wc.adminXPriv == nil { return WrapError(ErrAdminKey) } jsonStr, err := json.Marshal(map[string]interface{}{ - FieldMetadata: processMetadata(metadata), + FieldMetadata: metadata, FieldXpubKey: rawXPub, }) if err != nil { @@ -782,27 +720,45 @@ func (wc *WalletClient) AdminGetStats(ctx context.Context) (*models.AdminStats, } // AdminGetAccessKeys get all access keys filtered by conditions -func (wc *WalletClient) AdminGetAccessKeys(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, +func (wc *WalletClient) AdminGetAccessKeys( + ctx context.Context, + conditions *filter.AdminAccessKeyFilter, + metadata map[string]any, + queryParams *filter.QueryParams, ) ([]*models.AccessKey, ResponseError) { - var models []*models.AccessKey - if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/access-keys/search", &models); err != nil { - return nil, err - } - - return models, nil + return Search[filter.AdminAccessKeyFilter, []*models.AccessKey]( + ctx, http.MethodPost, + "/admin/access-keys/search", + wc.adminXPriv, + conditions, + metadata, + queryParams, + wc.doHTTPRequest, + ) } // AdminGetAccessKeysCount get a count of all the access keys filtered by conditions -func (wc *WalletClient) AdminGetAccessKeysCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, +func (wc *WalletClient) AdminGetAccessKeysCount( + ctx context.Context, + conditions *filter.AdminAccessKeyFilter, + metadata map[string]any, ) (int64, ResponseError) { - return wc.adminCount(ctx, conditions, metadata, "/admin/access-keys/count") + return Count[filter.AdminAccessKeyFilter]( + ctx, http.MethodPost, + "/admin/access-keys/count", + wc.adminXPriv, + conditions, + metadata, + wc.doHTTPRequest, + ) } // AdminGetBlockHeaders get all block headers filtered by conditions -func (wc *WalletClient) AdminGetBlockHeaders(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, +func (wc *WalletClient) AdminGetBlockHeaders( + ctx context.Context, + conditions map[string]interface{}, + metadata map[string]any, + queryParams *filter.QueryParams, ) ([]*models.BlockHeader, ResponseError) { var models []*models.BlockHeader if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/block-headers/search", &models); err != nil { @@ -813,29 +769,40 @@ func (wc *WalletClient) AdminGetBlockHeaders(ctx context.Context, conditions map } // AdminGetBlockHeadersCount get a count of all the block headers filtered by conditions -func (wc *WalletClient) AdminGetBlockHeadersCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, +func (wc *WalletClient) AdminGetBlockHeadersCount( + ctx context.Context, + conditions map[string]interface{}, + metadata map[string]any, ) (int64, ResponseError) { return wc.adminCount(ctx, conditions, metadata, "/admin/block-headers/count") } // AdminGetDestinations get all block destinations filtered by conditions -func (wc *WalletClient) AdminGetDestinations(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, +func (wc *WalletClient) AdminGetDestinations(ctx context.Context, conditions *filter.DestinationFilter, + metadata map[string]any, queryParams *filter.QueryParams, ) ([]*models.Destination, ResponseError) { - var models []*models.Destination - if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/destinations/search", &models); err != nil { - return nil, err - } - - return models, nil + return Search[filter.DestinationFilter, []*models.Destination]( + ctx, http.MethodPost, + "/admin/destinations/search", + wc.adminXPriv, + conditions, + metadata, + queryParams, + wc.doHTTPRequest, + ) } // AdminGetDestinationsCount get a count of all the destinations filtered by conditions -func (wc *WalletClient) AdminGetDestinationsCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, -) (int64, ResponseError) { - return wc.adminCount(ctx, conditions, metadata, "/admin/destinations/count") +func (wc *WalletClient) AdminGetDestinationsCount(ctx context.Context, conditions *filter.DestinationFilter, metadata map[string]any) (int64, ResponseError) { + return Count( + ctx, + http.MethodPost, + "/admin/destinations/count", + wc.adminXPriv, + conditions, + metadata, + wc.doHTTPRequest, + ) } // AdminGetPaymail get a paymail by address @@ -858,22 +825,33 @@ func (wc *WalletClient) AdminGetPaymail(ctx context.Context, address string) (*m } // AdminGetPaymails get all block paymails filtered by conditions -func (wc *WalletClient) AdminGetPaymails(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, +func (wc *WalletClient) AdminGetPaymails( + ctx context.Context, + conditions *filter.AdminPaymailFilter, + metadata map[string]any, + queryParams *filter.QueryParams, ) ([]*models.PaymailAddress, ResponseError) { - var models []*models.PaymailAddress - if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/paymails/search", &models); err != nil { - return nil, err - } - - return models, nil + return Search[filter.AdminPaymailFilter, []*models.PaymailAddress]( + ctx, http.MethodPost, + "/admin/paymails/search", + wc.adminXPriv, + conditions, + metadata, + queryParams, + wc.doHTTPRequest, + ) } // AdminGetPaymailsCount get a count of all the paymails filtered by conditions -func (wc *WalletClient) AdminGetPaymailsCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, -) (int64, ResponseError) { - return wc.adminCount(ctx, conditions, metadata, "/admin/paymails/count") +func (wc *WalletClient) AdminGetPaymailsCount(ctx context.Context, conditions *filter.AdminPaymailFilter, metadata map[string]any) (int64, ResponseError) { + return Count( + ctx, http.MethodPost, + "/admin/paymails/count", + wc.adminXPriv, + conditions, + metadata, + wc.doHTTPRequest, + ) } // AdminCreatePaymail create a new paymail for a xpub @@ -917,68 +895,115 @@ func (wc *WalletClient) AdminDeletePaymail(ctx context.Context, address string) } // AdminGetTransactions get all block transactions filtered by conditions -func (wc *WalletClient) AdminGetTransactions(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, +func (wc *WalletClient) AdminGetTransactions( + ctx context.Context, + conditions *filter.TransactionFilter, + metadata map[string]any, + queryParams *filter.QueryParams, ) ([]*models.Transaction, ResponseError) { - var models []*models.Transaction - if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/transactions/search", &models); err != nil { - return nil, err - } - - return models, nil + return Search[filter.TransactionFilter, []*models.Transaction]( + ctx, http.MethodPost, + "/admin/transactions/search", + wc.adminXPriv, + conditions, + metadata, + queryParams, + wc.doHTTPRequest, + ) } // AdminGetTransactionsCount get a count of all the transactions filtered by conditions -func (wc *WalletClient) AdminGetTransactionsCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, +func (wc *WalletClient) AdminGetTransactionsCount( + ctx context.Context, + conditions *filter.TransactionFilter, + metadata map[string]any, ) (int64, ResponseError) { - return wc.adminCount(ctx, conditions, metadata, "/admin/transactions/count") + return Count[filter.TransactionFilter]( + ctx, http.MethodPost, + "/admin/transactions/count", + wc.adminXPriv, + conditions, + metadata, + wc.doHTTPRequest, + ) } // AdminGetUtxos get all block utxos filtered by conditions -func (wc *WalletClient) AdminGetUtxos(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, +func (wc *WalletClient) AdminGetUtxos( + ctx context.Context, + conditions *filter.AdminUtxoFilter, + metadata map[string]any, + queryParams *filter.QueryParams, ) ([]*models.Utxo, ResponseError) { - var models []*models.Utxo - if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/utxos/search", &models); err != nil { - return nil, err - } - - return models, nil + return Search[filter.AdminUtxoFilter, []*models.Utxo]( + ctx, http.MethodPost, + "/admin/utxos/search", + wc.adminXPriv, + conditions, + metadata, + queryParams, + wc.doHTTPRequest, + ) } // AdminGetUtxosCount get a count of all the utxos filtered by conditions -func (wc *WalletClient) AdminGetUtxosCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, +func (wc *WalletClient) AdminGetUtxosCount( + ctx context.Context, + conditions *filter.AdminUtxoFilter, + metadata map[string]any, ) (int64, ResponseError) { - return wc.adminCount(ctx, conditions, metadata, "/admin/utxos/count") + return Count[filter.AdminUtxoFilter]( + ctx, http.MethodPost, + "/admin/utxos/count", + wc.adminXPriv, + conditions, + metadata, + wc.doHTTPRequest, + ) } // AdminGetXPubs get all block xpubs filtered by conditions -func (wc *WalletClient) AdminGetXPubs(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, +func (wc *WalletClient) AdminGetXPubs(ctx context.Context, conditions *filter.XpubFilter, + metadata map[string]any, queryParams *filter.QueryParams, ) ([]*models.Xpub, ResponseError) { - var models []*models.Xpub - if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/xpubs/search", &models); err != nil { - return nil, err - } - - return models, nil + return Search[filter.XpubFilter, []*models.Xpub]( + ctx, http.MethodPost, + "/admin/xpubs/search", + wc.adminXPriv, + conditions, + metadata, + queryParams, + wc.doHTTPRequest, + ) } // AdminGetXPubsCount get a count of all the xpubs filtered by conditions -func (wc *WalletClient) AdminGetXPubsCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, +func (wc *WalletClient) AdminGetXPubsCount( + ctx context.Context, + conditions *filter.XpubFilter, + metadata map[string]any, ) (int64, ResponseError) { - return wc.adminCount(ctx, conditions, metadata, "/admin/xpubs/count") + return Count[filter.XpubFilter]( + ctx, http.MethodPost, + "/admin/xpubs/count", + wc.adminXPriv, + conditions, + metadata, + wc.doHTTPRequest, + ) } -func (wc *WalletClient) adminGetModels(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, path string, models interface{}, +func (wc *WalletClient) adminGetModels( + ctx context.Context, + conditions map[string]interface{}, + metadata map[string]any, + queryParams *filter.QueryParams, + path string, + models interface{}, ) ResponseError { jsonStr, err := json.Marshal(map[string]interface{}{ FieldConditions: conditions, - FieldMetadata: processMetadata(metadata), + FieldMetadata: metadata, FieldQueryParams: queryParams, }) if err != nil { @@ -994,10 +1019,10 @@ func (wc *WalletClient) adminGetModels(ctx context.Context, conditions map[strin return nil } -func (wc *WalletClient) adminCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, path string) (int64, ResponseError) { +func (wc *WalletClient) adminCount(ctx context.Context, conditions map[string]interface{}, metadata map[string]any, path string) (int64, ResponseError) { jsonStr, err := json.Marshal(map[string]interface{}{ FieldConditions: conditions, - FieldMetadata: processMetadata(metadata), + FieldMetadata: metadata, }) if err != nil { return 0, WrapError(err) @@ -1033,26 +1058,23 @@ func (wc *WalletClient) AdminRecordTransaction(ctx context.Context, hex string) } // AdminGetContacts executes an HTTP POST request to search for contacts based on specified conditions, metadata, and query parameters. -func (wc *WalletClient) AdminGetContacts(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *QueryParams) (*models.SearchContactsResponse, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldConditions: conditions, - FieldMetadata: processMetadata(metadata), - FieldQueryParams: queryParams, - }) - if err != nil { - return nil, WrapError(err) - } - - var contacts *models.SearchContactsResponse - err = wc.doHTTPRequest(ctx, http.MethodPost, "/admin/contact/search", jsonStr, wc.adminXPriv, true, &contacts) - return contacts, WrapError(err) +func (wc *WalletClient) AdminGetContacts(ctx context.Context, conditions *filter.ContactFilter, metadata map[string]any, queryParams *filter.QueryParams) (*models.SearchContactsResponse, ResponseError) { + return Search[filter.ContactFilter, *models.SearchContactsResponse]( + ctx, http.MethodPost, + "/admin/contact/search", + wc.adminXPriv, + conditions, + metadata, + queryParams, + wc.doHTTPRequest, + ) } // AdminUpdateContact executes an HTTP PATCH request to update a specific contact's full name using their ID. -func (wc *WalletClient) AdminUpdateContact(ctx context.Context, id, fullName string, metadata *models.Metadata) (*models.Contact, ResponseError) { +func (wc *WalletClient) AdminUpdateContact(ctx context.Context, id, fullName string, metadata map[string]any) (*models.Contact, ResponseError) { jsonStr, err := json.Marshal(map[string]interface{}{ "fullName": fullName, - FieldMetadata: processMetadata(metadata), + FieldMetadata: metadata, }) if err != nil { return nil, WrapError(err) @@ -1093,7 +1115,7 @@ func (wc *WalletClient) FinalizeTransaction(draft *models.DraftTransaction) (str } // SendToRecipients send to recipients -func (wc *WalletClient) SendToRecipients(ctx context.Context, recipients []*Recipients, metadata *models.Metadata) (*models.Transaction, ResponseError) { +func (wc *WalletClient) SendToRecipients(ctx context.Context, recipients []*Recipients, metadata map[string]any) (*models.Transaction, ResponseError) { draft, err := wc.DraftToRecipients(ctx, recipients, metadata) if err != nil { return nil, err diff --git a/search.go b/search.go new file mode 100644 index 0000000..34344a0 --- /dev/null +++ b/search.go @@ -0,0 +1,72 @@ +package walletclient + +import ( + "context" + "encoding/json" + + "github.com/bitcoin-sv/spv-wallet/models/filter" + "github.com/libsv/go-bk/bip32" +) + +// SearchRequester is a function that sends a request to the server and returns the response. +type SearchRequester func(ctx context.Context, method string, path string, rawJSON []byte, xPriv *bip32.ExtendedKey, sign bool, responseJSON interface{}) ResponseError + +// Search prepares and sends a search request to the server. +func Search[TFilter any, TResp any]( + ctx context.Context, + method string, + path string, + xPriv *bip32.ExtendedKey, + f *TFilter, + metadata map[string]any, + queryParams *filter.QueryParams, + requester SearchRequester, +) (TResp, ResponseError) { + jsonStr, err := json.Marshal(filter.SearchModel[TFilter]{ + ConditionsModel: filter.ConditionsModel[TFilter]{ + Conditions: f, + Metadata: metadata, + }, + QueryParams: queryParams, + }) + var resp TResp // before initialization, this var is empty slice or nil so it can be returned in case of error + if err != nil { + return resp, WrapError(err) + } + + if err := requester(ctx, method, path, jsonStr, xPriv, true, &resp); err != nil { + return resp, err + } + + return resp, nil +} + +// Count prepares and sends a count request to the server. +func Count[TFilter any]( + ctx context.Context, + method string, + path string, + xPriv *bip32.ExtendedKey, + f *TFilter, + metadata map[string]any, + requester SearchRequester, +) (int64, ResponseError) { + jsonStr, err := json.Marshal(filter.ConditionsModel[TFilter]{ + Conditions: f, + Metadata: metadata, + }) + if err != nil { + return 0, WrapError(err) + } + var count int64 + if err := requester(ctx, method, path, jsonStr, xPriv, true, &count); err != nil { + return 0, err + } + + return count, nil +} + +// Optional returns a pointer to provided value, it's necessary to define "optional" fields in filters +func Optional[T any](val T) *T { + return &val +} diff --git a/transactions_test.go b/transactions_test.go index 77174cb..f359cce 100644 --- a/transactions_test.go +++ b/transactions_test.go @@ -9,6 +9,7 @@ import ( "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" "github.com/bitcoin-sv/spv-wallet/models" + "github.com/bitcoin-sv/spv-wallet/models/filter" "github.com/stretchr/testify/require" ) @@ -44,9 +45,9 @@ func TestTransactions(t *testing.T) { }) t.Run("GetTransactions", func(t *testing.T) { - conditions := map[string]interface{}{ - "fee": map[string]interface{}{"$lt": 100}, - "total_value": map[string]interface{}{"$lt": 740}, + conditions := &filter.TransactionFilter{ + Fee: Optional(uint64(97)), + TotalValue: Optional(uint64(6955)), } txs, err := client.GetTransactions(context.Background(), conditions, fixtures.TestMetadata, nil) require.NoError(t, err) diff --git a/walletclient.go b/walletclient.go index b676f6e..e5a50e7 100644 --- a/walletclient.go +++ b/walletclient.go @@ -3,7 +3,6 @@ package walletclient import ( "net/http" - "github.com/bitcoin-sv/spv-wallet/models" "github.com/libsv/go-bk/bec" "github.com/libsv/go-bk/bip32" ) @@ -78,16 +77,6 @@ func makeClient(configurators ...configurator) *WalletClient { return client } -// processMetadata will process the metadata -func processMetadata(metadata *models.Metadata) *models.Metadata { - if metadata == nil { - m := make(models.Metadata) - metadata = &m - } - - return metadata -} - // addSignature will add the signature to the request func addSignature(header *http.Header, xPriv *bip32.ExtendedKey, bodyString string) ResponseError { return setSignature(header, xPriv, bodyString) diff --git a/xpubs_test.go b/xpubs_test.go index 2430662..8a87d81 100644 --- a/xpubs_test.go +++ b/xpubs_test.go @@ -54,7 +54,7 @@ func TestXpub(t *testing.T) { t.Run("UpdateXPubMetadata", func(t *testing.T) { update = true - metadata := &models.Metadata{"updated": "info"} + metadata := map[string]any{"updated": "info"} xpub, err := client.UpdateXPubMetadata(context.Background(), metadata) require.NoError(t, err) require.NotNil(t, xpub)