terra/transaction.go

319 lines
7.6 KiB
Go

package terra
import (
"context"
"fmt"
"time"
"github.com/galacticship/terra/cosmos"
"github.com/galacticship/terra/crypto"
"github.com/galacticship/terra/terra"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
)
type Transaction struct {
builder cosmos.TxBuilder
config cosmos.TxConfig
q *Querier
errors *multierror.Error
gasLimit uint64
feeAmount cosmos.Coins
signMode cosmos.SignMode
accountNumber uint64
sequence uint64
messages []cosmos.Msg
}
func NewTransaction(q *Querier) *Transaction {
return &Transaction{
builder: terra.EncodingConfig.TxConfig.NewTxBuilder(),
config: terra.EncodingConfig.TxConfig,
q: q,
}
}
func (t *Transaction) Error() error {
return t.errors
}
func (t *Transaction) Message(message func() (cosmos.Msg, error)) *Transaction {
m, err := message()
if err != nil {
t.errors = multierror.Append(t.errors, errors.Wrap(err, "generating message"))
}
t.messages = append(t.messages, m)
return t
}
func (t *Transaction) Memo(memo string) *Transaction {
t.builder.SetMemo(memo)
return t
}
func (t *Transaction) FeeGranter(feeGranter cosmos.AccAddress) *Transaction {
t.builder.SetFeeGranter(feeGranter)
return t
}
func (t *Transaction) TimeoutHeight(timeoutHeight uint64) *Transaction {
t.builder.SetTimeoutHeight(timeoutHeight)
return t
}
func (t *Transaction) GasLimit(gasLimit uint64) *Transaction {
t.gasLimit = gasLimit
return t
}
func (t *Transaction) FeeAmount(feeAmount cosmos.Coins) *Transaction {
t.feeAmount = feeAmount
return t
}
func (t *Transaction) SignMode(signMode cosmos.SignMode) *Transaction {
t.signMode = signMode
return t
}
func (t *Transaction) AccountNumber(accountNumber uint64) *Transaction {
t.accountNumber = accountNumber
return t
}
func (t *Transaction) Sequence(sequence uint64) *Transaction {
t.sequence = sequence
return t
}
func (t *Transaction) simulate(ctx context.Context) (*cosmos.SimulateResponse, error) {
sig := cosmos.SignatureV2{
PubKey: &crypto.PubKey{},
Data: &cosmos.SingleSignatureData{
SignMode: t.signMode,
},
Sequence: t.sequence,
}
if err := t.builder.SetSignatures(sig); err != nil {
return nil, err
}
txBytes, err := t.GetTxBytes()
if err != nil {
return nil, err
}
var res cosmos.SimulateResponse
err = t.q.POSTProto(ctx, "cosmos/tx/v1beta1/simulate", cosmos.NewSimulateRequest(txBytes), &res)
if err != nil {
return nil, errors.Wrap(err, "querying")
}
return &res, nil
}
func (t *Transaction) computeTax(ctx context.Context) (*terra.ComputeTaxResponse, error) {
txBytes, err := t.GetTxBytes()
if err != nil {
return nil, errors.Wrap(err, "getting transaction bytes")
}
var res terra.ComputeTaxResponse
err = t.q.POSTProto(ctx, "terra/tx/v1beta1/compute_tax", terra.NewComputeTaxRequest(txBytes), &res)
if err != nil {
return nil, errors.Wrap(err, "querying")
}
return &res, nil
}
func (t *Transaction) validate(ctx context.Context, wallet *Wallet) error {
err := t.builder.SetMsgs(t.messages...)
if err != nil {
t.errors = multierror.Append(t.errors, errors.Wrap(err, "setting messages"))
}
if t.errors.ErrorOrNil() != nil {
return t.errors.ErrorOrNil()
}
if t.accountNumber == 0 || t.sequence == 0 {
state, err := wallet.State(ctx)
if err != nil {
return errors.Wrap(err, "getting wallet state")
}
t.accountNumber = state.AccountNumber
t.sequence = state.Sequence
}
if t.signMode == cosmos.SignModeUnspecified {
t.signMode = cosmos.SignModeDirect
}
gasLimit := int64(t.gasLimit)
if gasLimit == 0 {
simulateRes, err := t.simulate(ctx)
if err != nil {
return errors.Wrap(err, "simulating transaction for gas limit")
}
gasLimit = wallet.GasAdjustment().MulInt64(int64(simulateRes.GasInfo.GasUsed)).Ceil().RoundInt64()
}
t.builder.SetGasLimit(uint64(gasLimit))
feeAmount := t.feeAmount
if feeAmount.IsZero() {
//computeTaxRes, err := t.computeTax(ctx)
//if err != nil {
// return errors.Wrap(err, "computing taxes to determine feeAmount")
//}
gasPrice := wallet.GasPrice()
feeAmount = cosmos.NewCoins(cosmos.NewCoin(gasPrice.Denom, gasPrice.Amount.MulInt64(gasLimit).Ceil().RoundInt()))
}
t.builder.SetFeeAmount(feeAmount)
return nil
}
func (t *Transaction) broadcast(ctx context.Context) (*cosmos.TxResponse, error) {
txBytes, err := t.GetTxBytes()
if err != nil {
return nil, err
}
var res cosmos.BroadcastTxResponse
err = t.q.POSTProto(ctx, "cosmos/tx/v1beta1/txs", cosmos.NewBroadcastTxRequest(txBytes, cosmos.BroadcastModeAsync), &res)
if err != nil {
return nil, errors.Wrap(err, "querying")
}
txResponse := res.TxResponse
if txResponse.Code != 0 {
return txResponse, errors.Errorf("tx failed with code %d: %s", txResponse.Code, txResponse.RawLog)
}
return txResponse, nil
}
func (t *Transaction) ExecuteAndWaitFor(ctx context.Context, wallet *Wallet) error {
wallet.lock()
defer wallet.unlock()
err := t.validate(ctx, wallet)
if err != nil {
return errors.Wrap(err, "validating transaction")
}
err = wallet.SignTransaction(t)
if err != nil {
return errors.Wrap(err, "signing transaction")
}
transresp, err := t.broadcast(ctx)
if err != nil {
return errors.Wrap(err, "broadcasting transaction")
}
tick := time.NewTicker(2 * time.Second)
notfoundmax := 10
notfoundcount := 0
for {
select {
case <-ctx.Done():
return errors.New("context canceled")
case <-tick.C:
var res struct {
TxResponse struct {
Height int64 `json:"height,string"`
Txhash string `json:"txhash"`
Code int `json:"code"`
} `json:"tx_response"`
}
err := t.q.GET(ctx, fmt.Sprintf("cosmos/tx/v1beta1/txs/%s", transresp.TxHash), nil, &res)
if err != nil {
if notfoundcount < notfoundmax {
notfoundcount++
continue
}
return errors.Wrapf(err, "retrieving transaction state for hash %s", transresp.TxHash)
}
if res.TxResponse.Code != 0 {
return errors.Errorf("transaction %s failed with code %d", transresp.TxHash, res.TxResponse.Code)
}
t.waitForBlock(ctx, res.TxResponse.Height)
return nil
}
}
}
func (t *Transaction) waitForBlock(ctx context.Context, height int64) {
checkBlock := func(height int64) error {
latestBlockHeight, _, err := t.q.LatestBlockInfo(ctx)
if err != nil {
return errors.Wrap(err, "querying latest block")
}
if latestBlockHeight < height {
return errors.Wrap(err, "latest block height is less than asked height")
}
return nil
}
if err := checkBlock(height); err != nil {
tickHeight := time.NewTicker(5 * time.Second)
for {
select {
case <-ctx.Done():
return
case <-tickHeight.C:
err = checkBlock(height)
if err != nil {
continue
}
}
}
}
}
func (t *Transaction) sign(
signMode cosmos.SignMode, signerData cosmos.SignerData,
privKey crypto.PrivKey, overwriteSig bool) error {
sigData := cosmos.SingleSignatureData{
SignMode: signMode,
Signature: nil,
}
sig := cosmos.SignatureV2{
PubKey: privKey.PubKey(),
Data: &sigData,
Sequence: signerData.Sequence,
}
var err error
var prevSignatures []cosmos.SignatureV2
if !overwriteSig {
prevSignatures, err = t.builder.GetTx().GetSignaturesV2()
if err != nil {
return err
}
}
if err := t.builder.SetSignatures(sig); err != nil {
return err
}
signature, err := cosmos.SignWithPrivKey(
signMode,
signerData,
t.builder,
privKey,
t.config,
signerData.Sequence,
)
if err != nil {
return err
}
if overwriteSig {
return t.builder.SetSignatures(signature)
}
prevSignatures = append(prevSignatures, signature)
return t.builder.SetSignatures(prevSignatures...)
}
// GetTxBytes return tx bytes for broadcast
func (t Transaction) GetTxBytes() ([]byte, error) {
return t.config.TxEncoder()(t.builder.GetTx())
}