package account import ( "context" stdjson "encoding/json" "github.com/vapor/blockchain/signers" "github.com/vapor/blockchain/txbuilder" "github.com/vapor/common" "github.com/vapor/consensus" "github.com/vapor/crypto/ed25519/chainkd" "github.com/vapor/encoding/json" "github.com/vapor/errors" "github.com/vapor/protocol/bc" "github.com/vapor/protocol/bc/types" "github.com/vapor/protocol/vm/vmutil" ) var ( //chainTxUtxoNum maximum utxo quantity in a tx chainTxUtxoNum = 5 //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} return a, stdjson.Unmarshal(data, a) } type spendAction struct { accounts *Manager bc.AssetAmount AccountID string `json:"account_id"` 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{} spendActionMap := make(map[string]*spendAction) for _, act := range actions { switch act := act.(type) { case *spendAction: actionKey := act.AssetId.String() + act.AccountID if tmpAct, ok := spendActionMap[actionKey]; ok { tmpAct.Amount += act.Amount tmpAct.UseUnconfirmed = tmpAct.UseUnconfirmed || act.UseUnconfirmed } else { spendActionMap[actionKey] = act resultActions = append(resultActions, act) } default: resultActions = append(resultActions, act) } } 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, nil, 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.NewIntraChainOutput(*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.IntraChainOutput(*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, Change: acp.Change, }) 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.NewIntraChainOutput(*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 == "" { 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(a.AccountID) if err != nil { return errors.Wrap(err, "get account info") } res, err := a.accounts.utxoKeeper.Reserve(a.AccountID, a.AssetId, a.Amount, a.UseUnconfirmed, nil, b.MaxTime()) if err != nil { return errors.Wrap(err, "reserving utxos") } // Cancel the reservation if the build gets rolled back. b.OnRollback(func() { a.accounts.utxoKeeper.Cancel(res.id) }) for _, r := range res.utxos { txInput, sigInst, err := UtxoToInputs(acct.Signer, r) if err != nil { return errors.Wrap(err, "creating inputs") } if err = b.AddInput(txInput, sigInst); err != nil { return errors.Wrap(err, "adding inputs") } } if res.change > 0 { acp, err := a.accounts.CreateAddress(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(b, acp) if err = b.AddOutput(types.NewIntraChainOutput(*a.AssetId, res.change, acp.ControlProgram)); 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} return a, stdjson.Unmarshal(data, a) } type spendUTXOAction struct { accounts *Manager OutputID *bc.Hash `json:"output_id"` UseUnconfirmed bool `json:"use_unconfirmed"` 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") } res, err := a.accounts.utxoKeeper.ReserveParticular(*a.OutputID, a.UseUnconfirmed, b.MaxTime()) if err != nil { return err } b.OnRollback(func() { a.accounts.utxoKeeper.Cancel(res.id) }) var accountSigner *signers.Signer if len(res.utxos[0].AccountID) != 0 { account, err := a.accounts.FindByID(res.utxos[0].AccountID) if err != nil { return err } accountSigner = account.Signer } txInput, sigInst, err := UtxoToInputs(accountSigner, res.utxos[0]) if err != nil { return err } if a.Arguments == nil { return b.AddInput(txInput, sigInst) } sigInst = &txbuilder.SigningInstruction{} if err := txbuilder.AddContractArgs(sigInst, a.Arguments); err != nil { return err } return b.AddInput(txInput, sigInst) } // UtxoToInputs convert an utxo to the txinput func UtxoToInputs(signer *signers.Signer, u *UTXO) (*types.TxInput, *txbuilder.SigningInstruction, error) { txInput := &types.TxInput{} if u.Vote == nil { txInput = types.NewSpendInput(nil, u.SourceID, u.AssetID, u.Amount, u.SourcePos, u.ControlProgram) } else { txInput = types.NewVetoInput(nil, u.SourceID, u.AssetID, u.Amount, u.SourcePos, u.ControlProgram, u.Vote) } sigInst := &txbuilder.SigningInstruction{} if signer == nil { return txInput, sigInst, nil } path, err := signers.Path(signer, signers.AccountKeySpace, u.Change, u.ControlProgramIndex) if err != nil { return nil, nil, err } 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 } sigInst.AddRawWitnessKeys(signer.XPubs, path, signer.Quorum) derivedXPubs := chainkd.DeriveXPubs(signer.XPubs, path) switch address.(type) { case *common.AddressWitnessPubKeyHash: derivedPK := derivedXPubs[0].PublicKey() sigInst.WitnessComponents = append(sigInst.WitnessComponents, txbuilder.DataWitness([]byte(derivedPK))) case *common.AddressWitnessScriptHash: 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(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.SaveControlPrograms(acps...) }) } //DecodeVetoAction unmarshal JSON-encoded data of spend action func (m *Manager) DecodeVetoAction(data []byte) (txbuilder.Action, error) { a := &vetoAction{accounts: m} return a, stdjson.Unmarshal(data, a) } type vetoAction struct { accounts *Manager bc.AssetAmount AccountID string `json:"account_id"` Vote json.HexBytes `json:"vote"` UseUnconfirmed bool `json:"use_unconfirmed"` } func (a *vetoAction) ActionType() string { return "veto" } func (a *vetoAction) 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(a.AccountID) if err != nil { return errors.Wrap(err, "get account info") } res, err := a.accounts.utxoKeeper.Reserve(a.AccountID, a.AssetId, a.Amount, a.UseUnconfirmed, a.Vote, b.MaxTime()) if err != nil { return errors.Wrap(err, "reserving utxos") } // Cancel the reservation if the build gets rolled back. b.OnRollback(func() { a.accounts.utxoKeeper.Cancel(res.id) }) for _, r := range res.utxos { txInput, sigInst, err := UtxoToInputs(acct.Signer, r) if err != nil { return errors.Wrap(err, "creating inputs") } if err = b.AddInput(txInput, sigInst); err != nil { return errors.Wrap(err, "adding inputs") } } if res.change > 0 { acp, err := a.accounts.CreateAddress(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(b, acp) if err = b.AddOutput(types.NewVoteOutput(*a.AssetId, res.change, acp.ControlProgram, a.Vote)); err != nil { return errors.Wrap(err, "adding change voteOutput") } } return nil }