package account import ( "context" "encoding/json" log "github.com/sirupsen/logrus" "github.com/bytom/blockchain/signers" "github.com/bytom/blockchain/txbuilder" "github.com/bytom/common" "github.com/bytom/consensus" "github.com/bytom/crypto/ed25519/chainkd" "github.com/bytom/errors" "github.com/bytom/protocol/bc" "github.com/bytom/protocol/bc/types" "github.com/bytom/protocol/vm/vmutil" ) //DecodeSpendAction unmarshal JSON-encoded data of spend action func (m *Manager) DecodeSpendAction(data []byte) (txbuilder.Action, error) { a := &spendAction{accounts: m} err := json.Unmarshal(data, a) return a, err } type spendAction struct { accounts *Manager bc.AssetAmount AccountID string `json:"account_id"` ClientToken *string `json:"client_token"` } func (a *spendAction) Build(ctx context.Context, b *txbuilder.TemplateBuilder) error { var missing []string if a.AccountID == "" { missing = append(missing, "account_id") } if a.AssetId.IsZero() { missing = append(missing, "asset_id") } if len(missing) > 0 { return txbuilder.MissingFieldsError(missing...) } acct, err := a.accounts.FindByID(ctx, a.AccountID) if err != nil { return errors.Wrap(err, "get account info") } src := source{ AssetID: *a.AssetId, AccountID: a.AccountID, } res, err := a.accounts.utxoDB.Reserve(src, a.Amount, a.ClientToken, b.MaxTime()) if err != nil { return errors.Wrap(err, "reserving utxos") } // Cancel the reservation if the build gets rolled back. b.OnRollback(canceler(ctx, a.accounts, res.ID)) for _, r := range res.UTXOs { txInput, sigInst, err := UtxoToInputs(acct.Signer, r) if err != nil { return errors.Wrap(err, "creating inputs") } err = b.AddInput(txInput, sigInst) if err != nil { return errors.Wrap(err, "adding inputs") } } if res.Change > 0 { acp, err := a.accounts.CreateAddress(ctx, a.AccountID, true) if err != nil { return errors.Wrap(err, "creating control program") } // Don't insert the control program until callbacks are executed. a.accounts.insertControlProgramDelayed(ctx, b, acp) err = b.AddOutput(types.NewTxOutput(*a.AssetId, res.Change, acp.ControlProgram)) if err != nil { return errors.Wrap(err, "adding change output") } } return nil } //DecodeSpendUTXOAction unmarshal JSON-encoded data of spend utxo action func (m *Manager) DecodeSpendUTXOAction(data []byte) (txbuilder.Action, error) { a := &spendUTXOAction{accounts: m} err := json.Unmarshal(data, a) return a, err } type spendUTXOAction struct { accounts *Manager OutputID *bc.Hash `json:"output_id"` ClientToken *string `json:"client_token"` } func (a *spendUTXOAction) Build(ctx context.Context, b *txbuilder.TemplateBuilder) error { if a.OutputID == nil { return txbuilder.MissingFieldsError("output_id") } res, err := a.accounts.utxoDB.ReserveUTXO(ctx, *a.OutputID, a.ClientToken, b.MaxTime()) if err != nil { return err } b.OnRollback(canceler(ctx, a.accounts, res.ID)) var accountSigner *signers.Signer if len(res.Source.AccountID) != 0 { account, err := a.accounts.FindByID(ctx, res.Source.AccountID) if err != nil { return err } accountSigner = account.Signer } txInput, sigInst, err := UtxoToInputs(accountSigner, res.UTXOs[0]) if err != nil { return err } return b.AddInput(txInput, sigInst) } // Best-effort cancellation attempt to put in txbuilder.BuildResult.Rollback. func canceler(ctx context.Context, m *Manager, rid uint64) func() { return func() { if err := m.utxoDB.Cancel(ctx, rid); err != nil { log.WithField("error", err).Error("Best-effort cancellation attempt to put in txbuilder.BuildResult.Rollback") } } } // UtxoToInputs convert an utxo to the txinput func UtxoToInputs(signer *signers.Signer, u *UTXO) (*types.TxInput, *txbuilder.SigningInstruction, error) { txInput := types.NewSpendInput(nil, u.SourceID, u.AssetID, u.Amount, u.SourcePos, u.ControlProgram) sigInst := &txbuilder.SigningInstruction{} if signer == nil { return txInput, sigInst, nil } path := signers.Path(signer, signers.AccountKeySpace, u.ControlProgramIndex) if u.Address == "" { sigInst.AddWitnessKeys(signer.XPubs, path, signer.Quorum) return txInput, sigInst, nil } address, err := common.DecodeAddress(u.Address, &consensus.ActiveNetParams) if err != nil { return nil, nil, err } switch address.(type) { case *common.AddressWitnessPubKeyHash: sigInst.AddRawWitnessKeys(signer.XPubs, path, signer.Quorum) derivedXPubs := chainkd.DeriveXPubs(signer.XPubs, path) derivedPK := derivedXPubs[0].PublicKey() sigInst.WitnessComponents = append(sigInst.WitnessComponents, txbuilder.DataWitness([]byte(derivedPK))) case *common.AddressWitnessScriptHash: sigInst.AddRawWitnessKeys(signer.XPubs, path, signer.Quorum) path := signers.Path(signer, signers.AccountKeySpace, u.ControlProgramIndex) derivedXPubs := chainkd.DeriveXPubs(signer.XPubs, path) derivedPKs := chainkd.XPubKeys(derivedXPubs) script, err := vmutil.P2SPMultiSigProgram(derivedPKs, signer.Quorum) if err != nil { return nil, nil, err } sigInst.WitnessComponents = append(sigInst.WitnessComponents, txbuilder.DataWitness(script)) default: return nil, nil, errors.New("unsupport address type") } return txInput, sigInst, nil } // insertControlProgramDelayed takes a template builder and an account // control program that hasn't been inserted to the database yet. It // registers callbacks on the TemplateBuilder so that all of the template's // account control programs are batch inserted if building the rest of // the template is successful. func (m *Manager) insertControlProgramDelayed(ctx context.Context, b *txbuilder.TemplateBuilder, acp *CtrlProgram) { m.delayedACPsMu.Lock() m.delayedACPs[b] = append(m.delayedACPs[b], acp) m.delayedACPsMu.Unlock() b.OnRollback(func() { m.delayedACPsMu.Lock() delete(m.delayedACPs, b) m.delayedACPsMu.Unlock() }) b.OnBuild(func() error { m.delayedACPsMu.Lock() acps := m.delayedACPs[b] delete(m.delayedACPs, b) m.delayedACPsMu.Unlock() // Insert all of the account control programs at once. if len(acps) == 0 { return nil } return m.insertAccountControlProgram(ctx, acps...) }) }