Skip to content

Commit

Permalink
feat: add a wallet restore endpoint with mnemonic
Browse files Browse the repository at this point in the history
Signed-off-by: Ales Verbic <[email protected]>
  • Loading branch information
verbotenj committed Nov 26, 2023
1 parent 428f51b commit cfd1da9
Show file tree
Hide file tree
Showing 6 changed files with 306 additions and 2 deletions.
29 changes: 29 additions & 0 deletions bursa.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,32 @@ func GetKeyFile(keyFile KeyFile) string {
// Append newline
return fmt.Sprintf("%s\n", ret)
}

func RestoreWallet(mnemonic string) (*Wallet, error) {
rootKey, err := GetRootKeyFromMnemonic(mnemonic)
if err != nil {
return nil, fmt.Errorf("failed to get root key from mnemonic: %s", err)
}

// TODO: Support multiple accounts and addresses. Assuming default account and address IDs now as in createWallet.
accountId := uint(0)
paymentId, stakeId, addressId := uint32(0), uint32(0), uint32(0)

accountKey := GetAccountKey(rootKey, accountId)
paymentKey := GetPaymentKey(accountKey, paymentId)
stakeKey := GetStakeKey(accountKey, stakeId)
// TODO: Support differnet newtworks. Assuming mainnet now.
addr := GetAddress(accountKey, "mainnet", addressId)

wallet := &Wallet{
Mnemonic: mnemonic,
PaymentAddress: addr.String(),
StakeAddress: addr.ToReward().String(),
PaymentVKey: GetPaymentVKey(paymentKey),
PaymentSKey: GetPaymentSKey(paymentKey),
StakeVKey: GetStakeVKey(stakeKey),
StakeSKey: GetStakeSKey(stakeKey),
}

return wallet, nil
}
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"
36 changes: 36 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 @@ -110,6 +116,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 +162,32 @@ 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"})
return
}

// Restore the wallet using the mnemonic
wallet, err := bursa.RestoreWallet(request.Mnemonic)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
return
}

// Return the wallet details
c.JSON(http.StatusOK, wallet)
}
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)
}
})

}

0 comments on commit cfd1da9

Please sign in to comment.