Skip to content

Commit

Permalink
Merge remote-tracking branch 'pull/staging-pocketsell'
Browse files Browse the repository at this point in the history
  • Loading branch information
benma committed Aug 28, 2024
2 parents bdf270c + ba45e33 commit 996e3b7
Show file tree
Hide file tree
Showing 59 changed files with 1,255 additions and 589 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## Unreleased
- Add support for selling Bitcoin to Pocket using SLIP-0024 payment requests

## 4.43.0
- Bundle BitBox02 firmware version v9.19.0
Expand Down
24 changes: 21 additions & 3 deletions backend/accounts/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,33 @@ type AddressList struct {
Addresses []Address
}

// TextMemo represents a slip-0024 text memo.
type TextMemo struct {
Note string
}

// PaymentRequest contains the data needed to fulfill a slip-0024 payment request.
// Text memos are the only memo type supported, currently.
type PaymentRequest struct {
RecipientName string
Memos []TextMemo
Nonce []byte
TotalAmount uint64
Signature []byte
// TxOut is a pointer to the TxOut which will satisfay the payment request.
TxOut *wire.TxOut
}

// TxProposalArgs are the arguments needed when creating a tx proposal.
type TxProposalArgs struct {
RecipientAddress string
Amount coin.SendAmount
FeeTargetCode FeeTargetCode
// Only applies if FeeTargetCode == Custom. It is provided in sat/vB for BTC/LTC and Gwei for ETH.
CustomFee string
SelectedUTXOs map[wire.OutPoint]struct{}
Note string
CustomFee string
SelectedUTXOs map[wire.OutPoint]struct{}
Note string
PaymentRequests []*PaymentRequest
}

// Interface is the API of a Account.
Expand Down
8 changes: 4 additions & 4 deletions backend/coins/btc/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ func (account *Account) Notifier() accounts.Notifier {
// For the other coins or in case mempool.space is not available it fallbacks on Bitcoin Core.
// The minimum relay fee is used as a last resource fallback in case also Bitcoin Core is
// unavailable.
func (account *Account) feeTargets() []*FeeTarget {
func (account *Account) feeTargets() FeeTargets {
// for mainnet BTC we fetch mempool.space fees, as they should be more reliable.
var mempoolFees *accounts.MempoolSpaceFees
if account.coin.Code() == coin.CodeBTC {
Expand All @@ -493,16 +493,16 @@ func (account *Account) feeTargets() []*FeeTarget {
}

// feeTargets must be sorted by ascending priority.
var feeTargets []*FeeTarget
var feeTargets FeeTargets
if mempoolFees != nil {
feeTargets = []*FeeTarget{
feeTargets = FeeTargets{
{blocks: 12, code: accounts.FeeTargetCodeMempoolEconomy},
{blocks: 3, code: accounts.FeeTargetCodeMempoolHour},
{blocks: 2, code: accounts.FeeTargetCodeMempoolHalfHour},
{blocks: 1, code: accounts.FeeTargetCodeMempoolFastest},
}
} else {
feeTargets = []*FeeTarget{
feeTargets = FeeTargets{
{blocks: 24, code: accounts.FeeTargetCodeEconomy},
{blocks: 12, code: accounts.FeeTargetCodeLow},
{blocks: 6, code: accounts.FeeTargetCodeNormal},
Expand Down
18 changes: 18 additions & 0 deletions backend/coins/btc/feetarget.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,21 @@ func (feeTarget *FeeTarget) FormattedFeeRate() string {
feePerByte = strings.TrimRight(strings.TrimRight(feePerByte, "0"), ".")
return feePerByte + " sat/vB"
}

// FeeTargets represents an array of FeeTarget pointers.
type FeeTargets []*FeeTarget

// highest returns the feeTarget with the highest fee.
func (feeTargets FeeTargets) highest() *FeeTarget {
var highestFeeTarget *FeeTarget
for _, feeTarget := range feeTargets {
if feeTarget == nil || feeTarget.feeRatePerKb == nil {
continue
}

if highestFeeTarget == nil || *feeTarget.feeRatePerKb > *highestFeeTarget.feeRatePerKb {
highestFeeTarget = feeTarget
}
}
return highestFeeTarget
}
54 changes: 50 additions & 4 deletions backend/coins/btc/feetarget_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,17 @@ import (
"github.com/stretchr/testify/require"
)

func amt(v uint64) *btcutil.Amount {
x := btcutil.Amount(v)
return &x
}

func TestFeeTarget(t *testing.T) {
require.Equal(t,
accounts.FeeTargetCodeLow,
(&FeeTarget{code: accounts.FeeTargetCodeLow}).Code(),
)

amt := func(v uint64) *btcutil.Amount {
x := btcutil.Amount(v)
return &x
}
require.Equal(t,
"0.123 sat/vB",
(&FeeTarget{feeRatePerKb: amt(123)}).FormattedFeeRate(),
Expand All @@ -54,3 +55,48 @@ func TestFeeTarget(t *testing.T) {
(&FeeTarget{feeRatePerKb: amt(10001)}).FormattedFeeRate(),
)
}

func TestFeeTargets(t *testing.T) {
// empty slice
var feeTargets FeeTargets
require.Nil(t, feeTargets.highest())

// non-empty slice, with all nil feeRates
feeTargets = FeeTargets{
{blocks: 12, code: accounts.FeeTargetCodeMempoolEconomy},
{blocks: 3, code: accounts.FeeTargetCodeMempoolHour},
{blocks: 2, code: accounts.FeeTargetCodeMempoolHalfHour},
{blocks: 1, code: accounts.FeeTargetCodeMempoolFastest},
}
require.Nil(t, feeTargets.highest())

// non-empty slice, with all nil feeRates
feeTargets = FeeTargets{
{blocks: 12, code: accounts.FeeTargetCodeMempoolEconomy},
{blocks: 3, code: accounts.FeeTargetCodeMempoolHour},
{blocks: 2, code: accounts.FeeTargetCodeMempoolHalfHour},
{blocks: 1, code: accounts.FeeTargetCodeMempoolFastest},
}
require.Nil(t, feeTargets.highest())

// non-empty slice, with some nil feeRates
feeTargetsSlice := []*FeeTarget{
{blocks: 12, code: accounts.FeeTargetCodeMempoolEconomy},
{blocks: 3, code: accounts.FeeTargetCodeMempoolHour, feeRatePerKb: amt(12)},
{blocks: 2, code: accounts.FeeTargetCodeMempoolHalfHour},
{blocks: 1, code: accounts.FeeTargetCodeMempoolFastest, feeRatePerKb: amt(123)},
}
feeTargets = feeTargetsSlice
require.Equal(t, feeTargetsSlice[3], feeTargets.highest())

// non-empty slice, with unsorted not-nil feeRates
feeTargetsSlice = []*FeeTarget{
{blocks: 3, code: accounts.FeeTargetCodeMempoolHour, feeRatePerKb: amt(12)},
{blocks: 12, code: accounts.FeeTargetCodeMempoolEconomy, feeRatePerKb: amt(1)},
{blocks: 1, code: accounts.FeeTargetCodeMempoolFastest, feeRatePerKb: amt(1234)},
{blocks: 2, code: accounts.FeeTargetCodeMempoolHalfHour, feeRatePerKb: amt(123)},
}
feeTargets = feeTargetsSlice
require.Equal(t, feeTargetsSlice[2], feeTargets.highest())

}
92 changes: 87 additions & 5 deletions backend/coins/btc/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package handlers

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"math/big"
Expand Down Expand Up @@ -73,6 +74,7 @@ func NewHandlers(
handleFunc("/verify-extended-public-key", handlers.ensureAccountInitialized(handlers.postVerifyExtendedPublicKey)).Methods("POST")
handleFunc("/sign-address", handlers.ensureAccountInitialized(handlers.postSignBTCAddress)).Methods("POST")
handleFunc("/has-secure-output", handlers.ensureAccountInitialized(handlers.getHasSecureOutput)).Methods("GET")
handleFunc("/has-payment-request", handlers.ensureAccountInitialized(handlers.getHasPaymentRequest)).Methods("GET")
handleFunc("/propose-tx-note", handlers.ensureAccountInitialized(handlers.postProposeTxNote)).Methods("POST")
handleFunc("/notes/tx", handlers.ensureAccountInitialized(handlers.postSetTxNote)).Methods("POST")
handleFunc("/connect-keystore", handlers.ensureAccountInitialized(handlers.postConnectKeystore)).Methods("POST")
Expand Down Expand Up @@ -364,6 +366,51 @@ func (handlers *Handlers) getAccountBalance(*http.Request) (interface{}, error)
}, nil
}

type slip24Request struct {
RecipientName string `json:"recipientName"`
Nonce string `json:"nonce"`
Memos []struct {
Type string `json:"type"`
Text string `json:"text"`
} `json:"memos"`
Outputs []struct {
Amount uint64 `json:"amount"`
Address string `json:"address"`
} `json:"outputs"`
Signature string `json:"signature"`
}

func (slip24 slip24Request) toPaymentRequest() (*accounts.PaymentRequest, error) {
if len(slip24.Outputs) != 1 {
return nil, errp.New("Missing or multiple payment request output unsupported")
}

if len(slip24.Nonce) > 0 {
return nil, errp.New("Nonce value unsupported")
}

sigBytes, err := base64.StdEncoding.DecodeString(slip24.Signature)
if err != nil {
return nil, err
}

memos := []accounts.TextMemo{}
for _, memo := range slip24.Memos {
if memo.Type != "text" {
return nil, errp.New("Payment request non-text memo unsupported")
}
memos = append(memos, accounts.TextMemo{Note: memo.Text})
}

return &accounts.PaymentRequest{
RecipientName: slip24.RecipientName,
Nonce: nil,
Signature: sigBytes,
TotalAmount: slip24.Outputs[0].Amount,
Memos: memos,
}, nil
}

type sendTxInput struct {
accounts.TxProposalArgs
}
Expand All @@ -374,11 +421,12 @@ func (input *sendTxInput) UnmarshalJSON(jsonBytes []byte) error {
SendAll string `json:"sendAll"`
FeeTarget string `json:"feeTarget"`
// Provided in Sat/vByte for BTC/LTC and in Gwei for ETH.
CustomFee string `json:"customFee"`
Amount string `json:"amount"`
SelectedUTXOS []string `json:"selectedUTXOS"`
Note string `json:"note"`
Counter int `json:"counter"`
CustomFee string `json:"customFee"`
Amount string `json:"amount"`
SelectedUTXOS []string `json:"selectedUTXOS"`
Note string `json:"note"`
Counter int `json:"counter"`
PaymentRequests *slip24Request `json:"paymentRequest"`
}{}
if err := json.Unmarshal(jsonBytes, &jsonBody); err != nil {
return errp.WithStack(err)
Expand Down Expand Up @@ -406,6 +454,13 @@ func (input *sendTxInput) UnmarshalJSON(jsonBytes []byte) error {
input.SelectedUTXOs[*outPoint] = struct{}{}
}
input.Note = jsonBody.Note
if jsonBody.PaymentRequests != nil {
paymentRequest, err := jsonBody.PaymentRequests.toPaymentRequest()
if err != nil {
return err
}
input.PaymentRequests = append(input.PaymentRequests, paymentRequest)
}
return nil
}

Expand Down Expand Up @@ -757,3 +812,30 @@ func (handlers *Handlers) postSignBTCAddress(r *http.Request) (interface{}, erro
}
return response{Success: true, Address: address, Signature: signature}, nil
}

func (handlers *Handlers) getHasPaymentRequest(r *http.Request) (interface{}, error) {
type response struct {
Success bool `json:"success"`
ErrorMessage string `json:"errorMessage,omitempty"`
ErrorCode string `json:"errorCode,omitempty"`
}

account, ok := handlers.account.(*btc.Account)
if !ok {
return response{
Success: false,
ErrorMessage: "An account must be BTC based to support payment requests.",
}, nil
}

keystore, err := account.Config().ConnectKeystore()
if err != nil {
return response{Success: false, ErrorMessage: err.Error()}, nil
}
err = keystore.SupportsPaymentRequests()
if err != nil {
return response{Success: false, ErrorCode: err.Error()}, nil
}

return response{Success: true}, nil
}
2 changes: 2 additions & 0 deletions backend/coins/btc/maketx/maketx.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"sort"
"time"

"github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts"
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts/errors"
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/addresses"
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/transactions"
Expand Down Expand Up @@ -53,6 +54,7 @@ type TxProposal struct {
// ChangeAddress is the address of the wallet to which the change of the transaction is sent.
ChangeAddress *addresses.AccountAddress
PreviousOutputs PreviousOutputs
PaymentRequest []*accounts.PaymentRequest
}

// Total is amount+fee.
Expand Down
28 changes: 22 additions & 6 deletions backend/coins/btc/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ const unitSatoshi = 1e8
// fee target (priority) if one is given, or the provided args.FeePerKb if the fee taret is
// `FeeTargetCodeCustom`.
func (account *Account) getFeePerKb(args *accounts.TxProposalArgs) (btcutil.Amount, error) {
if args.FeeTargetCode == accounts.FeeTargetCodeCustom {
isPaymentRequest := len(args.PaymentRequests) > 0
if args.FeeTargetCode == accounts.FeeTargetCodeCustom && !isPaymentRequest {
float, err := strconv.ParseFloat(args.CustomFee, 64)
if err != nil {
return 0, err
Expand All @@ -58,11 +59,16 @@ func (account *Account) getFeePerKb(args *accounts.TxProposalArgs) (btcutil.Amou
}
return feePerKb, nil
}

var feeTarget *FeeTarget
for _, target := range account.feeTargets() {
if target.code == args.FeeTargetCode {
feeTarget = target
break
if isPaymentRequest {
feeTarget = account.feeTargets().highest()
} else {
for _, target := range account.feeTargets() {
if target.code == args.FeeTargetCode {
feeTarget = target
break
}
}
}
if feeTarget == nil || feeTarget.feeRatePerKb == nil {
Expand Down Expand Up @@ -166,6 +172,9 @@ func (account *Account) newTx(args *accounts.TxProposalArgs) (

var txProposal *maketx.TxProposal
if args.Amount.SendAll() {
if len(args.PaymentRequests) > 0 {
return nil, nil, errp.New("Payment Requests do not allow send-all transaction proposals")
}
txProposal, err = maketx.NewTxSpendAll(
account.coin,
wireUTXO,
Expand Down Expand Up @@ -195,18 +204,25 @@ func (account *Account) newTx(args *accounts.TxProposalArgs) (
if err != nil {
return nil, nil, err
}
txOut := wire.NewTxOut(parsedAmountInt64, pkScript)
account.log.Infof("Change address script type: %s", changeAddress.Configuration.ScriptType())
txProposal, err = maketx.NewTx(
account.coin,
wireUTXO,
wire.NewTxOut(parsedAmountInt64, pkScript),
txOut,
feeRatePerKb,
changeAddress,
account.log,
)
if err != nil {
return nil, nil, err
}

for _, paymentRequest := range args.PaymentRequests {
account.log.Info("Payment request tx proposal")
paymentRequest.TxOut = txOut
txProposal.PaymentRequest = append(txProposal.PaymentRequest, paymentRequest)
}
}
account.log.Debugf("creating tx with %d inputs, %d outputs",
len(txProposal.Transaction.TxIn), len(txProposal.Transaction.TxOut))
Expand Down
2 changes: 1 addition & 1 deletion backend/coins/coin/conversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
ratesPkg "github.com/BitBoxSwiss/bitbox-wallet-app/backend/rates"
)

// Btc2Sat is the sat equivalent of 1 BTC.
// btc2SatUnit is the sat equivalent of 1 BTC.
const btc2SatUnit = 1e8

// Sat2Btc converts a big.Rat amount of Sat in an equivalent amount of BTC.
Expand Down
5 changes: 5 additions & 0 deletions backend/devices/bitbox/keystore.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,8 @@ func (keystore *keystore) SignETHWalletConnectTransaction(chainID uint64, tx *ty
func (keystore *keystore) SupportsEIP1559() bool {
return false
}

// SupportsPaymentRequests implements keystore.Keystore.
func (keystore *keystore) SupportsPaymentRequests() error {
return keystorePkg.ErrUnsupportedFeature
}
Loading

0 comments on commit 996e3b7

Please sign in to comment.