From 894a81a65bcb2614590763e5ca45590ba50a0f59 Mon Sep 17 00:00:00 2001 From: josh Date: Tue, 18 Feb 2020 19:53:11 -0800 Subject: [PATCH] add Cost() function to transaction builder --- examples/get_transaction_cost/main.go | 1 + transaction.go | 166 ++++++++++++++------------ transaction_builder.go | 50 ++++++++ 3 files changed, 143 insertions(+), 74 deletions(-) create mode 100644 examples/get_transaction_cost/main.go diff --git a/examples/get_transaction_cost/main.go b/examples/get_transaction_cost/main.go new file mode 100644 index 000000000..b941854e2 --- /dev/null +++ b/examples/get_transaction_cost/main.go @@ -0,0 +1 @@ +package get_transaction_cost diff --git a/transaction.go b/transaction.go index f12820950..e22cdd22c 100644 --- a/transaction.go +++ b/transaction.go @@ -71,7 +71,7 @@ func (transaction Transaction) SignWith(publicKey Ed25519PublicKey, signer Trans return transaction } -func (transaction Transaction) Execute(client *Client) (TransactionID, error) { +func (transaction Transaction) executeForResponse(client *Client) (TransactionID, *proto.TransactionResponse, error) { if client.operator != nil { transaction.signWithOperator(*client.operator) } @@ -83,72 +83,13 @@ func (transaction Transaction) Execute(client *Client) (TransactionID, error) { node := client.node(nodeAccountID) if node == nil { - return id, newErrLocalValidationf("NodeAccountID %v not found on Client", nodeAccountID) + return id, nil, newErrLocalValidationf("NodeAccountID %v not found on Client", nodeAccountID) } - var methodName string - - switch transactionBody.Data.(type) { - case *proto.TransactionBody_CryptoCreateAccount: - methodName = "/proto.CryptoService/createAccount" - - case *proto.TransactionBody_CryptoTransfer: - methodName = "/proto.CryptoService/cryptoTransfer" - - case *proto.TransactionBody_CryptoUpdateAccount: - methodName = "/proto.CryptoService/updateAccount" - - case *proto.TransactionBody_CryptoDelete: - methodName = "/proto.CryptoService/cryptoDelete" - - // FileServices - case *proto.TransactionBody_FileCreate: - methodName = "/proto.FileService/createFile" - - case *proto.TransactionBody_FileUpdate: - methodName = "/proto.FileService/updateFile" - - case *proto.TransactionBody_FileAppend: - methodName = "/proto.FileService/appendFile" - - case *proto.TransactionBody_FileDelete: - methodName = "/proto.FileService/deleteFile" - - // Contract - case *proto.TransactionBody_ContractCreateInstance: - methodName = "/proto.SmartContractService/createContract" - - case *proto.TransactionBody_ContractDeleteInstance: - methodName = "/proto.SmartContractService/deleteContract" - - case *proto.TransactionBody_ContractUpdateInstance: - methodName = "/proto.SmartContractService/updateContract" - - case *proto.TransactionBody_ContractCall: - methodName = "/proto.SmartContractService/contractCallMethod" + methodName, err := getMethodName(transaction.body()) - // System - case *proto.TransactionBody_Freeze: - methodName = "/proto.FreezeService/freeze" - - case *proto.TransactionBody_SystemDelete: - methodName = "/proto.FileService/systemDelete" - - case *proto.TransactionBody_SystemUndelete: - methodName = "/proto.FileService/systemUndelete" - - // HCS - case *proto.TransactionBody_ConsensusCreateTopic: - methodName = "/proto.ConsensusService/createTopic" - case *proto.TransactionBody_ConsensusDeleteTopic: - methodName = "/proto.ConsensusService/deleteTopic" - case *proto.TransactionBody_ConsensusUpdateTopic: - methodName = "/proto.ConsensusService/updateTopic" - case *proto.TransactionBody_ConsensusSubmitMessage: - methodName = "/proto.ConsensusService/submitMessage" - - default: - return id, newErrLocalValidationf("Could not find method name for: %T", transactionBody.Data) + if err != nil { + return id, nil, err } validUntil := time.Now().Add(time.Duration(transactionBody.TransactionValidDuration.Seconds) * time.Second) @@ -168,7 +109,7 @@ func (transaction Transaction) Execute(client *Client) (TransactionID, error) { err := node.invoke(methodName, transaction.pb, resp) if err != nil { - return id, err + return id, resp, err } if resp.NodeTransactionPrecheckCode == proto.ResponseCodeEnum_BUSY { @@ -176,19 +117,29 @@ func (transaction Transaction) Execute(client *Client) (TransactionID, error) { continue } - status := Status(resp.NodeTransactionPrecheckCode) + return id, resp, nil + } + + // Timed out + return id, nil, newErrHederaPreCheckStatus(transaction.ID, Status(resp.NodeTransactionPrecheckCode)) +} - if status.isExceptional(true) { - // precheck failed - return id, newErrHederaPreCheckStatus(transaction.ID, status) - } +func (transaction Transaction) Execute(client *Client) (TransactionID, error) { + id, resp, err := transaction.executeForResponse(client) - // success - return id, nil + if err != nil { + return id, err } - // Timed out - return id, newErrHederaPreCheckStatus(transaction.ID, Status(resp.NodeTransactionPrecheckCode)) + status := Status(resp.NodeTransactionPrecheckCode) + + if status.isExceptional(true) { + // precheck failed + return id, newErrHederaPreCheckStatus(transaction.ID, status) + } + + // success + return id, nil } func (transaction Transaction) String() string { @@ -213,3 +164,70 @@ func (transaction Transaction) body() *proto.TransactionBody { return transactionBody } + +// getMethodName returns the proto method name of the transaction body +func getMethodName(transactionBody *proto.TransactionBody) (string, error) { + switch transactionBody.Data.(type) { + case *proto.TransactionBody_CryptoCreateAccount: + return "/proto.CryptoService/createAccount", nil + + case *proto.TransactionBody_CryptoTransfer: + return "/proto.CryptoService/cryptoTransfer", nil + + case *proto.TransactionBody_CryptoUpdateAccount: + return "/proto.CryptoService/updateAccount", nil + + case *proto.TransactionBody_CryptoDelete: + return "/proto.CryptoService/cryptoDelete", nil + + // FileServices + case *proto.TransactionBody_FileCreate: + return "/proto.FileService/createFile", nil + + case *proto.TransactionBody_FileUpdate: + return "/proto.FileService/updateFile", nil + + case *proto.TransactionBody_FileAppend: + return "/proto.FileService/appendFile", nil + + case *proto.TransactionBody_FileDelete: + return "/proto.FileService/deleteFile", nil + + // Contract + case *proto.TransactionBody_ContractCreateInstance: + return "/proto.SmartContractService/createContract", nil + + case *proto.TransactionBody_ContractDeleteInstance: + return "/proto.SmartContractService/deleteContract", nil + + case *proto.TransactionBody_ContractUpdateInstance: + return "/proto.SmartContractService/updateContract", nil + + case *proto.TransactionBody_ContractCall: + return "/proto.SmartContractService/contractCallMethod", nil + + // System + case *proto.TransactionBody_Freeze: + return "/proto.FreezeService/freeze", nil + + case *proto.TransactionBody_SystemDelete: + return "/proto.FileService/systemDelete", nil + + case *proto.TransactionBody_SystemUndelete: + return "/proto.FileService/systemUndelete", nil + + // HCS + case *proto.TransactionBody_ConsensusCreateTopic: + return "/proto.ConsensusService/createTopic", nil + case *proto.TransactionBody_ConsensusDeleteTopic: + return "/proto.ConsensusService/deleteTopic", nil + case *proto.TransactionBody_ConsensusUpdateTopic: + return "/proto.ConsensusService/updateTopic", nil + case *proto.TransactionBody_ConsensusSubmitMessage: + return "/proto.ConsensusService/submitMessage", nil + + default: + return "", newErrLocalValidationf("Could not find method name for: %T", transactionBody.Data) + } + +} diff --git a/transaction_builder.go b/transaction_builder.go index 7f1bf5556..09c8af4c3 100644 --- a/transaction_builder.go +++ b/transaction_builder.go @@ -3,6 +3,7 @@ package hedera import ( protobuf "github.com/golang/protobuf/proto" "github.com/hashgraph/hedera-sdk-go/proto" + "math" "time" ) @@ -66,6 +67,55 @@ func (builder TransactionBuilder) Execute(client *Client) (TransactionID, error) return tx.Execute(client) } +// Cost returns the estimated cost of the transaction. +// +// NOTE: The actual cost returned by Hedera is within 99.8% to 99.9% of the actual fee that will be assessed. We're +// unsure if this is because the fee fluctuates that much or if the calculations are simply incorrect on the server. To +// compensate for this we just bump by a 1% the value returned. As this would only ever be a maximum this will not cause +// you to be charged more. +func (builder TransactionBuilder) Cost(client *Client) (Hbar, error) { + // An operator must be set on the client + if client == nil || client.operator == nil { + return ZeroHbar, newErrLocalValidationf("calling .Cost() requires client.SetOperator") + } + + oldFee := builder.pb.TransactionFee + oldTxID := builder.pb.TransactionID + oldValidDuration := builder.pb.TransactionValidDuration + + defer func(){ + // always reset the state of the builder before exiting this function + builder.pb.TransactionFee = oldFee + builder.pb.TransactionID = oldTxID + builder.pb.TransactionValidDuration = oldValidDuration + }() + + costTx, err := builder. + SetMaxTransactionFee(ZeroHbar). + SetTransactionID(NewTransactionID(client.operator.accountID)). + Build(client) + + if err != nil { + return ZeroHbar, err + } + + _, resp, err := costTx. + executeForResponse(client) + + if err != nil { + return ZeroHbar, err + } + + status := Status(resp.NodeTransactionPrecheckCode) + + if status != StatusInsufficientTxFee { + // any status that is not insufficienttxfee should be considered an error in this case + return ZeroHbar, newErrHederaPreCheckStatus(transactionIDFromProto(builder.pb.TransactionID), status) + } + + return HbarFromTinybar(int64(math.Ceil(float64(resp.GetCost()) * 1.1))), nil +} + // // Shared //