OSDN Git Service

Address (#213)
[bytom/bytom.git] / blockchain / account / builder.go
1 package account
2
3 import (
4         "context"
5         "encoding/json"
6
7         log "github.com/sirupsen/logrus"
8
9         "github.com/bytom/blockchain/signers"
10         "github.com/bytom/blockchain/txbuilder"
11         "github.com/bytom/crypto/ed25519/chainkd"
12         chainjson "github.com/bytom/encoding/json"
13         "github.com/bytom/errors"
14         "github.com/bytom/protocol/bc"
15         "github.com/bytom/protocol/bc/legacy"
16 )
17
18 //DecodeSpendAction unmarshal JSON-encoded data of spend action
19 func (m *Manager) DecodeSpendAction(data []byte) (txbuilder.Action, error) {
20         a := &spendAction{accounts: m}
21         err := json.Unmarshal(data, a)
22         return a, err
23 }
24
25 type spendAction struct {
26         accounts *Manager
27         bc.AssetAmount
28         AccountID     string        `json:"account_id"`
29         ReferenceData chainjson.Map `json:"reference_data"`
30         ClientToken   *string       `json:"client_token"`
31 }
32
33 func (a *spendAction) Build(ctx context.Context, b *txbuilder.TemplateBuilder) error {
34         var missing []string
35         if a.AccountID == "" {
36                 missing = append(missing, "account_id")
37         }
38         if a.AssetId.IsZero() {
39                 missing = append(missing, "asset_id")
40         }
41         if len(missing) > 0 {
42                 return txbuilder.MissingFieldsError(missing...)
43         }
44
45         acct, err := a.accounts.findByID(ctx, a.AccountID)
46         if err != nil {
47                 return errors.Wrap(err, "get account info")
48         }
49
50         src := source{
51                 AssetID:   *a.AssetId,
52                 AccountID: a.AccountID,
53         }
54         res, err := a.accounts.utxoDB.Reserve(src, a.Amount, a.ClientToken, b.MaxTime())
55         if err != nil {
56                 return errors.Wrap(err, "reserving utxos")
57         }
58
59         // Cancel the reservation if the build gets rolled back.
60         b.OnRollback(canceler(ctx, a.accounts, res.ID))
61
62         for _, r := range res.UTXOs {
63                 txInput, sigInst, err := UtxoToInputs(acct, r, a.ReferenceData)
64                 if err != nil {
65                         return errors.Wrap(err, "creating inputs")
66                 }
67                 err = b.AddInput(txInput, sigInst)
68                 if err != nil {
69                         return errors.Wrap(err, "adding inputs")
70                 }
71         }
72
73         if res.Change > 0 {
74                 acp, err := a.accounts.createControlProgram(ctx, a.AccountID, true, b.MaxTime())
75                 if err != nil {
76                         return errors.Wrap(err, "creating control program")
77                 }
78
79                 // Don't insert the control program until callbacks are executed.
80                 a.accounts.insertControlProgramDelayed(ctx, b, acp)
81
82                 err = b.AddOutput(legacy.NewTxOutput(*a.AssetId, res.Change, acp.ControlProgram, nil))
83                 if err != nil {
84                         return errors.Wrap(err, "adding change output")
85                 }
86         }
87         return nil
88 }
89
90 //DecodeSpendUTXOAction unmarshal JSON-encoded data of spend utxo action
91 func (m *Manager) DecodeSpendUTXOAction(data []byte) (txbuilder.Action, error) {
92         a := &spendUTXOAction{accounts: m}
93         err := json.Unmarshal(data, a)
94         return a, err
95 }
96
97 type spendUTXOAction struct {
98         accounts *Manager
99         OutputID *bc.Hash `json:"output_id"`
100
101         ReferenceData chainjson.Map `json:"reference_data"`
102         ClientToken   *string       `json:"client_token"`
103 }
104
105 func (a *spendUTXOAction) Build(ctx context.Context, b *txbuilder.TemplateBuilder) error {
106         if a.OutputID == nil {
107                 return txbuilder.MissingFieldsError("output_id")
108         }
109
110         res, err := a.accounts.utxoDB.ReserveUTXO(ctx, *a.OutputID, a.ClientToken, b.MaxTime())
111         if err != nil {
112                 return err
113         }
114         b.OnRollback(canceler(ctx, a.accounts, res.ID))
115
116         var acct *signers.Signer
117         if res.Source.AccountID == "" {
118                 //TODO coinbase
119                 acct = &signers.Signer{}
120         } else {
121                 acct, err = a.accounts.findByID(ctx, res.Source.AccountID)
122                 if err != nil {
123                         return err
124                 }
125         }
126
127         txInput, sigInst, err := UtxoToInputs(acct, res.UTXOs[0], a.ReferenceData)
128         if err != nil {
129                 return err
130         }
131         return b.AddInput(txInput, sigInst)
132 }
133
134 // Best-effort cancellation attempt to put in txbuilder.BuildResult.Rollback.
135 func canceler(ctx context.Context, m *Manager, rid uint64) func() {
136         return func() {
137                 err := m.utxoDB.Cancel(ctx, rid)
138                 if err != nil {
139                         log.WithField("error", err).Error("Best-effort cancellation attempt to put in txbuilder.BuildResult.Rollback")
140                 }
141         }
142 }
143
144 func UtxoToInputs(account *signers.Signer, u *utxo, refData []byte) (*legacy.TxInput, *txbuilder.SigningInstruction, error) {
145         txInput := legacy.NewSpendInput(nil, u.SourceID, u.AssetID, u.Amount, u.SourcePos, u.ControlProgram, u.RefDataHash, refData)
146
147         path := signers.Path(account, signers.AccountKeySpace, u.ControlProgramIndex)
148         sigInst := &txbuilder.SigningInstruction{}
149
150         //TODO: handle pay to script hash address
151         if u.Address != "" {
152                 sigInst.AddRawWitnessKeys(account.XPubs, path, account.Quorum)
153                 derivedXPubs := chainkd.DeriveXPubs(account.XPubs, path)
154                 derivedPK := derivedXPubs[0].PublicKey()
155                 sigInst.WitnessComponents = append(sigInst.WitnessComponents, txbuilder.DataWitness([]byte(derivedPK)))
156         } else {
157                 sigInst.AddWitnessKeys(account.XPubs, path, account.Quorum)
158         }
159         return txInput, sigInst, nil
160 }
161
162 //NewControlAction create new control action
163 func (m *Manager) NewControlAction(amt bc.AssetAmount, accountID string, refData chainjson.Map) txbuilder.Action {
164         return &controlAction{
165                 accounts:      m,
166                 AssetAmount:   amt,
167                 AccountID:     accountID,
168                 ReferenceData: refData,
169         }
170 }
171
172 //DecodeControlAction unmarshal JSON-encoded data of control action
173 func (m *Manager) DecodeControlAction(data []byte) (txbuilder.Action, error) {
174         a := &controlAction{accounts: m}
175         err := json.Unmarshal(data, a)
176         return a, err
177 }
178
179 type controlAction struct {
180         accounts *Manager
181         bc.AssetAmount
182         AccountID     string        `json:"account_id"`
183         ReferenceData chainjson.Map `json:"reference_data"`
184 }
185
186 func (a *controlAction) Build(ctx context.Context, b *txbuilder.TemplateBuilder) error {
187         var missing []string
188         if a.AccountID == "" {
189                 missing = append(missing, "account_id")
190         }
191         if a.AssetId.IsZero() {
192                 missing = append(missing, "asset_id")
193         }
194         if len(missing) > 0 {
195                 return txbuilder.MissingFieldsError(missing...)
196         }
197
198         // Produce a control program, but don't insert it into the database yet.
199         acp, err := a.accounts.createControlProgram(ctx, a.AccountID, false, b.MaxTime())
200         if err != nil {
201                 return err
202         }
203         a.accounts.insertControlProgramDelayed(ctx, b, acp)
204
205         return b.AddOutput(legacy.NewTxOutput(*a.AssetId, a.Amount, acp.ControlProgram, a.ReferenceData))
206 }
207
208 // insertControlProgramDelayed takes a template builder and an account
209 // control program that hasn't been inserted to the database yet. It
210 // registers callbacks on the TemplateBuilder so that all of the template's
211 // account control programs are batch inserted if building the rest of
212 // the template is successful.
213 func (m *Manager) insertControlProgramDelayed(ctx context.Context, b *txbuilder.TemplateBuilder, acp *CtrlProgram) {
214         m.delayedACPsMu.Lock()
215         m.delayedACPs[b] = append(m.delayedACPs[b], acp)
216         m.delayedACPsMu.Unlock()
217
218         b.OnRollback(func() {
219                 m.delayedACPsMu.Lock()
220                 delete(m.delayedACPs, b)
221                 m.delayedACPsMu.Unlock()
222         })
223         b.OnBuild(func() error {
224                 m.delayedACPsMu.Lock()
225                 acps := m.delayedACPs[b]
226                 delete(m.delayedACPs, b)
227                 m.delayedACPsMu.Unlock()
228
229                 // Insert all of the account control programs at once.
230                 if len(acps) == 0 {
231                         return nil
232                 }
233                 return m.insertAccountControlProgram(ctx, acps...)
234         })
235 }