diff --git a/access_keys_test.go b/access_keys_test.go index 179a1b9..61004ad 100644 --- a/access_keys_test.go +++ b/access_keys_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" - "github.com/bitcoin-sv/spv-wallet/models" + "github.com/bitcoin-sv/spv-wallet/models/response" "github.com/stretchr/testify/require" ) @@ -17,13 +17,20 @@ import ( func TestAccessKeys(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case "/v1/access-key": + case "/api/v1/users/current/keys/" + fixtures.AccessKey.ID: switch r.Method { - case http.MethodGet, http.MethodPost, http.MethodDelete: + case http.MethodGet, http.MethodDelete: json.NewEncoder(w).Encode(fixtures.AccessKey) } - case "/v1/access-key/search": - json.NewEncoder(w).Encode([]*models.AccessKey{fixtures.AccessKey}) + case "/api/v1/users/current/keys": + switch r.Method { + case http.MethodPost: + json.NewEncoder(w).Encode(fixtures.AccessKey) + case http.MethodGet: + json.NewEncoder(w).Encode(response.PageModel[response.AccessKey]{ + Content: []*response.AccessKey{fixtures.AccessKey}, + }) + } default: w.WriteHeader(http.StatusNotFound) } @@ -42,7 +49,9 @@ func TestAccessKeys(t *testing.T) { t.Run("GetAccessKeys", func(t *testing.T) { accessKeys, err := client.GetAccessKeys(context.Background(), nil, nil, nil) require.NoError(t, err) - require.Equal(t, []*models.AccessKey{fixtures.AccessKey}, accessKeys) + require.Equal(t, response.PageModel[response.AccessKey]{ + Content: []*response.AccessKey{fixtures.AccessKey}, + }, accessKeys) }) t.Run("CreateAccessKey", func(t *testing.T) { diff --git a/admin_contacts_test.go b/admin_contacts_test.go index 5b2112e..91594a9 100644 --- a/admin_contacts_test.go +++ b/admin_contacts_test.go @@ -18,7 +18,7 @@ func TestAdminContactActions(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/v1/admin/contact/search" && r.Method == http.MethodPost: - c := fixtures.Contact + c := fixtures.OldContact c.ID = "1" content := models.PagedResponse[*models.Contact]{ Content: []*models.Contact{c}, diff --git a/client_options.go b/client_options.go index fcee338..edd9cfa 100644 --- a/client_options.go +++ b/client_options.go @@ -81,7 +81,7 @@ func (w *httpConf) Configure(c *WalletClient) { return } - const basePath = "/v1" + const basePath = "/api/v1" c.server = fmt.Sprintf("%s%s", baseURL, basePath) c.httpClient = w.HTTPClient diff --git a/config.go b/config.go index cde6aeb..cbd07d4 100644 --- a/config.go +++ b/config.go @@ -1,6 +1,6 @@ package walletclient -import "github.com/bitcoin-sv/spv-wallet/models" +import "github.com/bitcoin-sv/spv-wallet/models/response" // TransportType the type of transport being used ('http' for usage or 'mock' for testing) type TransportType string @@ -18,9 +18,9 @@ const ( // Recipients is a struct for recipients type Recipients struct { - OpReturn *models.OpReturn `json:"op_return"` - Satoshis uint64 `json:"satoshis"` - To string `json:"to"` + OpReturn *response.OpReturn `json:"opReturn"` + Satoshis uint64 `json:"satoshis"` + To string `json:"to"` } const ( diff --git a/contacts_test.go b/contacts_test.go index dc105f6..f615265 100644 --- a/contacts_test.go +++ b/contacts_test.go @@ -5,11 +5,10 @@ import ( "encoding/json" "net/http" "net/http/httptest" - "strings" "testing" "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" - "github.com/bitcoin-sv/spv-wallet/models" + "github.com/bitcoin-sv/spv-wallet/models/response" responsemodels "github.com/bitcoin-sv/spv-wallet/models/response" "github.com/stretchr/testify/require" ) @@ -18,25 +17,39 @@ import ( func TestContactActionsRouting(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - switch { - case strings.HasPrefix(r.URL.Path, "/v1/contact/rejected/"): - if r.Method == http.MethodPatch { - json.NewEncoder(w).Encode(map[string]string{"result": "rejected"}) + switch r.URL.Path { + case "/api/v1/contacts/" + fixtures.PaymailAddress: + switch r.Method { + case http.MethodPut: + content := response.CreateContactResponse{ + Contact: fixtures.Contact, + AdditionalInfo: map[string]string{}, + } + json.NewEncoder(w).Encode(content) + case http.MethodDelete: + json.NewEncoder(w).Encode(map[string]any{}) + case http.MethodGet: + json.NewEncoder(w).Encode(fixtures.Contact) } - case r.URL.Path == "/v1/contact/accepted/": - if r.Method == http.MethodPost { + case "/api/v1/contacts/" + fixtures.PaymailAddress + "/confirmation": + switch r.Method { + case http.MethodPost, http.MethodDelete: json.NewEncoder(w).Encode(map[string]string{"result": string(responsemodels.ContactNotConfirmed)}) } - case r.URL.Path == "/v1/contact/search": - if r.Method == http.MethodPost { - content := models.PagedResponse[*models.Contact]{ - Content: []*models.Contact{fixtures.Contact}, + case "/api/v1/contacts/": + if r.Method == http.MethodGet { + content := response.PageModel[response.Contact]{ + Content: []*response.Contact{fixtures.Contact}, } json.NewEncoder(w).Encode(content) } - case strings.HasPrefix(r.URL.Path, "/v1/contact/"): - if r.Method == http.MethodPost || r.Method == http.MethodPut { - json.NewEncoder(w).Encode(map[string]string{"result": "upserted"}) + case "/api/v1/invitations/" + fixtures.PaymailAddress + "/contacts": + if r.Method == http.MethodPost { + json.NewEncoder(w).Encode(map[string]any{}) + } + case "/api/v1/invitations/" + fixtures.PaymailAddress: + if r.Method == http.MethodDelete { + json.NewEncoder(w).Encode(map[string]any{}) } default: w.WriteHeader(http.StatusNotFound) diff --git a/destinations_test.go b/destinations_test.go deleted file mode 100644 index 40b294c..0000000 --- a/destinations_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package walletclient - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "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" -) - -func TestDestinations(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - sendJSONResponse := func(data interface{}) { - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(data); err != nil { - w.WriteHeader(http.StatusInternalServerError) - } - } - - const dest = "/v1/destination" - - switch { - case r.URL.Path == "/v1/v1/destination/address/"+fixtures.Destination.Address && r.Method == http.MethodGet: - sendJSONResponse(fixtures.Destination) - case r.URL.Path == "/v1/destination/lockingScript/"+fixtures.Destination.LockingScript && r.Method == http.MethodGet: - sendJSONResponse(fixtures.Destination) - case r.URL.Path == "/v1/destination/search" && r.Method == http.MethodPost: - sendJSONResponse([]*models.Destination{fixtures.Destination}) - case r.URL.Path == dest && r.Method == http.MethodGet: - sendJSONResponse(fixtures.Destination) - case r.URL.Path == dest && r.Method == http.MethodPatch: - sendJSONResponse(fixtures.Destination) - case r.URL.Path == dest && r.Method == http.MethodPost: - sendJSONResponse(fixtures.Destination) - default: - w.WriteHeader(http.StatusNotFound) - } - })) - defer server.Close() - client := NewWithAccessKey(server.URL, fixtures.AccessKeyString) - require.NotNil(t, client.accessKey) - - t.Run("GetDestinationByID", func(t *testing.T) { - destination, err := client.GetDestinationByID(context.Background(), fixtures.Destination.ID) - require.NoError(t, err) - require.Equal(t, fixtures.Destination, destination) - }) - - t.Run("GetDestinationByAddress", func(t *testing.T) { - destination, err := client.GetDestinationByAddress(context.Background(), fixtures.Destination.Address) - require.NoError(t, err) - require.Equal(t, fixtures.Destination, destination) - }) - - t.Run("GetDestinationByLockingScript", func(t *testing.T) { - destination, err := client.GetDestinationByLockingScript(context.Background(), fixtures.Destination.LockingScript) - require.NoError(t, err) - require.Equal(t, fixtures.Destination, destination) - }) - - t.Run("GetDestinations", func(t *testing.T) { - destinations, err := client.GetDestinations(context.Background(), &filter.DestinationFilter{}, nil, nil) - require.NoError(t, err) - require.Equal(t, []*models.Destination{fixtures.Destination}, destinations) - }) - - t.Run("NewDestination", func(t *testing.T) { - destination, err := client.NewDestination(context.Background(), fixtures.TestMetadata) - require.NoError(t, err) - require.Equal(t, fixtures.Destination, destination) - }) - - t.Run("UpdateDestinationMetadataByID", func(t *testing.T) { - destination, err := client.UpdateDestinationMetadataByID(context.Background(), fixtures.Destination.ID, fixtures.TestMetadata) - require.NoError(t, err) - require.Equal(t, fixtures.Destination, destination) - }) - - t.Run("UpdateDestinationMetadataByAddress", func(t *testing.T) { - destination, err := client.UpdateDestinationMetadataByAddress(context.Background(), fixtures.Destination.Address, fixtures.TestMetadata) - require.NoError(t, err) - require.Equal(t, fixtures.Destination, destination) - }) - - t.Run("UpdateDestinationMetadataByLockingScript", func(t *testing.T) { - destination, err := client.UpdateDestinationMetadataByLockingScript(context.Background(), fixtures.Destination.LockingScript, fixtures.TestMetadata) - require.NoError(t, err) - require.Equal(t, fixtures.Destination, destination) - }) -} diff --git a/examples/Taskfile.yml b/examples/Taskfile.yml index 81d7cbe..e966d64 100644 --- a/examples/Taskfile.yml +++ b/examples/Taskfile.yml @@ -61,13 +61,33 @@ tasks: cmds: - echo "running xpub_from_xpriv..." - go run ./xpub_from_xpriv/xpub_from_xpriv.go + generate_totp: desc: "running generate_totp..." cmds: - echo "running generate_totp..." - go run ./generate_totp/generate_totp.go + webhooks: desc: "running webhooks..." cmds: - echo "running webhooks..." - go run ./webhooks/webhooks.go || true + + access_key: + desc: "running access_key..." + cmds: + - echo "running access_key..." + - go run ./access_key/access_key.go + + get_shared_config: + desc: "running get_shared_config..." + cmds: + - echo "running get_shared_config..." + - go run ./get_shared_config/get_shared_config.go + + update_xpub_metadata: + desc: "running update_xpub_metadata..." + cmds: + - echo "running update_xpub_metadata..." + - go run ./update_xpub_metadata/update_xpub_metadata.go diff --git a/examples/access_key/access_key.go b/examples/access_key/access_key.go new file mode 100644 index 0000000..4886200 --- /dev/null +++ b/examples/access_key/access_key.go @@ -0,0 +1,49 @@ +/* +Package main - access_key example +*/ +package main + +import ( + "context" + "fmt" + "os" + + walletclient "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/examples" +) + +func main() { + defer examples.HandlePanic() + + examples.CheckIfXPrivExists() + + const server = "http://localhost:3003/api/v1" + + client := walletclient.NewWithXPriv(server, examples.ExampleXPriv) + ctx := context.Background() + + metadata := map[string]any{"some_metadata": "example"} + createdAccessKey, err := client.CreateAccessKey(ctx, metadata) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + fmt.Println("Created access key ID: ", createdAccessKey.ID) + fmt.Println("Metadata: ", createdAccessKey.Metadata) + fmt.Println("Created at: ", createdAccessKey.CreatedAt) + + fetchedAccessKey, err := client.GetAccessKey(ctx, createdAccessKey.ID) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + fmt.Println("Fetched access key ID: ", fetchedAccessKey.ID) + + revokedAccessKey, err := client.RevokeAccessKey(ctx, createdAccessKey.ID) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + fmt.Println("Revoked access key ID: ", revokedAccessKey.ID) + fmt.Println("Revoked at: ", revokedAccessKey.RevokedAt) +} diff --git a/examples/get_shared_config/get_shared_config.go b/examples/get_shared_config/get_shared_config.go new file mode 100644 index 0000000..796363f --- /dev/null +++ b/examples/get_shared_config/get_shared_config.go @@ -0,0 +1,32 @@ +/* +Package main - get_shared_config example +*/ +package main + +import ( + "context" + "fmt" + "os" + + walletclient "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/examples" +) + +func main() { + defer examples.HandlePanic() + + examples.CheckIfXPrivExists() + + const server = "http://localhost:3003/api/v1" + + client := walletclient.NewWithXPriv(server, examples.ExampleXPriv) + ctx := context.Background() + + sharedConfig, err := client.GetSharedConfig(ctx) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + fmt.Println("Shared config (PaymailDomains): ", sharedConfig.PaymailDomains) + fmt.Println("Shared config (ExperimentalFeatures): ", sharedConfig.ExperimentalFeatures) +} diff --git a/examples/update_xpub_metadata/update_xpub_metadata.go b/examples/update_xpub_metadata/update_xpub_metadata.go new file mode 100644 index 0000000..fefd076 --- /dev/null +++ b/examples/update_xpub_metadata/update_xpub_metadata.go @@ -0,0 +1,41 @@ +/* +Package main - update_xpub_metadata example +*/ +package main + +import ( + "context" + "fmt" + "os" + + walletclient "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/examples" +) + +func main() { + defer examples.HandlePanic() + + examples.CheckIfXPrivExists() + + const server = "http://localhost:3003/api/v1" + + client := walletclient.NewWithXPriv(server, examples.ExampleXPriv) + ctx := context.Background() + + xpubInfo, err := client.GetXPub(ctx) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + fmt.Println("XPub metadata: ", xpubInfo.Metadata) + fmt.Println("XPub (updated_at): ", xpubInfo.UpdatedAt) + + metadata := map[string]any{"some_metadata_2": "example2"} + updatedXpubInfo, err := client.UpdateXPubMetadata(ctx, metadata) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + fmt.Println("Updated XPub metadata: ", updatedXpubInfo.Metadata) + fmt.Println("Updated XPub (updated_at): ", updatedXpubInfo.UpdatedAt) +} diff --git a/fixtures/fixtures.go b/fixtures/fixtures.go index 12990b5..574258e 100644 --- a/fixtures/fixtures.go +++ b/fixtures/fixtures.go @@ -4,8 +4,7 @@ package fixtures import ( "encoding/json" - "github.com/bitcoin-sv/spv-wallet/models" - "github.com/bitcoin-sv/spv-wallet/models/common" + "github.com/bitcoin-sv/spv-wallet/models/response" responsemodels "github.com/bitcoin-sv/spv-wallet/models/response" ) @@ -41,8 +40,8 @@ func MarshallForTestHandler(object any) string { var TestMetadata = map[string]any{"test-key": "test-value"} // Xpub model for testing -var Xpub = &models.Xpub{ - Model: common.Model{Metadata: TestMetadata}, +var Xpub = &response.Xpub{ + Model: response.Model{Metadata: TestMetadata}, ID: "cba0be1e753a7609e1a2f792d2e80ea6fce241be86f0690ec437377477809ccc", CurrentBalance: 16680, NextInternalNum: 2, @@ -50,16 +49,16 @@ var Xpub = &models.Xpub{ } // AccessKey model for testing -var AccessKey = &models.AccessKey{ - Model: common.Model{Metadata: TestMetadata}, +var AccessKey = &response.AccessKey{ + Model: response.Model{Metadata: TestMetadata}, ID: "access-key-id", XpubID: Xpub.ID, Key: AccessKeyString, } // Destination model for testing -var Destination = &models.Destination{ - Model: common.Model{Metadata: TestMetadata}, +var Destination = &response.Destination{ + Model: response.Model{Metadata: TestMetadata}, ID: "90d10acb85f37dd009238fe7ec61a1411725825c82099bd8432fcb47ad8326ce", XpubID: Xpub.ID, LockingScript: "76a9140e0eb4911d79e9b7683f268964f595b66fa3604588ac", @@ -71,8 +70,8 @@ var Destination = &models.Destination{ } // Transaction model for testing -var Transaction = &models.Transaction{ - Model: common.Model{Metadata: TestMetadata}, +var Transaction = &response.Transaction{ + Model: response.Model{Metadata: TestMetadata}, ID: "caae6e799210dfea7591e3d55455437eb7e1091bb01463ae1e7ddf9e29c75eda", Hex: "0100000001cf4faa628ce1abdd2cfc641c948898bb7a3dbe043999236c3ea4436a0c79f5dc000000006a47304402206aeca14175e4477031970c1cda0af4d9d1206289212019b54f8e1c9272b5bac2022067c4d32086146ca77640f02a989f51b3c6738ebfa24683c4a923f647cf7f1c624121036295a81525ba33e22c6497c0b758e6a84b60d97c2d8905aa603dd364915c3a0effffffff023e030000000000001976a914f7fc6e0b05e91c3610efd0ce3f04f6502e2ed93d88ac99030000000000001976a914550e06a3aa71ba7414b53922c13f96a882bf027988ac00000000", XpubInIDs: []string{Xpub.ID}, @@ -91,33 +90,33 @@ var Transaction = &models.Transaction{ } // DraftTx model for testing -var DraftTx = &models.DraftTransaction{ - Model: common.Model{Metadata: TestMetadata}, +var DraftTx = &response.DraftTransaction{ + Model: response.Model{Metadata: TestMetadata}, ID: "3a0e1fdd9ac6046c0c82aa36b462e477a455880ceeb08d3aabb1bf031553d1df", Hex: "010000000123462f14e60556718916a8cff9dbf2258195a928777c0373200dba1cee105bdb0100000000ffffffff020c000000000000001976a914c4b15e7f65e3e6a062c1d21b7f1d7d2cd3b18e8188ac0b000000000000001976a91455873fd2baa7b51a624f6416b1d824939d99151a88ac00000000", XpubID: Xpub.ID, - Configuration: models.TransactionConfig{ - ChangeDestinations: []*models.Destination{Destination}, + Configuration: response.TransactionConfig{ + ChangeDestinations: []*response.Destination{Destination}, ChangeStrategy: "", ChangeMinimumSatoshis: 0, ChangeNumberOfDestinations: 0, ChangeSatoshis: 11, Fee: 1, - FeeUnit: &models.FeeUnit{ + FeeUnit: &response.FeeUnit{ Satoshis: 1, Bytes: 1000, }, - FromUtxos: []*models.UtxoPointer{{ + FromUtxos: []*response.UtxoPointer{{ TransactionID: "caae6e799210dfea7591e3d55455437eb7e1091bb01463ae1e7ddf9e29c75eda", OutputIndex: 1, }}, - IncludeUtxos: []*models.UtxoPointer{{ + IncludeUtxos: []*response.UtxoPointer{{ TransactionID: "caae6e799210dfea7591e3d55455437eb7e1091bb01463ae1e7ddf9e29c75eda", OutputIndex: 1, }}, - Inputs: []*models.TransactionInput{{ - Utxo: models.Utxo{ - UtxoPointer: models.UtxoPointer{ + Inputs: []*response.TransactionInput{{ + Utxo: response.Utxo{ + UtxoPointer: response.UtxoPointer{ TransactionID: "db5b10ee1cba0d2073037c7728a9958125f2dbf9cfa81689715605e6142f4623", OutputIndex: 1, }, @@ -131,9 +130,9 @@ var DraftTx = &models.DraftTransaction{ }, Destination: *Destination, }}, - Outputs: []*models.TransactionOutput{ + Outputs: []*response.TransactionOutput{ { - PaymailP4: &models.PaymailP4{ + PaymailP4: &response.PaymailP4{ Alias: "dorzepowski", Domain: "damiano.4chain.space", FromPaymail: "test3@kuba.4chain.space", @@ -144,7 +143,7 @@ var DraftTx = &models.DraftTransaction{ ResolutionType: "p2p", }, Satoshis: 12, - Scripts: []*models.ScriptOutput{{ + Scripts: []*response.ScriptOutput{{ Address: "1Jw1vRUq6pYqiMBAT6x3wBfebXCrXv6Qbr", Satoshis: 12, Script: "76a914c4b15e7f65e3e6a062c1d21b7f1d7d2cd3b18e8188ac", @@ -155,7 +154,7 @@ var DraftTx = &models.DraftTransaction{ }, { Satoshis: 11, - Scripts: []*models.ScriptOutput{{ + Scripts: []*response.ScriptOutput{{ Address: "18oETbMcqRB9S7NEGZgwsHKpoTpB3nKBMa", Satoshis: 11, Script: "76a91455873fd2baa7b51a624f6416b1d824939d99151a88ac", @@ -164,18 +163,18 @@ var DraftTx = &models.DraftTransaction{ To: "18oETbMcqRB9S7NEGZgwsHKpoTpB3nKBMa", }, }, - SendAllTo: &models.TransactionOutput{ - OpReturn: &models.OpReturn{ + SendAllTo: &response.TransactionOutput{ + OpReturn: &response.OpReturn{ Hex: "0100000001cf4faa628ce1abdd2cfc641c948898bb7a3dbe043999236c3ea4436a0c79f5dc000000006a47304402206aeca14175e4477031970c1cda0af4d9d1206289212019b54f8e1c9272b5bac2022067c4d32086146ca77640f02a989f51b3c6738ebfa24683c4a923f647cf7f1c624121036295a81525ba33e22c6497c0b758e6a84b60d97c2d8905aa603dd364915c3a0effffffff023e030000000000001976a914f7fc6e0b05e91c3610efd0ce3f04f6502e2ed93d88ac99030000000000001976a914550e06a3aa71ba7414b53922c13f96a882bf027988ac00000000", HexParts: []string{"0100000001cf4faa628ce1abdd2cfc641c948898bb7a3dbe043999236c3ea4436a0c79f5dc000000006a47304402206aeca14175e4477031970c1cda0af4d9d1206289212019b54f8e1c9272b5bac2022067c4d32086146ca77640f02a989f51b3c6738ebfa24683c4a923f647cf7f1c624121036295a81525ba33e22c6497c0b758e6a84b60d97c2d8905aa603dd364915c3a0effffffff023e030000000000001976a914f7fc6e0b05e91c3610efd0ce3f04f6502e2ed93d88ac99030000000000001976a914550e06a3aa71ba7414b53922c13f96a882bf027988ac00000000"}, - Map: &models.MapProtocol{ + Map: &response.MapProtocol{ App: "app_protocol", Keys: map[string]interface{}{"test-key": "test-value"}, Type: "app_protocol_type", }, StringParts: []string{"string", "parts"}, }, - PaymailP4: &models.PaymailP4{ + PaymailP4: &response.PaymailP4{ Alias: "alias", Domain: "domain.tld", FromPaymail: "alias@paymail.com", @@ -187,7 +186,7 @@ var DraftTx = &models.DraftTransaction{ }, Satoshis: 1220, Script: "script", - Scripts: []*models.ScriptOutput{{ + Scripts: []*response.ScriptOutput{{ Address: "12HL5RyEy3Rt6SCwxgpiFSTigem1Pzbq22", Satoshis: 1220, Script: "script", @@ -196,7 +195,7 @@ var DraftTx = &models.DraftTransaction{ To: "1DSsgJdB2AnWaFNgSbv4MZC2m71116JafG", UseForChange: false, }, - Sync: &models.SyncConfig{ + Sync: &response.SyncConfig{ Broadcast: true, BroadcastInstant: true, PaymailP2P: true, @@ -208,7 +207,7 @@ var DraftTx = &models.DraftTransaction{ } // Contact model for testing -var Contact = &models.Contact{ +var Contact = &response.Contact{ ID: "68af358bde7d8641621c7dd3de1a276c9a62cfa9e2d0740494519f1ba61e2f4a", FullName: "Test User", Paymail: "test@spv-wallet.com", diff --git a/fixtures/fixtures_old.go b/fixtures/fixtures_old.go new file mode 100644 index 0000000..fefd243 --- /dev/null +++ b/fixtures/fixtures_old.go @@ -0,0 +1,184 @@ +// Package fixtures contains fixtures for testing +package fixtures + +import ( + "github.com/bitcoin-sv/spv-wallet/models" + "github.com/bitcoin-sv/spv-wallet/models/common" + "github.com/bitcoin-sv/spv-wallet/models/response" +) + +// Xpub model for testing +var OldXpub = &models.Xpub{ + Model: common.Model{Metadata: TestMetadata}, + ID: "cba0be1e753a7609e1a2f792d2e80ea6fce241be86f0690ec437377477809ccc", + CurrentBalance: 16680, + NextInternalNum: 2, + NextExternalNum: 1, +} + +// AccessKey model for testing +var OldAccessKey = &models.AccessKey{ + Model: common.Model{Metadata: TestMetadata}, + ID: "access-key-id", + XpubID: Xpub.ID, + Key: AccessKeyString, +} + +// Destination model for testing +var OldDestination = &models.Destination{ + Model: common.Model{Metadata: TestMetadata}, + ID: "90d10acb85f37dd009238fe7ec61a1411725825c82099bd8432fcb47ad8326ce", + XpubID: Xpub.ID, + LockingScript: "76a9140e0eb4911d79e9b7683f268964f595b66fa3604588ac", + Type: "pubkeyhash", + Chain: 1, + Num: 19, + Address: "18oETbMcqRB9S7NEGZgwsHKpoTpB3nKBMa", + DraftID: "3a0e1fdd9ac6046c0c82aa36b462e477a455880ceeb08d3aabb1bf031553d1df", +} + +// Transaction model for testing +var OldTransaction = &models.Transaction{ + Model: common.Model{Metadata: TestMetadata}, + ID: "caae6e799210dfea7591e3d55455437eb7e1091bb01463ae1e7ddf9e29c75eda", + Hex: "0100000001cf4faa628ce1abdd2cfc641c948898bb7a3dbe043999236c3ea4436a0c79f5dc000000006a47304402206aeca14175e4477031970c1cda0af4d9d1206289212019b54f8e1c9272b5bac2022067c4d32086146ca77640f02a989f51b3c6738ebfa24683c4a923f647cf7f1c624121036295a81525ba33e22c6497c0b758e6a84b60d97c2d8905aa603dd364915c3a0effffffff023e030000000000001976a914f7fc6e0b05e91c3610efd0ce3f04f6502e2ed93d88ac99030000000000001976a914550e06a3aa71ba7414b53922c13f96a882bf027988ac00000000", + XpubInIDs: []string{Xpub.ID}, + XpubOutIDs: []string{Xpub.ID}, + BlockHash: "00000000000000000896d2b93efa4476c4bd47ed7a554aeac6b38044745a6257", + BlockHeight: 825599, + Fee: 97, + NumberOfInputs: 4, + NumberOfOutputs: 2, + DraftID: "fe6fe12c25b81106b7332d58fe87dab7bc6e56c8c21ca45b4de05f673f3f653c", + TotalValue: 6955, + OutputValue: 1725, + Outputs: map[string]int64{"680d975a403fd9ec90f613e87d17802c029d2d930df1c8373cdcdda2f536a1c0": 62}, + Status: "confirmed", + TransactionDirection: "incoming", +} + +// DraftTx model for testing +var OldDraftTx = &models.DraftTransaction{ + Model: common.Model{Metadata: TestMetadata}, + ID: "3a0e1fdd9ac6046c0c82aa36b462e477a455880ceeb08d3aabb1bf031553d1df", + Hex: "010000000123462f14e60556718916a8cff9dbf2258195a928777c0373200dba1cee105bdb0100000000ffffffff020c000000000000001976a914c4b15e7f65e3e6a062c1d21b7f1d7d2cd3b18e8188ac0b000000000000001976a91455873fd2baa7b51a624f6416b1d824939d99151a88ac00000000", + XpubID: Xpub.ID, + Configuration: models.TransactionConfig{ + ChangeDestinations: []*models.Destination{OldDestination}, + ChangeStrategy: "", + ChangeMinimumSatoshis: 0, + ChangeNumberOfDestinations: 0, + ChangeSatoshis: 11, + Fee: 1, + FeeUnit: &models.FeeUnit{ + Satoshis: 1, + Bytes: 1000, + }, + FromUtxos: []*models.UtxoPointer{{ + TransactionID: "caae6e799210dfea7591e3d55455437eb7e1091bb01463ae1e7ddf9e29c75eda", + OutputIndex: 1, + }}, + IncludeUtxos: []*models.UtxoPointer{{ + TransactionID: "caae6e799210dfea7591e3d55455437eb7e1091bb01463ae1e7ddf9e29c75eda", + OutputIndex: 1, + }}, + Inputs: []*models.TransactionInput{{ + Utxo: models.Utxo{ + UtxoPointer: models.UtxoPointer{ + TransactionID: "db5b10ee1cba0d2073037c7728a9958125f2dbf9cfa81689715605e6142f4623", + OutputIndex: 1, + }, + ID: "041479f86c475603fd510431cf702bc8c9849a9c350390eb86b467d82a13cc24", + XpubID: "9fe44728bf16a2dde3748f72cc65ea661f3bf18653b320d31eafcab37cf7fb36", + Satoshis: 24, + ScriptPubKey: "76a914673d3a53dade2723c48b446578681e253b5c548b88ac", + Type: "pubkeyhash", + DraftID: "3a0e1fdd9ac6046c0c82aa36b462e477a455880ceeb08d3aabb1bf031553d1df", + SpendingTxID: "", + }, + Destination: *OldDestination, + }}, + Outputs: []*models.TransactionOutput{ + { + PaymailP4: &models.PaymailP4{ + Alias: "dorzepowski", + Domain: "damiano.4chain.space", + FromPaymail: "test3@kuba.4chain.space", + Note: "paymail_note", + PubKey: "1DSsgJdB2AnWaFNgSbv4MZC2m71116JafG", + ReceiveEndpoint: "https://damiano.serveo.net/v1/bsvalias/receive-transaction/{alias}@{domain.tld}", + ReferenceID: "9b48dde1821fa82cf797372a297363c8", + ResolutionType: "p2p", + }, + Satoshis: 12, + Scripts: []*models.ScriptOutput{{ + Address: "1Jw1vRUq6pYqiMBAT6x3wBfebXCrXv6Qbr", + Satoshis: 12, + Script: "76a914c4b15e7f65e3e6a062c1d21b7f1d7d2cd3b18e8188ac", + ScriptType: "pubkeyhash", + }}, + To: "pubkeyhash", + UseForChange: false, + }, + { + Satoshis: 11, + Scripts: []*models.ScriptOutput{{ + Address: "18oETbMcqRB9S7NEGZgwsHKpoTpB3nKBMa", + Satoshis: 11, + Script: "76a91455873fd2baa7b51a624f6416b1d824939d99151a88ac", + ScriptType: "pubkeyhash", + }}, + To: "18oETbMcqRB9S7NEGZgwsHKpoTpB3nKBMa", + }, + }, + SendAllTo: &models.TransactionOutput{ + OpReturn: &models.OpReturn{ + Hex: "0100000001cf4faa628ce1abdd2cfc641c948898bb7a3dbe043999236c3ea4436a0c79f5dc000000006a47304402206aeca14175e4477031970c1cda0af4d9d1206289212019b54f8e1c9272b5bac2022067c4d32086146ca77640f02a989f51b3c6738ebfa24683c4a923f647cf7f1c624121036295a81525ba33e22c6497c0b758e6a84b60d97c2d8905aa603dd364915c3a0effffffff023e030000000000001976a914f7fc6e0b05e91c3610efd0ce3f04f6502e2ed93d88ac99030000000000001976a914550e06a3aa71ba7414b53922c13f96a882bf027988ac00000000", + HexParts: []string{"0100000001cf4faa628ce1abdd2cfc641c948898bb7a3dbe043999236c3ea4436a0c79f5dc000000006a47304402206aeca14175e4477031970c1cda0af4d9d1206289212019b54f8e1c9272b5bac2022067c4d32086146ca77640f02a989f51b3c6738ebfa24683c4a923f647cf7f1c624121036295a81525ba33e22c6497c0b758e6a84b60d97c2d8905aa603dd364915c3a0effffffff023e030000000000001976a914f7fc6e0b05e91c3610efd0ce3f04f6502e2ed93d88ac99030000000000001976a914550e06a3aa71ba7414b53922c13f96a882bf027988ac00000000"}, + Map: &models.MapProtocol{ + App: "app_protocol", + Keys: map[string]interface{}{"test-key": "test-value"}, + Type: "app_protocol_type", + }, + StringParts: []string{"string", "parts"}, + }, + PaymailP4: &models.PaymailP4{ + Alias: "alias", + Domain: "domain.tld", + FromPaymail: "alias@paymail.com", + Note: "paymail_note", + PubKey: "1DSsgJdB2AnWaFNgSbv4MZC2m71116JafG", + ReceiveEndpoint: "https://bsvalias.example.org/alias@domain.tld/payment-destination-models", + ReferenceID: "3d7c2ca83a46", + ResolutionType: "resolution_type", + }, + Satoshis: 1220, + Script: "script", + Scripts: []*models.ScriptOutput{{ + Address: "12HL5RyEy3Rt6SCwxgpiFSTigem1Pzbq22", + Satoshis: 1220, + Script: "script", + ScriptType: "pubkeyhash", + }}, + To: "1DSsgJdB2AnWaFNgSbv4MZC2m71116JafG", + UseForChange: false, + }, + Sync: &models.SyncConfig{ + Broadcast: true, + BroadcastInstant: true, + PaymailP2P: true, + SyncOnChain: true, + }, + }, + Status: "draft", + FinalTxID: "caae6e799210dfea7591e3d55455437eb7e1091bb01463ae1e7ddf9e29c75eda", +} + +// Contact model for testing +var OldContact = &models.Contact{ + ID: "68af358bde7d8641621c7dd3de1a276c9a62cfa9e2d0740494519f1ba61e2f4a", + FullName: "Test User", + Paymail: "test@spv-wallet.com", + PubKey: "xpub661MyMwAqRbcGpZVrSHU...", + Status: response.ContactStatus("unconfirmed"), +} diff --git a/http.go b/http.go index a896026..6b4e658 100644 --- a/http.go +++ b/http.go @@ -5,9 +5,7 @@ import ( "context" "encoding/hex" "encoding/json" - "fmt" "net/http" - "strconv" bip32 "github.com/bitcoin-sv/go-sdk/compat/bip32" ec "github.com/bitcoin-sv/go-sdk/primitives/ec" @@ -35,7 +33,7 @@ func (wc *WalletClient) SetAdminKey(adminKey *bip32.ExtendedKey) { func (wc *WalletClient) GetXPub(ctx context.Context) (*models.Xpub, error) { var xPub models.Xpub if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/xpub", nil, wc.xPriv, true, &xPub, + ctx, http.MethodGet, "/users/current", nil, wc.xPriv, true, &xPub, ); err != nil { return nil, err } @@ -54,7 +52,7 @@ func (wc *WalletClient) UpdateXPubMetadata(ctx context.Context, metadata map[str var xPub models.Xpub if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/xpub", jsonStr, wc.xPriv, true, &xPub, + ctx, http.MethodPatch, "/users/current", jsonStr, wc.xPriv, true, &xPub, ); err != nil { return nil, err } @@ -66,7 +64,7 @@ func (wc *WalletClient) UpdateXPubMetadata(ctx context.Context, metadata map[str func (wc *WalletClient) GetAccessKey(ctx context.Context, id string) (*models.AccessKey, error) { var accessKey models.AccessKey if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/access-key?"+FieldID+"="+id, nil, wc.xPriv, true, &accessKey, + ctx, http.MethodGet, "/users/current/keys/"+id, nil, wc.xPriv, true, &accessKey, ); err != nil { return nil, err } @@ -83,7 +81,7 @@ func (wc *WalletClient) GetAccessKeys( ) ([]*models.AccessKey, error) { return Search[filter.AccessKeyFilter, []*models.AccessKey]( ctx, http.MethodPost, - "/access-key/search", + "/users/current", wc.xPriv, conditions, metadata, @@ -92,27 +90,11 @@ func (wc *WalletClient) GetAccessKeys( ) } -// GetAccessKeysCount will get the count of access keys -func (wc *WalletClient) GetAccessKeysCount( - ctx context.Context, - conditions *filter.AccessKeyFilter, - metadata map[string]any, -) (int64, error) { - return Count[filter.AccessKeyFilter]( - ctx, http.MethodPost, - "/access-key/count", - wc.xPriv, - conditions, - metadata, - wc.doHTTPRequest, - ) -} - // RevokeAccessKey will revoke an access key by id func (wc *WalletClient) RevokeAccessKey(ctx context.Context, id string) (*models.AccessKey, error) { var accessKey models.AccessKey if err := wc.doHTTPRequest( - ctx, http.MethodDelete, "/access-key?"+FieldID+"="+id, nil, wc.xPriv, true, &accessKey, + ctx, http.MethodDelete, "/users/current/keys/"+id, nil, wc.xPriv, true, &accessKey, ); err != nil { return nil, err } @@ -130,7 +112,7 @@ func (wc *WalletClient) CreateAccessKey(ctx context.Context, metadata map[string } var accessKey models.AccessKey if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/access-key", jsonStr, wc.xPriv, true, &accessKey, + ctx, http.MethodPost, "/users/current/keys", jsonStr, wc.xPriv, true, &accessKey, ); err != nil { return nil, err } @@ -138,150 +120,10 @@ func (wc *WalletClient) CreateAccessKey(ctx context.Context, metadata map[string return &accessKey, nil } -// GetDestinationByID will get a destination by id -func (wc *WalletClient) GetDestinationByID(ctx context.Context, id string) (*models.Destination, error) { - var destination models.Destination - if err := wc.doHTTPRequest( - ctx, http.MethodGet, fmt.Sprintf("/destination?%s=%s", FieldID, id), nil, wc.xPriv, true, &destination, - ); err != nil { - return nil, err - } - - return &destination, nil -} - -// GetDestinationByAddress will get a destination by address -func (wc *WalletClient) GetDestinationByAddress(ctx context.Context, address string) (*models.Destination, error) { - var destination models.Destination - if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/destination?"+FieldAddress+"="+address, nil, wc.xPriv, true, &destination, - ); err != nil { - return nil, err - } - - return &destination, nil -} - -// GetDestinationByLockingScript will get a destination by locking script -func (wc *WalletClient) GetDestinationByLockingScript(ctx context.Context, lockingScript string) (*models.Destination, error) { - var destination models.Destination - if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/destination?"+FieldLockingScript+"="+lockingScript, nil, wc.xPriv, true, &destination, - ); err != nil { - return nil, err - } - - return &destination, nil -} - -// GetDestinations will get all destinations matching the metadata filter -func (wc *WalletClient) GetDestinations(ctx context.Context, conditions *filter.DestinationFilter, metadata map[string]any, queryParams *filter.QueryParams) ([]*models.Destination, error) { - 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 *filter.DestinationFilter, metadata map[string]any) (int64, error) { - 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 map[string]any) (*models.Destination, error) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldMetadata: metadata, - }) - if err != nil { - return nil, WrapError(err) - } - var destination models.Destination - if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/destination", jsonStr, wc.xPriv, true, &destination, - ); err != nil { - return nil, err - } - - return &destination, nil -} - -// UpdateDestinationMetadataByID updates the destination metadata by id -func (wc *WalletClient) UpdateDestinationMetadataByID(ctx context.Context, id string, metadata map[string]any) (*models.Destination, error) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldID: id, - FieldMetadata: metadata, - }) - if err != nil { - return nil, WrapError(err) - } - - var destination models.Destination - if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/destination", jsonStr, wc.xPriv, true, &destination, - ); err != nil { - return nil, err - } - - return &destination, nil -} - -// UpdateDestinationMetadataByAddress updates the destination metadata by address -func (wc *WalletClient) UpdateDestinationMetadataByAddress(ctx context.Context, address string, metadata map[string]any) (*models.Destination, error) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldAddress: address, - FieldMetadata: metadata, - }) - if err != nil { - return nil, WrapError(err) - } - - var destination models.Destination - if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/destination", jsonStr, wc.xPriv, true, &destination, - ); err != nil { - return nil, err - } - - return &destination, nil -} - -// UpdateDestinationMetadataByLockingScript updates the destination metadata by locking script -func (wc *WalletClient) UpdateDestinationMetadataByLockingScript(ctx context.Context, lockingScript string, metadata map[string]any) (*models.Destination, error) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldLockingScript: lockingScript, - FieldMetadata: metadata, - }) - if err != nil { - return nil, WrapError(err) - } - - var destination models.Destination - if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/destination", jsonStr, wc.xPriv, true, &destination, - ); err != nil { - return nil, err - } - - return &destination, nil -} - // GetTransaction will get a transaction by ID func (wc *WalletClient) GetTransaction(ctx context.Context, txID string) (*models.Transaction, error) { var transaction models.Transaction - if err := wc.doHTTPRequest(ctx, http.MethodGet, "/transaction?"+FieldID+"="+txID, nil, wc.xPriv, wc.signRequest, &transaction); err != nil { + if err := wc.doHTTPRequest(ctx, http.MethodGet, "/transactions/"+FieldID+"="+txID, nil, wc.xPriv, wc.signRequest, &transaction); err != nil { return nil, err } @@ -297,7 +139,7 @@ func (wc *WalletClient) GetTransactions( ) ([]*models.Transaction, error) { return Search[filter.TransactionFilter, []*models.Transaction]( ctx, http.MethodPost, - "/transaction/search", + "/transactions", wc.xPriv, conditions, metadata, @@ -306,22 +148,6 @@ func (wc *WalletClient) GetTransactions( ) } -// GetTransactionsCount get number of user transactions -func (wc *WalletClient) GetTransactionsCount( - ctx context.Context, - conditions *filter.TransactionFilter, - metadata map[string]any, -) (int64, error) { - 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 map[string]any) (*models.DraftTransaction, error) { outputs := make([]map[string]interface{}, 0) @@ -360,7 +186,7 @@ func (wc *WalletClient) createDraftTransaction(ctx context.Context, var draftTransaction *models.DraftTransaction if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/transaction", jsonStr, wc.xPriv, true, &draftTransaction, + ctx, http.MethodPost, "/transactions", jsonStr, wc.xPriv, true, &draftTransaction, ); err != nil { return nil, err } @@ -384,7 +210,7 @@ func (wc *WalletClient) RecordTransaction(ctx context.Context, hex, referenceID var transaction models.Transaction if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/transaction/record", jsonStr, wc.xPriv, wc.signRequest, &transaction, + ctx, http.MethodPost, "/transactions", jsonStr, wc.xPriv, wc.signRequest, &transaction, ); err != nil { return nil, err } @@ -404,7 +230,7 @@ func (wc *WalletClient) UpdateTransactionMetadata(ctx context.Context, txID stri var transaction models.Transaction if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/transaction", jsonStr, wc.xPriv, wc.signRequest, &transaction, + ctx, http.MethodPatch, "/transactions", jsonStr, wc.xPriv, wc.signRequest, &transaction, ); err != nil { return nil, err } @@ -428,27 +254,11 @@ func SetSignatureFromAccessKey(header *http.Header, privateKeyHex, bodyString st return nil } -// GetUtxo will get a utxo by transaction ID -func (wc *WalletClient) GetUtxo(ctx context.Context, txID string, outputIndex uint32) (*models.Utxo, error) { - outputIndexStr := strconv.FormatUint(uint64(outputIndex), 10) - - url := fmt.Sprintf("/utxo?%s=%s&%s=%s", FieldTransactionID, txID, FieldOutputIndex, outputIndexStr) - - var utxo models.Utxo - if err := wc.doHTTPRequest( - ctx, http.MethodGet, url, nil, wc.xPriv, true, &utxo, - ); err != nil { - return nil, err - } - - return &utxo, nil -} - // GetUtxos will get a list of utxos filtered by conditions and metadata func (wc *WalletClient) GetUtxos(ctx context.Context, conditions *filter.UtxoFilter, metadata map[string]any, queryParams *filter.QueryParams) ([]*models.Utxo, error) { return Search[filter.UtxoFilter, []*models.Utxo]( ctx, http.MethodPost, - "/utxo/search", + "/utxos", wc.xPriv, conditions, metadata, @@ -457,18 +267,6 @@ func (wc *WalletClient) GetUtxos(ctx context.Context, conditions *filter.UtxoFil ) } -// GetUtxosCount will get the count of utxos filtered by conditions and metadata -func (wc *WalletClient) GetUtxosCount(ctx context.Context, conditions *filter.UtxoFilter, metadata map[string]any) (int64, error) { - 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 func createSignatureAccessKey(privateKeyHex, bodyString string) (payload *models.AuthPayload, err error) { // No key? @@ -572,7 +370,7 @@ func (wc *WalletClient) authenticateWithAccessKey(req *http.Request, rawJSON []b // AcceptContact will accept the contact associated with the paymail func (wc *WalletClient) AcceptContact(ctx context.Context, paymail string) error { if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/contact/accepted/"+paymail, nil, wc.xPriv, wc.signRequest, nil, + ctx, http.MethodPatch, "/contacts/"+paymail, nil, wc.xPriv, wc.signRequest, nil, ); err != nil { return err } @@ -583,7 +381,7 @@ func (wc *WalletClient) AcceptContact(ctx context.Context, paymail string) error // RejectContact will reject the contact associated with the paymail func (wc *WalletClient) RejectContact(ctx context.Context, paymail string) error { if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/contact/rejected/"+paymail, nil, wc.xPriv, wc.signRequest, nil, + ctx, http.MethodPatch, "/contacts/"+paymail, nil, wc.xPriv, wc.signRequest, nil, ); err != nil { return err } @@ -603,7 +401,7 @@ func (wc *WalletClient) ConfirmContact(ctx context.Context, contact *models.Cont } if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/contact/confirmed/"+contact.Paymail, nil, wc.xPriv, wc.signRequest, nil, + ctx, http.MethodPatch, "/contacts/"+contact.Paymail, nil, wc.xPriv, wc.signRequest, nil, ); err != nil { return err } @@ -615,7 +413,7 @@ func (wc *WalletClient) ConfirmContact(ctx context.Context, contact *models.Cont func (wc *WalletClient) GetContacts(ctx context.Context, conditions *filter.ContactFilter, metadata map[string]any, queryParams *filter.QueryParams) (*models.SearchContactsResponse, error) { return Search[filter.ContactFilter, *models.SearchContactsResponse]( ctx, http.MethodPost, - "/contact/search", + "/contacts/", wc.xPriv, conditions, metadata, @@ -647,7 +445,7 @@ func (wc *WalletClient) UpsertContactForPaymail(ctx context.Context, paymail, fu var result models.Contact if err := wc.doHTTPRequest( - ctx, http.MethodPut, "/contact/"+paymail, jsonStr, wc.xPriv, wc.signRequest, &result, + ctx, http.MethodPut, "/contacts/"+paymail, jsonStr, wc.xPriv, wc.signRequest, &result, ); err != nil { return nil, err } @@ -667,210 +465,7 @@ func (wc *WalletClient) GetSharedConfig(ctx context.Context) (*models.SharedConf return nil, WrapError(ErrMissingKey) } if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/shared-config", nil, key, true, &model, - ); err != nil { - return nil, err - } - - return model, nil -} - -// AdminNewXpub will register an xPub -func (wc *WalletClient) AdminNewXpub(ctx context.Context, rawXPub string, metadata map[string]any) error { - // 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: metadata, - FieldXpubKey: rawXPub, - }) - if err != nil { - return WrapError(err) - } - - var xPubData models.Xpub - - return wc.doHTTPRequest( - ctx, http.MethodPost, "/admin/xpub", jsonStr, wc.adminXPriv, true, &xPubData, - ) -} - -// AdminGetStatus get whether admin key is valid -func (wc *WalletClient) AdminGetStatus(ctx context.Context) (bool, error) { - var status bool - if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/admin/status", nil, wc.adminXPriv, true, &status, - ); err != nil { - return false, err - } - - return status, nil -} - -// AdminGetStats get admin stats -func (wc *WalletClient) AdminGetStats(ctx context.Context) (*models.AdminStats, error) { - var stats *models.AdminStats - if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/admin/stats", nil, wc.adminXPriv, true, &stats, - ); err != nil { - return nil, err - } - - return stats, nil -} - -// AdminGetAccessKeys get all access keys filtered by conditions -func (wc *WalletClient) AdminGetAccessKeys( - ctx context.Context, - conditions *filter.AdminAccessKeyFilter, - metadata map[string]any, - queryParams *filter.QueryParams, -) ([]*models.AccessKey, error) { - 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 *filter.AdminAccessKeyFilter, - metadata map[string]any, -) (int64, error) { - 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 map[string]any, - queryParams *filter.QueryParams, -) ([]*models.BlockHeader, error) { - var models []*models.BlockHeader - if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/block-headers/search", &models); err != nil { - return nil, err - } - - return models, nil -} - -// AdminGetBlockHeadersCount get a count of all the block headers filtered by conditions -func (wc *WalletClient) AdminGetBlockHeadersCount( - ctx context.Context, - conditions map[string]interface{}, - metadata map[string]any, -) (int64, error) { - 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 *filter.DestinationFilter, - metadata map[string]any, queryParams *filter.QueryParams, -) ([]*models.Destination, error) { - 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 *filter.DestinationFilter, metadata map[string]any) (int64, error) { - return Count( - ctx, - http.MethodPost, - "/admin/destinations/count", - wc.adminXPriv, - conditions, - metadata, - wc.doHTTPRequest, - ) -} - -// AdminGetPaymail get a paymail by address -func (wc *WalletClient) AdminGetPaymail(ctx context.Context, address string) (*models.PaymailAddress, error) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldAddress: address, - }) - if err != nil { - return nil, WrapError(err) - } - - var model *models.PaymailAddress - if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/admin/paymail/get", jsonStr, wc.adminXPriv, true, &model, - ); err != nil { - return nil, err - } - - return model, nil -} - -// AdminGetPaymails get all block paymails filtered by conditions -func (wc *WalletClient) AdminGetPaymails( - ctx context.Context, - conditions *filter.AdminPaymailFilter, - metadata map[string]any, - queryParams *filter.QueryParams, -) ([]*models.PaymailAddress, error) { - 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 *filter.AdminPaymailFilter, metadata map[string]any) (int64, error) { - return Count( - ctx, http.MethodPost, - "/admin/paymails/count", - wc.adminXPriv, - conditions, - metadata, - wc.doHTTPRequest, - ) -} - -// AdminCreatePaymail create a new paymail for a xpub -func (wc *WalletClient) AdminCreatePaymail(ctx context.Context, rawXPub string, address string, publicName string, avatar string) (*models.PaymailAddress, error) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldXpubKey: rawXPub, - FieldAddress: address, - FieldPublicName: publicName, - FieldAvatar: avatar, - }) - if err != nil { - return nil, WrapError(err) - } - - var model *models.PaymailAddress - if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/admin/paymail/create", jsonStr, wc.adminXPriv, true, &model, + ctx, http.MethodGet, "/configs/shared", nil, key, true, &model, ); err != nil { return nil, err } @@ -878,234 +473,6 @@ func (wc *WalletClient) AdminCreatePaymail(ctx context.Context, rawXPub string, return model, nil } -// AdminDeletePaymail delete a paymail address from the database -func (wc *WalletClient) AdminDeletePaymail(ctx context.Context, address string) error { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldAddress: address, - }) - if err != nil { - return WrapError(err) - } - - if err := wc.doHTTPRequest( - ctx, http.MethodDelete, "/admin/paymail/delete", jsonStr, wc.adminXPriv, true, nil, - ); err != nil { - return err - } - - return nil -} - -// AdminGetTransactions get all block transactions filtered by conditions -func (wc *WalletClient) AdminGetTransactions( - ctx context.Context, - conditions *filter.TransactionFilter, - metadata map[string]any, - queryParams *filter.QueryParams, -) ([]*models.Transaction, error) { - 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 *filter.TransactionFilter, - metadata map[string]any, -) (int64, error) { - 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 *filter.AdminUtxoFilter, - metadata map[string]any, - queryParams *filter.QueryParams, -) ([]*models.Utxo, error) { - 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 *filter.AdminUtxoFilter, - metadata map[string]any, -) (int64, error) { - 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 *filter.XpubFilter, - metadata map[string]any, queryParams *filter.QueryParams, -) ([]*models.Xpub, error) { - 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 *filter.XpubFilter, - metadata map[string]any, -) (int64, error) { - 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 map[string]any, - queryParams *filter.QueryParams, - path string, - models interface{}, -) error { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldConditions: conditions, - FieldMetadata: metadata, - FieldQueryParams: queryParams, - }) - if err != nil { - return WrapError(err) - } - - if err := wc.doHTTPRequest( - ctx, http.MethodPost, path, jsonStr, wc.adminXPriv, true, &models, - ); err != nil { - return err - } - - return nil -} - -func (wc *WalletClient) adminCount(ctx context.Context, conditions map[string]interface{}, metadata map[string]any, path string) (int64, error) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldConditions: conditions, - FieldMetadata: metadata, - }) - if err != nil { - return 0, WrapError(err) - } - - var count int64 - if err := wc.doHTTPRequest( - ctx, http.MethodPost, path, jsonStr, wc.adminXPriv, true, &count, - ); err != nil { - return 0, err - } - - return count, nil -} - -// AdminRecordTransaction will record a transaction as an admin -func (wc *WalletClient) AdminRecordTransaction(ctx context.Context, hex string) (*models.Transaction, error) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldHex: hex, - }) - if err != nil { - return nil, WrapError(err) - } - - var transaction models.Transaction - if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/admin/transactions/record", jsonStr, wc.adminXPriv, wc.signRequest, &transaction, - ); err != nil { - return nil, err - } - - return &transaction, nil -} - -// 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 *filter.ContactFilter, metadata map[string]any, queryParams *filter.QueryParams) (*models.SearchContactsResponse, error) { - 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 map[string]any) (*models.Contact, error) { - jsonStr, err := json.Marshal(map[string]interface{}{ - "fullName": fullName, - FieldMetadata: metadata, - }) - if err != nil { - return nil, WrapError(err) - } - var contact models.Contact - err = wc.doHTTPRequest(ctx, http.MethodPatch, fmt.Sprintf("/admin/contact/%s", id), jsonStr, wc.adminXPriv, true, &contact) - return &contact, WrapError(err) -} - -// AdminDeleteContact executes an HTTP DELETE request to remove a contact using their ID. -func (wc *WalletClient) AdminDeleteContact(ctx context.Context, id string) error { - err := wc.doHTTPRequest(ctx, http.MethodDelete, fmt.Sprintf("/admin/contact/%s", id), nil, wc.adminXPriv, true, nil) - return WrapError(err) -} - -// AdminAcceptContact executes an HTTP PATCH request to mark a contact as accepted using their ID. -func (wc *WalletClient) AdminAcceptContact(ctx context.Context, id string) (*models.Contact, error) { - var contact models.Contact - err := wc.doHTTPRequest(ctx, http.MethodPatch, fmt.Sprintf("/admin/contact/accepted/%s", id), nil, wc.adminXPriv, true, &contact) - return &contact, WrapError(err) -} - -// AdminRejectContact executes an HTTP PATCH request to mark a contact as rejected using their ID. -func (wc *WalletClient) AdminRejectContact(ctx context.Context, id string) (*models.Contact, error) { - var contact models.Contact - err := wc.doHTTPRequest(ctx, http.MethodPatch, fmt.Sprintf("/admin/contact/rejected/%s", id), nil, wc.adminXPriv, true, &contact) - return &contact, WrapError(err) -} - // FinalizeTransaction will finalize the transaction func (wc *WalletClient) FinalizeTransaction(draft *models.DraftTransaction) (string, error) { res, err := GetSignedHex(draft, wc.xPriv) @@ -1130,41 +497,3 @@ func (wc *WalletClient) SendToRecipients(ctx context.Context, recipients []*Reci return wc.RecordTransaction(ctx, hex, draft.ID, metadata) } - -// AdminSubscribeWebhook subscribes to a webhook to receive notifications from spv-wallet -func (wc *WalletClient) AdminSubscribeWebhook(ctx context.Context, webhookURL, tokenHeader, tokenValue string) error { - requestModel := models.SubscribeRequestBody{ - URL: webhookURL, - TokenHeader: tokenHeader, - TokenValue: tokenValue, - } - rawJSON, err := json.Marshal(requestModel) - if err != nil { - return WrapError(err) - } - err = wc.doHTTPRequest(ctx, http.MethodPost, "/admin/webhooks/subscriptions", rawJSON, wc.adminXPriv, true, nil) - return WrapError(err) -} - -// AdminUnsubscribeWebhook unsubscribes from a webhook -func (wc *WalletClient) AdminUnsubscribeWebhook(ctx context.Context, webhookURL string) error { - requestModel := models.UnsubscribeRequestBody{ - URL: webhookURL, - } - rawJSON, err := json.Marshal(requestModel) - if err != nil { - return WrapError(err) - } - err = wc.doHTTPRequest(ctx, http.MethodDelete, "/admin/webhooks/subscriptions", rawJSON, wc.adminXPriv, true, nil) - return err -} - -// AdminGetWebhooks gets all webhooks -func (wc *WalletClient) AdminGetWebhooks(ctx context.Context) ([]*models.Webhook, error) { - var webhooks []*models.Webhook - err := wc.doHTTPRequest(ctx, http.MethodGet, "/admin/webhooks/subscriptions", nil, wc.adminXPriv, true, &webhooks) - if err != nil { - return nil, WrapError(err) - } - return webhooks, nil -} diff --git a/http_admin.go b/http_admin.go new file mode 100644 index 0000000..3441916 --- /dev/null +++ b/http_admin.go @@ -0,0 +1,480 @@ +package walletclient + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/bitcoin-sv/spv-wallet/models" + "github.com/bitcoin-sv/spv-wallet/models/filter" +) + +// AdminNewXpub will register an xPub +func (wc *WalletClient) AdminNewXpub(ctx context.Context, rawXPub string, metadata map[string]any) error { + // 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: metadata, + FieldXpubKey: rawXPub, + }) + if err != nil { + return WrapError(err) + } + + var xPubData models.Xpub + + return wc.doHTTPRequest( + ctx, http.MethodPost, "/admin/xpub", jsonStr, wc.adminXPriv, true, &xPubData, + ) +} + +// AdminGetStatus get whether admin key is valid +func (wc *WalletClient) AdminGetStatus(ctx context.Context) (bool, error) { + var status bool + if err := wc.doHTTPRequest( + ctx, http.MethodGet, "/admin/status", nil, wc.adminXPriv, true, &status, + ); err != nil { + return false, err + } + + return status, nil +} + +// AdminGetStats get admin stats +func (wc *WalletClient) AdminGetStats(ctx context.Context) (*models.AdminStats, error) { + var stats *models.AdminStats + if err := wc.doHTTPRequest( + ctx, http.MethodGet, "/admin/stats", nil, wc.adminXPriv, true, &stats, + ); err != nil { + return nil, err + } + + return stats, nil +} + +// AdminGetAccessKeys get all access keys filtered by conditions +func (wc *WalletClient) AdminGetAccessKeys( + ctx context.Context, + conditions *filter.AdminAccessKeyFilter, + metadata map[string]any, + queryParams *filter.QueryParams, +) ([]*models.AccessKey, error) { + 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 *filter.AdminAccessKeyFilter, + metadata map[string]any, +) (int64, error) { + 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 map[string]any, + queryParams *filter.QueryParams, +) ([]*models.BlockHeader, error) { + var models []*models.BlockHeader + if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/block-headers/search", &models); err != nil { + return nil, err + } + + return models, nil +} + +// AdminGetBlockHeadersCount get a count of all the block headers filtered by conditions +func (wc *WalletClient) AdminGetBlockHeadersCount( + ctx context.Context, + conditions map[string]interface{}, + metadata map[string]any, +) (int64, error) { + 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 *filter.DestinationFilter, + metadata map[string]any, queryParams *filter.QueryParams, +) ([]*models.Destination, error) { + 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 *filter.DestinationFilter, metadata map[string]any) (int64, error) { + return Count( + ctx, + http.MethodPost, + "/admin/destinations/count", + wc.adminXPriv, + conditions, + metadata, + wc.doHTTPRequest, + ) +} + +// AdminGetPaymail get a paymail by address +func (wc *WalletClient) AdminGetPaymail(ctx context.Context, address string) (*models.PaymailAddress, error) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldAddress: address, + }) + if err != nil { + return nil, WrapError(err) + } + + var model *models.PaymailAddress + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/admin/paymail/get", jsonStr, wc.adminXPriv, true, &model, + ); err != nil { + return nil, err + } + + return model, nil +} + +// AdminGetPaymails get all block paymails filtered by conditions +func (wc *WalletClient) AdminGetPaymails( + ctx context.Context, + conditions *filter.AdminPaymailFilter, + metadata map[string]any, + queryParams *filter.QueryParams, +) ([]*models.PaymailAddress, error) { + 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 *filter.AdminPaymailFilter, metadata map[string]any) (int64, error) { + return Count( + ctx, http.MethodPost, + "/admin/paymails/count", + wc.adminXPriv, + conditions, + metadata, + wc.doHTTPRequest, + ) +} + +// AdminCreatePaymail create a new paymail for a xpub +func (wc *WalletClient) AdminCreatePaymail(ctx context.Context, rawXPub string, address string, publicName string, avatar string) (*models.PaymailAddress, error) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldXpubKey: rawXPub, + FieldAddress: address, + FieldPublicName: publicName, + FieldAvatar: avatar, + }) + if err != nil { + return nil, WrapError(err) + } + + var model *models.PaymailAddress + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/admin/paymail/create", jsonStr, wc.adminXPriv, true, &model, + ); err != nil { + return nil, err + } + + return model, nil +} + +// AdminDeletePaymail delete a paymail address from the database +func (wc *WalletClient) AdminDeletePaymail(ctx context.Context, address string) error { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldAddress: address, + }) + if err != nil { + return WrapError(err) + } + + if err := wc.doHTTPRequest( + ctx, http.MethodDelete, "/admin/paymail/delete", jsonStr, wc.adminXPriv, true, nil, + ); err != nil { + return err + } + + return nil +} + +// AdminGetTransactions get all block transactions filtered by conditions +func (wc *WalletClient) AdminGetTransactions( + ctx context.Context, + conditions *filter.TransactionFilter, + metadata map[string]any, + queryParams *filter.QueryParams, +) ([]*models.Transaction, error) { + 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 *filter.TransactionFilter, + metadata map[string]any, +) (int64, error) { + 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 *filter.AdminUtxoFilter, + metadata map[string]any, + queryParams *filter.QueryParams, +) ([]*models.Utxo, error) { + 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 *filter.AdminUtxoFilter, + metadata map[string]any, +) (int64, error) { + 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 *filter.XpubFilter, + metadata map[string]any, queryParams *filter.QueryParams, +) ([]*models.Xpub, error) { + 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 *filter.XpubFilter, + metadata map[string]any, +) (int64, error) { + 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 map[string]any, + queryParams *filter.QueryParams, + path string, + models interface{}, +) error { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldConditions: conditions, + FieldMetadata: metadata, + FieldQueryParams: queryParams, + }) + if err != nil { + return WrapError(err) + } + + if err := wc.doHTTPRequest( + ctx, http.MethodPost, path, jsonStr, wc.adminXPriv, true, &models, + ); err != nil { + return err + } + + return nil +} + +func (wc *WalletClient) adminCount(ctx context.Context, conditions map[string]interface{}, metadata map[string]any, path string) (int64, error) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldConditions: conditions, + FieldMetadata: metadata, + }) + if err != nil { + return 0, WrapError(err) + } + + var count int64 + if err := wc.doHTTPRequest( + ctx, http.MethodPost, path, jsonStr, wc.adminXPriv, true, &count, + ); err != nil { + return 0, err + } + + return count, nil +} + +// AdminRecordTransaction will record a transaction as an admin +func (wc *WalletClient) AdminRecordTransaction(ctx context.Context, hex string) (*models.Transaction, error) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldHex: hex, + }) + if err != nil { + return nil, WrapError(err) + } + + var transaction models.Transaction + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/admin/transactions/record", jsonStr, wc.adminXPriv, wc.signRequest, &transaction, + ); err != nil { + return nil, err + } + + return &transaction, nil +} + +// 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 *filter.ContactFilter, metadata map[string]any, queryParams *filter.QueryParams) (*models.SearchContactsResponse, error) { + 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 map[string]any) (*models.Contact, error) { + jsonStr, err := json.Marshal(map[string]interface{}{ + "fullName": fullName, + FieldMetadata: metadata, + }) + if err != nil { + return nil, WrapError(err) + } + var contact models.Contact + err = wc.doHTTPRequest(ctx, http.MethodPatch, fmt.Sprintf("/admin/contact/%s", id), jsonStr, wc.adminXPriv, true, &contact) + return &contact, WrapError(err) +} + +// AdminDeleteContact executes an HTTP DELETE request to remove a contact using their ID. +func (wc *WalletClient) AdminDeleteContact(ctx context.Context, id string) error { + err := wc.doHTTPRequest(ctx, http.MethodDelete, fmt.Sprintf("/admin/contact/%s", id), nil, wc.adminXPriv, true, nil) + return WrapError(err) +} + +// AdminAcceptContact executes an HTTP PATCH request to mark a contact as accepted using their ID. +func (wc *WalletClient) AdminAcceptContact(ctx context.Context, id string) (*models.Contact, error) { + var contact models.Contact + err := wc.doHTTPRequest(ctx, http.MethodPatch, fmt.Sprintf("/admin/contact/accepted/%s", id), nil, wc.adminXPriv, true, &contact) + return &contact, WrapError(err) +} + +// AdminRejectContact executes an HTTP PATCH request to mark a contact as rejected using their ID. +func (wc *WalletClient) AdminRejectContact(ctx context.Context, id string) (*models.Contact, error) { + var contact models.Contact + err := wc.doHTTPRequest(ctx, http.MethodPatch, fmt.Sprintf("/admin/contact/rejected/%s", id), nil, wc.adminXPriv, true, &contact) + return &contact, WrapError(err) +} + +// AdminSubscribeWebhook subscribes to a webhook to receive notifications from spv-wallet +func (wc *WalletClient) AdminSubscribeWebhook(ctx context.Context, webhookURL, tokenHeader, tokenValue string) error { + requestModel := models.SubscribeRequestBody{ + URL: webhookURL, + TokenHeader: tokenHeader, + TokenValue: tokenValue, + } + rawJSON, err := json.Marshal(requestModel) + if err != nil { + return WrapError(err) + } + err = wc.doHTTPRequest(ctx, http.MethodPost, "/admin/webhooks/subscriptions", rawJSON, wc.adminXPriv, true, nil) + return WrapError(err) +} + +// AdminUnsubscribeWebhook unsubscribes from a webhook +func (wc *WalletClient) AdminUnsubscribeWebhook(ctx context.Context, webhookURL string) error { + requestModel := models.UnsubscribeRequestBody{ + URL: webhookURL, + } + rawJSON, err := json.Marshal(requestModel) + if err != nil { + return WrapError(err) + } + err = wc.doHTTPRequest(ctx, http.MethodDelete, "/admin/webhooks/subscriptions", rawJSON, wc.adminXPriv, true, nil) + return err +} + +// AdminGetWebhooks gets all webhooks +func (wc *WalletClient) AdminGetWebhooks(ctx context.Context) ([]*models.Webhook, error) { + var webhooks []*models.Webhook + err := wc.doHTTPRequest(ctx, http.MethodGet, "/admin/webhooks/subscriptions", nil, wc.adminXPriv, true, &webhooks) + if err != nil { + return nil, WrapError(err) + } + return webhooks, nil +} diff --git a/search.go b/search.go index aa23dcb..00d0920 100644 --- a/search.go +++ b/search.go @@ -41,6 +41,36 @@ func Search[TFilter any, TResp any]( return resp, nil } +// OldSearch prepares and sends a search request to the server. +func OldSearch[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, error) { + 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, diff --git a/transactions_test.go b/transactions_test.go index f359cce..782ecbc 100644 --- a/transactions_test.go +++ b/transactions_test.go @@ -8,26 +8,30 @@ import ( "testing" "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/bitcoin-sv/spv-wallet/models/response" "github.com/stretchr/testify/require" ) func TestTransactions(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case "/v1/transaction": - handleTransaction(w, r) - case "/v1/transaction/search": - json.NewEncoder(w).Encode([]*models.Transaction{fixtures.Transaction}) - case "/v1/transaction/count": - json.NewEncoder(w).Encode(1) - case "/v1/transaction/record": - if r.Method == http.MethodPost { + case "/api/v1/transactions": + switch r.Method { + case http.MethodGet: + json.NewEncoder(w).Encode([]*response.Transaction{fixtures.Transaction}) + case http.MethodPost: w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(fixtures.Transaction) - } else { - w.WriteHeader(http.StatusMethodNotAllowed) + } + case "/api/v1/transactions/" + fixtures.Transaction.ID: + switch r.Method { + case http.MethodGet, http.MethodPatch: + handleTransaction(w, r) + } + case "/api/v1/transactions/drafts": + if r.Method == http.MethodPost { + handleTransaction(w, r) } default: w.WriteHeader(http.StatusNotFound) @@ -51,13 +55,7 @@ func TestTransactions(t *testing.T) { } txs, err := client.GetTransactions(context.Background(), conditions, fixtures.TestMetadata, nil) require.NoError(t, err) - require.Equal(t, []*models.Transaction{fixtures.Transaction}, txs) - }) - - t.Run("GetTransactionsCount", func(t *testing.T) { - count, err := client.GetTransactionsCount(context.Background(), nil, fixtures.TestMetadata) - require.NoError(t, err) - require.Equal(t, int64(1), count) + require.Equal(t, []*response.Transaction{fixtures.Transaction}, txs) }) t.Run("RecordTransaction", func(t *testing.T) { diff --git a/xpubs_test.go b/xpubs_test.go index 8a87d81..8319e0a 100644 --- a/xpubs_test.go +++ b/xpubs_test.go @@ -24,7 +24,7 @@ func TestXpub(t *testing.T) { var response xpub // Check path and method to customize the response switch { - case r.URL.Path == "/v1/xpub": + case r.URL.Path == "/api/v1/users/current": metadata := &models.Metadata{"key": "value"} if update { metadata = &models.Metadata{"updated": "info"}