Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add a wallet restore endpoint with mnemonic #43

Merged
merged 1 commit into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 55 additions & 2 deletions docs/docs.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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": {
Expand Down
54 changes: 54 additions & 0 deletions docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
36 changes: 36 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
basePath: /
definitions:
api.WalletRestoreRequest:
properties:
mnemonic:
type: string
required:
- mnemonic
type: object
bursa.KeyFile:
properties:
cborHex:
Expand Down Expand Up @@ -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"
50 changes: 50 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package api

import (
"fmt"
"net/http"
"time"

ginzap "github.com/gin-contrib/zap"
Expand All @@ -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
Expand Down Expand Up @@ -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() {
Expand All @@ -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",
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Increment our failure metric before returning

}

// Return the wallet details
c.JSON(http.StatusOK, wallet)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Define a success metric and increment it here before returning.

_ = ginmetrics.GetMonitor().GetMetric("bursa_wallets_restore_count").Inc(nil)
}
96 changes: 96 additions & 0 deletions internal/api/api_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})

}