diff --git a/cmd/golang.mk b/cmd/golang.mk index 05891e51..cbd4832c 100644 --- a/cmd/golang.mk +++ b/cmd/golang.mk @@ -9,6 +9,8 @@ GO_FILES := $(shell find . -name '*.go' -or -name '*.json' -type f) CMDLET := $(shell basename ${PWD}) +CONFIG_DIR := ../../config + ${CMDLET}: ${EXTRA_FILES} ${GO_FILES} @${GO_BUILD} ${GO_BUILD_EXTRA_ARGS} diff --git a/cmd/graphql.ethereum/.gitignore b/cmd/graphql.ethereum/.gitignore index d07875fb..05401b32 100644 --- a/cmd/graphql.ethereum/.gitignore +++ b/cmd/graphql.ethereum/.gitignore @@ -1,3 +1,4 @@ graphql.ethereum *.zip bootstrap +pools.toml diff --git a/cmd/graphql.ethereum/Makefile b/cmd/graphql.ethereum/Makefile index 1bf373b8..8643d209 100644 --- a/cmd/graphql.ethereum/Makefile +++ b/cmd/graphql.ethereum/Makefile @@ -1,6 +1,11 @@ +EXTRA_FILES := pools.toml + include ../golang.mk +pools.toml: ${CONFIG_DIR}/pools.toml + @cp ${CONFIG_DIR}/pools.toml . + .PHONY: lambda lambda: bootstrap.zip diff --git a/cmd/graphql.ethereum/gqlgen.yml b/cmd/graphql.ethereum/gqlgen.yml index b821531a..d16bdf83 100644 --- a/cmd/graphql.ethereum/gqlgen.yml +++ b/cmd/graphql.ethereum/gqlgen.yml @@ -42,3 +42,6 @@ models: - github.com/99designs/gqlgen/graphql.Int - github.com/99designs/gqlgen/graphql.Int64 - github.com/99designs/gqlgen/graphql.Int32 + SeawaterPoolClassification: + model: + - github.com/fluidity-money/long.so/lib/types/seawater.Classification diff --git a/cmd/graphql.ethereum/graph/generated.go b/cmd/graphql.ethereum/graph/generated.go index 2f1dd711..d1034d43 100644 --- a/cmd/graphql.ethereum/graph/generated.go +++ b/cmd/graphql.ethereum/graph/generated.go @@ -41,6 +41,7 @@ type Config struct { type ResolverRoot interface { Amount() AmountResolver Query() QueryResolver + SeawaterConfig() SeawaterConfigResolver SeawaterLiquidity() SeawaterLiquidityResolver SeawaterPool() SeawaterPoolResolver SeawaterPosition() SeawaterPositionResolver @@ -101,6 +102,13 @@ type ComplexityRoot struct { Pools func(childComplexity int) int } + SeawaterConfig struct { + Classification func(childComplexity int) int + Displayed func(childComplexity int) int + ID func(childComplexity int) int + Pool func(childComplexity int) int + } + SeawaterLiquidity struct { ID func(childComplexity int) int Liquidity func(childComplexity int) int @@ -112,6 +120,7 @@ type ComplexityRoot struct { SeawaterPool struct { Address func(childComplexity int) int Amounts func(childComplexity int) int + Config func(childComplexity int) int EarnedFeesAPRToken1 func(childComplexity int) int EarnedFeesAprfusdc func(childComplexity int) int ID func(childComplexity int) int @@ -227,6 +236,10 @@ type QueryResolver interface { GetSwaps(ctx context.Context, pool string, first *int, after *int) (model.GetSwaps, error) GetSwapsForUser(ctx context.Context, wallet string, first *int, after *int) (model.GetSwapsForUser, error) } +type SeawaterConfigResolver interface { + ID(ctx context.Context, obj *model.SeawaterConfig) (string, error) + Pool(ctx context.Context, obj *model.SeawaterConfig) (seawater.Pool, error) +} type SeawaterLiquidityResolver interface { TickLower(ctx context.Context, obj *model.SeawaterLiquidity) (int, error) TickUpper(ctx context.Context, obj *model.SeawaterLiquidity) (int, error) @@ -252,6 +265,7 @@ type SeawaterPoolResolver interface { Liquidity(ctx context.Context, obj *seawater.Pool) ([]model.SeawaterLiquidity, error) Swaps(ctx context.Context, obj *seawater.Pool, first *int, after *int) (model.SeawaterSwaps, error) Amounts(ctx context.Context, obj *seawater.Pool) (model.PairAmount, error) + Config(ctx context.Context, obj *seawater.Pool) (model.SeawaterConfig, error) } type SeawaterPositionResolver interface { ID(ctx context.Context, obj *seawater.Position) (string, error) @@ -523,6 +537,34 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.Pools(childComplexity), true + case "SeawaterConfig.classification": + if e.complexity.SeawaterConfig.Classification == nil { + break + } + + return e.complexity.SeawaterConfig.Classification(childComplexity), true + + case "SeawaterConfig.displayed": + if e.complexity.SeawaterConfig.Displayed == nil { + break + } + + return e.complexity.SeawaterConfig.Displayed(childComplexity), true + + case "SeawaterConfig.id": + if e.complexity.SeawaterConfig.ID == nil { + break + } + + return e.complexity.SeawaterConfig.ID(childComplexity), true + + case "SeawaterConfig.pool": + if e.complexity.SeawaterConfig.Pool == nil { + break + } + + return e.complexity.SeawaterConfig.Pool(childComplexity), true + case "SeawaterLiquidity.id": if e.complexity.SeawaterLiquidity.ID == nil { break @@ -572,6 +614,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.SeawaterPool.Amounts(childComplexity), true + case "SeawaterPool.config": + if e.complexity.SeawaterPool.Config == nil { + break + } + + return e.complexity.SeawaterPool.Config(childComplexity), true + case "SeawaterPool.earnedFeesAPRToken1": if e.complexity.SeawaterPool.EarnedFeesAPRToken1 == nil { break @@ -1349,6 +1398,11 @@ type SeawaterPool { Amounts currently contained in this pool. """ amounts: PairAmount! + + """ + Configuration details available to this pool. Should be mostly static. + """ + config: SeawaterConfig! } """ @@ -1635,6 +1689,42 @@ type SeawaterSwap { amountOut: Amount! } +enum SeawaterPoolClassification { + STABLECOIN + VOLATILE + UNKNOWN +} + +""" +SeawaterConfig available to the pool. +""" +type SeawaterConfig { + """ + Identifier of this config. Should be config: + """ + id: ID! + + """ + Pool this configuration belongs to. + """ + pool: SeawaterPool! + + """ + Whether this pool should be displayed to frontend users. + """ + displayed: Boolean! + + """ + Classification of the type of pool. Non-volatile assets like stablecoins (` + "`" + `STABLECOIN` + "`" + `) + should have a range of -10%-10% suggested to the user for the pool, volatile assets + (` + "`" + `VOLATILE` + "`" + `) should have a suggestion based on the historical trading data in the + backend, with the lowest price in the last 7 days, and the highest price, and an extra + 5%. Unclear assets (` + "`" + `UNKNOWN` + "`" + `) should avoid these recommendations altogether, and only + allow the user to submit their price ranges without intervention. + """ + classification: SeawaterPoolClassification! +} + """ Pair amount, with the USD value that's available within determined at the timestamp given. The backend will make an effort seemingly at random to keep this consistent. @@ -3048,6 +3138,8 @@ func (ec *executionContext) fieldContext_Query_pools(_ context.Context, field gr return ec.fieldContext_SeawaterPool_swaps(ctx, field) case "amounts": return ec.fieldContext_SeawaterPool_amounts(ctx, field) + case "config": + return ec.fieldContext_SeawaterPool_config(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type SeawaterPool", field.Name) }, @@ -3131,6 +3223,8 @@ func (ec *executionContext) fieldContext_Query_getPool(ctx context.Context, fiel return ec.fieldContext_SeawaterPool_swaps(ctx, field) case "amounts": return ec.fieldContext_SeawaterPool_amounts(ctx, field) + case "config": + return ec.fieldContext_SeawaterPool_config(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type SeawaterPool", field.Name) }, @@ -3658,6 +3752,226 @@ func (ec *executionContext) fieldContext_Query___schema(_ context.Context, field return fc, nil } +func (ec *executionContext) _SeawaterConfig_id(ctx context.Context, field graphql.CollectedField, obj *model.SeawaterConfig) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SeawaterConfig_id(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.SeawaterConfig().ID(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNID2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SeawaterConfig_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SeawaterConfig", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _SeawaterConfig_pool(ctx context.Context, field graphql.CollectedField, obj *model.SeawaterConfig) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SeawaterConfig_pool(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.SeawaterConfig().Pool(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(seawater.Pool) + fc.Result = res + return ec.marshalNSeawaterPool2githubᚗcomᚋfluidityᚑmoneyᚋlongᚗsoᚋlibᚋtypesᚋseawaterᚐPool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SeawaterConfig_pool(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SeawaterConfig", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_SeawaterPool_id(ctx, field) + case "address": + return ec.fieldContext_SeawaterPool_address(ctx, field) + case "tickSpacing": + return ec.fieldContext_SeawaterPool_tickSpacing(ctx, field) + case "token": + return ec.fieldContext_SeawaterPool_token(ctx, field) + case "price": + return ec.fieldContext_SeawaterPool_price(ctx, field) + case "priceOverTime": + return ec.fieldContext_SeawaterPool_priceOverTime(ctx, field) + case "volumeOverTime": + return ec.fieldContext_SeawaterPool_volumeOverTime(ctx, field) + case "liquidityOverTime": + return ec.fieldContext_SeawaterPool_liquidityOverTime(ctx, field) + case "tvlOverTime": + return ec.fieldContext_SeawaterPool_tvlOverTime(ctx, field) + case "yieldOverTime": + return ec.fieldContext_SeawaterPool_yieldOverTime(ctx, field) + case "earnedFeesAPRFUSDC": + return ec.fieldContext_SeawaterPool_earnedFeesAPRFUSDC(ctx, field) + case "earnedFeesAPRToken1": + return ec.fieldContext_SeawaterPool_earnedFeesAPRToken1(ctx, field) + case "liquidityIncentives": + return ec.fieldContext_SeawaterPool_liquidityIncentives(ctx, field) + case "superIncentives": + return ec.fieldContext_SeawaterPool_superIncentives(ctx, field) + case "utilityIncentives": + return ec.fieldContext_SeawaterPool_utilityIncentives(ctx, field) + case "positions": + return ec.fieldContext_SeawaterPool_positions(ctx, field) + case "positionsForUser": + return ec.fieldContext_SeawaterPool_positionsForUser(ctx, field) + case "liquidity": + return ec.fieldContext_SeawaterPool_liquidity(ctx, field) + case "swaps": + return ec.fieldContext_SeawaterPool_swaps(ctx, field) + case "amounts": + return ec.fieldContext_SeawaterPool_amounts(ctx, field) + case "config": + return ec.fieldContext_SeawaterPool_config(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type SeawaterPool", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _SeawaterConfig_displayed(ctx context.Context, field graphql.CollectedField, obj *model.SeawaterConfig) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SeawaterConfig_displayed(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Displayed, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SeawaterConfig_displayed(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SeawaterConfig", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _SeawaterConfig_classification(ctx context.Context, field graphql.CollectedField, obj *model.SeawaterConfig) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SeawaterConfig_classification(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Classification, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(seawater.Classification) + fc.Result = res + return ec.marshalNSeawaterPoolClassification2githubᚗcomᚋfluidityᚑmoneyᚋlongᚗsoᚋlibᚋtypesᚋseawaterᚐClassification(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SeawaterConfig_classification(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SeawaterConfig", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type SeawaterPoolClassification does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _SeawaterLiquidity_id(ctx context.Context, field graphql.CollectedField, obj *model.SeawaterLiquidity) (ret graphql.Marshaler) { fc, err := ec.fieldContext_SeawaterLiquidity_id(ctx, field) if err != nil { @@ -4919,6 +5233,60 @@ func (ec *executionContext) fieldContext_SeawaterPool_amounts(_ context.Context, return fc, nil } +func (ec *executionContext) _SeawaterPool_config(ctx context.Context, field graphql.CollectedField, obj *seawater.Pool) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SeawaterPool_config(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.SeawaterPool().Config(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(model.SeawaterConfig) + fc.Result = res + return ec.marshalNSeawaterConfig2githubᚗcomᚋfluidityᚑmoneyᚋlongᚗsoᚋcmdᚋgraphqlᚗethereumᚋgraphᚋmodelᚐSeawaterConfig(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SeawaterPool_config(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SeawaterPool", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_SeawaterConfig_id(ctx, field) + case "pool": + return ec.fieldContext_SeawaterConfig_pool(ctx, field) + case "displayed": + return ec.fieldContext_SeawaterConfig_displayed(ctx, field) + case "classification": + return ec.fieldContext_SeawaterConfig_classification(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type SeawaterConfig", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _SeawaterPosition_id(ctx context.Context, field graphql.CollectedField, obj *seawater.Position) (ret graphql.Marshaler) { fc, err := ec.fieldContext_SeawaterPosition_id(ctx, field) if err != nil { @@ -5184,6 +5552,8 @@ func (ec *executionContext) fieldContext_SeawaterPosition_pool(_ context.Context return ec.fieldContext_SeawaterPool_swaps(ctx, field) case "amounts": return ec.fieldContext_SeawaterPool_amounts(ctx, field) + case "config": + return ec.fieldContext_SeawaterPool_config(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type SeawaterPool", field.Name) }, @@ -5894,6 +6264,8 @@ func (ec *executionContext) fieldContext_SeawaterSwap_pool(_ context.Context, fi return ec.fieldContext_SeawaterPool_swaps(ctx, field) case "amounts": return ec.fieldContext_SeawaterPool_amounts(ctx, field) + case "config": + return ec.fieldContext_SeawaterPool_config(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type SeawaterPool", field.Name) }, @@ -9565,6 +9937,122 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr return out } +var seawaterConfigImplementors = []string{"SeawaterConfig"} + +func (ec *executionContext) _SeawaterConfig(ctx context.Context, sel ast.SelectionSet, obj *model.SeawaterConfig) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, seawaterConfigImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("SeawaterConfig") + case "id": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._SeawaterConfig_id(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "pool": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._SeawaterConfig_pool(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "displayed": + out.Values[i] = ec._SeawaterConfig_displayed(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "classification": + out.Values[i] = ec._SeawaterConfig_classification(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var seawaterLiquidityImplementors = []string{"SeawaterLiquidity"} func (ec *executionContext) _SeawaterLiquidity(ctx context.Context, sel ast.SelectionSet, obj *model.SeawaterLiquidity) graphql.Marshaler { @@ -10416,6 +10904,42 @@ func (ec *executionContext) _SeawaterPool(ctx context.Context, sel ast.Selection continue } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "config": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._SeawaterPool_config(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) default: panic("unknown field " + strconv.Quote(field.Name)) @@ -12402,6 +12926,10 @@ func (ec *executionContext) marshalNPriceOverTime2githubᚗcomᚋfluidityᚑmone return ec._PriceOverTime(ctx, sel, &v) } +func (ec *executionContext) marshalNSeawaterConfig2githubᚗcomᚋfluidityᚑmoneyᚋlongᚗsoᚋcmdᚋgraphqlᚗethereumᚋgraphᚋmodelᚐSeawaterConfig(ctx context.Context, sel ast.SelectionSet, v model.SeawaterConfig) graphql.Marshaler { + return ec._SeawaterConfig(ctx, sel, &v) +} + func (ec *executionContext) marshalNSeawaterLiquidity2githubᚗcomᚋfluidityᚑmoneyᚋlongᚗsoᚋcmdᚋgraphqlᚗethereumᚋgraphᚋmodelᚐSeawaterLiquidity(ctx context.Context, sel ast.SelectionSet, v model.SeawaterLiquidity) graphql.Marshaler { return ec._SeawaterLiquidity(ctx, sel, &v) } @@ -12498,6 +13026,22 @@ func (ec *executionContext) marshalNSeawaterPool2ᚕgithubᚗcomᚋfluidityᚑmo return ret } +func (ec *executionContext) unmarshalNSeawaterPoolClassification2githubᚗcomᚋfluidityᚑmoneyᚋlongᚗsoᚋlibᚋtypesᚋseawaterᚐClassification(ctx context.Context, v interface{}) (seawater.Classification, error) { + tmp, err := graphql.UnmarshalString(v) + res := seawater.Classification(tmp) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNSeawaterPoolClassification2githubᚗcomᚋfluidityᚑmoneyᚋlongᚗsoᚋlibᚋtypesᚋseawaterᚐClassification(ctx context.Context, sel ast.SelectionSet, v seawater.Classification) graphql.Marshaler { + res := graphql.MarshalString(string(v)) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + func (ec *executionContext) marshalNSeawaterPosition2githubᚗcomᚋfluidityᚑmoneyᚋlongᚗsoᚋlibᚋtypesᚋseawaterᚐPosition(ctx context.Context, sel ast.SelectionSet, v seawater.Position) graphql.Marshaler { return ec._SeawaterPosition(ctx, sel, &v) } diff --git a/cmd/graphql.ethereum/graph/model/pool-config.go b/cmd/graphql.ethereum/graph/model/pool-config.go new file mode 100644 index 00000000..776761f2 --- /dev/null +++ b/cmd/graphql.ethereum/graph/model/pool-config.go @@ -0,0 +1,11 @@ +package model + +import ( + "github.com/fluidity-money/long.so/lib/config" + "github.com/fluidity-money/long.so/lib/types" +) + +type SeawaterConfig struct { + Addr types.Address + config.Pool +} diff --git a/cmd/graphql.ethereum/graph/resolver.go b/cmd/graphql.ethereum/graph/resolver.go index a97b8a37..907c18d4 100644 --- a/cmd/graphql.ethereum/graph/resolver.go +++ b/cmd/graphql.ethereum/graph/resolver.go @@ -7,11 +7,13 @@ import ( "github.com/fluidity-money/long.so/lib/config" "github.com/fluidity-money/long.so/lib/features" + "github.com/fluidity-money/long.so/lib/types" ) type Resolver struct { - DB *gorm.DB // db used to look up any fields that are missing from a request. - F features.F // features to have enabled when requested - Geth *ethclient.Client // needed to do lookups with geth - C config.C // config for connecting to the right endpoints + DB *gorm.DB // db used to look up any fields that are missing from a request. + F features.F // features to have enabled when requested + Geth *ethclient.Client // needed to do lookups with geth + C config.C // config for connecting to the right endpoints + PoolsConfig map[types.Address]config.Pool // config for pools deployed only the backend knows. } diff --git a/cmd/graphql.ethereum/graph/schema.resolvers.go b/cmd/graphql.ethereum/graph/schema.resolvers.go index 24db98ed..63471b08 100644 --- a/cmd/graphql.ethereum/graph/schema.resolvers.go +++ b/cmd/graphql.ethereum/graph/schema.resolvers.go @@ -14,11 +14,13 @@ import ( "github.com/fluidity-money/long.so/cmd/graphql.ethereum/graph/model" graphErc20 "github.com/fluidity-money/long.so/cmd/graphql.ethereum/lib/erc20" + "github.com/fluidity-money/long.so/lib/config" "github.com/fluidity-money/long.so/lib/features" "github.com/fluidity-money/long.so/lib/math" "github.com/fluidity-money/long.so/lib/types" "github.com/fluidity-money/long.so/lib/types/erc20" "github.com/fluidity-money/long.so/lib/types/seawater" + "gorm.io/gorm" ) @@ -394,6 +396,26 @@ func (r *queryResolver) GetSwapsForUser(ctx context.Context, wallet string, firs return } +// ID is the resolver for the id field. +func (r *seawaterConfigResolver) ID(ctx context.Context, obj *model.SeawaterConfig) (string, error) { + if obj == nil { + return "", fmt.Errorf("empty config") + } + return "config:" + obj.Addr.String(), nil +} + +// Pool is the resolver for the pool field. +func (r *seawaterConfigResolver) Pool(ctx context.Context, obj *model.SeawaterConfig) (pool seawater.Pool, err error) { + if obj == nil { + return pool, fmt.Errorf("empty config") + } + err = r.DB.Table("events_seawater_newpool"). + Where("token = ?", obj.Pool). + Scan(&pool). + Error + return +} + // TickLower is the resolver for the tickLower field. func (r *seawaterLiquidityResolver) TickLower(ctx context.Context, obj *model.SeawaterLiquidity) (tick int, err error) { if obj == nil { @@ -992,6 +1014,19 @@ func (r *seawaterPoolResolver) Amounts(ctx context.Context, obj *seawater.Pool) return } +// Config is the resolver for the config field. +func (r *seawaterPoolResolver) Config(ctx context.Context, obj *seawater.Pool) (m model.SeawaterConfig, err error) { + if obj == nil { + return m, fmt.Errorf("empty pool") + } + c, ok := r.PoolsConfig[obj.Token] + if !ok { + // Return the default pool configuration. + return model.SeawaterConfig{obj.Token, config.DefaultPoolConfiguration}, nil + } + return model.SeawaterConfig{obj.Token, c}, nil +} + // ID is the resolver for the id field. func (r *seawaterPositionResolver) ID(ctx context.Context, obj *seawater.Position) (string, error) { if obj == nil { @@ -1560,6 +1595,9 @@ func (r *Resolver) Amount() AmountResolver { return &amountResolver{r} } // Query returns QueryResolver implementation. func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } +// SeawaterConfig returns SeawaterConfigResolver implementation. +func (r *Resolver) SeawaterConfig() SeawaterConfigResolver { return &seawaterConfigResolver{r} } + // SeawaterLiquidity returns SeawaterLiquidityResolver implementation. func (r *Resolver) SeawaterLiquidity() SeawaterLiquidityResolver { return &seawaterLiquidityResolver{r} @@ -1595,6 +1633,7 @@ func (r *Resolver) Wallet() WalletResolver { return &walletResolver{r} } type amountResolver struct{ *Resolver } type queryResolver struct{ *Resolver } +type seawaterConfigResolver struct{ *Resolver } type seawaterLiquidityResolver struct{ *Resolver } type seawaterPoolResolver struct{ *Resolver } type seawaterPositionResolver struct{ *Resolver } diff --git a/cmd/graphql.ethereum/main.go b/cmd/graphql.ethereum/main.go index 23c8c2ce..d64c8579 100644 --- a/cmd/graphql.ethereum/main.go +++ b/cmd/graphql.ethereum/main.go @@ -61,10 +61,11 @@ func main() { defer geth.Close() srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{ Resolvers: &graph.Resolver{ - DB: db, - F: features.Get(), - Geth: geth, - C: config, + DB: db, + F: features.Get(), + Geth: geth, + C: config, + PoolsConfig: PoolsConfig, }, })) http.Handle("/", corsMiddleware{srv}) diff --git a/cmd/graphql.ethereum/pools.go b/cmd/graphql.ethereum/pools.go new file mode 100644 index 00000000..ecb8b4bd --- /dev/null +++ b/cmd/graphql.ethereum/pools.go @@ -0,0 +1,28 @@ +package main + +import ( + _ "embed" + + "github.com/fluidity-money/long.so/lib/config" + "github.com/fluidity-money/long.so/lib/types" + + "github.com/pelletier/go-toml/v2" +) + +//go:embed pools.toml +var poolsConfigBytes []byte + +// PoolsConfig loaded from pools.toml +var PoolsConfig map[types.Address]config.Pool + +func init() { + // Needed since the package doesn't do decoding for this properly. + var c map[string]config.Pool + if err := toml.Unmarshal(poolsConfigBytes, &c); err != nil { + panic(err) + } + PoolsConfig = make(map[types.Address]config.Pool, len(c)) + for k, v := range c { + PoolsConfig[types.AddressFromString(k)] = v + } +} diff --git a/cmd/graphql.ethereum/schema.graphqls b/cmd/graphql.ethereum/schema.graphqls index 0f5a4eca..8254acf8 100644 --- a/cmd/graphql.ethereum/schema.graphqls +++ b/cmd/graphql.ethereum/schema.graphqls @@ -224,6 +224,11 @@ type SeawaterPool { Amounts currently contained in this pool. """ amounts: PairAmount! + + """ + Configuration details available to this pool. Should be mostly static. + """ + config: SeawaterConfig! } """ @@ -510,6 +515,42 @@ type SeawaterSwap { amountOut: Amount! } +enum SeawaterPoolClassification { + STABLECOIN + VOLATILE + UNKNOWN +} + +""" +SeawaterConfig available to the pool. +""" +type SeawaterConfig { + """ + Identifier of this config. Should be config: + """ + id: ID! + + """ + Pool this configuration belongs to. + """ + pool: SeawaterPool! + + """ + Whether this pool should be displayed to frontend users. + """ + displayed: Boolean! + + """ + Classification of the type of pool. Non-volatile assets like stablecoins (`STABLECOIN`) + should have a range of -10%-10% suggested to the user for the pool, volatile assets + (`VOLATILE`) should have a suggestion based on the historical trading data in the + backend, with the lowest price in the last 7 days, and the highest price, and an extra + 5%. Unclear assets (`UNKNOWN`) should avoid these recommendations altogether, and only + allow the user to submit their price ranges without intervention. + """ + classification: SeawaterPoolClassification! +} + """ Pair amount, with the USD value that's available within determined at the timestamp given. The backend will make an effort seemingly at random to keep this consistent. diff --git a/config/README.md b/config/README.md new file mode 100644 index 00000000..dbb51a00 --- /dev/null +++ b/config/README.md @@ -0,0 +1,5 @@ + +# Configs + +Configuration files that're distributed with the codebase. Should configure how the +frontend displays data using the graph. diff --git a/config/pools.toml b/config/pools.toml new file mode 100644 index 00000000..1249344b --- /dev/null +++ b/config/pools.toml @@ -0,0 +1,16 @@ + +[0xA8EA92c819463EFbEdDFB670FEfC881A480f0115] +displayed = true +classification = "STABLECOIN" + +[0xde104342B32BCa03ec995f999181f7Cf1fFc04d7] +displayed = true +classification = "VOLATILE" + +[0x6437fdc89cED41941b97A9f1f8992D88718C81c5] +displayed = true +classification = "STABLECOIN" + +[0x09f7156aae9c903f90b1cb1e312582c4f208a759] +displayed = true +classification = "VOLATILE" diff --git a/go.mod b/go.mod index 35f39e3c..79afac05 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/getsentry/sentry-go v0.28.1 github.com/lib/pq v1.10.9 github.com/orandin/slog-gorm v1.3.2 + github.com/pelletier/go-toml/v2 v2.0.8 github.com/samber/slog-sentry/v2 v2.5.0 github.com/stretchr/testify v1.9.0 github.com/vektah/gqlparser/v2 v2.5.16 diff --git a/go.sum b/go.sum index a2595573..2e98990b 100644 --- a/go.sum +++ b/go.sum @@ -164,6 +164,8 @@ github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= github.com/orandin/slog-gorm v1.3.2 h1:C0lKDQPAx/pF+8K2HL7bdShPwOEJpPM0Bn80zTzxU1g= github.com/orandin/slog-gorm v1.3.2/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -201,8 +203,13 @@ github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERA github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= diff --git a/jq b/jq deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/config/defaults.go b/lib/config/defaults.go index 479124aa..d159dad7 100644 --- a/lib/config/defaults.go +++ b/lib/config/defaults.go @@ -1,6 +1,9 @@ package config -import "github.com/fluidity-money/long.so/lib/types" +import ( + "github.com/fluidity-money/long.so/lib/types" + "github.com/fluidity-money/long.so/lib/types/seawater" +) const ( // DefaultFusdcDecimals to use as the default for the base asset @@ -13,6 +16,13 @@ const ( DefaultFusdcName = "Fluid USDC" ) +// DefaultPoolConfig, for when we haven't identified the pool manually in +// the past. +var DefaultPoolConfiguration = Pool{ + Displayed: true, + Classification: seawater.ClassificationUnknown, +} + // DefaultFusdcTotalSupply from Superposition Testnet var DefaultFusdcTotalSupply = mustUnscaled("999999999999999999900750000") diff --git a/lib/config/pools.go b/lib/config/pools.go new file mode 100644 index 00000000..18346564 --- /dev/null +++ b/lib/config/pools.go @@ -0,0 +1,9 @@ +package config + +import "github.com/fluidity-money/long.so/lib/types/seawater" + +// Pool config, probably derived from config/pools.toml. +type Pool struct { + Displayed bool `toml:"displayed"` + Classification seawater.Classification `toml:"classification"` +} diff --git a/lib/types/seawater/classifications.go b/lib/types/seawater/classifications.go new file mode 100644 index 00000000..522a8acb --- /dev/null +++ b/lib/types/seawater/classifications.go @@ -0,0 +1,9 @@ +package seawater + +type Classification string + +const ( + ClassificationStablecoin Classification = "STABLECOIN" + ClassificationVolatile Classification = "VOLATILE" + ClassificationUnknown Classification = "UNKNOWN" +) diff --git a/lib/types/types.go b/lib/types/types.go index fc3a88a9..54530c03 100644 --- a/lib/types/types.go +++ b/lib/types/types.go @@ -7,9 +7,10 @@ package types import ( - "fmt" sqlDriver "database/sql/driver" "encoding/hex" + "encoding/json" + "fmt" "math/big" "strings" ) @@ -75,6 +76,7 @@ func (u Number) Hex() string { } return u.Int.Text(16) } + // String the Number, printing its base10 func (u Number) String() string { if u.Int == nil { @@ -127,7 +129,6 @@ func (int *Number) Scan(v interface{}) error { return nil } - func EmptyUnscaledNumber() UnscaledNumber { return UnscaledNumber{new(big.Int)} } @@ -158,6 +159,7 @@ func UnscaledNumberFromHex(v string) (*UnscaledNumber, error) { func (u UnscaledNumber) Hex() string { return u.Int.Text(16) } + // String the UnscaledNumber, printing its base 10 form func (u UnscaledNumber) String() string { return u.Int.Text(10) @@ -218,6 +220,14 @@ func (a Address) String() string { func (a Address) Value() (sqlDriver.Value, error) { return a.String(), nil } +func (a *Address) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + *a = AddressFromString(s) + return nil +} func DataFromString(s string) Data { return Data(strings.ToLower(s))