diff --git a/blockproducer/interfaces/transaction.go b/blockproducer/interfaces/transaction.go index efcc586c3..0282cbc2d 100644 --- a/blockproducer/interfaces/transaction.go +++ b/blockproducer/interfaces/transaction.go @@ -129,6 +129,23 @@ const ( TransactionStateNotFound ) +func (s TransactionState) String() string { + switch s { + case TransactionStatePending: + return "Pending" + case TransactionStatePacked: + return "Packed" + case TransactionStateConfirmed: + return "Confirmed" + case TransactionStateExpired: + return "Expired" + case TransactionStateNotFound: + return "Not Found" + default: + return "Unknown" + } +} + // Transaction is the interface implemented by an object that can be verified and processed by // block producers. type Transaction interface { diff --git a/client/driver.go b/client/driver.go index 5a77a31ce..5af1ace39 100644 --- a/client/driver.go +++ b/client/driver.go @@ -31,6 +31,7 @@ import ( "github.com/CovenantSQL/CovenantSQL/conf" "github.com/CovenantSQL/CovenantSQL/crypto" "github.com/CovenantSQL/CovenantSQL/crypto/asymmetric" + "github.com/CovenantSQL/CovenantSQL/crypto/hash" "github.com/CovenantSQL/CovenantSQL/crypto/kms" "github.com/CovenantSQL/CovenantSQL/proto" "github.com/CovenantSQL/CovenantSQL/route" @@ -260,7 +261,7 @@ func GetTokenBalance(tt types.TokenType) (balance uint64, err error) { // UpdatePermission sends UpdatePermission transaction to chain. func UpdatePermission(targetUser proto.AccountAddress, - targetChain proto.AccountAddress, perm types.UserPermission) (err error) { + targetChain proto.AccountAddress, perm types.UserPermission) (txHash hash.Hash, err error) { if atomic.LoadUint32(&driverInitialized) == 0 { err = ErrNotInitialized return @@ -307,11 +308,14 @@ func UpdatePermission(targetUser proto.AccountAddress, return } + txHash = up.Hash() return } // TransferToken send Transfer transaction to chain. -func TransferToken(targetUser proto.AccountAddress, amount uint64, tokenType types.TokenType) (err error) { +func TransferToken(targetUser proto.AccountAddress, amount uint64, tokenType types.TokenType) ( + txHash hash.Hash, err error, +) { if atomic.LoadUint32(&driverInitialized) == 0 { err = ErrNotInitialized return @@ -359,6 +363,53 @@ func TransferToken(targetUser proto.AccountAddress, amount uint64, tokenType typ return } + txHash = tran.Hash() + return +} + +// WaitTxConfirmation waits for the transaction with target hash txHash to be confirmed. It also +// returns if any error occurs or a final state is returned from BP. +func WaitTxConfirmation( + ctx context.Context, txHash hash.Hash) (state interfaces.TransactionState, err error, +) { + var ( + ticker = time.NewTicker(1 * time.Second) + method = route.MCCQueryTxState + req = &types.QueryTxStateReq{Hash: txHash} + resp = &types.QueryTxStateResp{} + ) + defer ticker.Stop() + for { + if err = requestBP(method, req, resp); err != nil { + err = errors.Wrapf(err, "failed to call %s", method) + return + } + + state = resp.State + log.WithFields(log.Fields{ + "tx_hash": txHash, + "tx_state": state, + }).Debug("waiting for tx confirmation") + + switch state { + case interfaces.TransactionStatePending: + case interfaces.TransactionStatePacked: + case interfaces.TransactionStateConfirmed, + interfaces.TransactionStateExpired, + interfaces.TransactionStateNotFound: + return + default: + err = errors.Errorf("unknown transaction state %d", state) + return + } + + select { + case <-ticker.C: + case <-ctx.Done(): + err = ctx.Err() + return + } + } return } diff --git a/cmd/cql-utils/rpc.go b/cmd/cql-utils/rpc.go index 1f868e964..bc83f4ceb 100644 --- a/cmd/cql-utils/rpc.go +++ b/cmd/cql-utils/rpc.go @@ -22,6 +22,7 @@ import ( "fmt" "reflect" "strings" + "time" bp "github.com/CovenantSQL/CovenantSQL/blockproducer" pi "github.com/CovenantSQL/CovenantSQL/blockproducer/interfaces" @@ -46,9 +47,10 @@ var ( route.SQLChainRPCName: &sqlchain.MuxService{}, route.BlockProducerRPCName: &bp.ChainRPCService{}, } - rpcName string - rpcEndpoint string - rpcReq string + rpcName string + rpcEndpoint string + rpcReq string + rpcTxWaitConfirm bool ) type canSign interface { @@ -59,6 +61,7 @@ func init() { flag.StringVar(&rpcName, "rpc", "", "rpc name to do test call") flag.StringVar(&rpcEndpoint, "rpc-endpoint", "", "rpc endpoint to do test call") flag.StringVar(&rpcReq, "rpc-req", "", "rpc request to do test call, in json format") + flag.BoolVar(&rpcTxWaitConfirm, "rpc-tx-wait-confirm", false, "wait for transaction confirmation") } func runRPC() { @@ -95,9 +98,10 @@ func runRPC() { } // fill nonce if this is a AddTx request + var tx pi.Transaction if rpcName == route.MCCAddTx.String() { if addTxReqType, ok := req.(*types.AddTxReq); ok { - var tx = addTxReqType.Tx + tx = addTxReqType.Tx for { if txWrapper, ok := tx.(*pi.TransactionWrapper); ok { tx = txWrapper.Unwrap() @@ -138,6 +142,42 @@ func runRPC() { // print the response log.Info("got response") spewCfg.Dump(resp) + + if rpcName == route.MCCAddTx.String() && rpcTxWaitConfirm { + log.Info("waiting for transaction confirmation...") + var ( + err error + ticker = time.NewTicker(1 * time.Second) + req = &types.QueryTxStateReq{Hash: tx.Hash()} + resp = &types.QueryTxStateResp{} + ) + defer ticker.Stop() + for { + if err = rpc.NewCaller().CallNode( + proto.NodeID(rpcEndpoint), + route.MCCQueryTxState.String(), + req, resp, + ); err != nil { + log.Fatalf("query transaction state failed: %v", err) + } + switch resp.State { + case pi.TransactionStatePending: + fmt.Print(".") + case pi.TransactionStatePacked: + fmt.Print("+") + case pi.TransactionStateConfirmed: + fmt.Print("✔\n") + return + case pi.TransactionStateExpired, pi.TransactionStateNotFound: + fmt.Print("✘\n") + log.Fatalf("bad transaction state: %s", resp.State) + default: + fmt.Print("✘\n") + log.Fatal("unknown transaction state") + } + <-ticker.C + } + } } func checkAndSign(req interface{}) (err error) { diff --git a/cmd/cql/main.go b/cmd/cql/main.go index 05225b3e4..07db14704 100644 --- a/cmd/cql/main.go +++ b/cmd/cql/main.go @@ -30,9 +30,13 @@ import ( "runtime" "strconv" "strings" + "time" + pi "github.com/CovenantSQL/CovenantSQL/blockproducer/interfaces" "github.com/CovenantSQL/CovenantSQL/client" + "github.com/CovenantSQL/CovenantSQL/conf" "github.com/CovenantSQL/CovenantSQL/crypto/asymmetric" + "github.com/CovenantSQL/CovenantSQL/crypto/hash" "github.com/CovenantSQL/CovenantSQL/proto" "github.com/CovenantSQL/CovenantSQL/types" "github.com/CovenantSQL/CovenantSQL/utils/log" @@ -67,6 +71,9 @@ var ( transferToken string // transfer token to target account getBalance bool // get balance of current account getBalanceWithTokenName string // get specific token's balance of current account + waitTxConfirmation bool // wait for transaction confirmation before exiting + + waitTxConfirmationMaxDuration time.Duration ) type userPermission struct { @@ -216,6 +223,7 @@ func init() { flag.StringVar(&transferToken, "transfer", "", "transfer token to target account") flag.BoolVar(&getBalance, "get-balance", false, "get balance of current account") flag.StringVar(&getBalanceWithTokenName, "token-balance", "", "get specific token's balance of current account, e.g. Particle, Wave, and etc.") + flag.BoolVar(&waitTxConfirmation, "wait-tx-confirm", false, "wait for transaction confirmation") } func main() { @@ -235,6 +243,11 @@ func main() { return } + // TODO(leventeliu): discover more specific confirmation duration from config. We don't have + // enough informations from config to do that currently, so just use a fixed and long enough + // duration. + waitTxConfirmationMaxDuration = 10 * conf.GConf.BPPeriod + if getBalance { var stableCoinBalance, covenantCoinBalance uint64 @@ -342,7 +355,7 @@ func main() { return } - err := client.UpdatePermission(perm.TargetUser, perm.TargetChain, p) + txHash, err := client.UpdatePermission(perm.TargetUser, perm.TargetChain, p) if err != nil { log.WithError(err).Error("update permission failed") @@ -350,6 +363,10 @@ func main() { return } + if waitTxConfirmation { + wait(txHash) + } + log.Info("succeed in sending transaction to CovenantSQL") return } @@ -391,13 +408,18 @@ func main() { return } - err = client.TransferToken(tran.TargetUser, amount, unit) + var txHash hash.Hash + txHash, err = client.TransferToken(tran.TargetUser, amount, unit) if err != nil { log.WithError(err).Error("transfer token failed") os.Exit(-1) return } + if waitTxConfirmation { + wait(txHash) + } + log.Info("succeed in sending transaction to CovenantSQL") return } @@ -445,6 +467,19 @@ func main() { } } +func wait(txHash hash.Hash) { + var ctx, cancel = context.WithTimeout(context.Background(), waitTxConfirmationMaxDuration) + defer cancel() + var state, err = client.WaitTxConfirmation(ctx, txHash) + log.WithFields(log.Fields{ + "tx_hash": txHash, + "tx_state": state, + }).WithError(err).Info("wait transaction confirmation") + if err != nil || state != pi.TransactionStateConfirmed { + os.Exit(1) + } +} + func run(u *user.User) (err error) { // get working directory wd, err := os.Getwd() diff --git a/route/acl.go b/route/acl.go index 8efa6a1e9..2e71fe144 100644 --- a/route/acl.go +++ b/route/acl.go @@ -111,10 +111,12 @@ const ( MCCNextAccountNonce // MCCAddTx is used by block producer main chain to upload transaction MCCAddTx - // MCCQuerySQLChainProfile is used by nodes to to query SQLChainProfile. + // MCCQuerySQLChainProfile is used by nodes to query SQLChainProfile. MCCQuerySQLChainProfile // MCCQueryAccountTokenBalance is used by block producer to provide account token balance MCCQueryAccountTokenBalance + // MCCQueryTxState is used by client to query transaction state. + MCCQueryTxState // DHTRPCName defines the block producer dh-rpc service name DHTRPCName = "DHT" // BlockProducerRPCName defines main chain rpc name @@ -186,6 +188,8 @@ func (s RemoteFunc) String() string { return "MCC.QuerySQLChainProfile" case MCCQueryAccountTokenBalance: return "MCC.QueryAccountTokenBalance" + case MCCQueryTxState: + return "MCC.QueryTxState" } return "Unknown" }