diff --git a/README.md b/README.md index 0575840..23bc471 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![build](https://github.com/hyperledger/firefly-tezosconnect/actions/workflows/go.yml/badge.svg?branch=main) +![build](https://github.com/hyperledger/firefly-tezosconnect/actions/workflows/build.yml/badge.svg?branch=main) [![codecov](https://codecov.io/gh/hyperledger/firefly-tezosconnect/branch/main/graph/badge.svg)](https://codecov.io/gh/hyperledger/firefly-tezosconnect) [![Go Reference](https://pkg.go.dev/badge/github.com/hyperledger/firefly-tezosconnect.svg)](https://pkg.go.dev/github.com/hyperledger/firefly-tezosconnect) diff --git a/internal/tezos/estimate_gas.go b/internal/tezos/estimate_gas.go index 130471d..54da800 100644 --- a/internal/tezos/estimate_gas.go +++ b/internal/tezos/estimate_gas.go @@ -2,12 +2,10 @@ package tezos import ( "context" - "errors" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) func (c *tezosConnector) GasEstimate(_ context.Context, _ *ffcapi.TransactionInput) (*ffcapi.GasEstimateResponse, ffcapi.ErrorReason, error) { - // TODO: implement - return nil, ffcapi.ErrorReason("not implemented"), errors.New("not implemented") + return nil, "", nil } diff --git a/internal/tezos/estimate_gas_test.go b/internal/tezos/estimate_gas_test.go new file mode 100644 index 0000000..c61eea6 --- /dev/null +++ b/internal/tezos/estimate_gas_test.go @@ -0,0 +1,18 @@ +package tezos + +import ( + "testing" + + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" +) + +func TestGasEstimate(t *testing.T) { + ctx, c, _, done := newTestConnector(t) + defer done() + + resp, reason, err := c.GasEstimate(ctx, &ffcapi.TransactionInput{}) + assert.Nil(t, resp) + assert.Empty(t, reason) + assert.NoError(t, err) +} diff --git a/internal/tezos/get_receipt.go b/internal/tezos/get_receipt.go index b3558a6..efccaca 100644 --- a/internal/tezos/get_receipt.go +++ b/internal/tezos/get_receipt.go @@ -32,12 +32,11 @@ type receiptExtraInfo struct { func (c *tezosConnector) TransactionReceipt(ctx context.Context, req *ffcapi.TransactionReceiptRequest) (*ffcapi.TransactionReceiptResponse, ffcapi.ErrorReason, error) { // ensure block observer is running rpcClient := c.client.(*rpc.Client) - mon := rpcClient.BlockObserver - mon.Listen(rpcClient) + rpcClient.Listen() // wait for confirmations res := rpc.NewResult(tezos.MustParseOpHash(req.TransactionHash)) // .WithTTL(op.TTL).WithConfirmations(opts.Confirmations) - res.Listen(mon) + res.Listen(rpcClient.BlockObserver) res.WaitContext(ctx) if err := res.Err(); err != nil { return nil, "", err diff --git a/internal/tezos/prepare_transaction.go b/internal/tezos/prepare_transaction.go index 131ef44..2566c40 100644 --- a/internal/tezos/prepare_transaction.go +++ b/internal/tezos/prepare_transaction.go @@ -137,7 +137,7 @@ func (c *tezosConnector) completeOp(ctx context.Context, op *codec.Op, fromStrin mayNeedReveal := len(op.Contents) > 0 && op.Contents[0].Kind() != tezos.OpTypeReveal // add reveal if necessary if mayNeedReveal && !state.IsRevealed() { - key, err := c.getPubKeyFromSignatory(op.Source.String()) + key, err := c.getPubKeyFromSignatory(ctx, op.Source.String()) if err != nil { return err } @@ -178,9 +178,9 @@ func getNetworkParamsByName(name string) *tezos.Params { } } -func (c *tezosConnector) getPubKeyFromSignatory(tezosAddress string) (*tezos.Key, error) { +func (c *tezosConnector) getPubKeyFromSignatory(ctx context.Context, tezosAddress string) (*tezos.Key, error) { url := c.signatoryURL + "/keys/" + tezosAddress - req, err := http.NewRequest("GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err } @@ -195,10 +195,7 @@ func (c *tezosConnector) getPubKeyFromSignatory(tezosAddress string) (*tezos.Key } defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } + body, _ := io.ReadAll(resp.Body) var pubKeyJSON struct { PubKey string `json:"public_key"` diff --git a/internal/tezos/prepare_transaction_test.go b/internal/tezos/prepare_transaction_test.go index 8cda76f..8954396 100644 --- a/internal/tezos/prepare_transaction_test.go +++ b/internal/tezos/prepare_transaction_test.go @@ -3,6 +3,8 @@ package tezos import ( "encoding/json" "errors" + "net/http" + "net/http/httptest" "testing" "blockwatch.cc/tzgo/codec" @@ -15,7 +17,7 @@ import ( "github.com/stretchr/testify/mock" ) -func TestTransactionPrepareOk(t *testing.T) { +func TestTransactionPrepareSuccess(t *testing.T) { ctx, c, mRPC, done := newTestConnector(t) defer done() @@ -66,6 +68,75 @@ func TestTransactionPrepareOk(t *testing.T) { assert.NotEmpty(t, res.TransactionData) } +func TestTransactionPrepareWithRevealSuccess(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("GetBlockHash", ctx, mock.Anything). + Return(tezos.NewBlockHash([]byte("BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg")), nil) + + mRPC.On("GetContractExt", ctx, mock.Anything, mock.Anything). + Return(&rpc.ContractInfo{ + Counter: 10, + Manager: "89Jj4aVWetK69CWm5ss1LayvK8dQoiFz7p995y1k3E8CZwqJ6", + }, nil) + + // Set up http mocks + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("{\"public_key\":\"edpkvHVuLHkr5eDiTtQKyUPqgYVAk3Sy4m7qBD8r6abemHkZsMU5Kh\"}")) + })) + defer svr.Close() + c.signatoryURL = svr.URL + + mRPC.On("Simulate", ctx, mock.Anything, mock.Anything). + Return(&rpc.Receipt{ + Op: &rpc.Operation{ + Contents: []rpc.TypedOperation{ + rpc.Reveal{ + Manager: rpc.Manager{ + Generic: rpc.Generic{ + Metadata: rpc.OperationMetadata{ + Result: rpc.OperationResult{ + Status: tezos.OpStatusApplied, + }, + }, + }, + }, + }, + rpc.Transaction{ + Manager: rpc.Manager{ + Generic: rpc.Generic{ + Metadata: rpc.OperationMetadata{ + Result: rpc.OperationResult{ + Status: tezos.OpStatusApplied, + }, + }, + }, + }, + }, + }, + }, + }, nil) + + req := &ffcapi.TransactionPrepareRequest{ + TransactionInput: ffcapi.TransactionInput{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN", + To: "KT1D254HTPKq5GZNVcF73XBinG9BLybHqu8s", + }, + Method: fftypes.JSONAnyPtr("\"pause\""), + Params: []*fftypes.JSONAny{ + fftypes.JSONAnyPtr("{\"entrypoint\":\"pause\",\"value\":{\"prim\":\"True\"}}"), + }, + }, + } + res, reason, err := c.TransactionPrepare(ctx, req) + assert.NoError(t, err) + assert.Empty(t, reason) + assert.NotNil(t, res) + assert.NotEmpty(t, res.TransactionData) +} + func TestTransactionPrepareWrongParamsError(t *testing.T) { ctx, c, _, done := newTestConnector(t) defer done() @@ -318,3 +389,122 @@ func Test_estimateAndAssignExceedMaxLimitError(t *testing.T) { _, err := c.estimateAndAssignTxCost(ctx, op, opts) assert.Error(t, err) } + +func TestTransactionPrepareWithRevealEmptyServerError(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("GetBlockHash", ctx, mock.Anything). + Return(tezos.NewBlockHash([]byte("BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg")), nil) + + mRPC.On("GetContractExt", ctx, mock.Anything, mock.Anything). + Return(&rpc.ContractInfo{ + Counter: 10, + Manager: "89Jj4aVWetK69CWm5ss1LayvK8dQoiFz7p995y1k3E8CZwqJ6", + }, nil) + + req := &ffcapi.TransactionPrepareRequest{ + TransactionInput: ffcapi.TransactionInput{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN", + To: "KT1D254HTPKq5GZNVcF73XBinG9BLybHqu8s", + }, + Method: fftypes.JSONAnyPtr("\"pause\""), + Params: []*fftypes.JSONAny{ + fftypes.JSONAnyPtr("{\"entrypoint\":\"pause\",\"value\":{\"prim\":\"True\"}}"), + }, + }, + } + resp, _, err := c.TransactionPrepare(ctx, req) + assert.Error(t, err) + assert.Nil(t, resp) +} + +func Test_getNetworkParamsByName(t *testing.T) { + params := getNetworkParamsByName("ghostnet") + assert.Equal(t, params, tezos.GhostnetParams) + + params = getNetworkParamsByName("nairobinet") + assert.Equal(t, params, tezos.NairobinetParams) + + params = getNetworkParamsByName("default") + assert.Equal(t, params, tezos.DefaultParams) +} + +func Test_getPubKeyFromSignatoryInvalidRespKeySuccess(t *testing.T) { + ctx, c, _, done := newTestConnector(t) + defer done() + + // Set up http mocks + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("{\"public_key\":\"edpkvHVuLHkr5eDiTtQKyUPqgYVAk3Sy4m7qBD8r6abemHkZsMU5Kh\"}")) + })) + defer svr.Close() + c.signatoryURL = svr.URL + + resp, err := c.getPubKeyFromSignatory(ctx, "tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN") + assert.NotNil(t, resp) + assert.NoError(t, err) +} + +func Test_getPubKeyFromSignatoryyNilContextError(t *testing.T) { + _, c, _, done := newTestConnector(t) + defer done() + + _, err := c.getPubKeyFromSignatory(nil, "tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN") + assert.Error(t, err) +} + +func Test_getPubKeyFromSignatoryHttpError(t *testing.T) { + ctx, c, _, done := newTestConnector(t) + defer done() + + _, err := c.getPubKeyFromSignatory(ctx, "tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN") + assert.Error(t, err) +} + +func Test_getPubKeyFromSignatoryHttpWrongStatusError(t *testing.T) { + ctx, c, _, done := newTestConnector(t) + defer done() + + // Set up http mocks + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + w.Write([]byte("internal error")) + })) + defer svr.Close() + c.signatoryURL = svr.URL + + _, err := c.getPubKeyFromSignatory(ctx, "tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN") + assert.Error(t, err) +} + +func Test_getPubKeyFromSignatoryUnmarshalRespError(t *testing.T) { + ctx, c, _, done := newTestConnector(t) + defer done() + + // Set up http mocks + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(nil) + })) + defer svr.Close() + c.signatoryURL = svr.URL + + _, err := c.getPubKeyFromSignatory(ctx, "tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN") + assert.Error(t, err) +} + +func Test_getPubKeyFromSignatoryInvalidRespKeyError(t *testing.T) { + ctx, c, _, done := newTestConnector(t) + defer done() + + // Set up http mocks + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("{\"public_key\":\"invalid\"}")) + })) + defer svr.Close() + c.signatoryURL = svr.URL + + _, err := c.getPubKeyFromSignatory(ctx, "tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN") + assert.Error(t, err) +} diff --git a/internal/tezos/send_transaction.go b/internal/tezos/send_transaction.go index e8dac73..c3cd2ae 100644 --- a/internal/tezos/send_transaction.go +++ b/internal/tezos/send_transaction.go @@ -5,6 +5,7 @@ import ( "context" "encoding/hex" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -49,11 +50,14 @@ func (c *tezosConnector) TransactionSend(ctx context.Context, req *ffcapi.Transa }, "", nil } -func (c *tezosConnector) signTxRemotely(_ context.Context, op *codec.Op) error { +func (c *tezosConnector) signTxRemotely(ctx context.Context, op *codec.Op) error { + if op == nil { + return errors.New("operation is empty") + } url := c.signatoryURL + "/keys/" + op.Source.String() requestBody, _ := json.Marshal(hex.EncodeToString(op.WatermarkedBytes())) - req, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBody)) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(requestBody)) if err != nil { return err } @@ -68,10 +72,7 @@ func (c *tezosConnector) signTxRemotely(_ context.Context, op *codec.Op) error { } defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - return err - } + body, _ := io.ReadAll(resp.Body) var signatureJSON struct { Signature string diff --git a/internal/tezos/send_transaction_test.go b/internal/tezos/send_transaction_test.go index dbeed43..1076d36 100644 --- a/internal/tezos/send_transaction_test.go +++ b/internal/tezos/send_transaction_test.go @@ -2,14 +2,54 @@ package tezos import ( "errors" + "net/http" + "net/http/httptest" "testing" + "blockwatch.cc/tzgo/codec" + "blockwatch.cc/tzgo/rpc" "blockwatch.cc/tzgo/tezos" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) +func TestTransactionSendSuccess(t *testing.T) { + // Set up tezos connector mocks + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + // Set up http mocks + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("{\"signature\":\"sigWetzF5zVM2qdYt8QToj7e5cNBm9neiPRc3rpePBDrr8N1brFbErv2YfXMSoSgemJ8AwZcLfmkBDg78bmUEzF1sf1YotnS\"}")) + })) + defer svr.Close() + c.signatoryURL = svr.URL + + mRPC.On("GetBlockHash", ctx, mock.Anything). + Return(tezos.NewBlockHash([]byte("BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg")), nil) + + mRPC.On("GetContractExt", ctx, mock.Anything, mock.Anything). + Return(&rpc.ContractInfo{ + Counter: 10, + Manager: "edpkv89Jj4aVWetK69CWm5ss1LayvK8dQoiFz7p995y1k3E8CZwqJ6", + }, nil) + + mRPC.On("Broadcast", ctx, mock.Anything). + Return(tezos.OpHash([]byte("oovD5cUigLGLT6kGDqsLMyF2sc3MLyfYhJWRymCPxUKEx3vtQ5v")), nil) + + req := &ffcapi.TransactionSendRequest{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN", + To: "KT1D254HTPKq5GZNVcF73XBinG9BLybHqu8s", + }, + TransactionData: "424d426559724d4a704c577271437337555463466155514365574271736a434c6c00889816a17ae688c971be1ad34bfe1990f8fa5e0f000b0000000130a980e6e41028da2cacfca4ddefea252d18bed900ffff05706175736500000002030a", + } + resp, _, err := c.TransactionSend(ctx, req) + assert.Nil(t, err) + assert.NotNil(t, resp) +} + func TestTransactionSendDecodeStrError(t *testing.T) { ctx, c, _, done := newTestConnector(t) defer done() @@ -70,3 +110,156 @@ func TestTransactionSendGetContractExtError(t *testing.T) { _, _, err := c.TransactionSend(ctx, req) assert.Error(t, err) } + +func TestTransactionSendBroadcastError(t *testing.T) { + // Set up tezos connector mocks + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + // Set up http mocks + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("{\"signature\":\"sigWetzF5zVM2qdYt8QToj7e5cNBm9neiPRc3rpePBDrr8N1brFbErv2YfXMSoSgemJ8AwZcLfmkBDg78bmUEzF1sf1YotnS\"}")) + })) + defer svr.Close() + c.signatoryURL = svr.URL + + mRPC.On("GetBlockHash", ctx, mock.Anything). + Return(tezos.NewBlockHash([]byte("BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg")), nil) + + mRPC.On("GetContractExt", ctx, mock.Anything, mock.Anything). + Return(&rpc.ContractInfo{ + Counter: 10, + Manager: "edpkv89Jj4aVWetK69CWm5ss1LayvK8dQoiFz7p995y1k3E8CZwqJ6", + }, nil) + + mRPC.On("Broadcast", ctx, mock.Anything). + Return(nil, errors.New("error")) + + req := &ffcapi.TransactionSendRequest{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN", + To: "KT1D254HTPKq5GZNVcF73XBinG9BLybHqu8s", + }, + TransactionData: "424d426559724d4a704c577271437337555463466155514365574271736a434c6c00889816a17ae688c971be1ad34bfe1990f8fa5e0f000b0000000130a980e6e41028da2cacfca4ddefea252d18bed900ffff05706175736500000002030a", + } + resp, _, err := c.TransactionSend(ctx, req) + assert.Nil(t, resp) + assert.Error(t, err) +} + +func TestTransactionSendSignTxError(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("GetBlockHash", ctx, mock.Anything). + Return(tezos.NewBlockHash([]byte("BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg")), nil) + + mRPC.On("GetContractExt", ctx, mock.Anything, mock.Anything). + Return(&rpc.ContractInfo{ + Counter: 10, + Manager: "edpkv89Jj4aVWetK69CWm5ss1LayvK8dQoiFz7p995y1k3E8CZwqJ6", + }, nil) + + // Set up http mocks + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + w.Write([]byte("internal error")) + })) + defer svr.Close() + c.signatoryURL = svr.URL + + req := &ffcapi.TransactionSendRequest{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN", + To: "KT1D254HTPKq5GZNVcF73XBinG9BLybHqu8s", + }, + TransactionData: "424d426559724d4a704c577271437337555463466155514365574271736a434c6c00889816a17ae688c971be1ad34bfe1990f8fa5e0f000b0000000130a980e6e41028da2cacfca4ddefea252d18bed900ffff05706175736500000002030a", + } + _, _, err := c.TransactionSend(ctx, req) + assert.Error(t, err) +} + +func Test_signTxRemotelyNilOperationError(t *testing.T) { + ctx, c, _, done := newTestConnector(t) + defer done() + + err := c.signTxRemotely(ctx, nil) + assert.Error(t, err) +} + +func Test_signTxRemotelyNilContextError(t *testing.T) { + _, c, _, done := newTestConnector(t) + defer done() + + op := codec.NewOp() + op.WithSource(tezos.MustParseAddress("tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN")) + + err := c.signTxRemotely(nil, op) + assert.Error(t, err) +} + +func Test_signTxRemotelyHttpError(t *testing.T) { + ctx, c, _, done := newTestConnector(t) + defer done() + + op := codec.NewOp() + op.WithSource(tezos.MustParseAddress("tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN")) + + err := c.signTxRemotely(ctx, op) + assert.Error(t, err) +} + +func Test_signTxRemotelyHttpWrongStatusError(t *testing.T) { + ctx, c, _, done := newTestConnector(t) + defer done() + + op := codec.NewOp() + op.WithSource(tezos.MustParseAddress("tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN")) + + // Set up http mocks + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + w.Write([]byte("internal error")) + })) + defer svr.Close() + c.signatoryURL = svr.URL + + err := c.signTxRemotely(ctx, op) + assert.Error(t, err) +} + +func Test_signTxRemotelyUnmarshalRespError(t *testing.T) { + ctx, c, _, done := newTestConnector(t) + defer done() + + op := codec.NewOp() + op.WithSource(tezos.MustParseAddress("tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN")) + + // Set up http mocks + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(nil) + })) + defer svr.Close() + c.signatoryURL = svr.URL + + err := c.signTxRemotely(ctx, op) + assert.Error(t, err) +} + +func Test_signTxRemotelyUnmarshalSignatureError(t *testing.T) { + ctx, c, _, done := newTestConnector(t) + defer done() + + op := codec.NewOp() + op.WithSource(tezos.MustParseAddress("tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN")) + + // Set up http mocks + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("{\"wrong\":\"sigWetzF5zVM2qdYt8QToj7e5cNBm9neiPRc3rpePBDrr8N1brFbErv2YfXMSoSgemJ8AwZcLfmkBDg78bmUEzF1sf1YotnS\"}")) + })) + defer svr.Close() + c.signatoryURL = svr.URL + + err := c.signTxRemotely(ctx, op) + assert.Error(t, err) +}