Merge pull request #7654 from ErikEk/listchaintxns-add-txhash

rpc: add gettx command to walletrpc
This commit is contained in:
Oliver Gugger 2023-12-11 10:08:42 +01:00 committed by GitHub
commit 0ec9ac7070
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1187 additions and 766 deletions

View File

@ -1853,9 +1853,9 @@ var listChainTxnsCommand = cli.Command{
cli.Int64Flag{
Name: "end_height",
Usage: "the block height until which to list " +
"transactions, inclusive, to get transactions " +
"until the chain tip, including unconfirmed, " +
"set this value to -1",
"transactions, inclusive, to get " +
"transactions until the chain tip, including " +
"unconfirmed, set this value to -1",
},
},
Description: `

View File

@ -74,6 +74,7 @@ func walletCommands() []cli.Command {
listSweepsCommand,
labelTxCommand,
publishTxCommand,
getTxCommand,
releaseOutputCommand,
leaseOutputCommand,
listLeasesCommand,
@ -561,6 +562,43 @@ func publishTransaction(ctx *cli.Context) error {
return nil
}
var getTxCommand = cli.Command{
Name: "gettx",
Usage: "Returns details of a transaction.",
ArgsUsage: "txid",
Description: `
Query the transaction using the given transaction id and return its
details. An error is returned if the transaction is not found.
`,
Action: actionDecorator(getTransaction),
}
func getTransaction(ctx *cli.Context) error {
ctxc := getContext()
// Display the command's help message if we do not have the expected
// number of arguments/flags.
if ctx.NArg() != 1 {
return cli.ShowCommandHelp(ctx, "gettx")
}
walletClient, cleanUp := getWalletClient(ctx)
defer cleanUp()
req := &walletrpc.GetTransactionRequest{
Txid: ctx.Args().First(),
}
res, err := walletClient.GetTransaction(ctxc, req)
if err != nil {
return err
}
printRespJSON(res)
return nil
}
// utxoLease contains JSON annotations for a lease on an unspent output.
type utxoLease struct {
ID string `json:"id"`

View File

@ -77,6 +77,9 @@
status, `StatusInitiated`, to explicitly report its current state. Before
running this new version, please make sure to upgrade your client application
to include this new status so it can understand the RPC response properly.
* Adds a new rpc endpoint gettx to the walletrpc sub-server to [fetch
transaction details](https://github.com/lightningnetwork/lnd/pull/7654).
## lncli Additions
@ -175,6 +178,7 @@
* Andras Banki-Horvath
* Carla Kirk-Cohen
* Elle Mouton
* ErikEk
* Keagan McClelland
* Marcos Fernandez Perez
* Matt Morehouse

View File

@ -829,32 +829,22 @@ func testSweepAllCoins(ht *lntest.HarnessTest) {
// assertTxLabel is a helper function which finds a target tx in our
// set of transactions and checks that it has the desired label.
assertTxLabel := func(targetTx, label string) error {
// List all transactions relevant to our wallet, and find the
// tx so that we can check the correct label has been set.
txResp := ainz.RPC.GetTransactions(nil)
// Get the transaction from our wallet so that we can check
// that the correct label has been set.
txResp := ainz.RPC.GetTransaction(
&walletrpc.GetTransactionRequest{
Txid: targetTx,
},
)
require.NotNilf(ht, txResp, "target tx %v not found", targetTx)
var target *lnrpc.Transaction
// First we need to find the target tx.
for _, txn := range txResp.Transactions {
if txn.TxHash == targetTx {
target = txn
break
}
}
// If we cannot find it, return an error.
if target == nil {
return fmt.Errorf("target tx %v not found", targetTx)
}
// Otherwise, check the labels are matched.
if target.Label == label {
// Make sure the labels match.
if txResp.Label == label {
return nil
}
return fmt.Errorf("labels not match, want: "+
"%v, got %v", label, target.Label)
"%v, got %v", label, txResp.Label)
}
// waitTxLabel waits until the desired tx label is found or timeout.

File diff suppressed because it is too large Load Diff

View File

@ -254,6 +254,42 @@ func local_request_WalletKit_NextAddr_0(ctx context.Context, marshaler runtime.M
}
var (
filter_WalletKit_GetTransaction_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_WalletKit_GetTransaction_0(ctx context.Context, marshaler runtime.Marshaler, client WalletKitClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetTransactionRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_WalletKit_GetTransaction_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.GetTransaction(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_WalletKit_GetTransaction_0(ctx context.Context, marshaler runtime.Marshaler, server WalletKitServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetTransactionRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_WalletKit_GetTransaction_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.GetTransaction(ctx, &protoReq)
return msg, metadata, err
}
var (
filter_WalletKit_ListAccounts_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
@ -1043,6 +1079,29 @@ func RegisterWalletKitHandlerServer(ctx context.Context, mux *runtime.ServeMux,
})
mux.Handle("GET", pattern_WalletKit_GetTransaction_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/walletrpc.WalletKit/GetTransaction", runtime.WithHTTPPathPattern("/v2/wallet/tx"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_WalletKit_GetTransaction_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_WalletKit_GetTransaction_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_WalletKit_ListAccounts_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@ -1638,6 +1697,26 @@ func RegisterWalletKitHandlerClient(ctx context.Context, mux *runtime.ServeMux,
})
mux.Handle("GET", pattern_WalletKit_GetTransaction_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/walletrpc.WalletKit/GetTransaction", runtime.WithHTTPPathPattern("/v2/wallet/tx"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_WalletKit_GetTransaction_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_WalletKit_GetTransaction_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_WalletKit_ListAccounts_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@ -2016,6 +2095,8 @@ var (
pattern_WalletKit_NextAddr_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v2", "wallet", "address", "next"}, ""))
pattern_WalletKit_GetTransaction_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v2", "wallet", "tx"}, ""))
pattern_WalletKit_ListAccounts_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v2", "wallet", "accounts"}, ""))
pattern_WalletKit_RequiredReserve_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v2", "wallet", "reserve"}, ""))
@ -2068,6 +2149,8 @@ var (
forward_WalletKit_NextAddr_0 = runtime.ForwardResponseMessage
forward_WalletKit_GetTransaction_0 = runtime.ForwardResponseMessage
forward_WalletKit_ListAccounts_0 = runtime.ForwardResponseMessage
forward_WalletKit_RequiredReserve_0 = runtime.ForwardResponseMessage

View File

@ -197,6 +197,31 @@ func RegisterWalletKitJSONCallbacks(registry map[string]func(ctx context.Context
callback(string(respBytes), nil)
}
registry["walletrpc.WalletKit.GetTransaction"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
req := &GetTransactionRequest{}
err := marshaler.Unmarshal([]byte(reqJSON), req)
if err != nil {
callback("", err)
return
}
client := NewWalletKitClient(conn)
resp, err := client.GetTransaction(ctx, req)
if err != nil {
callback("", err)
return
}
respBytes, err := marshaler.Marshal(resp)
if err != nil {
callback("", err)
return
}
callback(string(respBytes), nil)
}
registry["walletrpc.WalletKit.ListAccounts"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {

View File

@ -75,6 +75,11 @@ service WalletKit {
*/
rpc NextAddr (AddrRequest) returns (AddrResponse);
/* lncli: `wallet gettx`
GetTransaction returns details for a transaction found in the wallet.
*/
rpc GetTransaction (GetTransactionRequest) returns (lnrpc.Transaction);
/* lncli: `wallet accounts list`
ListAccounts retrieves all accounts belonging to the wallet by default. A
name and key scope filter can be provided to filter through all of the
@ -556,6 +561,11 @@ message ListAddressesResponse {
repeated AccountWithAddresses account_with_addresses = 1;
}
message GetTransactionRequest {
// The txid of the transaction.
string txid = 1;
}
message SignMessageWithAddrRequest {
// The message to be signed. When using REST, this field must be encoded as
// base64.

View File

@ -663,6 +663,36 @@
}
},
"/v2/wallet/tx": {
"get": {
"summary": "lncli: `wallet gettx`\nGetTransaction returns details for a transaction found in the wallet.",
"operationId": "WalletKit_GetTransaction",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/lnrpcTransaction"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "txid",
"description": "The txid of the transaction.",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"WalletKit"
]
},
"post": {
"summary": "lncli: `wallet publishtx`\nPublishTransaction attempts to publish the passed transaction to the\nnetwork. Once this returns without an error, the wallet will continually\nattempt to re-broadcast the transaction on start up, until it enters the\nchain.",
"operationId": "WalletKit_PublishTransaction",

View File

@ -29,6 +29,8 @@ http:
- selector: walletrpc.WalletKit.NextAddr
post: "/v2/wallet/address/next"
body: "*"
- selector: walletrpc.WalletKit.GetTransaction
get: "/v2/wallet/tx"
- selector: walletrpc.WalletKit.PublishTransaction
post: "/v2/wallet/tx"
body: "*"

View File

@ -4,6 +4,7 @@ package walletrpc
import (
context "context"
lnrpc "github.com/lightningnetwork/lnd/lnrpc"
signrpc "github.com/lightningnetwork/lnd/lnrpc/signrpc"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
@ -48,6 +49,9 @@ type WalletKitClient interface {
DeriveKey(ctx context.Context, in *signrpc.KeyLocator, opts ...grpc.CallOption) (*signrpc.KeyDescriptor, error)
// NextAddr returns the next unused address within the wallet.
NextAddr(ctx context.Context, in *AddrRequest, opts ...grpc.CallOption) (*AddrResponse, error)
// lncli: `wallet gettx`
// GetTransaction returns details for a transaction found in the wallet.
GetTransaction(ctx context.Context, in *GetTransactionRequest, opts ...grpc.CallOption) (*lnrpc.Transaction, error)
// lncli: `wallet accounts list`
// ListAccounts retrieves all accounts belonging to the wallet by default. A
// name and key scope filter can be provided to filter through all of the
@ -326,6 +330,15 @@ func (c *walletKitClient) NextAddr(ctx context.Context, in *AddrRequest, opts ..
return out, nil
}
func (c *walletKitClient) GetTransaction(ctx context.Context, in *GetTransactionRequest, opts ...grpc.CallOption) (*lnrpc.Transaction, error) {
out := new(lnrpc.Transaction)
err := c.cc.Invoke(ctx, "/walletrpc.WalletKit/GetTransaction", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletKitClient) ListAccounts(ctx context.Context, in *ListAccountsRequest, opts ...grpc.CallOption) (*ListAccountsResponse, error) {
out := new(ListAccountsResponse)
err := c.cc.Invoke(ctx, "/walletrpc.WalletKit/ListAccounts", in, out, opts...)
@ -521,6 +534,9 @@ type WalletKitServer interface {
DeriveKey(context.Context, *signrpc.KeyLocator) (*signrpc.KeyDescriptor, error)
// NextAddr returns the next unused address within the wallet.
NextAddr(context.Context, *AddrRequest) (*AddrResponse, error)
// lncli: `wallet gettx`
// GetTransaction returns details for a transaction found in the wallet.
GetTransaction(context.Context, *GetTransactionRequest) (*lnrpc.Transaction, error)
// lncli: `wallet accounts list`
// ListAccounts retrieves all accounts belonging to the wallet by default. A
// name and key scope filter can be provided to filter through all of the
@ -754,6 +770,9 @@ func (UnimplementedWalletKitServer) DeriveKey(context.Context, *signrpc.KeyLocat
func (UnimplementedWalletKitServer) NextAddr(context.Context, *AddrRequest) (*AddrResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method NextAddr not implemented")
}
func (UnimplementedWalletKitServer) GetTransaction(context.Context, *GetTransactionRequest) (*lnrpc.Transaction, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetTransaction not implemented")
}
func (UnimplementedWalletKitServer) ListAccounts(context.Context, *ListAccountsRequest) (*ListAccountsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListAccounts not implemented")
}
@ -947,6 +966,24 @@ func _WalletKit_NextAddr_Handler(srv interface{}, ctx context.Context, dec func(
return interceptor(ctx, in, info, handler)
}
func _WalletKit_GetTransaction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetTransactionRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletKitServer).GetTransaction(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/walletrpc.WalletKit/GetTransaction",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletKitServer).GetTransaction(ctx, req.(*GetTransactionRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletKit_ListAccounts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListAccountsRequest)
if err := dec(in); err != nil {
@ -1306,6 +1343,10 @@ var WalletKit_ServiceDesc = grpc.ServiceDesc{
MethodName: "NextAddr",
Handler: _WalletKit_NextAddr_Handler,
},
{
MethodName: "GetTransaction",
Handler: _WalletKit_GetTransaction_Handler,
},
{
MethodName: "ListAccounts",
Handler: _WalletKit_ListAccounts_Handler,

View File

@ -80,6 +80,10 @@ var (
Entity: "address",
Action: "read",
}},
"/walletrpc.WalletKit/GetTransaction": {{
Entity: "onchain",
Action: "read",
}},
"/walletrpc.WalletKit/PublishTransaction": {{
Entity: "onchain",
Action: "write",
@ -612,6 +616,29 @@ func (w *WalletKit) NextAddr(ctx context.Context,
}, nil
}
// GetTransaction returns a transaction from the wallet given its hash.
func (w *WalletKit) GetTransaction(_ context.Context,
req *GetTransactionRequest) (*lnrpc.Transaction, error) {
// If the client doesn't specify a hash, then there's nothing to
// return.
if req.Txid == "" {
return nil, fmt.Errorf("must provide a transaction hash")
}
txHash, err := chainhash.NewHashFromStr(req.Txid)
if err != nil {
return nil, err
}
res, err := w.cfg.Wallet.GetTransactionDetails(txHash)
if err != nil {
return nil, err
}
return lnrpc.RPCTransaction(res), nil
}
// Attempts to publish the passed transaction to the network. Once this returns
// without an error, the wallet will continually attempt to re-broadcast the
// transaction on start up, until it enters the chain.

View File

@ -233,6 +233,13 @@ func (w *WalletController) PublishTransaction(tx *wire.MsgTx, _ string) error {
return nil
}
// GetTransactionDetails currently does nothing.
func (w *WalletController) GetTransactionDetails(
txHash *chainhash.Hash) (*lnwallet.TransactionDetail, error) {
return nil, nil
}
// LabelTransaction currently does nothing.
func (w *WalletController) LabelTransaction(chainhash.Hash, string,
bool) error {

View File

@ -3,6 +3,7 @@ package rpc
import (
"context"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/stretchr/testify/require"
@ -196,6 +197,19 @@ func (h *HarnessRPC) PublishTransaction(
return resp
}
// GetTransaction makes a RPC call to the node's WalletKitClient and asserts.
func (h *HarnessRPC) GetTransaction(
req *walletrpc.GetTransactionRequest) *lnrpc.Transaction {
ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
defer cancel()
resp, err := h.WalletKit.GetTransaction(ctxt, req)
h.NoError(err, "GetTransaction")
return resp
}
// BumpFee makes a RPC call to the node's WalletKitClient and asserts.
func (h *HarnessRPC) BumpFee(
req *walletrpc.BumpFeeRequest) *walletrpc.BumpFeeResponse {

View File

@ -1285,6 +1285,45 @@ func getPreviousOutpoints(wireTx *wire.MsgTx,
return previousOutpoints
}
// GetTransactionDetails returns details of a transaction given its
// transaction hash.
func (b *BtcWallet) GetTransactionDetails(
txHash *chainhash.Hash) (*lnwallet.TransactionDetail, error) {
// Grab the best block the wallet knows of, we'll use this to calculate
// # of confirmations shortly below.
bestBlock := b.wallet.Manager.SyncedTo()
currentHeight := bestBlock.Height
tx, err := b.wallet.GetTransaction(*txHash)
if err != nil {
return nil, err
}
// For both confirmed and unconfirmed transactions, create a
// TransactionDetail which re-packages the data returned by the base
// wallet.
if tx.Confirmations > 0 {
txDetails, err := minedTransactionsToDetails(
currentHeight,
base.Block{
Transactions: []base.TransactionSummary{
tx.Summary,
},
Hash: tx.BlockHash,
Height: tx.Height,
Timestamp: tx.Summary.Timestamp},
b.netParams,
)
if err != nil {
return nil, err
}
return txDetails[0], nil
}
return unminedTransactionsToDetail(tx.Summary, b.netParams)
}
// minedTransactionsToDetails is a helper function which converts a summary
// information about mined transactions to a TransactionDetail.
func minedTransactionsToDetails(

View File

@ -356,6 +356,11 @@ type WalletController interface {
CreateSimpleTx(outputs []*wire.TxOut, feeRate chainfee.SatPerKWeight,
minConfs int32, dryRun bool) (*txauthor.AuthoredTx, error)
// GetTransactionDetails returns a detailed description of a transaction
// given its transaction hash.
GetTransactionDetails(txHash *chainhash.Hash) (
*TransactionDetail, error)
// ListUnspentWitness returns all unspent outputs which are version 0
// witness programs. The 'minConfs' and 'maxConfs' parameters
// indicate the minimum and maximum number of confirmations an output

View File

@ -243,6 +243,13 @@ func (w *mockWalletController) PublishTransaction(tx *wire.MsgTx,
return nil
}
// GetTransactionDetails currently does nothing.
func (w *mockWalletController) GetTransactionDetails(*chainhash.Hash) (
*TransactionDetail, error) {
return nil, nil
}
// LabelTransaction currently does nothing.
func (w *mockWalletController) LabelTransaction(chainhash.Hash, string,
bool) error {

View File

@ -1685,6 +1685,30 @@ func newTx(t *testing.T, r *rpctest.Harness, pubKey *btcec.PublicKey,
return tx1
}
// testGetTransactionDetails checks that GetTransactionDetails returns the
// correct amount after a transaction has been sent from alice to bob.
func testGetTransactionDetails(r *rpctest.Harness,
alice, bob *lnwallet.LightningWallet, t *testing.T) {
const txFee = int64(14500)
bobPkScript := newPkScript(t, bob, lnwallet.WitnessPubKey)
txFeeRate := chainfee.SatPerKWeight(2500)
amountSats := btcutil.Amount(btcutil.SatoshiPerBitcoin - txFee)
output := &wire.TxOut{
Value: int64(amountSats),
PkScript: bobPkScript,
}
tx := sendCoins(t, r, alice, bob, output, txFeeRate, true, 1)
txHash := tx.TxHash()
assertTxInWallet(t, alice, txHash, true)
assertTxInWallet(t, bob, txHash, true)
txDetails, err := bob.GetTransactionDetails(&txHash)
require.NoError(t, err, "unable to receive transaction details")
require.Equal(t, txDetails.Value, amountSats, "tx value")
}
// testPublishTransaction checks that PublishTransaction returns the expected
// error types in case the transaction being published conflicts with the
// current mempool or chain.
@ -2794,6 +2818,10 @@ var walletTests = []walletTestCase{
name: "transaction details",
test: testListTransactionDetails,
},
{
name: "get transaction details",
test: testGetTransactionDetails,
},
{
name: "publish transaction",
test: testPublishTransaction,