diff --git a/config/dependency.go b/config/dependency.go index 95ee7517..28ac6a3e 100644 --- a/config/dependency.go +++ b/config/dependency.go @@ -49,11 +49,12 @@ type Source struct { } type Dependency struct { - Name string - Source Source - Hash string - Aliases Aliases - Canonical string + Name string + Source Source + Hash string + BlockHeight uint64 + Aliases Aliases + Canonical string } type Dependencies []Dependency diff --git a/config/json/dependency.go b/config/json/dependency.go index 803a9cde..c500f30a 100644 --- a/config/json/dependency.go +++ b/config/json/dependency.go @@ -59,9 +59,10 @@ func (j jsonDependencies) transformToConfig() (config.Dependencies, error) { } dep = config.Dependency{ - Name: dependencyName, - Hash: dependency.Extended.Hash, - Canonical: dependency.Extended.Canonical, + Name: dependencyName, + Hash: dependency.Extended.Hash, + BlockHeight: dependency.Extended.BlockHeight, + Canonical: dependency.Extended.Canonical, Source: config.Source{ NetworkName: depNetwork, Address: flow.HexToAddress(depAddress), @@ -100,10 +101,11 @@ func transformDependenciesToJSON(configDependencies config.Dependencies, configC jsonDeps[dep.Name] = jsonDependency{ Extended: jsonDependencyExtended{ - Source: buildSourceString(dep.Source), - Hash: dep.Hash, - Aliases: aliases, - Canonical: dep.Canonical, + Source: buildSourceString(dep.Source), + Hash: dep.Hash, + BlockHeight: dep.BlockHeight, + Aliases: aliases, + Canonical: dep.Canonical, }, } } @@ -125,10 +127,11 @@ func buildSourceString(source config.Source) string { // jsonDependencyExtended for json parsing advanced config. type jsonDependencyExtended struct { - Source string `json:"source"` - Hash string `json:"hash"` - Aliases map[string]string `json:"aliases"` - Canonical string `json:"canonical,omitempty"` + Source string `json:"source"` + Hash string `json:"hash"` + BlockHeight uint64 `json:"block_height,omitempty"` + Aliases map[string]string `json:"aliases"` + Canonical string `json:"canonical,omitempty"` } // jsonDependency structure for json parsing. diff --git a/config/json/dependency_test.go b/config/json/dependency_test.go index bac31f56..b5a54ca2 100644 --- a/config/json/dependency_test.go +++ b/config/json/dependency_test.go @@ -162,3 +162,67 @@ func Test_TransformDependenciesWithCanonicalToJSON(t *testing.T) { assert.Equal(t, "", result["NumberFormatter"].Extended.Canonical) assert.Equal(t, "NumberFormatter", result["NumberFormatterAlias"].Extended.Canonical) } + +func Test_ConfigDependenciesWithBlockHeight(t *testing.T) { + b := []byte(`{ + "HelloWorld": { + "source": "testnet://877931736ee77cff.HelloWorld", + "hash": "abcd1234", + "block_height": 12345678, + "aliases": { + "emulator": "f8d6e0586b0a20c7" + } + } + }`) + + var jsonDependencies jsonDependencies + err := json.Unmarshal(b, &jsonDependencies) + assert.NoError(t, err) + + dependencies, err := jsonDependencies.transformToConfig() + assert.NoError(t, err) + + assert.Len(t, dependencies, 1) + + dep := dependencies.ByName("HelloWorld") + assert.NotNil(t, dep) + assert.Equal(t, "abcd1234", dep.Hash) + assert.Equal(t, uint64(12345678), dep.BlockHeight) +} + +func Test_TransformDependenciesWithBlockHeightToJSON(t *testing.T) { + b := []byte(`{ + "HelloWorld": { + "source": "testnet://877931736ee77cff.HelloWorld", + "hash": "abcd1234", + "block_height": 12345678, + "aliases": { + "emulator": "f8d6e0586b0a20c7" + } + } + }`) + + var jsonContracts jsonContracts + errContracts := json.Unmarshal(b, &jsonContracts) + assert.NoError(t, errContracts) + + var jsonDependencies jsonDependencies + err := json.Unmarshal(b, &jsonDependencies) + assert.NoError(t, err) + + contracts, err := jsonContracts.transformToConfig() + assert.NoError(t, err) + dependencies, err := jsonDependencies.transformToConfig() + assert.NoError(t, err) + + j := transformDependenciesToJSON(dependencies, contracts) + x, _ := json.Marshal(j) + + // Parse back and check block_height field + var result map[string]jsonDependency + err = json.Unmarshal(x, &result) + assert.NoError(t, err) + + assert.Equal(t, "abcd1234", result["HelloWorld"].Extended.Hash) + assert.Equal(t, uint64(12345678), result["HelloWorld"].Extended.BlockHeight) +} diff --git a/gateway/emulator.go b/gateway/emulator.go index c3692ca3..64c7322d 100644 --- a/gateway/emulator.go +++ b/gateway/emulator.go @@ -111,6 +111,15 @@ func (g *EmulatorGateway) GetAccount(ctx context.Context, address flow.Address) return account, nil } +func (g *EmulatorGateway) GetAccountAtBlockHeight(ctx context.Context, address flow.Address, blockHeight uint64) (*flow.Account, error) { + account, err := g.adapter.GetAccountAtBlockHeight(ctx, address, blockHeight) + if err != nil { + return nil, UnwrapStatusError(err) + } + return account, nil +} + + func (g *EmulatorGateway) SendSignedTransaction(ctx context.Context, tx *flow.Transaction) (*flow.Transaction, error) { err := g.adapter.SendTransaction(ctx, *tx) if err != nil { diff --git a/gateway/gateway.go b/gateway/gateway.go index 5279dbf0..412941d8 100644 --- a/gateway/gateway.go +++ b/gateway/gateway.go @@ -30,6 +30,7 @@ import ( // Gateway describes blockchain access interface type Gateway interface { GetAccount(context.Context, flow.Address) (*flow.Account, error) + GetAccountAtBlockHeight(context.Context, flow.Address, uint64) (*flow.Account, error) SendSignedTransaction(context.Context, *flow.Transaction) (*flow.Transaction, error) GetTransaction(context.Context, flow.Identifier) (*flow.Transaction, error) GetTransactionResultsByBlockID(ctx context.Context, blockID flow.Identifier) ([]*flow.TransactionResult, error) diff --git a/gateway/grpc.go b/gateway/grpc.go index 86028551..73e96b24 100644 --- a/gateway/grpc.go +++ b/gateway/grpc.go @@ -109,6 +109,17 @@ func (g *GrpcGateway) GetAccount(ctx context.Context, address flow.Address) (*fl return account, nil } +// GetAccountAtBlockHeight gets an account by address at a specific block height from the Flow Access API. +func (g *GrpcGateway) GetAccountAtBlockHeight(ctx context.Context, address flow.Address, blockHeight uint64) (*flow.Account, error) { + account, err := g.client.GetAccountAtBlockHeight(ctx, address, blockHeight) + if err != nil { + return nil, fmt.Errorf("failed to get account with address %s at block height %d: %w", address, blockHeight, err) + } + + return account, nil +} + + // SendSignedTransaction sends a transaction to flow that is already prepared and signed. func (g *GrpcGateway) SendSignedTransaction(ctx context.Context, tx *flow.Transaction) (*flow.Transaction, error) { err := g.client.SendTransaction(ctx, *tx) diff --git a/gateway/mocks/Gateway.go b/gateway/mocks/Gateway.go index c290107c..a8311189 100644 --- a/gateway/mocks/Gateway.go +++ b/gateway/mocks/Gateway.go @@ -137,6 +137,36 @@ func (_m *Gateway) GetAccount(_a0 context.Context, _a1 flow.Address) (*flow.Acco return r0, r1 } +// GetAccountAtBlockHeight provides a mock function with given fields: _a0, _a1, _a2 +func (_m *Gateway) GetAccountAtBlockHeight(_a0 context.Context, _a1 flow.Address, _a2 uint64) (*flow.Account, error) { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for GetAccountAtBlockHeight") + } + + var r0 *flow.Account + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, flow.Address, uint64) (*flow.Account, error)); ok { + return rf(_a0, _a1, _a2) + } + if rf, ok := ret.Get(0).(func(context.Context, flow.Address, uint64) *flow.Account); ok { + r0 = rf(_a0, _a1, _a2) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*flow.Account) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, flow.Address, uint64) error); ok { + r1 = rf(_a0, _a1, _a2) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetBlockByHeight provides a mock function with given fields: _a0, _a1 func (_m *Gateway) GetBlockByHeight(_a0 context.Context, _a1 uint64) (*flow.Block, error) { ret := _m.Called(_a0, _a1) diff --git a/gateway/mocks/gateway_mock.go b/gateway/mocks/gateway_mock.go index 0773ae6c..29e26f7e 100644 --- a/gateway/mocks/gateway_mock.go +++ b/gateway/mocks/gateway_mock.go @@ -30,6 +30,7 @@ import ( const ( GetAccountFunc = "GetAccount" + GetAccountAtBlockHeightFunc = "GetAccountAtBlockHeight" SendSignedTransactionFunc = "SendSignedTransaction" GetCollectionFunc = "GetCollection" GetTransactionResultFunc = "GetTransactionResult" @@ -49,6 +50,7 @@ type TestGateway struct { Mock *Gateway SendSignedTransaction *mock.Call GetAccount *mock.Call + GetAccountAtBlockHeight *mock.Call GetCollection *mock.Call GetTransactionResult *mock.Call GetEvents *mock.Call @@ -83,6 +85,12 @@ func DefaultMockGateway() *TestGateway { ctxMock, mock.AnythingOfType("flow.Address"), ), + GetAccountAtBlockHeight: m.On( + GetAccountAtBlockHeightFunc, + ctxMock, + mock.AnythingOfType("flow.Address"), + mock.AnythingOfType("uint64"), + ), GetCollection: m.On( GetCollectionFunc, ctxMock, @@ -146,7 +154,16 @@ func DefaultMockGateway() *TestGateway { t.GetAccount.Run(func(args mock.Arguments) { addr := args.Get(1).(flow.Address) - t.GetAccount.Return(tests.NewAccountWithAddress(addr.String()), nil) + acc := tests.NewAccountWithAddress(addr.String()) + t.GetAccount.Return(acc, nil) + }) + + t.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { + addr := args.Get(1).(flow.Address) + // Return the same account structure as GetAccount for consistency + // If the test needs specific contracts, it should override this mock + acc := tests.NewAccountWithAddress(addr.String()) + t.GetAccountAtBlockHeight.Return(acc, nil) }) t.ExecuteScript.Run(func(args mock.Arguments) { diff --git a/schema.json b/schema.json index f6276b4d..16d8ab7e 100644 --- a/schema.json +++ b/schema.json @@ -240,6 +240,9 @@ "hash": { "type": "string" }, + "block_height": { + "type": "integer" + }, "aliases": { "patternProperties": { ".*": {