diff --git a/docs/docs.go b/docs/docs.go index 091721e..2a00515 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1,5 +1,4 @@ -// Code generated by swaggo/swag. DO NOT EDIT. - +// Package docs Code generated by swaggo/swag. DO NOT EDIT package docs import "github.com/swaggo/swag" @@ -40,9 +39,63 @@ const docTemplate = `{ } } } + }, + "/api/wallet/restore": { + "post": { + "description": "Restores a wallet using the provided mnemonic seed phrase and returns wallet details.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Restore a wallet using a mnemonic seed phrase", + "parameters": [ + { + "description": "Wallet Restore Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.WalletRestoreRequest" + } + } + ], + "responses": { + "200": { + "description": "Wallet successfully restored", + "schema": { + "$ref": "#/definitions/bursa.Wallet" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } } }, "definitions": { + "api.WalletRestoreRequest": { + "type": "object", + "required": [ + "mnemonic" + ], + "properties": { + "mnemonic": { + "type": "string" + } + } + }, "bursa.KeyFile": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 44112ab..f22b375 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -35,9 +35,63 @@ } } } + }, + "/api/wallet/restore": { + "post": { + "description": "Restores a wallet using the provided mnemonic seed phrase and returns wallet details.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Restore a wallet using a mnemonic seed phrase", + "parameters": [ + { + "description": "Wallet Restore Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.WalletRestoreRequest" + } + } + ], + "responses": { + "200": { + "description": "Wallet successfully restored", + "schema": { + "$ref": "#/definitions/bursa.Wallet" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } } }, "definitions": { + "api.WalletRestoreRequest": { + "type": "object", + "required": [ + "mnemonic" + ], + "properties": { + "mnemonic": { + "type": "string" + } + } + }, "bursa.KeyFile": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 646e094..b8141d9 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,5 +1,12 @@ basePath: / definitions: + api.WalletRestoreRequest: + properties: + mnemonic: + type: string + required: + - mnemonic + type: object bursa.KeyFile: properties: cborHex: @@ -49,6 +56,35 @@ paths: schema: $ref: '#/definitions/bursa.Wallet' summary: CreateWallet + /api/wallet/restore: + post: + consumes: + - application/json + description: Restores a wallet using the provided mnemonic seed phrase and returns + wallet details. + parameters: + - description: Wallet Restore Request + in: body + name: request + required: true + schema: + $ref: '#/definitions/api.WalletRestoreRequest' + produces: + - application/json + responses: + "200": + description: Wallet successfully restored + schema: + $ref: '#/definitions/bursa.Wallet' + "400": + description: Invalid request + schema: + type: string + "500": + description: Internal server error + schema: + type: string + summary: Restore a wallet using a mnemonic seed phrase schemes: - http swagger: "2.0" diff --git a/internal/api/api.go b/internal/api/api.go index 9ffa12a..1de22b1 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -16,6 +16,7 @@ package api import ( "fmt" + "net/http" "time" ginzap "github.com/gin-contrib/zap" @@ -31,6 +32,11 @@ import ( _ "github.com/blinklabs-io/bursa/docs" // docs is generated by Swag CLI ) +// WalletRestoreRequest defines the request payload for wallet restoration +type WalletRestoreRequest struct { + Mnemonic string `json:"mnemonic" binding:"required"` +} + // @title bursa // @version v0 // @description Programmable Cardano Wallet API @@ -91,9 +97,16 @@ func Start(cfg *config.Config) error { Description: "total number of wallet failures", Labels: nil, } + restoreMetric := &ginmetrics.Metric{ + Type: ginmetrics.Counter, + Name: "bursa_wallets_restore_count", + Description: "total number of wallets restored", + Labels: nil, + } // Add to global monitor object _ = ginmetrics.GetMonitor().AddMetric(createdMetric) _ = ginmetrics.GetMonitor().AddMetric(failureMetric) + _ = ginmetrics.GetMonitor().AddMetric(restoreMetric) // Start metrics listener go func() { @@ -110,6 +123,7 @@ func Start(cfg *config.Config) error { // Configure API routes router.GET("/api/wallet/create", handleWalletCreate) + router.POST("/api/wallet/restore", handleWalletRestore) // Start API listener err := router.Run(fmt.Sprintf("%s:%d", @@ -155,3 +169,39 @@ func handleWalletCreate(c *gin.Context) { c.JSON(200, w) _ = ginmetrics.GetMonitor().GetMetric("bursa_wallets_create_count").Inc(nil) } + +// handleWalletRestore handles the wallet restoration request. +// +// @Summary Restore a wallet using a mnemonic seed phrase +// @Description Restores a wallet using the provided mnemonic seed phrase and returns wallet details. +// @Accept json +// @Produce json +// @Param request body WalletRestoreRequest true "Wallet Restore Request" +// @Success 200 {object} bursa.Wallet "Wallet successfully restored" +// @Failure 400 {string} string "Invalid request" +// @Failure 500 {string} string "Internal server error" +// @Router /api/wallet/restore [post] +func handleWalletRestore(c *gin.Context) { + var request WalletRestoreRequest + if err := c.BindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) + _ = ginmetrics.GetMonitor(). + GetMetric("bursa_wallets_fail_count"). + Inc(nil) + return + } + + // Restore the wallet using the mnemonic + wallet, err := bursa.NewDefaultWallet(request.Mnemonic) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"}) + _ = ginmetrics.GetMonitor(). + GetMetric("bursa_wallets_fail_count"). + Inc(nil) + return + } + + // Return the wallet details + c.JSON(http.StatusOK, wallet) + _ = ginmetrics.GetMonitor().GetMetric("bursa_wallets_restore_count").Inc(nil) +} diff --git a/internal/api/api_test.go b/internal/api/api_test.go new file mode 100644 index 0000000..3260f15 --- /dev/null +++ b/internal/api/api_test.go @@ -0,0 +1,96 @@ +package api + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "reflect" + "testing" +) + +// Mock JSON data for successful wallet restoration +var mockWalletResponseJSON = `{ + "mnemonic": "depth kitchen crystal history rabbit brief harbor palace tent frog city charge inflict tiger negative young furnace solid august educate bounce canal someone erode", + "payment_address": "addr1qxwqkfd3qz5pdwmemtv2llmetegdyku4ffxuldjcfrs05nfjtw33ktf3j6amgxsgnj9u3fa5nrle79nv2g24npnth0esk2dy7q", + "stake_address": "stake1uye9hgcm95cedwa5rgyfez7g576f3lulzek9y92ese4mhucu439t0", + "payment_vkey": { + "type": "PaymentVerificationKeyShelley_ed25519", + "description": "Payment Verification Key", + "cborHex": "582040c99562052dc67d0e265bf183d2e376905972346a11eec2dbb714600bb28911" + }, + "payment_skey": { + "type": "PaymentExtendedSigningKeyShelley_ed25519_bip32", + "description": "Payment Signing Key", + "cborHex": "5880d8f05d500419f363eb81d5ed832f7264b24cb529e6e2cb643a495e82c3aa6c4203089cdb6ed0f2d0db817b5a90e9f5b689a6e4da1f1c2157b463dd6690bee72840c99562052dc67d0e265bf183d2e376905972346a11eec2dbb714600bb28911c938975e7bec39ea8e57613558571b72eb4f399ab7967e985174a23c6e767840" + }, + "stake_vkey": { + "type": "StakeVerificationKeyShelley_ed25519", + "description": "Stake Verification Key", + "cborHex": "58202a786a251854a5f459a856e7ae8f9289be9a3a7a1bf421e35bfaab815868e0fd" + }, + "stake_skey": { + "type": "StakeExtendedSigningKeyShelley_ed25519_bip32", + "description": "Stake Signing Key", + "cborHex": "5880b0a9a8bcddc391c2cc79dbbac792e21f21fa8a3572e8591235bdc802c9aa6c4210eee620765fb6f569ab6b2916001cdd6d289067b022847d62ea19160463bcf72a786a251854a5f459a856e7ae8f9289be9a3a7a1bf421e35bfaab815868e0fd258df1a6eb6b51f6769afcdf4634594b13dba6433ec3670ea9ea742a09e8711e" + } +}` + +func TestRestoreWallet(t *testing.T) { + t.Run("Successful Wallet Restoration", func(t *testing.T) { + t.Parallel() + + // Create a mock server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/api/wallet/restore" && r.Method == "POST" { + var request WalletRestoreRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + // Respond with the mock wallet JSON data + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(mockWalletResponseJSON)) + if err != nil { + t.Fatalf("Failed to write response: %v", err) + } + + } else { + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + + // Prepare the request body + requestBody, _ := json.Marshal(WalletRestoreRequest{ + Mnemonic: "depth kitchen crystal history rabbit brief harbor palace tent frog city charge inflict tiger negative young furnace solid august educate bounce canal someone erode", + }) + + resp, err := http.Post(server.URL+"/api/wallet/restore", "application/json", bytes.NewBuffer(requestBody)) + if err != nil { + t.Fatalf("Failed to make request: %v", err) + } + defer resp.Body.Close() + + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("Failed to read response body: %v", err) + } + + var expectedResponse, actualResponse map[string]interface{} + + if err := json.Unmarshal([]byte(mockWalletResponseJSON), &expectedResponse); err != nil { + t.Fatalf("Failed to unmarshal expected response: %v", err) + } + if err := json.Unmarshal(responseBody, &actualResponse); err != nil { + t.Fatalf("Failed to unmarshal actual response: %v", err) + } + + if !reflect.DeepEqual(expectedResponse, actualResponse) { + t.Errorf("Expected response %v, got %v", expectedResponse, actualResponse) + } + }) + +}