diff --git a/errors.go b/errors.go index b9eb5740..e7051fe8 100644 --- a/errors.go +++ b/errors.go @@ -10,18 +10,30 @@ import ( var ErrAdminKey = models.SPVError{Message: "an admin key must be set to be able to create an xpub", StatusCode: 401, Code: "error-unauthorized-admin-key-not-set"} // ErrMissingXpriv is when xpriv is missing -var ErrMissingXpriv = models.SPVError{Message: "xpriv missing", StatusCode: 401, Code: "error-unauthorized-xpriv-missing"} +var ErrMissingXpriv = models.SPVError{Message: "xpriv is missing", StatusCode: 401, Code: "error-unauthorized-xpriv-missing"} + +// ErrMissingKey is when neither xPriv nor adminXPriv is provided +var ErrMissingKey = models.SPVError{Message: "neither xPriv nor adminXPriv is provided", StatusCode: 404, Code: "error-shared-config-key-missing"} + +// ErrMissingAccessKey is when access key is missing +var ErrMissingAccessKey = models.SPVError{Message: "access key is missing", StatusCode: 401, Code: "error-unauthorized-access-key-missing"} // ErrCouldNotFindDraftTransaction is when draft transaction is not found var ErrCouldNotFindDraftTransaction = models.SPVError{Message: "could not find draft transaction", StatusCode: 404, Code: "error-draft-transaction-not-found"} +// ErrTotpInvalid is when totp is invalid +var ErrTotpInvalid = models.SPVError{Message: "totp is invalid", StatusCode: 400, Code: "error-totp-invalid"} + +// ErrContactPubKeyInvalid is when contact's PubKey is invalid +var ErrContactPubKeyInvalid = models.SPVError{Message: "contact's PubKey is invalid", StatusCode: 400, Code: "error-contact-pubkey-invalid"} + // WrapError wraps an error into SPVError func WrapError(err error) error { if err == nil { return nil } - return &models.SPVError{ + return models.SPVError{ StatusCode: http.StatusInternalServerError, Message: err.Error(), Code: models.UnknownErrorCode, @@ -34,14 +46,14 @@ func WrapResponseError(res *http.Response) error { return nil } - var resError *models.ResponseError + var resError models.ResponseError err := json.NewDecoder(res.Body).Decode(&resError) if err != nil { return WrapError(err) } - return &models.SPVError{ + return models.SPVError{ StatusCode: res.StatusCode, Code: resError.Code, Message: resError.Message, @@ -49,7 +61,7 @@ func WrapResponseError(res *http.Response) error { } func CreateErrorResponse(code string, message string) error { - return &models.SPVError{ + return models.SPVError{ StatusCode: http.StatusInternalServerError, Code: code, Message: message, diff --git a/examples/README.md b/examples/README.md index f08e3a53..73b27adb 100644 --- a/examples/README.md +++ b/examples/README.md @@ -35,7 +35,6 @@ In this directory you can find examples of how to use the `spv-wallet-go-client` In addition to the above, there are additional examples showing how to use the client from a developer perspective: - `handle_exceptions` - presents how to "catch" exceptions which the client can throw -- `custom_logger` - shows different ways you can configure (or disable) internal logger ## Util examples diff --git a/examples/admin_add_user/admin_add_user.go b/examples/admin_add_user/admin_add_user.go index 4476f51d..440f3936 100644 --- a/examples/admin_add_user/admin_add_user.go +++ b/examples/admin_add_user/admin_add_user.go @@ -24,12 +24,15 @@ func main() { metadata := map[string]any{"some_metadata": "example"} - newXPubRes := adminClient.AdminNewXpub(ctx, examples.ExampleXPub, metadata) - fmt.Println("AdminNewXpub response: ", newXPubRes) + err := adminClient.AdminNewXpub(ctx, examples.ExampleXPub, metadata) + if err != nil { + examples.GetFullErrorMessage(err) + os.Exit(1) + } createPaymailRes, err := adminClient.AdminCreatePaymail(ctx, examples.ExampleXPub, examples.ExamplePaymail, "Some public name", "") if err != nil { - fmt.Println(err) + examples.GetFullErrorMessage(err) os.Exit(1) } fmt.Println("AdminCreatePaymail response: ", createPaymailRes) diff --git a/examples/admin_remove_user/admin_remove_user.go b/examples/admin_remove_user/admin_remove_user.go index 02c1475e..db01d5f3 100644 --- a/examples/admin_remove_user/admin_remove_user.go +++ b/examples/admin_remove_user/admin_remove_user.go @@ -5,7 +5,6 @@ package main import ( "context" - "fmt" "os" walletclient "github.com/bitcoin-sv/spv-wallet-go-client" @@ -24,7 +23,7 @@ func main() { err := adminClient.AdminDeletePaymail(ctx, examples.ExamplePaymail) if err != nil { - fmt.Println(err) + examples.GetFullErrorMessage(err) os.Exit(1) } } diff --git a/examples/create_transaction/create_transaction.go b/examples/create_transaction/create_transaction.go index 25be7744..b929d40d 100644 --- a/examples/create_transaction/create_transaction.go +++ b/examples/create_transaction/create_transaction.go @@ -22,20 +22,20 @@ func main() { client := walletclient.NewWithXPriv(server, examples.ExampleXPriv) ctx := context.Background() - recipient := walletclient.Recipients{To: "receiver@example.com", Satoshis: 1} + recipient := walletclient.Recipients{To: "test-multiple1@pawel.test.4chain.space", Satoshis: 1} recipients := []*walletclient.Recipients{&recipient} metadata := map[string]any{"some_metadata": "example"} newTransaction, err := client.SendToRecipients(ctx, recipients, metadata) if err != nil { - fmt.Println(err) + examples.GetFullErrorMessage(err) os.Exit(1) } fmt.Println("SendToRecipients response: ", newTransaction) tx, err := client.GetTransaction(ctx, newTransaction.ID) if err != nil { - fmt.Println(err) + examples.GetFullErrorMessage(err) os.Exit(1) } fmt.Println("GetTransaction response: ", tx) diff --git a/examples/errors.go b/examples/errors.go new file mode 100644 index 00000000..932a7527 --- /dev/null +++ b/examples/errors.go @@ -0,0 +1,21 @@ +package examples + +import ( + "errors" + "fmt" + + "github.com/bitcoin-sv/spv-wallet/models" +) + +// GetFullErrorMessage prints detailed info about the error +func GetFullErrorMessage(err error) { + var errMsg string + + var spvError models.SPVError + if errors.As(err, &spvError) { + errMsg = fmt.Sprintf("Error, Message: %s, Code: %s, HTTP status code: %d", spvError.GetMessage(), spvError.GetCode(), spvError.GetStatusCode()) + } else { + errMsg = fmt.Sprintf("Error, Message: %s, Code: %s, HTTP status code: %d", err.Error(), models.UnknownErrorCode, 500) + } + fmt.Println(errMsg) +} diff --git a/examples/get_balance/get_balance.go b/examples/get_balance/get_balance.go index 9908492e..c26bf791 100644 --- a/examples/get_balance/get_balance.go +++ b/examples/get_balance/get_balance.go @@ -24,7 +24,7 @@ func main() { xpubInfo, err := client.GetXPub(ctx) if err != nil { - fmt.Println(err) + examples.GetFullErrorMessage(err) os.Exit(1) } fmt.Println("Current balance: ", xpubInfo.CurrentBalance) diff --git a/examples/handle_exceptions/handle_exceptions.go b/examples/handle_exceptions/handle_exceptions.go index 29fcfeea..7bb7b31b 100644 --- a/examples/handle_exceptions/handle_exceptions.go +++ b/examples/handle_exceptions/handle_exceptions.go @@ -5,34 +5,33 @@ package main import ( "context" - "errors" "fmt" "os" walletclient "github.com/bitcoin-sv/spv-wallet-go-client" "github.com/bitcoin-sv/spv-wallet-go-client/examples" - "github.com/bitcoin-sv/spv-wallet/models" ) func main() { defer examples.HandlePanic() + fmt.Println("Handle exceptions example") + examples.CheckIfXPubExists() + fmt.Println("XPub exists") + const server = "http://localhost:3003/v1" client := walletclient.NewWithXPub(server, examples.ExampleXPub) ctx := context.Background() + fmt.Println("Client created") + status, err := client.AdminGetStatus(ctx) if err != nil { - var extendedErr models.ExtendedError - if errors.As(err, &extendedErr) { - fmt.Printf("Extended error: [%d] '%s': %s\n", extendedErr.GetStatusCode(), extendedErr.GetCode(), extendedErr.GetMessage()) - } else { - fmt.Println("Error: ", err.Error()) - } - + fmt.Println("Error: ", err) + examples.GetFullErrorMessage(err) os.Exit(1) } diff --git a/examples/list_transactions/list_transactions.go b/examples/list_transactions/list_transactions.go index d1d0c6f5..086e1e9c 100644 --- a/examples/list_transactions/list_transactions.go +++ b/examples/list_transactions/list_transactions.go @@ -30,17 +30,18 @@ func main() { txs, err := client.GetTransactions(ctx, &conditions, metadata, &queryParams) if err != nil { - fmt.Println(err) + examples.GetFullErrorMessage(err) os.Exit(1) } fmt.Println("GetTransactions response: ", txs) - conditions = filter.TransactionFilter{BlockHeight: func(i uint64) *uint64 { return &i }(839228)} + targetBlockHeight := uint64(839228) + conditions = filter.TransactionFilter{BlockHeight: &targetBlockHeight} queryParams = filter.QueryParams{PageSize: 100, Page: 1} txsFiltered, err := client.GetTransactions(ctx, &conditions, metadata, &queryParams) if err != nil { - fmt.Println(err) + examples.GetFullErrorMessage(err) os.Exit(1) } fmt.Println("Filtered GetTransactions response: ", txsFiltered) diff --git a/examples/send_op_return/send_op_return.go b/examples/send_op_return/send_op_return.go index 93ae290b..3a3fc14e 100644 --- a/examples/send_op_return/send_op_return.go +++ b/examples/send_op_return/send_op_return.go @@ -30,19 +30,19 @@ func main() { draftTransaction, err := client.DraftTransaction(ctx, &transactionConfig, metadata) if err != nil { - fmt.Println(err) + examples.GetFullErrorMessage(err) os.Exit(1) } fmt.Println("DraftTransaction response: ", draftTransaction) finalized, err := client.FinalizeTransaction(draftTransaction) if err != nil { - fmt.Println(err) + examples.GetFullErrorMessage(err) os.Exit(1) } transaction, err := client.RecordTransaction(ctx, finalized, draftTransaction.ID, metadata) if err != nil { - fmt.Println(err) + examples.GetFullErrorMessage(err) os.Exit(1) } fmt.Println("Transaction with OP_RETURN: ", transaction) diff --git a/examples/xpriv_from_mnemonic/xpriv_from_mnemonic.go b/examples/xpriv_from_mnemonic/xpriv_from_mnemonic.go index 8e35cbe8..2332ea30 100644 --- a/examples/xpriv_from_mnemonic/xpriv_from_mnemonic.go +++ b/examples/xpriv_from_mnemonic/xpriv_from_mnemonic.go @@ -5,6 +5,7 @@ package main import ( "fmt" + "github.com/bitcoin-sv/spv-wallet-go-client/examples" "os" "github.com/bitcoin-sv/spv-wallet-go-client/xpriv" @@ -16,7 +17,7 @@ func main() { keys, err := xpriv.FromMnemonic(mnemonicPhrase) if err != nil { - fmt.Println(err) + examples.GetFullErrorMessage(err) os.Exit(1) } diff --git a/examples/xpub_from_xpriv/xpub_from_xpriv.go b/examples/xpub_from_xpriv/xpub_from_xpriv.go index 66fc8cd2..83e077d1 100644 --- a/examples/xpub_from_xpriv/xpub_from_xpriv.go +++ b/examples/xpub_from_xpriv/xpub_from_xpriv.go @@ -5,6 +5,7 @@ package main import ( "fmt" + "github.com/bitcoin-sv/spv-wallet-go-client/examples" "os" "github.com/bitcoin-sv/spv-wallet-go-client/xpriv" @@ -16,7 +17,7 @@ func main() { keys, err := xpriv.FromString(xPriv) if err != nil { - fmt.Println(err) + examples.GetFullErrorMessage(err) os.Exit(1) } diff --git a/http.go b/http.go index 8bf6144e..6f49f7c5 100644 --- a/http.go +++ b/http.go @@ -5,7 +5,6 @@ import ( "context" "encoding/hex" "encoding/json" - "errors" "fmt" "net/http" "strconv" @@ -566,7 +565,7 @@ func (wc *WalletClient) authenticateWithXpriv(sign bool, req *http.Request, xPri func (wc *WalletClient) authenticateWithAccessKey(req *http.Request, rawJSON []byte) error { if wc.accessKey == nil { - return WrapError(errors.New("access key is missing")) + return ErrMissingAccessKey } return SetSignatureFromAccessKey(&req.Header, hex.EncodeToString(wc.accessKey.Serialise()), string(rawJSON)) } @@ -597,11 +596,11 @@ func (wc *WalletClient) RejectContact(ctx context.Context, paymail string) error func (wc *WalletClient) ConfirmContact(ctx context.Context, contact *models.Contact, passcode, requesterPaymail string, period, digits uint) error { isTotpValid, err := wc.ValidateTotpForContact(contact, passcode, requesterPaymail, period, digits) if err != nil { - return WrapError(fmt.Errorf("totp validation failed: %w", err)) + return WrapError(ErrTotpInvalid) } if !isTotpValid { - return WrapError(errors.New("totp is invalid")) + return WrapError(ErrTotpInvalid) } if err := wc.doHTTPRequest( @@ -666,7 +665,7 @@ func (wc *WalletClient) GetSharedConfig(ctx context.Context) (*models.SharedConf key = wc.adminXPriv } if key == nil { - return nil, WrapError(errors.New("neither xPriv nor adminXPriv is provided")) + return nil, WrapError(ErrMissingKey) } if err := wc.doHTTPRequest( ctx, http.MethodGet, "/shared-config", nil, key, true, &model, diff --git a/totp.go b/totp.go index 336f6d6b..e202e964 100644 --- a/totp.go +++ b/totp.go @@ -3,8 +3,6 @@ package walletclient import ( "encoding/base32" "encoding/hex" - "errors" - "fmt" "time" "github.com/bitcoin-sv/spv-wallet-go-client/utils" @@ -16,9 +14,6 @@ import ( "github.com/pquerna/otp/totp" ) -// ErrClientInitNoXpriv error per init client with first xpriv key -var ErrClientInitNoXpriv = errors.New("init client with xPriv first") - const ( // TotpDefaultPeriod - Default number of seconds a TOTP is valid for. TotpDefaultPeriod uint = 30 @@ -84,7 +79,7 @@ func getTotpOpts(period, digits uint) *totp.ValidateOpts { func getSharedSecretFactors(b *WalletClient, c *models.Contact) (*bec.PrivateKey, *bec.PublicKey, error) { if b.xPriv == nil { - return nil, nil, ErrClientInitNoXpriv + return nil, nil, ErrMissingXpriv } xpriv, err := deriveXprivForPki(b.xPriv) @@ -99,7 +94,7 @@ func getSharedSecretFactors(b *WalletClient, c *models.Contact) (*bec.PrivateKey pubKey, err := convertPubKey(c.PubKey) if err != nil { - return nil, nil, fmt.Errorf("contact's PubKey is invalid: %w", err) + return nil, nil, ErrContactPubKeyInvalid } return privKey, pubKey, nil diff --git a/totp_test.go b/totp_test.go index e6be2e40..5ad10600 100644 --- a/totp_test.go +++ b/totp_test.go @@ -37,7 +37,7 @@ func TestGenerateTotpForContact(t *testing.T) { _, err := sut.GenerateTotpForContact(nil, 30, 2) // then - require.ErrorIs(t, err, ErrClientInitNoXpriv) + require.ErrorIs(t, err, ErrMissingXpriv) }) t.Run("contact has invalid PubKey - returns error", func(t *testing.T) { @@ -50,7 +50,7 @@ func TestGenerateTotpForContact(t *testing.T) { _, err := sut.GenerateTotpForContact(&contact, 30, 2) // then - require.ErrorContains(t, err, "contact's PubKey is invalid:") + require.ErrorIs(t, err, ErrContactPubKeyInvalid) }) }