From 4d39d7da416bf9d357d04831f52a5ffe2aa46925 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Wed, 14 Aug 2024 17:49:01 +0200 Subject: [PATCH] bus: wallet/send endpoint --- api/wallet.go | 7 +++ bus/bus.go | 10 +++++ bus/client/wallet.go | 32 ++++--------- bus/routes.go | 75 +++++++++++++++++++++++++++++++ internal/node/syncer.go | 13 +----- internal/test/e2e/cluster.go | 9 +--- internal/test/e2e/cluster_test.go | 32 +++---------- 7 files changed, 109 insertions(+), 69 deletions(-) diff --git a/api/wallet.go b/api/wallet.go index 2fc6f1e60..510e7b95b 100644 --- a/api/wallet.go +++ b/api/wallet.go @@ -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"` diff --git a/bus/bus.go b/bus/bus.go index d5f0ad383..c72d4ad6e 100644 --- a/bus/bus.go +++ b/bus/bus.go @@ -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. @@ -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 { @@ -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 } @@ -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) @@ -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, diff --git a/bus/client/wallet.go b/bus/client/wallet.go index 554746cd1..9733ed335 100644 --- a/bus/client/wallet.go +++ b/bus/client/wallet.go @@ -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. diff --git a/bus/routes.go b/bus/routes.go index 81f180402..3ee5f1931 100644 --- a/bus/routes.go +++ b/bus/routes.go @@ -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())) } } @@ -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 { diff --git a/internal/node/syncer.go b/internal/node/syncer.go index 0a374c78e..d5da68dd3 100644 --- a/internal/node/syncer.go +++ b/internal/node/syncer.go @@ -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") diff --git a/internal/test/e2e/cluster.go b/internal/test/e2e/cluster.go index df62fdf87..76c3a507f 100644 --- a/internal/test/e2e/cluster.go +++ b/internal/test/e2e/cluster.go @@ -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) diff --git a/internal/test/e2e/cluster_test.go b/internal/test/e2e/cluster_test.go index 3d5435c47..a410c892c 100644 --- a/internal/test/e2e/cluster_test.go +++ b/internal/test/e2e/cluster_test.go @@ -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 @@ -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()) @@ -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())