OSDN Git Service

Support chain tx (#1365)
authoryahtoo <yahtoo.ma@gmail.com>
Tue, 2 Oct 2018 11:22:12 +0000 (19:22 +0800)
committerPaladz <yzhu101@uottawa.ca>
Tue, 2 Oct 2018 11:22:12 +0000 (19:22 +0800)
* Support chain tx

* Optimization code

* Optimization code

* Update test file

* Optimization code

* Fix chain tx packaging problem

* Review chain tx v2 (#1373)

* edit the code formate

* edit the code format

* edit the formate

* edit the code format

* edit the code format

* edit the dormat

* edit the code format

* edit the code format

* edit the code

* edit the code

* edit the code format

* edit the code format

* edit the code formate

* edit the code format

* edit the code

* add unit test for calcMergeGas

* add unit test for TestReserveBtmUtxoChain

* add unit test for TestBuildBtmTxChain

* edit for code style

* edit the api parameter

* fix wallet display bug

14 files changed:
account/accounts_test.go
account/builder.go
account/builder_test.go
api/api.go
api/hsm.go
api/transact.go
asset/builder.go
blockchain/txbuilder/actions.go
blockchain/txbuilder/builder.go
blockchain/txbuilder/txbuilder.go
blockchain/txbuilder/txbuilder_test.go
blockchain/txbuilder/types.go
mining/sort.go
wallet/annotated.go

index 7b6ea74..f980ed3 100644 (file)
@@ -146,7 +146,7 @@ func mockAccountManager(t *testing.T) *Manager {
        }
        defer os.RemoveAll(dirPath)
 
-       testDB := dbm.NewDB("testdb", "leveldb", "temp")
+       testDB := dbm.NewDB("testdb", "memdb", "temp")
        defer os.RemoveAll("temp")
 
        store := leveldb.NewStore(testDB)
index 7679492..69d53a5 100644 (file)
@@ -15,6 +15,13 @@ import (
        "github.com/bytom/protocol/vm/vmutil"
 )
 
+var (
+       //chainTxUtxoNum maximum utxo quantity in a tx
+       chainTxUtxoNum = 10
+       //chainTxMergeGas chain tx gas
+       chainTxMergeGas = uint64(10000000)
+)
+
 //DecodeSpendAction unmarshal JSON-encoded data of spend action
 func (m *Manager) DecodeSpendAction(data []byte) (txbuilder.Action, error) {
        a := &spendAction{accounts: m}
@@ -28,6 +35,10 @@ type spendAction struct {
        UseUnconfirmed bool   `json:"use_unconfirmed"`
 }
 
+func (a *spendAction) ActionType() string {
+       return "spend_account"
+}
+
 // MergeSpendAction merge common assetID and accountID spend action
 func MergeSpendAction(actions []txbuilder.Action) []txbuilder.Action {
        resultActions := []txbuilder.Action{}
@@ -51,6 +62,144 @@ func MergeSpendAction(actions []txbuilder.Action) []txbuilder.Action {
        return resultActions
 }
 
+//calcMergeGas calculate the gas required that n utxos are merged into one
+func calcMergeGas(num int) uint64 {
+       gas := uint64(0)
+       for num > 1 {
+               gas += chainTxMergeGas
+               num -= chainTxUtxoNum - 1
+       }
+       return gas
+}
+
+func (m *Manager) reserveBtmUtxoChain(builder *txbuilder.TemplateBuilder, accountID string, amount uint64, useUnconfirmed bool) ([]*UTXO, error) {
+       reservedAmount := uint64(0)
+       utxos := []*UTXO{}
+       for gasAmount := uint64(0); reservedAmount < gasAmount+amount; gasAmount = calcMergeGas(len(utxos)) {
+               reserveAmount := amount + gasAmount - reservedAmount
+               res, err := m.utxoKeeper.Reserve(accountID, consensus.BTMAssetID, reserveAmount, useUnconfirmed, builder.MaxTime())
+               if err != nil {
+                       return nil, err
+               }
+
+               builder.OnRollback(func() { m.utxoKeeper.Cancel(res.id) })
+               reservedAmount += reserveAmount + res.change
+               utxos = append(utxos, res.utxos[:]...)
+       }
+       return utxos, nil
+}
+
+func (m *Manager) buildBtmTxChain(utxos []*UTXO, signer *signers.Signer) ([]*txbuilder.Template, *UTXO, error) {
+       if len(utxos) == 0 {
+               return nil, nil, errors.New("mergeSpendActionUTXO utxos num 0")
+       }
+
+       tpls := []*txbuilder.Template{}
+       if len(utxos) == 1 {
+               return tpls, utxos[len(utxos)-1], nil
+       }
+
+       acp, err := m.GetLocalCtrlProgramByAddress(utxos[0].Address)
+       if err != nil {
+               return nil, nil, err
+       }
+
+       buildAmount := uint64(0)
+       builder := &txbuilder.TemplateBuilder{}
+       for index := 0; index < len(utxos); index++ {
+               input, sigInst, err := UtxoToInputs(signer, utxos[index])
+               if err != nil {
+                       return nil, nil, err
+               }
+
+               if err = builder.AddInput(input, sigInst); err != nil {
+                       return nil, nil, err
+               }
+
+               buildAmount += input.Amount()
+               if builder.InputCount() != chainTxUtxoNum && index != len(utxos)-1 {
+                       continue
+               }
+
+               outAmount := buildAmount - chainTxMergeGas
+               output := types.NewTxOutput(*consensus.BTMAssetID, outAmount, acp.ControlProgram)
+               if err := builder.AddOutput(output); err != nil {
+                       return nil, nil, err
+               }
+
+               tpl, _, err := builder.Build()
+               if err != nil {
+                       return nil, nil, err
+               }
+
+               bcOut, err := tpl.Transaction.Output(*tpl.Transaction.ResultIds[0])
+               if err != nil {
+                       return nil, nil, err
+               }
+
+               utxos = append(utxos, &UTXO{
+                       OutputID:            *tpl.Transaction.ResultIds[0],
+                       AssetID:             *consensus.BTMAssetID,
+                       Amount:              outAmount,
+                       ControlProgram:      acp.ControlProgram,
+                       SourceID:            *bcOut.Source.Ref,
+                       SourcePos:           bcOut.Source.Position,
+                       ControlProgramIndex: acp.KeyIndex,
+                       Address:             acp.Address,
+               })
+
+               tpls = append(tpls, tpl)
+               buildAmount = 0
+               builder = &txbuilder.TemplateBuilder{}
+               if index == len(utxos)-2 {
+                       break
+               }
+       }
+       return tpls, utxos[len(utxos)-1], nil
+}
+
+// SpendAccountChain build the spend action with auto merge utxo function
+func SpendAccountChain(ctx context.Context, builder *txbuilder.TemplateBuilder, action txbuilder.Action) ([]*txbuilder.Template, error) {
+       act, ok := action.(*spendAction)
+       if !ok {
+               return nil, errors.New("fail to convert the spend action")
+       }
+       if *act.AssetId != *consensus.BTMAssetID {
+               return nil, errors.New("spend chain action only support BTM")
+       }
+
+       utxos, err := act.accounts.reserveBtmUtxoChain(builder, act.AccountID, act.Amount, act.UseUnconfirmed)
+       if err != nil {
+               return nil, err
+       }
+
+       acct, err := act.accounts.FindByID(act.AccountID)
+       if err != nil {
+               return nil, err
+       }
+
+       tpls, utxo, err := act.accounts.buildBtmTxChain(utxos, acct.Signer)
+       if err != nil {
+               return nil, err
+       }
+
+       input, sigInst, err := UtxoToInputs(acct.Signer, utxo)
+       if err != nil {
+               return nil, err
+       }
+
+       if err := builder.AddInput(input, sigInst); err != nil {
+               return nil, err
+       }
+
+       if utxo.Amount > act.Amount {
+               if err = builder.AddOutput(types.NewTxOutput(*consensus.BTMAssetID, utxo.Amount-act.Amount, utxo.ControlProgram)); err != nil {
+                       return nil, errors.Wrap(err, "adding change output")
+               }
+       }
+       return tpls, nil
+}
+
 func (a *spendAction) Build(ctx context.Context, b *txbuilder.TemplateBuilder) error {
        var missing []string
        if a.AccountID == "" {
@@ -114,6 +263,10 @@ type spendUTXOAction struct {
        Arguments      []txbuilder.ContractArgument `json:"arguments"`
 }
 
+func (a *spendUTXOAction) ActionType() string {
+       return "spend_account_unspent_output"
+}
+
 func (a *spendUTXOAction) Build(ctx context.Context, b *txbuilder.TemplateBuilder) error {
        if a.OutputID == nil {
                return txbuilder.MissingFieldsError("output_id")
index 9ebe5ac..a8e13fd 100644 (file)
 package account
 
 import (
+       "encoding/json"
        "testing"
+       "time"
 
        "github.com/bytom/blockchain/txbuilder"
+       "github.com/bytom/consensus"
+       "github.com/bytom/crypto/ed25519/chainkd"
        "github.com/bytom/protocol/bc"
+       "github.com/bytom/testutil"
 )
 
+func TestReserveBtmUtxoChain(t *testing.T) {
+       chainTxUtxoNum = 3
+       utxos := []*UTXO{}
+       m := mockAccountManager(t)
+       for i := uint64(1); i <= 20; i++ {
+               utxo := &UTXO{
+                       OutputID:  bc.Hash{V0: i},
+                       AccountID: "TestAccountID",
+                       AssetID:   *consensus.BTMAssetID,
+                       Amount:    i * chainTxMergeGas,
+               }
+               utxos = append(utxos, utxo)
+
+               data, err := json.Marshal(utxo)
+               if err != nil {
+                       t.Fatal(err)
+               }
+
+               m.db.Set(StandardUTXOKey(utxo.OutputID), data)
+       }
+
+       cases := []struct {
+               amount uint64
+               want   []uint64
+               err    bool
+       }{
+               {
+                       amount: 1 * chainTxMergeGas,
+                       want:   []uint64{1},
+               },
+               {
+                       amount: 888888 * chainTxMergeGas,
+                       want:   []uint64{},
+                       err:    true,
+               },
+               {
+                       amount: 7 * chainTxMergeGas,
+                       want:   []uint64{4, 3, 1},
+               },
+               {
+                       amount: 15 * chainTxMergeGas,
+                       want:   []uint64{5, 4, 3, 2, 1, 6},
+               },
+               {
+                       amount: 163 * chainTxMergeGas,
+                       want:   []uint64{20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 2, 1, 3},
+               },
+       }
+
+       for i, c := range cases {
+               m.utxoKeeper.expireReservation(time.Unix(999999999, 0))
+               utxos, err := m.reserveBtmUtxoChain(&txbuilder.TemplateBuilder{}, "TestAccountID", c.amount, false)
+
+               if err != nil != c.err {
+                       t.Fatalf("case %d got err %v want err = %v", i, err, c.err)
+               }
+
+               got := []uint64{}
+               for _, utxo := range utxos {
+                       got = append(got, utxo.Amount/chainTxMergeGas)
+               }
+
+               if !testutil.DeepEqual(got, c.want) {
+                       t.Fatalf("case %d got %d want %d", i, got, c.want)
+               }
+       }
+
+}
+
+func TestBuildBtmTxChain(t *testing.T) {
+       chainTxUtxoNum = 3
+       m := mockAccountManager(t)
+       cases := []struct {
+               inputUtxo  []uint64
+               wantInput  [][]uint64
+               wantOutput [][]uint64
+               wantUtxo   uint64
+       }{
+               {
+                       inputUtxo:  []uint64{5},
+                       wantInput:  [][]uint64{},
+                       wantOutput: [][]uint64{},
+                       wantUtxo:   5 * chainTxMergeGas,
+               },
+               {
+                       inputUtxo: []uint64{5, 4},
+                       wantInput: [][]uint64{
+                               []uint64{5, 4},
+                       },
+                       wantOutput: [][]uint64{
+                               []uint64{8},
+                       },
+                       wantUtxo: 8 * chainTxMergeGas,
+               },
+               {
+                       inputUtxo: []uint64{5, 4, 1, 1},
+                       wantInput: [][]uint64{
+                               []uint64{5, 4, 1},
+                               []uint64{1, 9},
+                       },
+                       wantOutput: [][]uint64{
+                               []uint64{9},
+                               []uint64{9},
+                       },
+                       wantUtxo: 9 * chainTxMergeGas,
+               },
+               {
+                       inputUtxo: []uint64{22, 123, 53, 234, 23, 4, 2423, 24, 23, 43, 34, 234, 234, 24},
+                       wantInput: [][]uint64{
+                               []uint64{22, 123, 53},
+                               []uint64{234, 23, 4},
+                               []uint64{2423, 24, 23},
+                               []uint64{43, 34, 234},
+                               []uint64{234, 24, 197},
+                               []uint64{260, 2469, 310},
+                               []uint64{454, 3038},
+                       },
+                       wantOutput: [][]uint64{
+                               []uint64{197},
+                               []uint64{260},
+                               []uint64{2469},
+                               []uint64{310},
+                               []uint64{454},
+                               []uint64{3038},
+                               []uint64{3491},
+                       },
+                       wantUtxo: 3491 * chainTxMergeGas,
+               },
+       }
+
+       acct, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, "testAccount")
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       acp, err := m.CreateAddress(acct.ID, false)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       for caseIndex, c := range cases {
+               utxos := []*UTXO{}
+               for _, amount := range c.inputUtxo {
+                       utxos = append(utxos, &UTXO{
+                               Amount:         amount * chainTxMergeGas,
+                               AssetID:        *consensus.BTMAssetID,
+                               Address:        acp.Address,
+                               ControlProgram: acp.ControlProgram,
+                       })
+               }
+
+               tpls, gotUtxo, err := m.buildBtmTxChain(utxos, acct.Signer)
+               if err != nil {
+                       t.Fatal(err)
+               }
+
+               for i, tpl := range tpls {
+                       gotInput := []uint64{}
+                       for _, input := range tpl.Transaction.Inputs {
+                               gotInput = append(gotInput, input.Amount()/chainTxMergeGas)
+                       }
+
+                       gotOutput := []uint64{}
+                       for _, output := range tpl.Transaction.Outputs {
+                               gotOutput = append(gotOutput, output.Amount/chainTxMergeGas)
+                       }
+
+                       if !testutil.DeepEqual(c.wantInput[i], gotInput) {
+                               t.Fatalf("case %d tx %d input got %d want %d", caseIndex, i, gotInput, c.wantInput[i])
+                       }
+                       if !testutil.DeepEqual(c.wantOutput[i], gotOutput) {
+                               t.Fatalf("case %d tx %d output got %d want %d", caseIndex, i, gotOutput, c.wantOutput[i])
+                       }
+               }
+
+               if c.wantUtxo != gotUtxo.Amount {
+                       t.Fatalf("case %d got utxo=%d want utxo=%d", caseIndex, gotUtxo.Amount, c.wantUtxo)
+               }
+       }
+
+}
+
 func TestMergeSpendAction(t *testing.T) {
        testBTM := &bc.AssetID{}
        if err := testBTM.UnmarshalText([]byte("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")); err != nil {
@@ -350,3 +537,51 @@ func TestMergeSpendAction(t *testing.T) {
                }
        }
 }
+
+func TestCalcMergeGas(t *testing.T) {
+       chainTxUtxoNum = 10
+       cases := []struct {
+               utxoNum int
+               gas     uint64
+       }{
+               {
+                       utxoNum: 0,
+                       gas:     0,
+               },
+               {
+                       utxoNum: 1,
+                       gas:     0,
+               },
+               {
+                       utxoNum: 9,
+                       gas:     chainTxMergeGas,
+               },
+               {
+                       utxoNum: 10,
+                       gas:     chainTxMergeGas,
+               },
+               {
+                       utxoNum: 11,
+                       gas:     chainTxMergeGas * 2,
+               },
+               {
+                       utxoNum: 20,
+                       gas:     chainTxMergeGas * 3,
+               },
+               {
+                       utxoNum: 21,
+                       gas:     chainTxMergeGas * 3,
+               },
+               {
+                       utxoNum: 74,
+                       gas:     chainTxMergeGas * 9,
+               },
+       }
+
+       for i, c := range cases {
+               gas := calcMergeGas(c.utxoNum)
+               if gas != c.gas {
+                       t.Fatalf("case %d got %d want %d", i, gas, c.gas)
+               }
+       }
+}
index b3d378d..c37b55e 100644 (file)
@@ -224,7 +224,9 @@ func (a *API) buildHandler() {
                m.Handle("/sign-message", jsonHandler(a.signMessage))
 
                m.Handle("/build-transaction", jsonHandler(a.build))
-               m.Handle("/sign-transaction", jsonHandler(a.pseudohsmSignTemplates))
+               m.Handle("/build-chain-transactions", jsonHandler(a.buildChainTxs))
+               m.Handle("/sign-transaction", jsonHandler(a.signTemplate))
+               m.Handle("/sign-transactions", jsonHandler(a.signTemplates))
 
                m.Handle("/get-transaction", jsonHandler(a.getTransaction))
                m.Handle("/list-transactions", jsonHandler(a.listTransactions))
@@ -257,6 +259,7 @@ func (a *API) buildHandler() {
        m.Handle("/list-transaction-feeds", jsonHandler(a.listTxFeeds))
 
        m.Handle("/submit-transaction", jsonHandler(a.submit))
+       m.Handle("/submit-transactions", jsonHandler(a.submitTxs))
        m.Handle("/estimate-transaction-gas", jsonHandler(a.estimateTxGas))
 
        m.Handle("/get-unconfirmed-transaction", jsonHandler(a.getUnconfirmedTx))
index fead0b3..4237fff 100644 (file)
@@ -58,12 +58,12 @@ func (a *API) pseudohsmDeleteKey(ctx context.Context, x struct {
        return NewSuccessResponse(nil)
 }
 
-type signResp struct {
+type signTemplateResp struct {
        Tx           *txbuilder.Template `json:"transaction"`
        SignComplete bool                `json:"sign_complete"`
 }
 
-func (a *API) pseudohsmSignTemplates(ctx context.Context, x struct {
+func (a *API) signTemplate(ctx context.Context, x struct {
        Password string             `json:"password"`
        Txs      txbuilder.Template `json:"transaction"`
 }) Response {
@@ -72,7 +72,29 @@ func (a *API) pseudohsmSignTemplates(ctx context.Context, x struct {
                return NewErrorResponse(err)
        }
        log.Info("Sign Transaction complete.")
-       return NewSuccessResponse(&signResp{Tx: &x.Txs, SignComplete: txbuilder.SignProgress(&x.Txs)})
+       return NewSuccessResponse(&signTemplateResp{Tx: &x.Txs, SignComplete: txbuilder.SignProgress(&x.Txs)})
+}
+
+type signTemplatesResp struct {
+       Tx           []*txbuilder.Template `json:"transaction"`
+       SignComplete bool                  `json:"sign_complete"`
+}
+
+func (a *API) signTemplates(ctx context.Context, x struct {
+       Password string                `json:"password"`
+       Txs      []*txbuilder.Template `json:"transactions"`
+}) Response {
+       signComplete := true
+       for _, tx := range x.Txs {
+               if err := txbuilder.Sign(ctx, tx, x.Password, a.pseudohsmSignTemplate); err != nil {
+                       log.WithField("build err", err).Error("fail on sign transaction.")
+                       return NewErrorResponse(err)
+               }
+               signComplete = signComplete && txbuilder.SignProgress(tx)
+       }
+
+       log.Info("Sign Chain Tx complete.")
+       return NewSuccessResponse(&signTemplatesResp{Tx: x.Txs, SignComplete: signComplete})
 }
 
 func (a *API) pseudohsmSignTemplate(ctx context.Context, xpub chainkd.XPub, path [][]byte, data [32]byte, password string) ([]byte, error) {
index 4531743..3dcad4c 100644 (file)
@@ -56,16 +56,67 @@ func onlyHaveInputActions(req *BuildRequest) (bool, error) {
 }
 
 func (a *API) buildSingle(ctx context.Context, req *BuildRequest) (*txbuilder.Template, error) {
-       if err := a.completeMissingIDs(ctx, req); err != nil {
+       if err := a.checkRequestValidity(ctx, req); err != nil {
+               return nil, err
+       }
+       actions, err := a.mergeSpendActions(req)
+       if err != nil {
                return nil, err
        }
 
-       if ok, err := onlyHaveInputActions(req); err != nil {
+       maxTime := time.Now().Add(req.TTL.Duration)
+       tpl, err := txbuilder.Build(ctx, req.Tx, actions, maxTime, req.TimeRange)
+       if errors.Root(err) == txbuilder.ErrAction {
+               // append each of the inner errors contained in the data.
+               var Errs string
+               var rootErr error
+               for i, innerErr := range errors.Data(err)["actions"].([]error) {
+                       if i == 0 {
+                               rootErr = errors.Root(innerErr)
+                       }
+                       Errs = Errs + innerErr.Error()
+               }
+               err = errors.WithDetail(rootErr, Errs)
+       }
+       if err != nil {
                return nil, err
+       }
+
+       // ensure null is never returned for signing instructions
+       if tpl.SigningInstructions == nil {
+               tpl.SigningInstructions = []*txbuilder.SigningInstruction{}
+       }
+       return tpl, nil
+}
+
+// POST /build-transaction
+func (a *API) build(ctx context.Context, buildReqs *BuildRequest) Response {
+       subctx := reqid.NewSubContext(ctx, reqid.New())
+       tmpl, err := a.buildSingle(subctx, buildReqs)
+       if err != nil {
+               return NewErrorResponse(err)
+       }
+
+       return NewSuccessResponse(tmpl)
+}
+func (a *API) checkRequestValidity(ctx context.Context, req *BuildRequest) error {
+       if err := a.completeMissingIDs(ctx, req); err != nil {
+               return err
+       }
+
+       if req.TTL.Duration == 0 {
+               req.TTL.Duration = defaultTxTTL
+       }
+
+       if ok, err := onlyHaveInputActions(req); err != nil {
+               return err
        } else if ok {
-               return nil, errors.WithDetail(ErrBadActionConstruction, "transaction contains only input actions and no output actions")
+               return errors.WithDetail(ErrBadActionConstruction, "transaction contains only input actions and no output actions")
        }
+       return nil
+}
 
+func (a *API) mergeSpendActions(req *BuildRequest) ([]txbuilder.Action, error) {
        actions := make([]txbuilder.Action, 0, len(req.Actions))
        for i, act := range req.Actions {
                typ, ok := act["type"].(string)
@@ -90,47 +141,51 @@ func (a *API) buildSingle(ctx context.Context, req *BuildRequest) (*txbuilder.Te
                actions = append(actions, action)
        }
        actions = account.MergeSpendAction(actions)
+       return actions, nil
+}
 
-       ttl := req.TTL.Duration
-       if ttl == 0 {
-               ttl = defaultTxTTL
+func (a *API) buildTxs(ctx context.Context, req *BuildRequest) ([]*txbuilder.Template, error) {
+       if err := a.checkRequestValidity(ctx, req); err != nil {
+               return nil, err
+       }
+       actions, err := a.mergeSpendActions(req)
+       if err != nil {
+               return nil, err
        }
-       maxTime := time.Now().Add(ttl)
 
-       tpl, err := txbuilder.Build(ctx, req.Tx, actions, maxTime, req.TimeRange)
-       if errors.Root(err) == txbuilder.ErrAction {
-               // append each of the inner errors contained in the data.
-               var Errs string
-               var rootErr error
-               for i, innerErr := range errors.Data(err)["actions"].([]error) {
-                       if i == 0 {
-                               rootErr = errors.Root(innerErr)
-                       }
-                       Errs = Errs + innerErr.Error()
+       builder := txbuilder.NewBuilder(time.Now().Add(req.TTL.Duration))
+       tpls := []*txbuilder.Template{}
+       for _, action := range actions {
+               if action.ActionType() == "spend_account" {
+                       tpls, err = account.SpendAccountChain(ctx, builder, action)
+               } else {
+                       err = action.Build(ctx, builder)
+               }
+
+               if err != nil {
+                       builder.Rollback()
+                       return nil, err
                }
-               err = errors.WithDetail(rootErr, Errs)
        }
+
+       tpl, _, err := builder.Build()
        if err != nil {
+               builder.Rollback()
                return nil, err
        }
 
-       // ensure null is never returned for signing instructions
-       if tpl.SigningInstructions == nil {
-               tpl.SigningInstructions = []*txbuilder.SigningInstruction{}
-       }
-       return tpl, nil
+       tpls = append(tpls, tpl)
+       return tpls, nil
 }
 
-// POST /build-transaction
-func (a *API) build(ctx context.Context, buildReqs *BuildRequest) Response {
+// POST /build-chain-transactions
+func (a *API) buildChainTxs(ctx context.Context, buildReqs *BuildRequest) Response {
        subctx := reqid.NewSubContext(ctx, reqid.New())
-
-       tmpl, err := a.buildSingle(subctx, buildReqs)
+       tmpls, err := a.buildTxs(subctx, buildReqs)
        if err != nil {
                return NewErrorResponse(err)
        }
-
-       return NewSuccessResponse(tmpl)
+       return NewSuccessResponse(tmpls)
 }
 
 type submitTxResp struct {
@@ -149,6 +204,25 @@ func (a *API) submit(ctx context.Context, ins struct {
        return NewSuccessResponse(&submitTxResp{TxID: &ins.Tx.ID})
 }
 
+type submitTxsResp struct {
+       TxID []*bc.Hash `json:"tx_id"`
+}
+
+// POST /submit-transactions
+func (a *API) submitTxs(ctx context.Context, ins struct {
+       Tx []types.Tx `json:"raw_transactions"`
+}) Response {
+       txHashs := []*bc.Hash{}
+       for i := range ins.Tx {
+               if err := txbuilder.FinalizeTx(ctx, a.chain, &ins.Tx[i]); err != nil {
+                       return NewErrorResponse(err)
+               }
+               log.WithField("tx_id", ins.Tx[i].ID.String()).Info("submit single tx")
+               txHashs = append(txHashs, &ins.Tx[i].ID)
+       }
+       return NewSuccessResponse(&submitTxsResp{TxID: txHashs})
+}
+
 // EstimateTxGasResp estimate transaction consumed gas
 type EstimateTxGasResp struct {
        TotalNeu   int64 `json:"total_neu"`
index 25fef0c..7f8c946 100644 (file)
@@ -66,3 +66,7 @@ func (a *issueAction) Build(ctx context.Context, builder *txbuilder.TemplateBuil
        builder.RestrictMinTime(time.Now())
        return builder.AddInput(txin, tplIn)
 }
+
+func (a *issueAction) ActionType() string {
+       return "issue"
+}
index c758ba5..1075475 100644 (file)
@@ -66,6 +66,10 @@ func (a *controlAddressAction) Build(ctx context.Context, b *TemplateBuilder) er
        return b.AddOutput(out)
 }
 
+func (a *controlAddressAction) ActionType() string {
+       return "control_address"
+}
+
 // DecodeControlProgramAction convert input data to action struct
 func DecodeControlProgramAction(data []byte) (Action, error) {
        a := new(controlProgramAction)
@@ -97,6 +101,10 @@ func (a *controlProgramAction) Build(ctx context.Context, b *TemplateBuilder) er
        return b.AddOutput(out)
 }
 
+func (a *controlProgramAction) ActionType() string {
+       return "control_program"
+}
+
 // DecodeRetireAction convert input data to action struct
 func DecodeRetireAction(data []byte) (Action, error) {
        a := new(retireAction)
@@ -128,3 +136,7 @@ func (a *retireAction) Build(ctx context.Context, b *TemplateBuilder) error {
        out := types.NewTxOutput(*a.AssetId, a.Amount, program)
        return b.AddOutput(out)
 }
+
+func (a *retireAction) ActionType() string {
+       return "retire"
+}
index 16264a1..871da01 100644 (file)
@@ -46,6 +46,11 @@ func (b *TemplateBuilder) AddOutput(o *types.TxOutput) error {
        return nil
 }
 
+// InputCount return number of input in the template builder
+func (b *TemplateBuilder) InputCount() int {
+       return len(b.inputs)
+}
+
 // RestrictMinTime set minTime
 func (b *TemplateBuilder) RestrictMinTime(t time.Time) {
        if t.After(b.minTime) {
@@ -82,7 +87,8 @@ func (b *TemplateBuilder) OnBuild(buildFn func() error) {
        b.callbacks = append(b.callbacks, buildFn)
 }
 
-func (b *TemplateBuilder) rollback() {
+// Rollback action for handle fail build
+func (b *TemplateBuilder) Rollback() {
        for _, f := range b.rollbacks {
                f()
        }
index 4b19e54..9048b41 100644 (file)
@@ -61,14 +61,14 @@ func Build(ctx context.Context, tx *types.TxData, actions []Action, maxTime time
 
        // If there were any errors, rollback and return a composite error.
        if len(errs) > 0 {
-               builder.rollback()
+               builder.Rollback()
                return nil, errors.WithData(ErrAction, "actions", errs)
        }
 
        // Build the transaction template.
        tpl, tx, err := builder.Build()
        if err != nil {
-               builder.rollback()
+               builder.Rollback()
                return nil, err
        }
 
index 05e022b..8cd939e 100644 (file)
@@ -38,6 +38,10 @@ func (t testAction) Build(ctx context.Context, b *TemplateBuilder) error {
        return b.AddOutput(types.NewTxOutput(*t.AssetId, t.Amount, []byte("change")))
 }
 
+func (t testAction) ActionType() string {
+       return "test-action"
+}
+
 func newControlProgramAction(assetAmt bc.AssetAmount, script []byte) *controlProgramAction {
        return &controlProgramAction{
                AssetAmount: assetAmt,
index d6c8742..7aee4da 100644 (file)
@@ -31,6 +31,7 @@ func (t *Template) Hash(idx uint32) bc.Hash {
 // Action is a interface
 type Action interface {
        Build(context.Context, *TemplateBuilder) error
+       ActionType() string
 }
 
 // Receiver encapsulates information about where to send assets.
index 507f50d..40a4f01 100644 (file)
@@ -6,4 +6,4 @@ type byTime []*protocol.TxDesc
 
 func (a byTime) Len() int           { return len(a) }
 func (a byTime) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
-func (a byTime) Less(i, j int) bool { return a[i].Added.Unix() < a[j].Added.Unix() }
+func (a byTime) Less(i, j int) bool { return a[i].Added.Before(a[j].Added) }
index c5540e0..4199b1c 100644 (file)
@@ -92,7 +92,7 @@ func annotateTxsAccount(txs []*query.AnnotatedTx, walletDB db.DB) {
                        if input.SpentOutputID == nil {
                                continue
                        }
-                       localAccount, err := getAccountFromUTXO(*input.SpentOutputID, walletDB)
+                       localAccount, err := getAccountFromACP(input.ControlProgram, walletDB)
                        if localAccount == nil || err != nil {
                                continue
                        }
@@ -110,30 +110,6 @@ func annotateTxsAccount(txs []*query.AnnotatedTx, walletDB db.DB) {
        }
 }
 
-func getAccountFromUTXO(outputID bc.Hash, walletDB db.DB) (*account.Account, error) {
-       accountUTXO := account.UTXO{}
-       localAccount := account.Account{}
-
-       accountUTXOValue := walletDB.Get(account.StandardUTXOKey(outputID))
-       if accountUTXOValue == nil {
-               return nil, fmt.Errorf("failed get account utxo:%x ", outputID)
-       }
-
-       if err := json.Unmarshal(accountUTXOValue, &accountUTXO); err != nil {
-               return nil, err
-       }
-
-       accountValue := walletDB.Get(account.Key(accountUTXO.AccountID))
-       if accountValue == nil {
-               return nil, fmt.Errorf("failed get account:%s ", accountUTXO.AccountID)
-       }
-       if err := json.Unmarshal(accountValue, &localAccount); err != nil {
-               return nil, err
-       }
-
-       return &localAccount, nil
-}
-
 func getAccountFromACP(program []byte, walletDB db.DB) (*account.Account, error) {
        var hash common.Hash
        accountCP := account.CtrlProgram{}