Skip to content

Commit

Permalink
bus: wallet/send endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisSchinnerl committed Aug 14, 2024
1 parent 83fcbbb commit 4d39d7d
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 69 deletions.
7 changes: 7 additions & 0 deletions api/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ type (
Immature types.Currency `json:"immature"`
}

WalletSendRequest struct {
Address types.Address `json:"address"`
Amount types.Currency `json:"amount"`
SubtractMinerFee bool `json:"subtractMinerFee"`
UseUnconfirmed bool `json:"useUnconfirmed"`
}

// WalletSignRequest is the request type for the /wallet/sign endpoint.
WalletSignRequest struct {
Transaction types.Transaction `json:"transaction"`
Expand Down
10 changes: 10 additions & 0 deletions bus/bus.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
const (
defaultPinUpdateInterval = 5 * time.Minute
defaultPinRateWindow = 6 * time.Hour
stdTxnSize = 1200 // bytes
)

// Client re-exports the client from the client package.
Expand All @@ -50,15 +51,18 @@ type (
ChainManager interface {
AddBlocks(blocks []types.Block) error
AddPoolTransactions(txns []types.Transaction) (bool, error)
AddV2PoolTransactions(basis types.ChainIndex, txns []types.V2Transaction) (known bool, err error)
Block(id types.BlockID) (types.Block, bool)
OnReorg(fn func(types.ChainIndex)) (cancel func())
PoolTransaction(txid types.TransactionID) (types.Transaction, bool)
PoolTransactions() []types.Transaction
V2PoolTransactions() []types.V2Transaction
RecommendedFee() types.Currency
Tip() types.ChainIndex
TipState() consensus.State
UnconfirmedParents(txn types.Transaction) []types.Transaction
UpdatesSince(index types.ChainIndex, max int) (rus []chain.RevertUpdate, aus []chain.ApplyUpdate, err error)
V2UnconfirmedParents(txn types.V2Transaction) []types.V2Transaction
}

ChainSubscriber interface {
Expand Down Expand Up @@ -206,7 +210,9 @@ type (
Syncer interface {
Addr() string
BroadcastHeader(h gateway.BlockHeader)
BroadcastV2BlockOutline(bo gateway.V2BlockOutline)
BroadcastTransactionSet([]types.Transaction)
BroadcastV2TransactionSet(index types.ChainIndex, txns []types.V2Transaction)
Connect(ctx context.Context, addr string) (*syncer.Peer, error)
Peers() []*syncer.Peer
}
Expand All @@ -216,9 +222,12 @@ type (
Balance() (wallet.Balance, error)
Close() error
FundTransaction(txn *types.Transaction, amount types.Currency, useUnconfirmed bool) ([]types.Hash256, error)
FundV2Transaction(txn *types.V2Transaction, amount types.Currency, useUnconfirmed bool) (consensus.State, []int, error)
Redistribute(outputs int, amount, feePerByte types.Currency) (txns []types.Transaction, toSign []types.Hash256, err error)
RedistributeV2(outputs int, amount, feePerByte types.Currency) (txns []types.V2Transaction, toSign [][]int, err error)
ReleaseInputs(txns []types.Transaction, v2txns []types.V2Transaction)
SignTransaction(txn *types.Transaction, toSign []types.Hash256, cf types.CoveredFields)
SignV2Inputs(state consensus.State, txn *types.V2Transaction, toSign []int)
SpendableOutputs() ([]types.SiacoinElement, error)
Tip() (types.ChainIndex, error)
UnconfirmedEvents() ([]wallet.Event, error)
Expand Down Expand Up @@ -437,6 +446,7 @@ func (b *bus) Handler() http.Handler {
"POST /wallet/prepare/form": b.walletPrepareFormHandler,
"POST /wallet/prepare/renew": b.walletPrepareRenewHandler,
"POST /wallet/redistribute": b.walletRedistributeHandler,
"POST /wallet/send": b.walletSendSiacoinsHandler,
"POST /wallet/sign": b.walletSignHandler,
"GET /wallet/transactions": b.walletTransactionsHandler,

Expand Down
32 changes: 9 additions & 23 deletions bus/client/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,15 @@ import (
"go.sia.tech/renterd/api"
)

// SendSiacoins is a helper method that sends siacoins to the given outputs.
func (c *Client) SendSiacoins(ctx context.Context, scos []types.SiacoinOutput, useUnconfirmedTxns bool) (err error) {
var value types.Currency
for _, sco := range scos {
value = value.Add(sco.Value)
}
txn := types.Transaction{
SiacoinOutputs: scos,
}
toSign, parents, err := c.WalletFund(ctx, &txn, value, useUnconfirmedTxns)
if err != nil {
return err
}
defer func() {
if err != nil {
_ = c.WalletDiscard(ctx, txn)
}
}()
err = c.WalletSign(ctx, &txn, toSign, types.CoveredFields{WholeTransaction: true})
if err != nil {
return err
}
return c.BroadcastTransaction(ctx, append(parents, txn))
// SendSiacoins is a helper method that sends siacoins to the given address.
func (c *Client) SendSiacoins(ctx context.Context, addr types.Address, amt types.Currency, useUnconfirmedTxns bool) (txnID types.TransactionID, err error) {
err = c.c.WithContext(ctx).POST("/wallet/send", api.WalletSendRequest{
Address: addr,
Amount: amt,
SubtractMinerFee: false,
UseUnconfirmed: useUnconfirmedTxns,
}, &txnID)
return
}

// Wallet calls the /wallet endpoint on the bus.
Expand Down
75 changes: 75 additions & 0 deletions bus/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ func (b *bus) consensusAcceptBlock(jc jape.Context) {
Timestamp: block.Timestamp,
MerkleRoot: block.MerkleRoot(),
})
} else {
b.s.BroadcastV2BlockOutline(gateway.OutlineBlock(block, b.cm.PoolTransactions(), b.cm.V2PoolTransactions()))
}
}

Expand Down Expand Up @@ -331,6 +333,79 @@ func (b *bus) walletFundHandler(jc jape.Context) {
})
}

func (b *bus) walletSendSiacoinsHandler(jc jape.Context) {
var req api.WalletSendRequest
if jc.Decode(&req) != nil {
return
} else if req.Address == types.VoidAddress {
jc.Error(errors.New("cannot send to void address"), http.StatusBadRequest)
return
}

// estimate miner fee
feePerByte := b.cm.RecommendedFee()
minerFee := feePerByte.Mul64(stdTxnSize)
if req.SubtractMinerFee {
var underflow bool
req.Amount, underflow = req.Amount.SubWithUnderflow(minerFee)
if underflow {
jc.Error(fmt.Errorf("amount must be greater than miner fee: %s", minerFee), http.StatusBadRequest)
return
}
}

state := b.cm.TipState()
// if the current height is below the v2 hardfork height, send a v1
// transaction
if state.Index.Height < state.Network.HardforkV2.AllowHeight {
// build transaction
txn := types.Transaction{
MinerFees: []types.Currency{minerFee},
SiacoinOutputs: []types.SiacoinOutput{
{Address: req.Address, Value: req.Amount},
},
}
toSign, err := b.w.FundTransaction(&txn, req.Amount.Add(minerFee), req.UseUnconfirmed)
if jc.Check("failed to fund transaction", err) != nil {
return
}
b.w.SignTransaction(&txn, toSign, types.CoveredFields{WholeTransaction: true})
// shouldn't be necessary to get parents since the transaction is
// not using unconfirmed outputs, but good practice
txnset := append(b.cm.UnconfirmedParents(txn), txn)
// verify the transaction and add it to the transaction pool
if _, err := b.cm.AddPoolTransactions(txnset); jc.Check("failed to add transaction set", err) != nil {
b.w.ReleaseInputs([]types.Transaction{txn}, nil)
return
}
// broadcast the transaction
b.s.BroadcastTransactionSet(txnset)
jc.Encode(txn.ID())
} else {
txn := types.V2Transaction{
MinerFee: minerFee,
SiacoinOutputs: []types.SiacoinOutput{
{Address: req.Address, Value: req.Amount},
},
}
// fund and sign transaction
state, toSign, err := b.w.FundV2Transaction(&txn, req.Amount.Add(minerFee), req.UseUnconfirmed)
if jc.Check("failed to fund transaction", err) != nil {
return
}
b.w.SignV2Inputs(state, &txn, toSign)
txnset := append(b.cm.V2UnconfirmedParents(txn), txn)
// verify the transaction and add it to the transaction pool
if _, err := b.cm.AddV2PoolTransactions(state.Index, txnset); jc.Check("failed to add v2 transaction set", err) != nil {
b.w.ReleaseInputs(nil, []types.V2Transaction{txn})
return
}
// broadcast the transaction
b.s.BroadcastV2TransactionSet(state.Index, txnset)
jc.Encode(txn.ID())
}
}

func (b *bus) walletSignHandler(jc jape.Context) {
var wsr api.WalletSignRequest
if jc.Decode(&wsr) != nil {
Expand Down
13 changes: 1 addition & 12 deletions internal/node/syncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,18 @@ import (
"context"
"errors"
"fmt"
"io"
"net"
"time"

"go.sia.tech/core/gateway"
"go.sia.tech/core/types"
"go.sia.tech/coreutils/syncer"
"go.uber.org/zap"
)

type Syncer interface {
io.Closer
Addr() string
BroadcastHeader(h gateway.BlockHeader)
BroadcastTransactionSet([]types.Transaction)
Connect(ctx context.Context, addr string) (*syncer.Peer, error)
Peers() []*syncer.Peer
}

// NewSyncer creates a syncer using the given configuration. The syncer that is
// returned is already running, closing it will close the underlying listener
// causing the syncer to stop.
func NewSyncer(cfg BusConfig, cm syncer.ChainManager, ps syncer.PeerStore, logger *zap.Logger) (Syncer, error) {
func NewSyncer(cfg BusConfig, cm syncer.ChainManager, ps syncer.PeerStore, logger *zap.Logger) (*syncer.Syncer, error) {
// validate config
if cfg.Bootstrap && cfg.Network == nil {
return nil, errors.New("cannot bootstrap without a network")
Expand Down
9 changes: 1 addition & 8 deletions internal/test/e2e/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -697,14 +697,7 @@ func (c *TestCluster) AddHost(h *Host) {

// Fund host from bus.
fundAmt := types.Siacoins(25e3)
var scos []types.SiacoinOutput
for i := 0; i < 10; i++ {
scos = append(scos, types.SiacoinOutput{
Value: fundAmt.Div64(10),
Address: h.WalletAddress(),
})
}
c.tt.OK(c.Bus.SendSiacoins(context.Background(), scos, true))
c.tt.OKAll(c.Bus.SendSiacoins(context.Background(), h.WalletAddress(), fundAmt, true))

// Mine transaction.
c.MineBlocks(1)
Expand Down
32 changes: 6 additions & 26 deletions internal/test/e2e/cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2329,13 +2329,8 @@ func TestWalletSendUnconfirmed(t *testing.T) {
}

// send the full balance back to the weallet
toSend := wr.Confirmed.Sub(types.Siacoins(1).Div64(100)) // leave some for the fee
tt.OK(b.SendSiacoins(context.Background(), []types.SiacoinOutput{
{
Address: wr.Address,
Value: toSend,
},
}, false))
toSend := wr.Confirmed.Sub(types.Siacoins(1)) // leave some for the fee
tt.OKAll(b.SendSiacoins(context.Background(), wr.Address, toSend, false))

// the unconfirmed balance should have changed to slightly more than toSend
// since we paid a fee
Expand All @@ -2348,21 +2343,11 @@ func TestWalletSendUnconfirmed(t *testing.T) {
fmt.Println(wr.Confirmed, wr.Unconfirmed)

// try again - this should fail
err = b.SendSiacoins(context.Background(), []types.SiacoinOutput{
{
Address: wr.Address,
Value: toSend,
},
}, false)
_, err = b.SendSiacoins(context.Background(), wr.Address, toSend, false)
tt.AssertIs(err, wallet.ErrNotEnoughFunds)

// try again - this time using unconfirmed transactions
tt.OK(b.SendSiacoins(context.Background(), []types.SiacoinOutput{
{
Address: wr.Address,
Value: toSend,
},
}, true))
tt.OKAll(b.SendSiacoins(context.Background(), wr.Address, toSend, true))

// the unconfirmed balance should be almost the same
wr, err = b.Wallet(context.Background())
Expand Down Expand Up @@ -2403,15 +2388,10 @@ func TestWalletFormUnconfirmed(t *testing.T) {
cluster.AddHosts(1)

// send all money to ourselves, making sure it's unconfirmed
feeReserve := types.Siacoins(1).Div64(100)
feeReserve := types.Siacoins(1)
wr, err := b.Wallet(context.Background())
tt.OK(err)
tt.OK(b.SendSiacoins(context.Background(), []types.SiacoinOutput{
{
Address: wr.Address,
Value: wr.Confirmed.Sub(feeReserve), // leave some for the fee
},
}, false))
tt.OKAll(b.SendSiacoins(context.Background(), wr.Address, wr.Confirmed.Sub(feeReserve), false)) // leave some for the fee

// check wallet only has the reserve in the confirmed balance
wr, err = b.Wallet(context.Background())
Expand Down

0 comments on commit 4d39d7d

Please sign in to comment.