diff --git a/tx_builder.go b/tx_builder.go index 556db99..89939bc 100644 --- a/tx_builder.go +++ b/tx_builder.go @@ -15,6 +15,8 @@ type TxBuilder struct { pkeys []crypto.PrvKey changeReceiver *Address + + additionalWitnesses uint } // NewTxBuilder returns a new instance of TxBuilder. @@ -48,6 +50,12 @@ func (tb *TxBuilder) SetFee(fee Coin) { tb.tx.Body.Fee = fee } +// SetAdditionalWitnesses sets future witnesses for a partially signed transction. +// This is useful to compute the real length and so fee in advance +func (tb *TxBuilder) SetAdditionalWitnesses(witnesses uint) { + tb.additionalWitnesses = witnesses +} + // AddAuxiliaryData adds auxiliary data to the transaction. func (tb *TxBuilder) AddAuxiliaryData(data *AuxiliaryData) { tb.tx.AuxiliaryData = data @@ -141,7 +149,24 @@ func (tb *TxBuilder) MinCoinsForTxOut(txOut *TxOutput) Coin { // calculateMinFee computes the minimal fee required for the transaction. func (tb *TxBuilder) calculateMinFee() Coin { + if tb.additionalWitnesses > 0 { + // we can asssume the list of VKeyWitnessSet is not a nil value, as `build()` method is always allocating a slice + additionalVKeyWitnessSet := make([]VKeyWitness, tb.additionalWitnesses) + for i := uint(0); i < tb.additionalWitnesses; i++ { + additionalVKeyWitnessSet[i] = VKeyWitness{ + VKey: crypto.PubKey(make([]byte, 32)), + Signature: make([]byte, 64), + } + } + tb.tx.WitnessSet.VKeyWitnessSet = append(tb.tx.WitnessSet.VKeyWitnessSet, additionalVKeyWitnessSet...) + } + txBytes := tb.tx.Bytes() + + if tb.additionalWitnesses > 0 { + tb.tx.WitnessSet.VKeyWitnessSet = tb.tx.WitnessSet.VKeyWitnessSet[:len(tb.tx.WitnessSet.VKeyWitnessSet)-int(tb.additionalWitnesses)] + } + txLength := uint64(len(txBytes)) return tb.protocol.MinFeeA*Coin(txLength) + tb.protocol.MinFeeB } @@ -208,7 +233,7 @@ func (tb *TxBuilder) addChangeIfNeeded(inputAmount, outputAmount *Value) error { if inputOutputCmp := inputAmount.Cmp(outputAmount); inputOutputCmp == -1 || inputOutputCmp == 2 { return fmt.Errorf( - "insuficient input in transaction, got %v want atleast %v", + "insufficient input in transaction, got %v want atleast %v", inputAmount, outputAmount, ) diff --git a/tx_builder_test.go b/tx_builder_test.go index 0bc823c..64e0ef3 100644 --- a/tx_builder_test.go +++ b/tx_builder_test.go @@ -593,3 +593,157 @@ func TestAddChangeIfNeeded(t *testing.T) { }) } } + +func TestCalculateMinFee(t *testing.T) { + key := crypto.NewXPrvKeyFromEntropy([]byte("receiver address"), "foo") + payment, err := NewKeyCredential(key.PubKey()) + if err != nil { + t.Fatal(err) + } + receiver, err := NewEnterpriseAddress(Testnet, payment) + if err != nil { + t.Fatal(err) + } + + type fields struct { + tx Tx + protocol *ProtocolParams + inputs []*TxInput + outputs []*TxOutput + certs []Certificate + // ttl uint64 + // fee uint64 + } + + testcases := []struct { + name string + fields fields + expectedFee Coin + additionalWitnesses uint + }{ + { + name: "input == output + fee", + fields: fields{ + protocol: alonzoProtocol, + inputs: []*TxInput{ + { + TxHash: make([]byte, 32), + Index: uint64(0), + Amount: NewValue(20000000), + }, + }, + outputs: []*TxOutput{ + { + Address: receiver, + Amount: NewValue(18831991), + }, + }, + }, + expectedFee: Coin(165413), //Coin(168009), + }, + { + name: "with only change address", + fields: fields{ + protocol: alonzoProtocol, + inputs: []*TxInput{ + { + TxHash: make([]byte, 32), + Index: uint64(0), + Amount: NewValue(20000000), + }, + }, + }, + expectedFee: Coin(163785), // cardano-cli would propose Coin(165149) as fee for a tx with only change address and no output, + }, + { + name: "input == output + fee (two additional witness)", + fields: fields{ + protocol: alonzoProtocol, + inputs: []*TxInput{ + { + TxHash: make([]byte, 32), + Index: uint64(0), + Amount: NewValue(20000000), + }, + }, + outputs: []*TxOutput{ + { + Address: receiver, + // Amount: NewValue(19823103), + Amount: NewValue(1982310), + }, + }, + }, + expectedFee: Coin(174301), //Coin(176897), + additionalWitnesses: 2, + }, + { + name: "input == output + fee, one delegation certificate, implies one additional witness", + fields: fields{ + protocol: alonzoProtocol, + inputs: []*TxInput{ + { + TxHash: make([]byte, 32), + Index: uint64(0), + Amount: NewValue(20000000), + }, + }, + outputs: []*TxOutput{ + { + Address: receiver, + Amount: NewValue(19827547), + }, + }, + certs: []Certificate{ + func() Certificate { + c, _ := NewStakeDelegationCertificate(crypto.PubKey(make([]byte, 32)), Hash28(make([]byte, 28))) + return c + }(), + }, + }, + expectedFee: Coin(172453), + additionalWitnesses: 1, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + key := crypto.NewXPrvKeyFromEntropy([]byte("change address"), "foo") + payment, err := NewKeyCredential(key.PubKey()) + if err != nil { + t.Fatal(err) + } + changeAddr, err := NewEnterpriseAddress(Testnet, payment) + if err != nil { + t.Fatal(err) + } + txBuilder := NewTxBuilder(alonzoProtocol) + txBuilder.AddInputs(tc.fields.inputs...) + txBuilder.AddOutputs(tc.fields.outputs...) + for _, cert := range tc.fields.certs { + txBuilder.AddCertificate(cert) + } + txBuilder.AddChangeIfNeeded(changeAddr) + txBuilder.SetAdditionalWitnesses(tc.additionalWitnesses) + txBuilder.Sign(key.PrvKey()) + tx, err := txBuilder.Build() + if err != nil { + t.Fatal(err) + } + var totalIn Coin + for _, input := range tx.Body.Inputs { + totalIn += input.Amount.Coin + } + var totalOut Coin + for _, output := range tx.Body.Outputs { + totalOut += output.Amount.Coin + } + if got, want := tx.Body.Fee+totalOut, totalIn; got != want { + t.Errorf("invalid fee+totalOut: got %v want %v", got, want) + } + if got, want := tx.Body.Fee, tc.expectedFee; got != want { + t.Errorf("calculated fee is not the expected one: got %v want %v", got, want) + } + }) + } +}