package test import ( "encoding/json" "fmt" "time" "github.com/vapor/account" "github.com/vapor/asset" "github.com/vapor/blockchain/pseudohsm" "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/crypto/sha3pool" "github.com/vapor/database" dbm "github.com/vapor/database/leveldb" "github.com/vapor/errors" "github.com/vapor/protocol/bc" "github.com/vapor/protocol/bc/types" "github.com/vapor/protocol/vm" "github.com/vapor/protocol/vm/vmutil" ) // TxGenerator used to generate new tx type TxGenerator struct { Builder *txbuilder.TemplateBuilder AccountManager *account.Manager Assets *asset.Registry Hsm *pseudohsm.HSM } // NewTxGenerator create a TxGenerator func NewTxGenerator(accountManager *account.Manager, assets *asset.Registry, hsm *pseudohsm.HSM) *TxGenerator { return &TxGenerator{ Builder: txbuilder.NewBuilder(time.Now()), AccountManager: accountManager, Assets: assets, Hsm: hsm, } } // Reset reset transaction builder, used to create a new tx func (g *TxGenerator) Reset() { g.Builder = txbuilder.NewBuilder(time.Now()) } func (g *TxGenerator) createKey(alias string, auth string) error { _, _, err := g.Hsm.XCreate(alias, auth, "en") return err } func (g *TxGenerator) getPubkey(keyAlias string) *chainkd.XPub { pubKeys := g.Hsm.ListKeys() for i, key := range pubKeys { if key.Alias == keyAlias { return &pubKeys[i].XPub } } return nil } func (g *TxGenerator) createAccount(name string, keys []string, quorum int) error { xpubs := []chainkd.XPub{} for _, alias := range keys { xpub := g.getPubkey(alias) if xpub == nil { return fmt.Errorf("can't find pubkey for %s", alias) } xpubs = append(xpubs, *xpub) } _, err := g.AccountManager.Create(xpubs, quorum, name, signers.BIP0044) return err } func (g *TxGenerator) mockUtxo(accountAlias, assetAlias string, amount uint64) (*account.UTXO, error) { ctrlProg, err := g.createControlProgram(accountAlias, false) if err != nil { return nil, err } assetAmount, err := g.assetAmount(assetAlias, amount) if err != nil { return nil, err } utxo := &account.UTXO{ OutputID: bc.Hash{V0: 1}, SourceID: bc.Hash{V0: 1}, AssetID: *assetAmount.AssetId, Amount: assetAmount.Amount, SourcePos: 0, ControlProgram: ctrlProg.ControlProgram, ControlProgramIndex: ctrlProg.KeyIndex, AccountID: ctrlProg.AccountID, Address: ctrlProg.Address, ValidHeight: 0, Change: ctrlProg.Change, } return utxo, nil } func (g *TxGenerator) assetAmount(assetAlias string, amount uint64) (*bc.AssetAmount, error) { if assetAlias == "BTM" { a := &bc.AssetAmount{ Amount: amount, AssetId: consensus.BTMAssetID, } return a, nil } asset, err := g.Assets.FindByAlias(assetAlias) if err != nil { return nil, err } return &bc.AssetAmount{ Amount: amount, AssetId: &asset.AssetID, }, nil } func (g *TxGenerator) createControlProgram(accountAlias string, change bool) (*account.CtrlProgram, error) { acc, err := g.AccountManager.FindByAlias(accountAlias) if err != nil { return nil, err } return g.AccountManager.CreateAddress(acc.ID, change) } // AddSpendInput add a spend input func (g *TxGenerator) AddSpendInput(accountAlias, assetAlias string, amount uint64) error { assetAmount, err := g.assetAmount(assetAlias, amount) if err != nil { return err } acc, err := g.AccountManager.FindByAlias(accountAlias) if err != nil { return err } reqAction := make(map[string]interface{}) reqAction["account_id"] = acc.ID reqAction["amount"] = amount reqAction["asset_id"] = assetAmount.AssetId.String() data, err := json.Marshal(reqAction) if err != nil { return err } spendAction, err := g.AccountManager.DecodeSpendAction(data) if err != nil { return err } return spendAction.Build(nil, g.Builder) } // AddTxInput add a tx input and signing instruction func (g *TxGenerator) AddTxInput(txInput *types.TxInput, signInstruction *txbuilder.SigningInstruction) error { return g.Builder.AddInput(txInput, signInstruction) } // AddTxInputFromUtxo add a tx input which spent the utxo func (g *TxGenerator) AddTxInputFromUtxo(utxo *account.UTXO, accountAlias string) error { acc, err := g.AccountManager.FindByAlias(accountAlias) if err != nil { return err } txInput, signInst, err := account.UtxoToInputs(acc.Signer, utxo) if err != nil { return err } return g.AddTxInput(txInput, signInst) } // AddTxOutput add a tx output func (g *TxGenerator) AddTxOutput(accountAlias, assetAlias string, amount uint64) error { assetAmount, err := g.assetAmount(assetAlias, uint64(amount)) if err != nil { return err } controlProgram, err := g.createControlProgram(accountAlias, false) if err != nil { return err } out := types.NewIntraChainOutput(*assetAmount.AssetId, assetAmount.Amount, controlProgram.ControlProgram) return g.Builder.AddOutput(out) } // AddRetirement add a retirement output func (g *TxGenerator) AddRetirement(assetAlias string, amount uint64) error { assetAmount, err := g.assetAmount(assetAlias, uint64(amount)) if err != nil { return err } retirementProgram := []byte{byte(vm.OP_FAIL)} out := types.NewIntraChainOutput(*assetAmount.AssetId, assetAmount.Amount, retirementProgram) return g.Builder.AddOutput(out) } // Sign used to sign tx func (g *TxGenerator) Sign(passwords []string) (*types.Tx, error) { tpl, _, err := g.Builder.Build() if err != nil { return nil, err } txSerialized, err := tpl.Transaction.MarshalText() if err != nil { return nil, err } tpl.Transaction.Tx.SerializedSize = uint64(len(txSerialized)) tpl.Transaction.TxData.SerializedSize = uint64(len(txSerialized)) for _, password := range passwords { _, err = MockSign(tpl, g.Hsm, password) if err != nil { return nil, err } } return tpl.Transaction, nil } func txFee(tx *types.Tx) uint64 { if len(tx.Inputs) == 1 && tx.Inputs[0].InputType() == types.CoinbaseInputType { return 0 } inputSum := uint64(0) outputSum := uint64(0) for _, input := range tx.Inputs { if input.AssetID() == *consensus.BTMAssetID { inputSum += input.Amount() } } for _, output := range tx.Outputs { if *output.AssetAmount().AssetId == *consensus.BTMAssetID { outputSum += output.AssetAmount().Amount } } return inputSum - outputSum } // CreateSpendInput create SpendInput which spent the output from tx func CreateSpendInput(tx *types.Tx, outputIndex uint64) (*types.SpendInput, error) { outputID := tx.ResultIds[outputIndex] output, ok := tx.Entries[*outputID].(*bc.IntraChainOutput) if !ok { return nil, fmt.Errorf("retirement can't be spent") } sc := types.SpendCommitment{ AssetAmount: *output.Source.Value, SourceID: *output.Source.Ref, SourcePosition: output.Ordinal, VMVersion: vmVersion, ControlProgram: output.ControlProgram.Code, } return &types.SpendInput{ SpendCommitment: sc, }, nil } // SignInstructionFor read CtrlProgram from db, construct SignInstruction for SpendInput func SignInstructionFor(input *types.SpendInput, db dbm.DB, signer *signers.Signer) (*txbuilder.SigningInstruction, error) { cp := account.CtrlProgram{} var hash [32]byte sha3pool.Sum256(hash[:], input.ControlProgram) bytes := db.Get(database.ContractKey(bc.NewHash(hash))) if bytes == nil { return nil, fmt.Errorf("can't find CtrlProgram for the SpendInput") } err := json.Unmarshal(bytes, &cp) if err != nil { return nil, err } sigInst := &txbuilder.SigningInstruction{} if signer == nil { return sigInst, nil } // FIXME: code duplicate with account/builder.go path, err := signers.Path(signer, signers.AccountKeySpace, cp.Change, cp.KeyIndex) if err != nil { return nil, err } if cp.Address == "" { sigInst.AddWitnessKeys(signer.XPubs, path, signer.Quorum) return sigInst, nil } address, err := common.DecodeAddress(cp.Address, &consensus.MainNetParams) if err != nil { return 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, err := signers.Path(signer, signers.AccountKeySpace, cp.Change, cp.KeyIndex) if err != nil { return nil, err } derivedXPubs := chainkd.DeriveXPubs(signer.XPubs, path) derivedPKs := chainkd.XPubKeys(derivedXPubs) script, err := vmutil.P2SPMultiSigProgram(derivedPKs, signer.Quorum) if err != nil { return nil, err } sigInst.WitnessComponents = append(sigInst.WitnessComponents, txbuilder.DataWitness(script)) default: return nil, errors.New("unsupport address type") } return sigInst, nil } // CreateCoinbaseTx create coinbase tx at block height func CreateCoinbaseTx(controlProgram []byte, height, txsFee uint64) (*types.Tx, error) { coinbaseValue := consensus.BlockSubsidy(height) + txsFee builder := txbuilder.NewBuilder(time.Now()) if err := builder.AddInput(types.NewCoinbaseInput([]byte(string(height))), &txbuilder.SigningInstruction{}); err != nil { return nil, err } if err := builder.AddOutput(types.NewIntraChainOutput(*consensus.BTMAssetID, coinbaseValue, controlProgram)); err != nil { return nil, err } tpl, _, err := builder.Build() if err != nil { return nil, err } txSerialized, err := tpl.Transaction.MarshalText() if err != nil { return nil, err } tpl.Transaction.Tx.SerializedSize = uint64(len(txSerialized)) tpl.Transaction.TxData.SerializedSize = uint64(len(txSerialized)) return tpl.Transaction, nil } // CreateTxFromTx create a tx spent the output in outputIndex at baseTx func CreateTxFromTx(baseTx *types.Tx, outputIndex uint64, outputAmount uint64, ctrlProgram []byte) (*types.Tx, error) { spendInput, err := CreateSpendInput(baseTx, outputIndex) if err != nil { return nil, err } txInput := &types.TxInput{ AssetVersion: assetVersion, TypedInput: spendInput, } output := types.NewIntraChainOutput(*consensus.BTMAssetID, outputAmount, ctrlProgram) builder := txbuilder.NewBuilder(time.Now()) if err := builder.AddInput(txInput, &txbuilder.SigningInstruction{}); err != nil { return nil, err } if err := builder.AddOutput(output); err != nil { return nil, err } tpl, _, err := builder.Build() if err != nil { return nil, err } txSerialized, err := tpl.Transaction.MarshalText() if err != nil { return nil, err } tpl.Transaction.Tx.SerializedSize = uint64(len(txSerialized)) tpl.Transaction.TxData.SerializedSize = uint64(len(txSerialized)) return tpl.Transaction, nil }