From e09cce1e0d41cad173699e48ade2e062e101553e Mon Sep 17 00:00:00 2001 From: Cosmin Damian <17934949+cdamian@users.noreply.github.com> Date: Tue, 14 Feb 2023 17:51:39 +0200 Subject: [PATCH] nft: Batch NFT mint extrinsics (#1576) --- centchain/api.go | 2 + centchain/test_client.go | 2 - documents/anchor_job.go | 6 +- identity/v2/test_utils.go | 28 +-- nft/v3/bootstrapper.go | 11 +- nft/v3/jobs.go | 203 +++++++++-------- pallets/bootstrapper.go | 6 + pallets/utility/api.go | 47 +++- pallets/utility/api_integration_test.go | 120 ++++++++++ pallets/utility/api_mock.go | 61 +++++ pallets/utility/api_test.go | 285 ++++++++++++++++++++++++ 11 files changed, 638 insertions(+), 133 deletions(-) create mode 100644 pallets/utility/api_integration_test.go create mode 100644 pallets/utility/api_mock.go create mode 100644 pallets/utility/api_test.go diff --git a/centchain/api.go b/centchain/api.go index e48752224..2f47387b7 100644 --- a/centchain/api.go +++ b/centchain/api.go @@ -43,6 +43,8 @@ func init() { var log = logging.Logger("centchain-client") +type CallProviderFn func(metadata *types.Metadata) (*types.Call, error) + // ExtrinsicInfo holds details of a successful extrinsic type ExtrinsicInfo struct { Hash types.Hash diff --git a/centchain/test_client.go b/centchain/test_client.go index 69eb268b9..93064eb94 100644 --- a/centchain/test_client.go +++ b/centchain/test_client.go @@ -13,8 +13,6 @@ import ( "github.com/centrifuge/go-substrate-rpc-client/v4/types" ) -type CallProviderFn func(metadata *types.Metadata) (*types.Call, error) - type TestClient struct { api *gsrpc.SubstrateAPI meta *types.Metadata diff --git a/documents/anchor_job.go b/documents/anchor_job.go index a6c00dbe4..77471d962 100644 --- a/documents/anchor_job.go +++ b/documents/anchor_job.go @@ -90,8 +90,10 @@ func (a *AnchorJob) loadTasks() { next: "pre_commit", }, "pre_commit": { - runnerFunc: func(args []interface{}, overrides map[string]interface{}) (interface{}, - error) { + runnerFunc: func( + args []interface{}, + overrides map[string]interface{}, + ) (interface{}, error) { preCommit := args[2].(bool) if !preCommit { return nil, nil diff --git a/identity/v2/test_utils.go b/identity/v2/test_utils.go index 61575f0a9..e1e6a92d4 100644 --- a/identity/v2/test_utils.go +++ b/identity/v2/test_utils.go @@ -10,6 +10,8 @@ import ( "math/big" "time" + "github.com/centrifuge/go-centrifuge/pallets/utility" + keystoreTypes "github.com/centrifuge/chain-custom-types/pkg/keystore" proxyType "github.com/centrifuge/chain-custom-types/pkg/proxy" "github.com/centrifuge/go-centrifuge/centchain" @@ -215,7 +217,7 @@ func ExecutePostAccountBootstrap( defer testClient.Close() - if _, err = testClient.SubmitAndWait(ctx, originKrp, batchCalls(callCreationFns...)); err != nil { + if _, err = testClient.SubmitAndWait(ctx, originKrp, utility.BatchCalls(callCreationFns...)); err != nil { return fmt.Errorf("couldn't submit post account bootstrap batch call: %w", err) } @@ -466,27 +468,3 @@ func GetAddKeysCall( return &proxyCall, nil }, nil } - -func batchCalls(callCreationFns ...centchain.CallProviderFn) centchain.CallProviderFn { - return func(meta *types.Metadata) (*types.Call, error) { - var calls []*types.Call - - for _, callCreationFn := range callCreationFns { - call, err := callCreationFn(meta) - - if err != nil { - return nil, fmt.Errorf("couldn't create call: %w", err) - } - - calls = append(calls, call) - } - - batchCall, err := types.NewCall(meta, "Utility.batch_all", calls) - - if err != nil { - return nil, err - } - - return &batchCall, nil - } -} diff --git a/nft/v3/bootstrapper.go b/nft/v3/bootstrapper.go index a1867f4c0..44bb1b7f6 100644 --- a/nft/v3/bootstrapper.go +++ b/nft/v3/bootstrapper.go @@ -8,6 +8,7 @@ import ( "github.com/centrifuge/go-centrifuge/jobs" "github.com/centrifuge/go-centrifuge/pallets" "github.com/centrifuge/go-centrifuge/pallets/uniques" + "github.com/centrifuge/go-centrifuge/pallets/utility" "github.com/centrifuge/go-centrifuge/pending" ) @@ -54,12 +55,18 @@ func (*Bootstrapper) Bootstrap(ctx map[string]interface{}) error { return errors.New("proxy API not initialised") } + utilityAPI, ok := ctx[pallets.BootstrappedUtilityAPI].(utility.API) + + if !ok { + return errors.New("utility API not initialised") + } + go dispatcher.RegisterRunner(commitAndMintNFTV3Job, &CommitAndMintNFTJobRunner{ accountsSrv: accountsSrv, pendingDocsSrv: pendingDocsSrv, docSrv: docSrv, dispatcher: dispatcher, - api: uniquesAPI, + utilityAPI: utilityAPI, ipfsPinningSrv: ipfsPinningSrv, }) @@ -67,7 +74,7 @@ func (*Bootstrapper) Bootstrap(ctx map[string]interface{}) error { accountsSrv: accountsSrv, docSrv: docSrv, dispatcher: dispatcher, - api: uniquesAPI, + utilityAPI: utilityAPI, ipfsPinningSrv: ipfsPinningSrv, }) diff --git a/nft/v3/jobs.go b/nft/v3/jobs.go index 22262ef94..d73351884 100644 --- a/nft/v3/jobs.go +++ b/nft/v3/jobs.go @@ -5,12 +5,15 @@ import ( "errors" "fmt" + "github.com/centrifuge/go-centrifuge/pallets/utility" + + "github.com/centrifuge/go-centrifuge/centchain" + "github.com/centrifuge/go-centrifuge/config" "github.com/centrifuge/go-centrifuge/contextutil" "github.com/centrifuge/go-centrifuge/documents" "github.com/centrifuge/go-centrifuge/ipfs" "github.com/centrifuge/go-centrifuge/jobs" - "github.com/centrifuge/go-centrifuge/pallets/anchors" "github.com/centrifuge/go-centrifuge/pallets/uniques" "github.com/centrifuge/go-centrifuge/pending" "github.com/centrifuge/go-substrate-rpc-client/v4/types" @@ -112,7 +115,7 @@ type CommitAndMintNFTJobRunner struct { pendingDocsSrv pending.Service docSrv documents.Service dispatcher jobs.Dispatcher - api uniques.API + utilityAPI utility.API ipfsPinningSrv ipfs.PinningServiceClient } @@ -123,7 +126,7 @@ func (c *CommitAndMintNFTJobRunner) New() gocelery.Runner { pendingDocsSrv: c.pendingDocsSrv, docSrv: c.docSrv, dispatcher: c.dispatcher, - api: c.api, + utilityAPI: c.utilityAPI, ipfsPinningSrv: c.ipfsPinningSrv, } @@ -131,7 +134,7 @@ func (c *CommitAndMintNFTJobRunner) New() gocelery.Runner { c.pendingDocsSrv, c.docSrv, c.dispatcher, - c.api, + c.utilityAPI, c.ipfsPinningSrv, ) @@ -155,7 +158,7 @@ func loadCommitAndMintTasks( pendingDocsSrv pending.Service, docSrv documents.Service, dispatcher jobs.Dispatcher, - api uniques.API, + utilityAPI utility.API, ipfsPinningSrv ipfs.PinningServiceClient, ) map[string]jobs.Task { commitTasks := map[string]jobs.Task{ @@ -217,7 +220,7 @@ func loadCommitAndMintTasks( }, } - return mergeTaskMaps(commitTasks, loadNFTMintTasks(docSrv, dispatcher, api, ipfsPinningSrv)) + return mergeTaskMaps(commitTasks, loadNFTMintTasks(docSrv, dispatcher, utilityAPI, ipfsPinningSrv)) } type MintNFTJobRunner struct { @@ -226,7 +229,7 @@ type MintNFTJobRunner struct { accountsSrv config.Service docSrv documents.Service dispatcher jobs.Dispatcher - api uniques.API + utilityAPI utility.API ipfsPinningSrv ipfs.PinningServiceClient } @@ -236,11 +239,11 @@ func (m *MintNFTJobRunner) New() gocelery.Runner { accountsSrv: m.accountsSrv, docSrv: m.docSrv, dispatcher: m.dispatcher, - api: m.api, + utilityAPI: m.utilityAPI, ipfsPinningSrv: m.ipfsPinningSrv, } - nftMintTasks := loadNFTMintTasks(m.docSrv, m.dispatcher, m.api, m.ipfsPinningSrv) + nftMintTasks := loadNFTMintTasks(m.docSrv, m.dispatcher, m.utilityAPI, m.ipfsPinningSrv) mj.Base = jobs.NewBase(nftMintTasks) return mj @@ -283,7 +286,7 @@ const ( func loadNFTMintTasks( docSrv documents.Service, dispatcher jobs.Dispatcher, - api uniques.API, + utilityAPI utility.API, ipfsPinningSrv ipfs.PinningServiceClient, ) map[string]jobs.Task { return map[string]jobs.Task{ @@ -357,59 +360,11 @@ func loadNFTMintTasks( return nil, nil }, - Next: "mint_nft_v3", - }, - "mint_nft_v3": { - RunnerFunc: func(args []interface{}, overrides map[string]interface{}) (result interface{}, err error) { - account, itemID, req, err := convertArgs(args) - - if err != nil { - return nil, err - } - - log.Info("Minting NFT on Centrifuge chain...") - - ctx := contextutil.WithAccount(context.Background(), account) - - doc, err := docSrv.GetCurrentVersion(ctx, req.DocumentID) - - if err != nil { - log.Errorf("Couldn't get current document version: %s", err) - - return nil, err - } - - anchorID, err := anchors.ToAnchorID(doc.CurrentVersion()) - - if err != nil { - log.Errorf("Couldn't get anchor for document: %s", err) - - return nil, err - } - - extInfo, err := api.Mint(ctx, req.CollectionID, itemID, req.Owner) - - if err != nil { - log.Errorf("Couldn't mint item: %s", err) - - return nil, err - } - - log.Infof( - "Successfully minted NFT on Centrifuge chain, collection ID - %d, item ID - %d, anchor ID - %s, ext hash - %s", - req.CollectionID, - itemID, - anchorID.String(), - extInfo.Hash.Hex(), - ) - - return nil, nil - }, - Next: "store_nft_v3_metadata", + Next: "store_nft_on_ipfs", }, - "store_nft_v3_metadata": { + "store_nft_on_ipfs": { RunnerFunc: func(args []interface{}, overrides map[string]interface{}) (result interface{}, err error) { - account, itemID, req, err := convertArgs(args) + account, _, req, err := convertArgs(args) if err != nil { return nil, err @@ -455,28 +410,13 @@ func loadNFTMintTasks( ipfsPath := path.New(ipfsPinningRes.CID).String() - log.Infof("Setting the IPFS path as NFT metadata in Centrifuge chain, IPFS path - %s", ipfsPath) - - _, err = api.SetMetadata(ctx, req.CollectionID, itemID, []byte(ipfsPath), false) - - if err != nil { - log.Errorf("Couldn't set IPFS CID: %s", err) - - return nil, err - } - - log.Infof( - "Successfully stored NFT metadata, collection ID - %d, item ID - %d, IPFS path - %s", - req.CollectionID, - itemID, - ipfsPath, - ) + overrides["ipfsPath"] = ipfsPath return nil, nil }, - Next: "set_nft_v3_attributes", + Next: "execute_nft_batch", }, - "set_nft_v3_attributes": { + "execute_nft_batch": { RunnerFunc: func(args []interface{}, overrides map[string]interface{}) (result interface{}, err error) { account, itemID, req, err := convertArgs(args) @@ -484,30 +424,33 @@ func loadNFTMintTasks( return nil, err } - ctx := contextutil.WithAccount(context.Background(), account) - - doc, err := docSrv.GetCurrentVersion(ctx, req.DocumentID) + ipfsPath, ok := overrides["ipfsPath"].(string) - if err != nil { - log.Errorf("Couldn't get document: %s", err) - - return nil, fmt.Errorf("failed to get document: %w", err) + if !ok { + return nil, errors.New("invalid IPFS path detected") } - _, err = api.SetAttribute(ctx, req.CollectionID, itemID, []byte(DocumentIDAttributeKey), doc.ID()) + ctx := contextutil.WithAccount(context.Background(), account) + + doc, err := docSrv.GetCurrentVersion(ctx, req.DocumentID) if err != nil { - log.Errorf("Couldn't set document ID attribute: %s", err) + log.Errorf("Couldn't get current document version: %s", err) - return nil, fmt.Errorf("couldn't set document ID attribute") + return nil, err } - _, err = api.SetAttribute(ctx, req.CollectionID, itemID, []byte(DocumentVersionAttributeKey), doc.CurrentVersion()) + _, err = utilityAPI.BatchAll(ctx, + getMintNFTCallProviderFn(req.CollectionID, itemID, req.Owner), + getSetMetadataCallProviderFn(req.CollectionID, itemID, []byte(ipfsPath), false), + getSetAttributeCallProviderFn(req.CollectionID, itemID, []byte(DocumentIDAttributeKey), doc.ID()), + getSetAttributeCallProviderFn(req.CollectionID, itemID, []byte(DocumentVersionAttributeKey), doc.CurrentVersion()), + ) if err != nil { - log.Errorf("Couldn't set document version attribute: %s", err) + log.Errorf("Couldn't execute NFT batch: %s", err) - return nil, fmt.Errorf("couldn't set document version attribute") + return nil, err } return nil, nil @@ -516,6 +459,82 @@ func loadNFTMintTasks( } } +func getMintNFTCallProviderFn( + collectionID types.U64, + itemID types.U128, + owner *types.AccountID, +) centchain.CallProviderFn { + return func(meta *types.Metadata) (*types.Call, error) { + ownerMultiAddress, err := types.NewMultiAddressFromAccountID(owner.ToBytes()) + + if err != nil { + return nil, fmt.Errorf("couldn't create owner multi address: %w", err) + } + + call, err := types.NewCall( + meta, + uniques.MintCall, + collectionID, + itemID, + ownerMultiAddress, + ) + + if err != nil { + return nil, fmt.Errorf("couldn't create MintNFT call: %w", err) + } + + return &call, nil + } +} + +func getSetMetadataCallProviderFn( + collectionID types.U64, + itemID types.U128, + ipfsPath []byte, + freezeMetadata bool, +) centchain.CallProviderFn { + return func(meta *types.Metadata) (*types.Call, error) { + call, err := types.NewCall( + meta, + uniques.SetMetadataCall, + collectionID, + itemID, + ipfsPath, + freezeMetadata, + ) + + if err != nil { + return nil, fmt.Errorf("couldn't create SetMetadata call: %w", err) + } + + return &call, nil + } +} + +func getSetAttributeCallProviderFn( + collectionID types.U64, + itemID types.U128, + attributeKey []byte, + attributeValue []byte, +) centchain.CallProviderFn { + return func(meta *types.Metadata) (*types.Call, error) { + call, err := types.NewCall( + meta, + uniques.SetAttributeCall, + collectionID, + types.NewOption(itemID), + attributeKey, + attributeValue, + ) + + if err != nil { + return nil, fmt.Errorf("couldn't create SetAttribute call: %w", err) + } + + return &call, nil + } +} + func GetDocAttributes(doc documents.Document, attrLabels []string) (map[string]string, error) { attrMap := make(map[string]string) diff --git a/pallets/bootstrapper.go b/pallets/bootstrapper.go index 4249acdab..9b9076178 100644 --- a/pallets/bootstrapper.go +++ b/pallets/bootstrapper.go @@ -8,6 +8,7 @@ import ( "github.com/centrifuge/go-centrifuge/pallets/keystore" "github.com/centrifuge/go-centrifuge/pallets/proxy" "github.com/centrifuge/go-centrifuge/pallets/uniques" + "github.com/centrifuge/go-centrifuge/pallets/utility" ) const ( @@ -15,6 +16,7 @@ const ( BootstrappedKeystoreAPI = "BootstrappedKeystoreAPI" BootstrappedProxyAPI = "BootstrappedProxyAPI" BootstrappedUniquesAPI = "BootstrappedUniquesAPI" + BootstrappedUtilityAPI = "BootstrappedUtilityAPI" ) type Bootstrapper struct{} @@ -59,5 +61,9 @@ func (b *Bootstrapper) Bootstrap(context map[string]interface{}) error { context[BootstrappedAnchorService] = anchorsAPI + utilityAPI := utility.NewAPI(centAPI, proxyAPI, podOperator) + + context[BootstrappedUtilityAPI] = utilityAPI + return nil } diff --git a/pallets/utility/api.go b/pallets/utility/api.go index 8956fa584..30a5ba22f 100644 --- a/pallets/utility/api.go +++ b/pallets/utility/api.go @@ -2,6 +2,7 @@ package utility import ( "context" + "fmt" proxyType "github.com/centrifuge/chain-custom-types/pkg/proxy" "github.com/centrifuge/go-centrifuge/centchain" @@ -13,6 +14,10 @@ import ( logging "github.com/ipfs/go-log" ) +const ( + ErrBatchCallCreation = errors.Error("couldn't create batch call") +) + var ( log = logging.Logger("utility_api") ) @@ -23,8 +28,10 @@ const ( BatchAllCall = PalletName + ".batch_all" ) +//go:generate mockery --name API --structname APIMock --filename api_mock.go --inpackage + type API interface { - BatchAll(ctx context.Context, calls ...types.Call) (*centchain.ExtrinsicInfo, error) + BatchAll(ctx context.Context, callProviderFns ...centchain.CallProviderFn) (*centchain.ExtrinsicInfo, error) } type api struct { @@ -42,7 +49,7 @@ func NewAPI(centAPI centchain.API, proxyAPI proxy.API, podOperator config.PodOpe } } -func (a *api) BatchAll(ctx context.Context, calls ...types.Call) (*centchain.ExtrinsicInfo, error) { +func (a *api) BatchAll(ctx context.Context, callProviderFns ...centchain.CallProviderFn) (*centchain.ExtrinsicInfo, error) { identity, err := contextutil.Identity(ctx) if err != nil { @@ -59,16 +66,12 @@ func (a *api) BatchAll(ctx context.Context, calls ...types.Call) (*centchain.Ext return nil, errors.ErrMetadataRetrieval } - call, err := types.NewCall( - meta, - BatchAllCall, - calls, - ) + batchCall, err := BatchCalls(callProviderFns...)(meta) if err != nil { - log.Errorf("Couldn't create call: %s", err) + log.Errorf("Couldn't create batch call: %s", err) - return nil, errors.ErrCallCreation + return nil, ErrBatchCallCreation } extInfo, err := a.proxyAPI.ProxyCall( @@ -76,7 +79,7 @@ func (a *api) BatchAll(ctx context.Context, calls ...types.Call) (*centchain.Ext identity, a.podOperator.ToKeyringPair(), types.NewOption(proxyType.PodOperation), - call, + *batchCall, ) if err != nil { @@ -87,3 +90,27 @@ func (a *api) BatchAll(ctx context.Context, calls ...types.Call) (*centchain.Ext return extInfo, nil } + +func BatchCalls(callCreationFns ...centchain.CallProviderFn) centchain.CallProviderFn { + return func(meta *types.Metadata) (*types.Call, error) { + var calls []*types.Call + + for _, callCreationFn := range callCreationFns { + call, err := callCreationFn(meta) + + if err != nil { + return nil, fmt.Errorf("couldn't create call: %w", err) + } + + calls = append(calls, call) + } + + batchCall, err := types.NewCall(meta, BatchAllCall, calls) + + if err != nil { + return nil, err + } + + return &batchCall, nil + } +} diff --git a/pallets/utility/api_integration_test.go b/pallets/utility/api_integration_test.go new file mode 100644 index 000000000..4f1048cf3 --- /dev/null +++ b/pallets/utility/api_integration_test.go @@ -0,0 +1,120 @@ +//go:build integration + +package utility_test + +import ( + "context" + "math/rand" + "os" + "testing" + "time" + + "github.com/centrifuge/go-centrifuge/bootstrap" + "github.com/centrifuge/go-centrifuge/bootstrap/bootstrappers/integration_test" + "github.com/centrifuge/go-centrifuge/bootstrap/bootstrappers/testlogging" + "github.com/centrifuge/go-centrifuge/centchain" + "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/config/configstore" + "github.com/centrifuge/go-centrifuge/contextutil" + "github.com/centrifuge/go-centrifuge/dispatcher" + v2 "github.com/centrifuge/go-centrifuge/identity/v2" + "github.com/centrifuge/go-centrifuge/jobs" + "github.com/centrifuge/go-centrifuge/pallets" + "github.com/centrifuge/go-centrifuge/pallets/uniques" + "github.com/centrifuge/go-centrifuge/pallets/utility" + "github.com/centrifuge/go-centrifuge/storage/leveldb" + genericUtils "github.com/centrifuge/go-centrifuge/testingutils/generic" + "github.com/centrifuge/go-substrate-rpc-client/v4/types" + "github.com/stretchr/testify/assert" +) + +var integrationTestBootstrappers = []bootstrap.TestBootstrapper{ + &integration_test.Bootstrapper{}, + &testlogging.TestLoggingBootstrapper{}, + &config.Bootstrapper{}, + &leveldb.Bootstrapper{}, + &configstore.Bootstrapper{}, + &jobs.Bootstrapper{}, + centchain.Bootstrapper{}, + &pallets.Bootstrapper{}, + &dispatcher.Bootstrapper{}, + &v2.AccountTestBootstrapper{}, +} + +var ( + cfgService config.Service + uniquesAPI uniques.API + utilityAPI utility.API +) + +func TestMain(m *testing.M) { + ctx := bootstrap.RunTestBootstrappers(integrationTestBootstrappers, nil) + cfgService = genericUtils.GetService[config.Service](ctx) + uniquesAPI = genericUtils.GetService[uniques.API](ctx) + utilityAPI = genericUtils.GetService[utility.API](ctx) + + rand.Seed(time.Now().Unix()) + + result := m.Run() + + bootstrap.RunTestTeardown(integrationTestBootstrappers) + + os.Exit(result) +} + +func TestIntegration_API_BatchALl(t *testing.T) { + accs, err := cfgService.GetAccounts() + assert.NoError(t, err) + assert.NotEmpty(t, accs) + + acc := accs[0] + + ctx := contextutil.WithAccount(context.Background(), acc) + + collectionID1 := types.U64(rand.Uint64()) + collectionID2 := types.U64(rand.Uint64()) + + collectionAdminMultiAddress, err := types.NewMultiAddressFromAccountID(acc.GetIdentity().ToBytes()) + assert.NoError(t, err) + + callCreationFn1 := centchain.CallProviderFn(func(meta *types.Metadata) (*types.Call, error) { + call, err := types.NewCall( + meta, + uniques.CreateCollectionCall, + collectionID1, + collectionAdminMultiAddress, + ) + + if err != nil { + return nil, err + } + + return &call, nil + }) + + callCreationFn2 := centchain.CallProviderFn(func(meta *types.Metadata) (*types.Call, error) { + call, err := types.NewCall( + meta, + uniques.CreateCollectionCall, + collectionID2, + collectionAdminMultiAddress, + ) + + if err != nil { + return nil, err + } + + return &call, nil + }) + + _, err = utilityAPI.BatchAll(ctx, callCreationFn1, callCreationFn2) + assert.NoError(t, err) + + collectionDetails, err := uniquesAPI.GetCollectionDetails(collectionID1) + assert.NoError(t, err) + assert.True(t, collectionDetails.Owner.Equal(acc.GetIdentity())) + + collectionDetails, err = uniquesAPI.GetCollectionDetails(collectionID2) + assert.NoError(t, err) + assert.True(t, collectionDetails.Owner.Equal(acc.GetIdentity())) +} diff --git a/pallets/utility/api_mock.go b/pallets/utility/api_mock.go new file mode 100644 index 000000000..3a2762775 --- /dev/null +++ b/pallets/utility/api_mock.go @@ -0,0 +1,61 @@ +// Code generated by mockery v2.13.0-beta.1. DO NOT EDIT. + +package utility + +import ( + context "context" + + centchain "github.com/centrifuge/go-centrifuge/centchain" + + mock "github.com/stretchr/testify/mock" +) + +// APIMock is an autogenerated mock type for the API type +type APIMock struct { + mock.Mock +} + +// BatchAll provides a mock function with given fields: ctx, callProviderFns +func (_m *APIMock) BatchAll(ctx context.Context, callProviderFns ...centchain.CallProviderFn) (*centchain.ExtrinsicInfo, error) { + _va := make([]interface{}, len(callProviderFns)) + for _i := range callProviderFns { + _va[_i] = callProviderFns[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *centchain.ExtrinsicInfo + if rf, ok := ret.Get(0).(func(context.Context, ...centchain.CallProviderFn) *centchain.ExtrinsicInfo); ok { + r0 = rf(ctx, callProviderFns...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*centchain.ExtrinsicInfo) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, ...centchain.CallProviderFn) error); ok { + r1 = rf(ctx, callProviderFns...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type NewAPIMockT interface { + mock.TestingT + Cleanup(func()) +} + +// NewAPIMock creates a new instance of APIMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewAPIMock(t NewAPIMockT) *APIMock { + mock := &APIMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pallets/utility/api_test.go b/pallets/utility/api_test.go new file mode 100644 index 000000000..ce90ff341 --- /dev/null +++ b/pallets/utility/api_test.go @@ -0,0 +1,285 @@ +//go:build unit + +package utility + +import ( + "context" + "testing" + + "github.com/centrifuge/go-centrifuge/errors" + + proxyType "github.com/centrifuge/chain-custom-types/pkg/proxy" + "github.com/centrifuge/go-substrate-rpc-client/v4/signature" + + "github.com/centrifuge/go-centrifuge/contextutil" + "github.com/centrifuge/go-centrifuge/testingutils" + testingcommons "github.com/centrifuge/go-centrifuge/testingutils/common" + genericUtils "github.com/centrifuge/go-centrifuge/testingutils/generic" + "github.com/centrifuge/go-substrate-rpc-client/v4/types" + "github.com/stretchr/testify/assert" + + "github.com/centrifuge/go-centrifuge/centchain" + "github.com/centrifuge/go-centrifuge/config" + "github.com/centrifuge/go-centrifuge/pallets/proxy" +) + +func TestAPI_BatchAll(t *testing.T) { + ctx := context.Background() + + api, mocks := getAPIWithMocks(t) + + identity, err := testingcommons.GetRandomAccountID() + assert.NoError(t, err) + + accountMock := config.NewAccountMock(t) + accountMock.On("GetIdentity"). + Return(identity) + + ctx = contextutil.WithAccount(ctx, accountMock) + + meta, err := testingutils.GetTestMetadata() + assert.NoError(t, err) + + genericUtils.GetMock[*centchain.APIMock](mocks). + On("GetMetadataLatest"). + Return(meta, nil) + + callCreationFn1 := centchain.CallProviderFn(func(meta *types.Metadata) (*types.Call, error) { + call, err := types.NewCall(meta, "System.remark", []byte{1, 2, 3}) + + if err != nil { + return nil, err + } + + return &call, nil + }) + + callCreationFn2 := centchain.CallProviderFn(func(meta *types.Metadata) (*types.Call, error) { + call, err := types.NewCall(meta, "System.remark", []byte{1, 2, 3}) + + if err != nil { + return nil, err + } + + return &call, nil + }) + + batchCall, err := BatchCalls(callCreationFn1, callCreationFn2)(meta) + assert.NoError(t, err) + + var krp signature.KeyringPair + + genericUtils.GetMock[*config.PodOperatorMock](mocks). + On("ToKeyringPair"). + Return(krp).Once() + + extInfo := ¢chain.ExtrinsicInfo{} + + genericUtils.GetMock[*proxy.APIMock](mocks). + On( + "ProxyCall", + ctx, + identity, + krp, + types.NewOption(proxyType.PodOperation), + *batchCall, + ). + Return(extInfo, nil).Once() + + res, err := api.BatchAll(ctx, callCreationFn1, callCreationFn2) + assert.NoError(t, err) + assert.Equal(t, extInfo, res) +} + +func TestAPI_BatchAll_IdentityRetrievalError(t *testing.T) { + ctx := context.Background() + + api, _ := getAPIWithMocks(t) + + callCreationFn1 := centchain.CallProviderFn(func(meta *types.Metadata) (*types.Call, error) { + call, err := types.NewCall(meta, "System.remark", []byte{1, 2, 3}) + + if err != nil { + return nil, err + } + + return &call, nil + }) + + callCreationFn2 := centchain.CallProviderFn(func(meta *types.Metadata) (*types.Call, error) { + call, err := types.NewCall(meta, "System.remark", []byte{1, 2, 3}) + + if err != nil { + return nil, err + } + + return &call, nil + }) + + res, err := api.BatchAll(ctx, callCreationFn1, callCreationFn2) + assert.ErrorIs(t, err, errors.ErrContextIdentityRetrieval) + assert.Nil(t, res) +} + +func TestAPI_BatchAll_MetadataRetrievalError(t *testing.T) { + ctx := context.Background() + + api, mocks := getAPIWithMocks(t) + + identity, err := testingcommons.GetRandomAccountID() + assert.NoError(t, err) + + accountMock := config.NewAccountMock(t) + accountMock.On("GetIdentity"). + Return(identity) + + ctx = contextutil.WithAccount(ctx, accountMock) + + genericUtils.GetMock[*centchain.APIMock](mocks). + On("GetMetadataLatest"). + Return(nil, errors.New("error")) + + callCreationFn1 := centchain.CallProviderFn(func(meta *types.Metadata) (*types.Call, error) { + call, err := types.NewCall(meta, "System.remark", []byte{1, 2, 3}) + + if err != nil { + return nil, err + } + + return &call, nil + }) + + callCreationFn2 := centchain.CallProviderFn(func(meta *types.Metadata) (*types.Call, error) { + call, err := types.NewCall(meta, "System.remark", []byte{1, 2, 3}) + + if err != nil { + return nil, err + } + + return &call, nil + }) + + res, err := api.BatchAll(ctx, callCreationFn1, callCreationFn2) + assert.ErrorIs(t, err, errors.ErrMetadataRetrieval) + assert.Nil(t, res) +} + +func TestAPI_BatchAll_BatchCallCreationError(t *testing.T) { + ctx := context.Background() + + api, mocks := getAPIWithMocks(t) + + identity, err := testingcommons.GetRandomAccountID() + assert.NoError(t, err) + + accountMock := config.NewAccountMock(t) + accountMock.On("GetIdentity"). + Return(identity) + + ctx = contextutil.WithAccount(ctx, accountMock) + + meta, err := testingutils.GetTestMetadata() + assert.NoError(t, err) + + genericUtils.GetMock[*centchain.APIMock](mocks). + On("GetMetadataLatest"). + Return(meta, nil) + + callCreationFn1 := centchain.CallProviderFn(func(meta *types.Metadata) (*types.Call, error) { + call, err := types.NewCall(meta, "System.remark", []byte{1, 2, 3}) + + if err != nil { + return nil, err + } + + return &call, nil + }) + + callCreationFn2 := centchain.CallProviderFn(func(meta *types.Metadata) (*types.Call, error) { + return nil, errors.New("error") + }) + + res, err := api.BatchAll(ctx, callCreationFn1, callCreationFn2) + assert.ErrorIs(t, err, ErrBatchCallCreation) + assert.Nil(t, res) +} + +func TestAPI_BatchAll_ProxyCallError(t *testing.T) { + ctx := context.Background() + + api, mocks := getAPIWithMocks(t) + + identity, err := testingcommons.GetRandomAccountID() + assert.NoError(t, err) + + accountMock := config.NewAccountMock(t) + accountMock.On("GetIdentity"). + Return(identity) + + ctx = contextutil.WithAccount(ctx, accountMock) + + meta, err := testingutils.GetTestMetadata() + assert.NoError(t, err) + + genericUtils.GetMock[*centchain.APIMock](mocks). + On("GetMetadataLatest"). + Return(meta, nil) + + callCreationFn1 := centchain.CallProviderFn(func(meta *types.Metadata) (*types.Call, error) { + call, err := types.NewCall(meta, "System.remark", []byte{1, 2, 3}) + + if err != nil { + return nil, err + } + + return &call, nil + }) + + callCreationFn2 := centchain.CallProviderFn(func(meta *types.Metadata) (*types.Call, error) { + call, err := types.NewCall(meta, "System.remark", []byte{1, 2, 3}) + + if err != nil { + return nil, err + } + + return &call, nil + }) + + batchCall, err := BatchCalls(callCreationFn1, callCreationFn2)(meta) + assert.NoError(t, err) + + var krp signature.KeyringPair + + genericUtils.GetMock[*config.PodOperatorMock](mocks). + On("ToKeyringPair"). + Return(krp).Once() + + genericUtils.GetMock[*proxy.APIMock](mocks). + On( + "ProxyCall", + ctx, + identity, + krp, + types.NewOption(proxyType.PodOperation), + *batchCall, + ). + Return(nil, errors.New("error")).Once() + + res, err := api.BatchAll(ctx, callCreationFn1, callCreationFn2) + assert.ErrorIs(t, err, errors.ErrProxyCall) + assert.Nil(t, res) +} + +func getAPIWithMocks(t *testing.T) (*api, []any) { + centAPIMock := centchain.NewAPIMock(t) + proxyAPIMock := proxy.NewAPIMock(t) + podOperatorMock := config.NewPodOperatorMock(t) + + utilityAPI := NewAPI(centAPIMock, proxyAPIMock, podOperatorMock) + + return utilityAPI.(*api), []any{ + centAPIMock, + proxyAPIMock, + podOperatorMock, + } +}