diff --git a/internal/tezos/deploy_contract_prepare.go b/internal/tezos/deploy_contract_prepare.go index d29e28f..216f667 100644 --- a/internal/tezos/deploy_contract_prepare.go +++ b/internal/tezos/deploy_contract_prepare.go @@ -2,11 +2,54 @@ package tezos import ( "context" - "errors" + "encoding/hex" + "encoding/json" + "blockwatch.cc/tzgo/codec" + "blockwatch.cc/tzgo/micheline" + "blockwatch.cc/tzgo/rpc" + "blockwatch.cc/tzgo/tezos" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-tezosconnect/internal/msgs" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) -func (c *tezosConnector) DeployContractPrepare(_ context.Context, _ *ffcapi.ContractDeployPrepareRequest) (*ffcapi.TransactionPrepareResponse, ffcapi.ErrorReason, error) { - return nil, "", errors.New("contract deployment is not supported") +func (c *tezosConnector) DeployContractPrepare(ctx context.Context, req *ffcapi.ContractDeployPrepareRequest) (*ffcapi.TransactionPrepareResponse, ffcapi.ErrorReason, error) { + if req.Contract == nil { + return nil, ffcapi.ErrorReasonInvalidInputs, i18n.NewError(ctx, "Missing contract", req.Contract) + } + + var sc micheline.Script + _ = json.Unmarshal([]byte(req.Contract.String()), &sc) + orig := &codec.Origination{ + Script: sc, + } + + addr, err := tezos.ParseAddress(req.From) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, i18n.NewError(ctx, msgs.MsgInvalidFromAddress, req.From, err) + } + + headBlockHash, _ := c.client.GetBlockHash(ctx, rpc.Head) + op := codec.NewOp(). + WithContents(orig). + WithSource(addr). + WithBranch(headBlockHash) + + err = c.completeOp(ctx, op, req.From, req.Nonce) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + opts := &rpc.DefaultOptions + if reason, err := c.estimateAndAssignTxCost(ctx, op, opts); err != nil { + return nil, reason, err + } + + log.L(ctx).Infof("Prepared deploy transaction dataLen=%d", len(op.Bytes())) + + return &ffcapi.TransactionPrepareResponse{ + Gas: req.Gas, + TransactionData: hex.EncodeToString(op.Bytes()), + }, "", nil } diff --git a/internal/tezos/deploy_contract_prepare_test.go b/internal/tezos/deploy_contract_prepare_test.go index ccf3bad..a238860 100644 --- a/internal/tezos/deploy_contract_prepare_test.go +++ b/internal/tezos/deploy_contract_prepare_test.go @@ -1,18 +1,135 @@ package tezos import ( - "context" + "blockwatch.cc/tzgo/rpc" + "blockwatch.cc/tzgo/tezos" + "github.com/stretchr/testify/mock" "testing" + "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" ) func TestDeployContractPrepare(t *testing.T) { - _, c, _, done := newTestConnector(t) + ctx, c, mRPC, done := newTestConnector(t) defer done() + mRPC.On("GetBlockHash", ctx, mock.Anything, mock.Anything). + Return(tezos.BlockHash{}, nil) + mRPC.On("GetContractExt", ctx, mock.Anything, mock.Anything). + Return(&rpc.ContractInfo{ + Counter: 10, + Manager: "edpkv89Jj4aVWetK69CWm5ss1LayvK8dQoiFz7p995y1k3E8CZwqJ6", + }, nil) + mRPC.On("Simulate", ctx, mock.Anything, mock.Anything). + Return(&rpc.Receipt{ + Op: &rpc.Operation{ + Contents: []rpc.TypedOperation{ + rpc.Transaction{ + Manager: rpc.Manager{ + Generic: rpc.Generic{ + Metadata: rpc.OperationMetadata{ + Result: rpc.OperationResult{ + Status: tezos.OpStatusApplied, + }, + }, + }, + }, + }, + }, + }, + }, nil) - _, _, err := c.DeployContractPrepare(context.Background(), &ffcapi.ContractDeployPrepareRequest{}) + resp, reason, err := c.DeployContractPrepare(ctx, &ffcapi.ContractDeployPrepareRequest{ + Contract: fftypes.JSONAnyPtr("{\"code\":[{\"args\":[{\"prim\":\"string\"}],\"prim\":\"parameter\"},{\"args\":[{\"prim\":\"string\"}],\"prim\":\"storage\"},{\"args\":[[{\"prim\":\"CAR\"},{\"args\":[{\"prim\":\"operation\"}],\"prim\":\"NIL\"},{\"prim\":\"PAIR\"}]],\"prim\":\"code\"}],\"storage\":{\"string\":\"hello\"}}"), + }) + + assert.NotNil(t, resp) + assert.Equal(t, reason, ffcapi.ErrorReason("")) + assert.NoError(t, err) +} + +func TestDeployContractPrepareGetContractExtError(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + mRPC.On("GetBlockHash", ctx, mock.Anything, mock.Anything). + Return(tezos.BlockHash{}, nil) + mRPC.On("GetContractExt", ctx, mock.Anything, mock.Anything). + Return(&rpc.ContractInfo{ + Counter: 10, + Manager: "edpkv89Jj4aVWetK69CWm5ss1LayvK8dQoiFz7p995y1k3E8CZwqJ6", + }, assert.AnError) + + resp, reason, err := c.DeployContractPrepare(ctx, &ffcapi.ContractDeployPrepareRequest{ + Contract: fftypes.JSONAnyPtr("{\"code\":[{\"args\":[{\"prim\":\"string\"}],\"prim\":\"parameter\"},{\"args\":[{\"prim\":\"string\"}],\"prim\":\"storage\"},{\"args\":[[{\"prim\":\"CAR\"},{\"args\":[{\"prim\":\"operation\"}],\"prim\":\"NIL\"},{\"prim\":\"PAIR\"}]],\"prim\":\"code\"}],\"storage\":{\"string\":\"hello\"}}"), + }) + + assert.Nil(t, resp) + assert.Error(t, err) + assert.Equal(t, reason, ffcapi.ErrorReasonInvalidInputs) +} + +func TestDeployContractPrepareSimulateError(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + mRPC.On("GetBlockHash", ctx, mock.Anything, mock.Anything). + Return(tezos.BlockHash{}, nil) + mRPC.On("GetContractExt", ctx, mock.Anything, mock.Anything). + Return(&rpc.ContractInfo{ + Counter: 10, + Manager: "edpkv89Jj4aVWetK69CWm5ss1LayvK8dQoiFz7p995y1k3E8CZwqJ6", + }, nil) + mRPC.On("Simulate", ctx, mock.Anything, mock.Anything). + Return(&rpc.Receipt{ + Op: &rpc.Operation{ + Contents: []rpc.TypedOperation{ + rpc.Transaction{ + Manager: rpc.Manager{ + Generic: rpc.Generic{ + Metadata: rpc.OperationMetadata{ + Result: rpc.OperationResult{ + Status: tezos.OpStatusApplied, + }, + }, + }, + }, + }, + }, + }, + }, assert.AnError) + + resp, reason, err := c.DeployContractPrepare(ctx, &ffcapi.ContractDeployPrepareRequest{ + Contract: fftypes.JSONAnyPtr("{\"code\":[{\"args\":[{\"prim\":\"string\"}],\"prim\":\"parameter\"},{\"args\":[{\"prim\":\"string\"}],\"prim\":\"storage\"},{\"args\":[[{\"prim\":\"CAR\"},{\"args\":[{\"prim\":\"operation\"}],\"prim\":\"NIL\"},{\"prim\":\"PAIR\"}]],\"prim\":\"code\"}],\"storage\":{\"string\":\"hello\"}}"), + }) + + assert.Nil(t, resp) + assert.Error(t, err) + assert.Equal(t, reason, ffcapi.ErrorReason("")) +} + +func TestDeployContractPrepareMisingContractError(t *testing.T) { + ctx, c, _, done := newTestConnector(t) + defer done() + + resp, reason, err := c.DeployContractPrepare(ctx, &ffcapi.ContractDeployPrepareRequest{}) + + assert.Nil(t, resp) + assert.Error(t, err) + assert.Equal(t, reason, ffcapi.ErrorReasonInvalidInputs) +} + +func TestDeployContractPrepareParseAddressError(t *testing.T) { + ctx, c, _, done := newTestConnector(t) + defer done() + + resp, reason, err := c.DeployContractPrepare(ctx, &ffcapi.ContractDeployPrepareRequest{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "wrong", + }, + Contract: fftypes.JSONAnyPtr("{\"code\":[{\"args\":[{\"prim\":\"string\"}],\"prim\":\"parameter\"},{\"args\":[{\"prim\":\"string\"}],\"prim\":\"storage\"},{\"args\":[[{\"prim\":\"CAR\"},{\"args\":[{\"prim\":\"operation\"}],\"prim\":\"NIL\"},{\"prim\":\"PAIR\"}]],\"prim\":\"code\"}],\"storage\":{\"string\":\"hello\"}}"), + }) + + assert.Nil(t, resp) assert.Error(t, err) - assert.Equal(t, err.Error(), "contract deployment is not supported") + assert.Equal(t, reason, ffcapi.ErrorReasonInvalidInputs) } diff --git a/internal/tezos/get_receipt.go b/internal/tezos/get_receipt.go index efccaca..267e272 100644 --- a/internal/tezos/get_receipt.go +++ b/internal/tezos/get_receipt.go @@ -12,6 +12,8 @@ import ( "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) +const _address = "address" + type receiptExtraInfo struct { ContractAddress *tezos.Address `json:"contractAddress"` ConsumedGas *fftypes.FFBigInt `json:"consumedGas"` @@ -91,7 +93,7 @@ func (c *tezosConnector) TransactionReceipt(ctx context.Context, req *ffcapi.Tra var script *micheline.Script if tx.Destination.IsContract() { location, _ := json.Marshal(map[string]string{ - "address": tx.Destination.String(), + _address: tx.Destination.String(), }) receiptResponse.ContractLocation = fftypes.JSONAnyPtrBytes(location) extraInfo.ContractAddress = &tx.Destination @@ -120,6 +122,16 @@ func (c *tezosConnector) TransactionReceipt(ctx context.Context, req *ffcapi.Tra operationReceipts = append(operationReceipts, extraInfo) fullReceipt, _ = json.Marshal(operationReceipts) + } else if o.Kind() == tezos.OpTypeOrigination { + result := o.(*rpc.Origination).Result() + originatedContracts := result.OriginatedContracts + if len(originatedContracts) > 0 { + location, _ := json.Marshal(map[string]string{ + _address: originatedContracts[0].ContractAddress(), + }) + receiptResponse.ContractLocation = fftypes.JSONAnyPtrBytes(location) + } + fullReceipt = c.extraInfoForDeployTransactionReceipt(ctx, result, operationReceipts) } } @@ -128,3 +140,40 @@ func (c *tezosConnector) TransactionReceipt(ctx context.Context, req *ffcapi.Tra return receiptResponse, "", nil } + +func (c *tezosConnector) extraInfoForDeployTransactionReceipt(ctx context.Context, res rpc.OperationResult, operationReceipts []receiptExtraInfo) []byte { + status := res.Status.String() + extraInfo := receiptExtraInfo{ + ConsumedGas: fftypes.NewFFBigInt(res.ConsumedMilliGas / 1000), + StorageSize: fftypes.NewFFBigInt(res.StorageSize), + PaidStorageSizeDiff: fftypes.NewFFBigInt(res.PaidStorageSizeDiff), + Status: &status, + } + + if len(res.Errors) > 0 { + errorMessage := "" + for _, err := range res.Errors { + errorMessage += err.Error() + } + extraInfo.ErrorMessage = &errorMessage + } + + if prim := res.Storage; prim != nil { + val := micheline.NewValue(res.Storage.BuildType(), *prim) + m, err := val.Map() + if err != nil { + log.L(ctx).Error("error parsing contract storage: ", err) + } + storageBytes, _ := json.Marshal(m) + extraInfo.Storage = fftypes.JSONAnyPtrBytes(storageBytes) + } + + if len(res.OriginatedContracts) > 0 { + extraInfo.ContractAddress = &res.OriginatedContracts[0] + } + + operationReceipts = append(operationReceipts, extraInfo) + fullReceipt, _ := json.Marshal(operationReceipts) + + return fullReceipt +}