diff --git a/src/RpcClient/RpcClient.cs b/src/RpcClient/RpcClient.cs index 957025f1a..6291ce044 100644 --- a/src/RpcClient/RpcClient.cs +++ b/src/RpcClient/RpcClient.cs @@ -645,6 +645,16 @@ public async Task SendToAddressAsync(string assetId, string address, st .ConfigureAwait(false); } + /// + /// Cancel Tx. + /// + /// This function returns Signed Transaction JSON if successful, ContractParametersContext JSON if signing failed. + public async Task CancelTransactionAsync(UInt256 txId, string[] signers, string extraFee) + { + JToken[] parameters = signers.Select(s => (JString)s.AsScriptHash()).ToArray(); + return (JObject)await RpcSendAsync(GetRpcName(), txId.ToString(), new JArray(parameters), extraFee).ConfigureAwait(false); + } + #endregion Wallet #region Plugins diff --git a/src/RpcServer/RpcServer.Wallet.cs b/src/RpcServer/RpcServer.Wallet.cs index 7ced0421a..097dafcd3 100644 --- a/src/RpcServer/RpcServer.Wallet.cs +++ b/src/RpcServer/RpcServer.Wallet.cs @@ -319,6 +319,57 @@ protected virtual JToken SendToAddress(JArray _params) return SignAndRelay(snapshot, tx); } + [RpcMethod] + protected virtual JToken CancelTransaction(JArray _params) + { + CheckWallet(); + var txid = UInt256.Parse(_params[0].AsString()); + TransactionState state = NativeContract.Ledger.GetTransactionState(system.StoreView, txid); + if (state != null) + { + throw new RpcException(32700, "This tx is already confirmed, can't be cancelled."); + } + + var conflict = new TransactionAttribute[] { new Conflicts() { Hash = txid } }; + Signer[] signers = _params.Count >= 2 ? ((JArray)_params[1]).Select(j => new Signer() { Account = AddressToScriptHash(j.AsString(), system.Settings.AddressVersion), Scopes = WitnessScope.None }).ToArray() : Array.Empty(); + if (!signers.Any()) + { + throw new RpcException(32701, "no signers"); + } + + Transaction tx = new Transaction + { + Signers = signers, + Attributes = conflict, + Witnesses = Array.Empty(), + }; + + try + { + tx = wallet.MakeTransaction(system.StoreView, new[] { (byte)OpCode.RET }, signers[0].Account, signers, conflict); + } + catch (InvalidOperationException e) + { + throw new RpcException(-500, GetExceptionMessage(e)); + } + + if (system.MemPool.TryGetValue(txid, out Transaction conflictTx)) + { + tx.NetworkFee = Math.Max(tx.NetworkFee, conflictTx.NetworkFee) + 1; + } + else if (_params.Count >= 3) + { + var extraFee = _params[2].AsString(); + AssetDescriptor descriptor = new(system.StoreView, system.Settings, NativeContract.GAS.Hash); + if (!BigDecimal.TryParse(extraFee, descriptor.Decimals, out BigDecimal decimalExtraFee) || decimalExtraFee.Sign <= 0) + { + throw new RpcException(32702, "Incorrect Amount Format"); + } + tx.NetworkFee += (long)decimalExtraFee.Value; + }; + return SignAndRelay(system.StoreView, tx); + } + [RpcMethod] protected virtual JToken InvokeContractVerify(JArray _params) {