Skip to content

Commit 03097c7

Browse files
committed
fix: validate TX builder inputs
Fixes #180 Signed-off-by: Aurora Gaffney <aurora@blinklabs.io>
1 parent 1c49b6a commit 03097c7

5 files changed

Lines changed: 70 additions & 15 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ go.work.sum
2626

2727
# Program binary
2828
/vpn-indexer
29+
/vpn-cli
2930

3031
# Local data dir
3132
/.vpn-indexer

internal/api/tx.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package api
1717
import (
1818
"encoding/hex"
1919
"encoding/json"
20+
"errors"
2021
"fmt"
2122
"io"
2223
"log/slog"
@@ -79,8 +80,14 @@ func (a *Api) handleTxSignup(w http.ResponseWriter, r *http.Request) {
7980
"error",
8081
err,
8182
)
82-
w.WriteHeader(http.StatusInternalServerError)
83-
_, _ = w.Write([]byte(`{"error":"Internal server error"}`))
83+
var validationErr txbuilder.InputValidationError
84+
if errors.As(err, &validationErr) {
85+
w.WriteHeader(http.StatusBadRequest)
86+
_, _ = w.Write([]byte(fmt.Sprintf(`{"error":"Invalid request: %s"}`, validationErr)))
87+
} else {
88+
w.WriteHeader(http.StatusInternalServerError)
89+
_, _ = w.Write([]byte(`{"error":"Internal server error"}`))
90+
}
8491
return
8592
}
8693

@@ -146,8 +153,14 @@ func (a *Api) handleTxRenew(w http.ResponseWriter, r *http.Request) {
146153
"error",
147154
err,
148155
)
149-
w.WriteHeader(http.StatusInternalServerError)
150-
_, _ = w.Write([]byte(`{"error":"Internal server error"}`))
156+
var validationErr txbuilder.InputValidationError
157+
if errors.As(err, &validationErr) {
158+
w.WriteHeader(http.StatusBadRequest)
159+
_, _ = w.Write([]byte(fmt.Sprintf(`{"error":"Invalid request: %s"}`, validationErr)))
160+
} else {
161+
w.WriteHeader(http.StatusInternalServerError)
162+
_, _ = w.Write([]byte(`{"error":"Internal server error"}`))
163+
}
151164
return
152165
}
153166

@@ -210,8 +223,14 @@ func (a *Api) handleTxTransfer(w http.ResponseWriter, r *http.Request) {
210223
"error",
211224
err,
212225
)
213-
w.WriteHeader(http.StatusInternalServerError)
214-
_, _ = w.Write([]byte(`{"error":"Internal server error"}`))
226+
var validationErr txbuilder.InputValidationError
227+
if errors.As(err, &validationErr) {
228+
w.WriteHeader(http.StatusBadRequest)
229+
_, _ = w.Write([]byte(fmt.Sprintf(`{"error":"Invalid request: %s"}`, validationErr)))
230+
} else {
231+
w.WriteHeader(http.StatusInternalServerError)
232+
_, _ = w.Write([]byte(`{"error":"Internal server error"}`))
233+
}
215234
return
216235
}
217236

internal/txbuilder/renew.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
"bytes"
1919
"context"
2020
"encoding/hex"
21-
"errors"
2221
"fmt"
2322
"time"
2423

@@ -41,6 +40,10 @@ func BuildRenewTransferTx(
4140
price int,
4241
duration int,
4342
) ([]byte, error) {
43+
// Validate inputs
44+
if paymentAddress == "" {
45+
return nil, NewInputValidationError("empty payment address provided")
46+
}
4447
cfg := config.GetConfig()
4548
cc, err := apolloBackend()
4649
if err != nil {
@@ -58,15 +61,15 @@ func BuildRenewTransferTx(
5861
// Decode payment address
5962
paymentAddr, err := serAddress.DecodeAddress(paymentAddress)
6063
if err != nil {
61-
return nil, fmt.Errorf("payment address: %w", err)
64+
return nil, NewInputValidationError("failed to decode payment address")
6265
}
6366
// Determine owner credential
6467
// Use existing owner for client by default
6568
ownerCredential := client.Credential
6669
if ownerAddress != "" && ownerAddress != paymentAddress {
6770
ownerAddr, err := serAddress.DecodeAddress(ownerAddress)
6871
if err != nil {
69-
return nil, fmt.Errorf("owner address: %w", err)
72+
return nil, NewInputValidationError("failed to decode owner address")
7073
}
7174
ownerCredential = ownerAddr.PaymentPart
7275
}
@@ -113,7 +116,7 @@ func BuildRenewTransferTx(
113116
return nil, fmt.Errorf("choose input UTxOs: %w", err)
114117
}
115118
if len(inputUtxos) == 0 {
116-
return nil, errors.New("no input UTxOs found")
119+
return nil, NewInputValidationError("no input UTxOs found")
117120
}
118121
// Lookup UTxO for client asset
119122
clientUtxo, err := cc.GetUtxoFromRef(
@@ -130,7 +133,7 @@ func BuildRenewTransferTx(
130133
if price > 0 && duration > 0 {
131134
selectionId, err = determinePlanSelection(refData, price, duration)
132135
if err != nil {
133-
return nil, fmt.Errorf("determine plan selection: %w", err)
136+
return nil, NewInputValidationError("could not determine plan selection from provided price/duration")
134137
}
135138
}
136139
// Get last known slot

internal/txbuilder/signup.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ func BuildSignupTx(
4545
duration int,
4646
region string,
4747
) ([]byte, []byte, error) {
48+
// Validate inputs
49+
if region == "" {
50+
return nil, nil, NewInputValidationError("empty region provided")
51+
}
52+
if paymentAddress == "" {
53+
return nil, nil, NewInputValidationError("empty payment address provided")
54+
}
4855
cfg := config.GetConfig()
4956
cc, err := apolloBackend()
5057
if err != nil {
@@ -53,14 +60,14 @@ func BuildSignupTx(
5360
// Decode payment address
5461
paymentAddr, err := serAddress.DecodeAddress(paymentAddress)
5562
if err != nil {
56-
return nil, nil, fmt.Errorf("payment address: %w", err)
63+
return nil, nil, NewInputValidationError("failed to decode payment address")
5764
}
5865
// Determine owner credential
5966
ownerCredential := paymentAddr.PaymentPart
6067
if ownerAddress != "" && ownerAddress != paymentAddress {
6168
ownerAddr, err := serAddress.DecodeAddress(ownerAddress)
6269
if err != nil {
63-
return nil, nil, fmt.Errorf("owner address: %w", err)
70+
return nil, nil, NewInputValidationError("failed to decode owner address")
6471
}
6572
ownerCredential = ownerAddr.PaymentPart
6673
}
@@ -87,6 +94,16 @@ func BuildSignupTx(
8794
default:
8895
return nil, nil, errors.New("reference data not provided (missing deps.Ref and deps.DB)")
8996
}
97+
// Validate region
98+
foundRegion := false
99+
for _, refDataRegion := range refData.Regions {
100+
if region == refDataRegion.Name {
101+
foundRegion = true
102+
}
103+
}
104+
if !foundRegion {
105+
return nil, nil, NewInputValidationError("provided region not valid")
106+
}
90107
// Parse script ref
91108
scriptRef, err := inputRefFromString(cfg.TxBuilder.ScriptRefInput)
92109
if err != nil {
@@ -107,14 +124,14 @@ func BuildSignupTx(
107124
return nil, nil, fmt.Errorf("choose input UTxOs: %w", err)
108125
}
109126
if len(inputUtxos) == 0 {
110-
return nil, nil, errors.New("no input UTxOs found")
127+
return nil, nil, NewInputValidationError("no input UTxOs found")
111128
}
112129
// Determine client ID from first selected input UTxO
113130
clientId := clientIdFromInput(inputUtxos[0].Input)
114131
// Determine plan selection ID from price/duration
115132
selectionId, err := determinePlanSelection(refData, price, duration)
116133
if err != nil {
117-
return nil, nil, fmt.Errorf("determine plan selection: %w", err)
134+
return nil, nil, NewInputValidationError("could not determine plan selection from provided price/duration")
118135
}
119136
// Get last known slot
120137
curSlot, err := cc.LastBlockSlot()

internal/txbuilder/txbuilder.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,18 @@ func determinePlanSelection(
166166
}
167167
return 0, errors.New("selection not found")
168168
}
169+
170+
// InputValidationError is a custom error type representing input validation errors
171+
type InputValidationError struct {
172+
msg string
173+
}
174+
175+
func NewInputValidationError(msg string) error {
176+
return InputValidationError{
177+
msg: msg,
178+
}
179+
}
180+
181+
func (e InputValidationError) Error() string {
182+
return e.msg
183+
}

0 commit comments

Comments
 (0)