package account import ( "context" "encoding/hex" "encoding/json" "github.com/bytom/blockchain/signers" "github.com/bytom/blockchain/txbuilder" "github.com/bytom/common" "github.com/bytom/consensus" "github.com/bytom/crypto/ed25519/chainkd" chainjson "github.com/bytom/encoding/json" "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} return a, json.Unmarshal(data, a) } type spendAction struct { accounts *Manager bc.AssetAmount AccountID string `json:"account_id"` UseUnconfirmed bool `json:"use_unconfirmed"` } // 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 } 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, 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.NewTxOutput(*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, json.Unmarshal(data, a) } type spendUTXOAction struct { accounts *Manager OutputID *bc.Hash `json:"output_id"` UseUnconfirmed bool `json:"use_unconfirmed"` Arguments []contractArgument `json:"arguments"` } // contractArgument for smart contract type contractArgument struct { Type string `json:"type"` RawData json.RawMessage `json:"raw_data"` } // rawTxSigArgument is signature-related argument for run contract type rawTxSigArgument struct { RootXPub chainkd.XPub `json:"xpub"` Path []chainjson.HexBytes `json:"derivation_path"` } // dataArgument is the other argument for run contract type dataArgument struct { Value string `json:"value"` } 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{} for _, arg := range a.Arguments { switch arg.Type { case "raw_tx_signature": rawTxSig := &rawTxSigArgument{} if err = json.Unmarshal(arg.RawData, rawTxSig); err != nil { return err } // convert path form chainjson.HexBytes to byte var path [][]byte for _, p := range rawTxSig.Path { path = append(path, []byte(p)) } sigInst.AddRawWitnessKeys([]chainkd.XPub{rawTxSig.RootXPub}, path, 1) case "data": data := &dataArgument{} if err = json.Unmarshal(arg.RawData, data); err != nil { return err } value, err := hex.DecodeString(data.Value) if err != nil { return err } sigInst.WitnessComponents = append(sigInst.WitnessComponents, txbuilder.DataWitness(value)) default: return errors.New("contract argument type is not exist") } } 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.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(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.insertControlPrograms(acps...) }) }