diff --git a/core/config/docs/core.toml b/core/config/docs/core.toml index b9c1063c12a..a149a7cf4c0 100644 --- a/core/config/docs/core.toml +++ b/core/config/docs/core.toml @@ -13,6 +13,8 @@ FeedsManager = true # Default LogPoller = false # Default # UICSAKeys enables CSA Keys in the UI. UICSAKeys = false # Default +# EAL (Enterprise Abstraction Layer) exposes API endpoints for reading and writing to blockchains. +EAL = false # Default [Database] # DefaultIdleInTxSessionTimeout is the maximum time allowed for a transaction to be open and idle before timing out. See Postgres `idle_in_transaction_session_timeout` for more details. diff --git a/core/config/feature_config.go b/core/config/feature_config.go index fbb3a4ea541..ff9bf89ce21 100644 --- a/core/config/feature_config.go +++ b/core/config/feature_config.go @@ -4,4 +4,5 @@ type Feature interface { FeedsManager() bool UICSAKeys() bool LogPoller() bool + EAL() bool } diff --git a/core/config/toml/types.go b/core/config/toml/types.go index 5fba0c7ea5e..75c30120aaf 100644 --- a/core/config/toml/types.go +++ b/core/config/toml/types.go @@ -286,6 +286,7 @@ type Feature struct { FeedsManager *bool LogPoller *bool UICSAKeys *bool + EAL *bool } func (f *Feature) setFrom(f2 *Feature) { @@ -298,6 +299,9 @@ func (f *Feature) setFrom(f2 *Feature) { if v := f2.UICSAKeys; v != nil { f.UICSAKeys = v } + if v := f2.EAL; v != nil { + f.EAL = v + } } type Database struct { diff --git a/core/scripts/go.mod b/core/scripts/go.mod index bb4b33eb521..f522f5558e9 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -171,7 +171,7 @@ require ( github.com/hdevalence/ed25519consensus v0.1.0 // indirect github.com/holiman/big v0.0.0-20221017200358-a027dc42d04e // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect - github.com/holiman/uint256 v1.2.2 // indirect + github.com/holiman/uint256 v1.2.3 // indirect github.com/huandu/skiplist v1.2.0 // indirect github.com/huin/goupnp v1.0.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -299,6 +299,8 @@ require ( github.com/shirou/gopsutil/v3 v3.23.8 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 // indirect + github.com/smartcontractkit/capital-markets-projects v0.0.0-20230926155417-a3ac6c7f5bea // indirect + github.com/smartcontractkit/capital-markets-projects/lib v0.0.0-20230926155417-a3ac6c7f5bea // indirect github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230913032705-f924d753cc47 // indirect github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230926113942-a871b2976dc1 // indirect github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230831134610-680240b97aca // indirect @@ -350,16 +352,16 @@ require ( go.uber.org/ratelimit v0.2.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/arch v0.4.0 // indirect - golang.org/x/crypto v0.11.0 // indirect + golang.org/x/crypto v0.12.0 // indirect golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.12.0 // indirect + golang.org/x/net v0.14.0 // indirect golang.org/x/sync v0.3.0 // indirect golang.org/x/sys v0.11.0 // indirect - golang.org/x/term v0.10.0 // indirect - golang.org/x/text v0.11.0 // indirect + golang.org/x/term v0.11.0 // indirect + golang.org/x/text v0.12.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.11.0 // indirect + golang.org/x/tools v0.12.0 // indirect gonum.org/v1/gonum v0.13.0 // indirect google.golang.org/genproto v0.0.0-20230717213848-3f92550aa753 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20230717213848-3f92550aa753 // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index 571b9b0e8ef..33f3df6a9d2 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -708,8 +708,8 @@ github.com/holiman/big v0.0.0-20221017200358-a027dc42d04e h1:pIYdhNkDh+YENVNi3gt github.com/holiman/big v0.0.0-20221017200358-a027dc42d04e/go.mod h1:j9cQbcqHQujT0oKJ38PylVfqohClLr3CvDC+Qcg+lhU= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= -github.com/holiman/uint256 v1.2.2 h1:TXKcSGc2WaxPD2+bmzAsVthL4+pEN0YwXcL5qED83vk= -github.com/holiman/uint256 v1.2.2/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= +github.com/holiman/uint256 v1.2.3 h1:K8UWO1HUJpRMXBxbmaY1Y8IAMZC/RsKB+ArEnnK4l5o= +github.com/holiman/uint256 v1.2.3/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c= github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= @@ -1452,6 +1452,10 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 h1:T3lFWumvbfM1u/etVq42Afwq/jtNSBSOA8n5jntnNPo= github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704/go.mod h1:2QuJdEouTWjh5BDy5o/vgGXQtR4Gz8yH1IYB5eT7u4M= +github.com/smartcontractkit/capital-markets-projects v0.0.0-20230926155417-a3ac6c7f5bea h1:20YSrEi88NimVAKuDiwmbI4xbcGEvBb4MlW1vig0euU= +github.com/smartcontractkit/capital-markets-projects v0.0.0-20230926155417-a3ac6c7f5bea/go.mod h1:5G+3CGpX2DYoVOfcrp9IZy7CPTCpL0+pCOmB/3R7t7k= +github.com/smartcontractkit/capital-markets-projects/lib v0.0.0-20230926155417-a3ac6c7f5bea h1:R/e1SuWpEooOxr8F+1kA6PK5vZ9mW3F3BNwg2W6aREE= +github.com/smartcontractkit/capital-markets-projects/lib v0.0.0-20230926155417-a3ac6c7f5bea/go.mod h1:6sAI/Wb424wYzwCa2tn+r+eup2JtxZvY/On3Ct38xp0= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230913032705-f924d753cc47 h1:vdieOW3CZGdD2R5zvCSMS+0vksyExPN3/Fa1uVfld/A= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230913032705-f924d753cc47/go.mod h1:xMwqRdj5vqYhCJXgKVqvyAwdcqM6ZAEhnwEQ4Khsop8= github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230926113942-a871b2976dc1 h1:Db333w+fSm2e18LMikcIQHIZqgxZruW9uCUGJLUC9mI= @@ -1734,8 +1738,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1835,8 +1839,8 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1962,8 +1966,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= -golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1974,8 +1978,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -2050,8 +2054,8 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8= -golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= +golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go index 814c8a1c10b..1095267c66a 100644 --- a/core/services/chainlink/application.go +++ b/core/services/chainlink/application.go @@ -38,6 +38,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/job" "github.com/smartcontractkit/chainlink/v2/core/services/keeper" "github.com/smartcontractkit/chainlink/v2/core/services/keystore" + lgsservice "github.com/smartcontractkit/chainlink/v2/core/services/legacygasstation" "github.com/smartcontractkit/chainlink/v2/core/services/ocr" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2" "github.com/smartcontractkit/chainlink/v2/core/services/ocrbootstrap" @@ -103,6 +104,8 @@ type Application interface { ID() uuid.UUID SecretGenerator() SecretGenerator + + LegacyGasStationRequestRouter() lgsservice.RequestRouter } // ChainlinkApplication contains fields for the JobSubscriber, Scheduler, @@ -135,6 +138,7 @@ type ChainlinkApplication struct { secretGenerator SecretGenerator profiler *pyroscope.Profiler loopRegistry *plugins.LoopRegistry + lgsRequestRouter lgsservice.RequestRouter started bool startStopMu sync.Mutex @@ -322,8 +326,22 @@ func NewApplication(opts ApplicationOpts) (Application, error) { legacyEVMChains, keyStore.Eth(), globalLogger), + job.LegacyGasStationServer: lgsservice.NewServerDelegate( + globalLogger, + legacyEVMChains, + keyStore.Eth(), + db, + cfg.Database(), + ), + job.LegacyGasStationSidecar: lgsservice.NewSidecarDelegate( + globalLogger, + legacyEVMChains, + keyStore.Eth(), + db, + ), } webhookJobRunner = delegates[job.Webhook].(*webhook.Delegate).WebhookJobRunner() + lgsRequestRouter = delegates[job.LegacyGasStationServer].(*lgsservice.Delegate).RequestRouter() ) // Flux monitor requires ethereum just to boot, silence errors with a null delegate @@ -474,7 +492,8 @@ func NewApplication(opts ApplicationOpts) (Application, error) { profiler: profiler, loopRegistry: loopRegistry, - sqlxDB: opts.SqlxDB, + sqlxDB: opts.SqlxDB, + lgsRequestRouter: lgsRequestRouter, // NOTE: Can keep things clean by putting more things in srvcs instead of manually start/closing srvcs: srvcs, @@ -654,6 +673,10 @@ func (app *ChainlinkApplication) SecretGenerator() SecretGenerator { return app.secretGenerator } +func (app *ChainlinkApplication) LegacyGasStationRequestRouter() lgsservice.RequestRouter { + return app.lgsRequestRouter +} + // WakeSessionReaper wakes up the reaper to do its reaping. func (app *ChainlinkApplication) WakeSessionReaper() { app.SessionReaper.WakeUp() diff --git a/core/services/chainlink/config_feature.go b/core/services/chainlink/config_feature.go index 2e968df052d..087b5352d81 100644 --- a/core/services/chainlink/config_feature.go +++ b/core/services/chainlink/config_feature.go @@ -17,3 +17,7 @@ func (f *featureConfig) LogPoller() bool { func (f *featureConfig) UICSAKeys() bool { return *f.c.UICSAKeys } + +func (f *featureConfig) EAL() bool { + return *f.c.EAL +} diff --git a/core/services/chainlink/config_general_test.go b/core/services/chainlink/config_general_test.go index 8e95e389ffc..38c8968fd4b 100644 --- a/core/services/chainlink/config_general_test.go +++ b/core/services/chainlink/config_general_test.go @@ -29,6 +29,7 @@ func TestTOMLGeneralConfig_Defaults(t *testing.T) { assert.False(t, config.CosmosEnabled()) assert.False(t, config.SolanaEnabled()) assert.False(t, config.StarkNetEnabled()) + assert.False(t, config.Feature().EAL()) assert.Equal(t, false, config.JobPipeline().ExternalInitiatorsEnabled()) assert.Equal(t, 15*time.Minute, config.WebServer().SessionTimeout().Duration()) } diff --git a/core/services/chainlink/config_test.go b/core/services/chainlink/config_test.go index 480d06b5806..67a07a4e03c 100644 --- a/core/services/chainlink/config_test.go +++ b/core/services/chainlink/config_test.go @@ -242,6 +242,7 @@ func TestConfig_Marshal(t *testing.T) { FeedsManager: ptr(true), LogPoller: ptr(true), UICSAKeys: ptr(true), + EAL: ptr(true), } full.Database = toml.Database{ DefaultIdleInTxSessionTimeout: models.MustNewDuration(time.Minute), @@ -654,6 +655,7 @@ Headers = ['Authorization: token', 'X-SomeOther-Header: value with spaces | and FeedsManager = true LogPoller = true UICSAKeys = true +EAL = true `}, {"Database", Config{Core: toml.Core{Database: full.Database}}, `[Database] DefaultIdleInTxSessionTimeout = '1m0s' diff --git a/core/services/chainlink/testdata/config-empty-effective.toml b/core/services/chainlink/testdata/config-empty-effective.toml index 45e92a147d3..e3f6bc12ec8 100644 --- a/core/services/chainlink/testdata/config-empty-effective.toml +++ b/core/services/chainlink/testdata/config-empty-effective.toml @@ -6,6 +6,7 @@ ShutdownGracePeriod = '5s' FeedsManager = true LogPoller = false UICSAKeys = false +EAL = false [Database] DefaultIdleInTxSessionTimeout = '1h0m0s' diff --git a/core/services/chainlink/testdata/config-full.toml b/core/services/chainlink/testdata/config-full.toml index 92d0b553d6e..17a181feeac 100644 --- a/core/services/chainlink/testdata/config-full.toml +++ b/core/services/chainlink/testdata/config-full.toml @@ -6,6 +6,7 @@ ShutdownGracePeriod = '10s' FeedsManager = true LogPoller = true UICSAKeys = true +EAL = true [Database] DefaultIdleInTxSessionTimeout = '1m0s' diff --git a/core/services/chainlink/testdata/config-multi-chain-effective.toml b/core/services/chainlink/testdata/config-multi-chain-effective.toml index 665de9be8cb..38c60d10cf7 100644 --- a/core/services/chainlink/testdata/config-multi-chain-effective.toml +++ b/core/services/chainlink/testdata/config-multi-chain-effective.toml @@ -6,6 +6,7 @@ ShutdownGracePeriod = '5s' FeedsManager = true LogPoller = false UICSAKeys = false +EAL = false [Database] DefaultIdleInTxSessionTimeout = '1h0m0s' diff --git a/core/services/eal/blockchain_client.go b/core/services/eal/blockchain_client.go new file mode 100644 index 00000000000..a148133a4eb --- /dev/null +++ b/core/services/eal/blockchain_client.go @@ -0,0 +1,117 @@ +package eal + +import ( + "context" + "math/big" + + "github.com/pkg/errors" + + "github.com/ethereum/go-ethereum" + gethcommon "github.com/ethereum/go-ethereum/common" + libcommon "github.com/smartcontractkit/capital-markets-projects/lib/common" + "github.com/smartcontractkit/capital-markets-projects/lib/web/jsonrpc" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ethkey" +) + +var _ libcommon.BlockchainClientInterface = &BlockchainClient{} + +type BlockchainClient struct { + lggr logger.Logger + txm txmgr.TxManager + gethks keystore.Eth + fromAddresses []ethkey.EIP55Address + chainID uint64 + maxGasLimit uint32 + client client.Client +} + +func NewBlockchainClient( + lggr logger.Logger, + txm txmgr.TxManager, + gethks keystore.Eth, + fromAddresses []ethkey.EIP55Address, + chainID uint64, + cfg config.EVM, + client client.Client, +) (*BlockchainClient, error) { + return &BlockchainClient{ + lggr: lggr, + txm: txm, + gethks: gethks, + fromAddresses: fromAddresses, + chainID: chainID, + maxGasLimit: cfg.GasEstimator().LimitMax(), + client: client, + }, nil +} + +func (c *BlockchainClient) EstimateGas( + ctx context.Context, + address gethcommon.Address, + payload []byte, +) (uint32, error) { + fromAddresses := c.sendingKeys() + fromAddress, err := c.gethks.GetRoundRobinAddress(big.NewInt(0).SetUint64(c.chainID), fromAddresses...) + if err != nil { + return 0, err + } + c.lggr.Debugw("estimate gas details", + "toAddress", address, + "fromAddress", fromAddress, + ) + gasLimit, err := c.client.EstimateGas(ctx, ethereum.CallMsg{ + From: fromAddress, + To: &address, + Data: payload, + }) + if err != nil { + return 0, err + } + + if gasLimit > uint64(c.maxGasLimit) { + return 0, errors.New(jsonrpc.EstimateGasExceededErrorMsg) + } + // safe cast because gas estimator limit max is uint32 + return uint32(gasLimit), nil +} + +// SimulateTransaction makes eth_call to simulate transaction +// TODO: look into accepting optional parameters (gas, gasPrice, value) +func (c *BlockchainClient) SimulateTransaction( + ctx context.Context, + address gethcommon.Address, + payload []byte, + gasLimit uint32, +) error { + fromAddresses := c.sendingKeys() + fromAddress, err := c.gethks.GetRoundRobinAddress(big.NewInt(0).SetUint64(c.chainID), fromAddresses...) + if err != nil { + return err + } + c.lggr.Debugw("eth_call details", + "toAddress", address, + "fromAddress", fromAddress, + "gasLimit", gasLimit, + ) + _, err = c.client.CallContract(ctx, ethereum.CallMsg{ + To: &address, + From: fromAddress, + Data: payload, + Gas: uint64(gasLimit), + }, nil /*blocknumber*/) + + return err +} + +func (c *BlockchainClient) sendingKeys() []gethcommon.Address { + var addresses []gethcommon.Address + for _, a := range c.fromAddresses { + addresses = append(addresses, a.Address()) + } + return addresses +} diff --git a/core/services/eal/blockchain_client_test.go b/core/services/eal/blockchain_client_test.go new file mode 100644 index 00000000000..0292b203135 --- /dev/null +++ b/core/services/eal/blockchain_client_test.go @@ -0,0 +1,139 @@ +package eal + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + "github.com/smartcontractkit/capital-markets-projects/lib/web/jsonrpc" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" + txmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr/mocks" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ethkey" + ksmocks "github.com/smartcontractkit/chainlink/v2/core/services/keystore/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestBlockchainClientEstimateGas(t *testing.T) { + lggr := logger.TestLogger(t) + fromAddress := "0x469aA2CD13e037DC5236320783dCfd0e641c0559" + fromAddresses := []ethkey.EIP55Address{ethkey.EIP55Address(fromAddress)} + ks := ksmocks.NewEth(t) + ks.On("GetRoundRobinAddress", testutils.FixtureChainID, mock.Anything).Maybe().Return(common.HexToAddress(fromAddress), nil) + txm := new(txmmocks.MockEvmTxManager) + + t.Run("happy case", func(t *testing.T) { + mockClient := new(mocks.Client) + mockClient.On("EstimateGas", mock.Anything, mock.Anything).Return(uint64(100_000), nil).Once() + + blockchainClient := &BlockchainClient{ + client: mockClient, + txm: txm, + lggr: lggr, + gethks: ks, + fromAddresses: fromAddresses, + chainID: testutils.FixtureChainID.Uint64(), + maxGasLimit: 1_000_000, + } + + ctx := testutils.Context(t) + gasLimit, err := blockchainClient.EstimateGas(ctx, common.Address{}, []byte{}) + assert.NoError(t, err) + assert.Equal(t, uint32(100_000), gasLimit) + + // Verify that the EstimateGas method on the mock client was called with the expected arguments + mockClient.AssertCalled(t, "EstimateGas", ctx, mock.Anything) + }) + + t.Run("execution reverted", func(t *testing.T) { + mockClient := new(mocks.Client) + mockClient.On("EstimateGas", mock.Anything, mock.Anything).Return(uint64(0), errors.New(jsonrpc.ExecutionRevertedErrorMsg)).Once() + + blockchainClient := &BlockchainClient{ + client: mockClient, + txm: txm, + lggr: lggr, + gethks: ks, + fromAddresses: fromAddresses, + chainID: testutils.FixtureChainID.Uint64(), + maxGasLimit: 1_000_000, + } + + ctx := testutils.Context(t) + _, err := blockchainClient.EstimateGas(ctx, common.Address{}, []byte{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), jsonrpc.ExecutionRevertedErrorMsg) + + // Verify that the EstimateGas method on the mock client was called with the expected arguments + mockClient.AssertCalled(t, "EstimateGas", ctx, mock.Anything) + }) + + t.Run("gas limit exceeded", func(t *testing.T) { + mockClient := new(mocks.Client) + mockClient.On("EstimateGas", mock.Anything, mock.Anything).Return(uint64(100_000_000), nil).Once() + + blockchainClient := &BlockchainClient{ + client: mockClient, + txm: txm, + lggr: lggr, + gethks: ks, + fromAddresses: fromAddresses, + chainID: testutils.FixtureChainID.Uint64(), + maxGasLimit: 1_000_000, + } + + ctx := testutils.Context(t) + _, err := blockchainClient.EstimateGas(ctx, common.Address{}, []byte{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), jsonrpc.EstimateGasExceededErrorMsg) + + // Verify that the EstimateGas method on the mock client was called with the expected arguments + mockClient.AssertCalled(t, "EstimateGas", ctx, mock.Anything) + }) +} + +func TestBlockchainClientSimulateTransaction(t *testing.T) { + lggr := logger.TestLogger(t) + fromAddress := "0x469aA2CD13e037DC5236320783dCfd0e641c0559" + fromAddresses := []ethkey.EIP55Address{ethkey.EIP55Address(fromAddress)} + ks := ksmocks.NewEth(t) + ks.On("GetRoundRobinAddress", testutils.FixtureChainID, mock.Anything).Maybe().Return(common.HexToAddress(fromAddress), nil) + txm := new(txmmocks.MockEvmTxManager) + + t.Run("happy case", func(t *testing.T) { + mockClient := new(mocks.Client) + result := []byte("result") + mockClient.On("CallContract", mock.Anything, mock.Anything, mock.Anything).Return(result, nil).Once() + blockchainClient := &BlockchainClient{ + client: mockClient, + txm: txm, + lggr: lggr, + gethks: ks, + fromAddresses: fromAddresses, + chainID: testutils.FixtureChainID.Uint64(), + maxGasLimit: 1_000_000, + } + ctx := testutils.Context(t) + err := blockchainClient.SimulateTransaction(ctx, common.Address{}, []byte{}, uint32(100_000)) + assert.NoError(t, err) + }) + + t.Run("execution reverted", func(t *testing.T) { + mockClient := new(mocks.Client) + mockClient.On("CallContract", mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New(jsonrpc.ExecutionRevertedErrorMsg)).Once() + blockchainClient := &BlockchainClient{ + client: mockClient, + txm: txm, + lggr: lggr, + gethks: ks, + fromAddresses: fromAddresses, + chainID: testutils.FixtureChainID.Uint64(), + maxGasLimit: 1_000_000, + } + ctx := testutils.Context(t) + err := blockchainClient.SimulateTransaction(ctx, common.Address{}, []byte{}, uint32(100_000)) + assert.Error(t, err) + }) +} diff --git a/core/services/job/job_orm_test.go b/core/services/job/job_orm_test.go index bf81064e6a6..232d22f96c7 100644 --- a/core/services/job/job_orm_test.go +++ b/core/services/job/job_orm_test.go @@ -17,6 +17,7 @@ import ( "github.com/smartcontractkit/chainlink-relay/pkg/types" + "github.com/smartcontractkit/chainlink/v2/core/services/legacygasstation" "github.com/smartcontractkit/chainlink/v2/core/services/relay" "github.com/smartcontractkit/chainlink/v2/core/assets" @@ -396,6 +397,59 @@ func TestORM_DeleteJob_DeletesAssociatedRecords(t *testing.T) { cltest.AssertCount(t, db, "jobs", 0) }) + t.Run("it creates and deletes records for legacy gas station server job", func(t *testing.T) { + jb, err := legacygasstation.ValidatedServerSpec( + testspecs.GenerateLegacyGasStationServerSpec(testspecs.LegacyGasStationServerSpecParams{}).Toml()) + require.NoError(t, err) + + err = jobORM.CreateJob(&jb) + require.NoError(t, err) + savedJob, err := jobORM.FindJob(testutils.Context(t), jb.ID) + require.NoError(t, err) + require.Equal(t, jb.ID, savedJob.ID) + require.Equal(t, jb.Type, savedJob.Type) + require.Equal(t, jb.LegacyGasStationServerSpec.ID, savedJob.LegacyGasStationServerSpec.ID) + require.Equal(t, jb.LegacyGasStationServerSpec.ForwarderAddress, savedJob.LegacyGasStationServerSpec.ForwarderAddress) + require.Equal(t, jb.LegacyGasStationServerSpec.EVMChainID, savedJob.LegacyGasStationServerSpec.EVMChainID) + require.Equal(t, jb.LegacyGasStationServerSpec.CCIPChainSelector, savedJob.LegacyGasStationServerSpec.CCIPChainSelector) + require.Equal(t, jb.LegacyGasStationServerSpec.FromAddresses, savedJob.LegacyGasStationServerSpec.FromAddresses) + err = jobORM.DeleteJob(jb.ID) + require.NoError(t, err) + _, err = jobORM.FindJob(testutils.Context(t), jb.ID) + require.Error(t, err) + }) + + t.Run("it creates and deletes records for legacy gas station sidecar jobs", func(t *testing.T) { + jb, err := legacygasstation.ValidatedSidecarSpec( + testspecs.GenerateLegacyGasStationSidecarSpec(testspecs.LegacyGasStationSidecarSpecParams{ + ClientCertificate: ptr("clientCertificate"), + ClientKey: ptr("clientKey"), + }).Toml()) + require.NoError(t, err) + + err = jobORM.CreateJob(&jb) + require.NoError(t, err) + savedJob, err := jobORM.FindJob(testutils.Context(t), jb.ID) + require.NoError(t, err) + require.Equal(t, jb.ID, savedJob.ID) + require.Equal(t, jb.Type, savedJob.Type) + require.Equal(t, jb.LegacyGasStationSidecarSpec.ID, savedJob.LegacyGasStationSidecarSpec.ID) + require.Equal(t, jb.LegacyGasStationSidecarSpec.ForwarderAddress, savedJob.LegacyGasStationSidecarSpec.ForwarderAddress) + require.Equal(t, jb.LegacyGasStationSidecarSpec.OffRampAddress, savedJob.LegacyGasStationSidecarSpec.OffRampAddress) + require.Equal(t, jb.LegacyGasStationSidecarSpec.LookbackBlocks, savedJob.LegacyGasStationSidecarSpec.LookbackBlocks) + require.Equal(t, jb.LegacyGasStationSidecarSpec.PollPeriod, savedJob.LegacyGasStationSidecarSpec.PollPeriod) + require.Equal(t, jb.LegacyGasStationSidecarSpec.RunTimeout, savedJob.LegacyGasStationSidecarSpec.RunTimeout) + require.Equal(t, jb.LegacyGasStationSidecarSpec.EVMChainID, savedJob.LegacyGasStationSidecarSpec.EVMChainID) + require.Equal(t, jb.LegacyGasStationSidecarSpec.CCIPChainSelector, savedJob.LegacyGasStationSidecarSpec.CCIPChainSelector) + require.Equal(t, jb.LegacyGasStationSidecarSpec.StatusUpdateURL, savedJob.LegacyGasStationSidecarSpec.StatusUpdateURL) + require.Equal(t, jb.LegacyGasStationSidecarSpec.ClientCertificate, savedJob.LegacyGasStationSidecarSpec.ClientCertificate) + require.Equal(t, jb.LegacyGasStationSidecarSpec.ClientKey, savedJob.LegacyGasStationSidecarSpec.ClientKey) + err = jobORM.DeleteJob(jb.ID) + require.NoError(t, err) + _, err = jobORM.FindJob(testutils.Context(t), jb.ID) + require.Error(t, err) + }) + t.Run("does not allow to delete external initiators if they have referencing external_initiator_webhook_specs", func(t *testing.T) { // create new db because this will rollback transaction and poison it db := pgtest.NewSqlxDB(t) diff --git a/core/services/job/models.go b/core/services/job/models.go index 5787ce5fb5f..101bdae9809 100644 --- a/core/services/job/models.go +++ b/core/services/job/models.go @@ -714,6 +714,15 @@ type LegacyGasStationSidecarSpec struct { // CCIPChainSelector is the CCIP chain selector that corresponds to EVMChainID param CCIPChainSelector *utils.Big `toml:"ccipChainSelector"` + // StatusUpdateURL is the endpoint URL where the sidecar posts status updates + StatusUpdateURL string `toml:"statusUpdateURL"` + + // ClientCertificate is the x.509 certificate used for mTLS connection with StatusUpdateURL + ClientCertificate *string `toml:"clientCertificate"` + + // ClientCertificate is the x.509 key used for mTLS connection with StatusUpdateURL + ClientKey *string `toml:"clientKey"` + // CreatedAt is the time this job was created. CreatedAt time.Time `toml:"-"` diff --git a/core/services/job/orm.go b/core/services/job/orm.go index 369ce039ad4..5ea50cf82e3 100644 --- a/core/services/job/orm.go +++ b/core/services/job/orm.go @@ -424,8 +424,8 @@ func (o *orm) CreateJob(jb *Job, qopts ...pg.QOpt) error { return errors.New("evm chain id must be defined") } var specID int32 - sql := `INSERT INTO legacy_gas_station_sidecar_specs (forwarder_address, off_ramp_address, lookback_blocks, poll_period, run_timeout, evm_chain_id, ccip_chain_selector, created_at, updated_at) - VALUES (:forwarder_address, :off_ramp_address, :lookback_blocks, :poll_period, :run_timeout, :evm_chain_id, :ccip_chain_selector, NOW(), NOW()) + sql := `INSERT INTO legacy_gas_station_sidecar_specs (forwarder_address, off_ramp_address, lookback_blocks, poll_period, run_timeout, evm_chain_id, ccip_chain_selector, status_update_url, client_certificate, client_key, created_at, updated_at) + VALUES (:forwarder_address, :off_ramp_address, :lookback_blocks, :poll_period, :run_timeout, :evm_chain_id, :ccip_chain_selector, :status_update_url, :client_certificate, :client_key, NOW(), NOW()) RETURNING id;` if err := pg.PrepareQueryRowx(tx, sql, &specID, jb.LegacyGasStationSidecarSpec); err != nil { return errors.Wrap(err, "failed to create LegacyGasStationSidecar spec") diff --git a/core/services/legacygasstation/blockchain_transactor.go b/core/services/legacygasstation/blockchain_transactor.go new file mode 100644 index 00000000000..2f4d5be9e00 --- /dev/null +++ b/core/services/legacygasstation/blockchain_transactor.go @@ -0,0 +1,112 @@ +package legacygasstation + +import ( + "context" + "encoding/hex" + "math/big" + + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/smartcontractkit/capital-markets-projects/lib/services/legacygasstation" + "github.com/smartcontractkit/capital-markets-projects/lib/services/legacygasstation/types" + txmgrcommon "github.com/smartcontractkit/chainlink/v2/common/txmgr" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ethkey" + "github.com/smartcontractkit/sqlx" +) + +var _ legacygasstation.BlockchainTransactor = &blockchainTransactor{} + +type blockchainTransactor struct { + lggr logger.Logger + db *sqlx.DB + txm txmgr.TxManager + gethks keystore.Eth + fromAddresses []ethkey.EIP55Address + chainID uint64 + orm legacygasstation.ORM +} + +func NewBlockchainTransactor( + lggr logger.Logger, + db *sqlx.DB, + txm txmgr.TxManager, + gethks keystore.Eth, + fromAddresses []ethkey.EIP55Address, + chainID uint64, + orm legacygasstation.ORM, +) (*blockchainTransactor, error) { + return &blockchainTransactor{ + lggr: lggr, + db: db, + txm: txm, + gethks: gethks, + fromAddresses: fromAddresses, + chainID: chainID, + orm: orm, + }, nil +} + +// CreateAndStoreTransaction creates eth transaction and persists data in a transaction +// to avoid partial failures, which would leave the persistence layer in inconsistent state +func (t *blockchainTransactor) CreateAndStoreTransaction( + ctx context.Context, + address gethcommon.Address, + payload []byte, + gasLimit uint32, + req types.SendTransactionRequest, + requestID string, +) error { + fromAddresses := t.sendingKeys() + fromAddress, err := t.gethks.GetRoundRobinAddress(big.NewInt(0).SetUint64(t.chainID), fromAddresses...) + if err != nil { + return err + } + + // idempotencyKey is unique because payload contains nonce that can only be used once + idempotencyKey := hex.EncodeToString(crypto.Keccak256(payload)[:]) + + txmTx, err := t.txm.CreateTransaction(txmgr.TxRequest{ + IdempotencyKey: &idempotencyKey, + FromAddress: fromAddress, + ToAddress: address, + EncodedPayload: payload, + FeeLimit: gasLimit, + Strategy: txmgrcommon.NewSendEveryStrategy(), + }) + if err != nil { + return err + } + + t.lggr.Debugw("created Eth tx", "ethTxID", txmTx.GetID()) + + gaslessTx := types.LegacyGaslessTx{ + ID: requestID, + From: req.From, + Target: req.Target, + Forwarder: address, + Nonce: req.Nonce, + Receiver: req.Receiver, + Amount: req.Amount, + SourceChainID: req.SourceChainID, + DestinationChainID: req.DestinationChainID, + ValidUntilTime: req.ValidUntilTime, + Signature: req.Signature, + Status: types.Submitted, + TokenName: req.TargetName, + TokenVersion: req.Version, + EthTxID: txmTx.GetID(), + } + + return t.orm.InsertLegacyGaslessTx(ctx, gaslessTx) +} + +func (t *blockchainTransactor) sendingKeys() []gethcommon.Address { + var addresses []gethcommon.Address + for _, a := range t.fromAddresses { + addresses = append(addresses, a.Address()) + } + return addresses +} diff --git a/core/services/legacygasstation/integration_test.go b/core/services/legacygasstation/integration_test.go new file mode 100644 index 00000000000..2e98660473f --- /dev/null +++ b/core/services/legacygasstation/integration_test.go @@ -0,0 +1,316 @@ +package legacygasstation_test + +import ( + "bytes" + "encoding/json" + "fmt" + "math/big" + "net/http" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/eth/ethconfig" + "github.com/google/uuid" + "github.com/onsi/gomega" + "github.com/smartcontractkit/sqlx" + "github.com/test-go/testify/require" + + "github.com/smartcontractkit/capital-markets-projects/core/gethwrappers/legacygasstation/generated/bank_erc20" + forwarder_wrapper "github.com/smartcontractkit/capital-markets-projects/core/gethwrappers/legacygasstation/generated/legacy_gas_station_forwarder" + lgslib "github.com/smartcontractkit/capital-markets-projects/lib/services/legacygasstation" + "github.com/smartcontractkit/capital-markets-projects/lib/services/legacygasstation/types" + "github.com/smartcontractkit/chainlink/v2/core/assets" + "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" + "github.com/smartcontractkit/chainlink/v2/core/internal/cltest/heavyweight" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" + "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ethkey" + "github.com/smartcontractkit/chainlink/v2/core/services/legacygasstation" + "github.com/smartcontractkit/chainlink/v2/core/store/models" + "github.com/smartcontractkit/chainlink/v2/core/testdata/testspecs" +) + +const ( + BankERC20TokenName = "BankToken" + BankERC20TokenVersion = "1" +) + +func TestIntegration_LegacyGasStation_SameChainTransfer(t *testing.T) { + // owner deploys forwarder and token contracts + _, owner := generateKeyAndTransactor(t, testutils.SimulatedChainID) + // relay is a CL-owned address that posts txs + relayKey, relay := generateKeyAndTransactor(t, testutils.SimulatedChainID) + // sender transfers token to receiver + senderKey, sender := generateKeyAndTransactor(t, testutils.SimulatedChainID) + // receiver receives token from sender + _, receiver := generateKeyAndTransactor(t, testutils.SimulatedChainID) + + genesisData := core.GenesisAlloc{ + owner.From: {Balance: assets.Ether(1000).ToInt()}, + relay.From: {Balance: assets.Ether(1000).ToInt()}, + } + gasLimit := uint32(ethconfig.Defaults.Miner.GasCeil) + backend := cltest.NewSimulatedBackend(t, genesisData, gasLimit) + + ccipChainSelector := uint64(12345) + // no offramp or router address needed for same-chain transfer + dummyOffRampAddress := common.HexToAddress("0x1") + dummyCCIPRouter := common.HexToAddress("0x2") + forwarder, bankERC20, forwarderAddress, bankERC20Address := setupTokenAndForwarderContracts(t, owner, backend, dummyCCIPRouter, ccipChainSelector) + + amount := big.NewInt(1e18) + transferToken(t, bankERC20, owner, sender, amount, backend) + + config, db := setUpDB(t) + app := cltest.NewApplicationWithConfigV2AndKeyOnSimulatedBlockchain(t, config, backend, relayKey) + require.NoError(t, app.Start(testutils.Context(t))) + orm := legacygasstation.NewORM(db, app.Logger, app.Config.Database()) + + createLegacyGasStationServerJob(t, app, forwarderAddress, testutils.SimulatedChainID.Uint64(), ccipChainSelector, relayKey) + statusUpdateServer := lgslib.NewUnstartedStatusUpdateServer(t) + go statusUpdateServer.Start() + defer statusUpdateServer.Stop() + createLegacyGasStationSidecarJob(t, app, forwarderAddress, dummyOffRampAddress, ccipChainSelector, testutils.SimulatedChainID.Uint64(), statusUpdateServer) + + t.Run("single same-chain meta transfer", func(t *testing.T) { + req := generateRequest(t, backend, forwarder, bankERC20Address, senderKey, receiver.From, amount, ccipChainSelector, ccipChainSelector) + requestID := sendTransaction(t, req, app.Server.URL) + verifySameChainTransfer(t, orm, backend, bankERC20, requestID, receiver, amount, ccipChainSelector) + }) +} + +func verifyTxStatus(t *testing.T, orm lgslib.ORM, backend *backends.SimulatedBackend, requestID string, sourceChainCCIPSelector uint64, status types.Status) { + gomega.NewWithT(t).Eventually(func() bool { + backend.Commit() + txs, err := orm.SelectBySourceChainIDAndStatus(testutils.Context(t), sourceChainCCIPSelector, status) + require.NoError(t, err) + for _, tx := range txs { + if tx.Status == status && tx.ID == requestID { + return true + } + } + return false + }, testutils.WaitTimeout(t), time.Second).Should(gomega.BeTrue()) +} + +func verifySameChainTransfer(t *testing.T, orm lgslib.ORM, backend *backends.SimulatedBackend, bankERC20 *bank_erc20.BankERC20, requestID string, receiver *bind.TransactOpts, amount *big.Int, ccipChainSelector uint64) { + // verify same-chain meta transaction + gomega.NewWithT(t).Eventually(func() bool { + backend.Commit() + amountReceived, err := bankERC20.BalanceOf(nil, receiver.From) + require.NoError(t, err) + return amountReceived.Cmp(amount) == 0 + }, testutils.WaitTimeout(t), time.Second).Should(gomega.BeTrue()) + + // verify legacy_gasless_txs has correct status + verifyTxStatus(t, orm, backend, requestID, ccipChainSelector, types.Finalized) +} + +func ptr[T any](t T) *T { return &t } + +func createLegacyGasStationServerJob( + t *testing.T, + app chainlink.Application, + forwarderAddress common.Address, + evmChainID uint64, + ccipChainSelector uint64, + fromKey ethkey.KeyV2, +) job.Job { + jid := uuid.New() + jobName := fmt.Sprintf("legacygasstationserver-%s", jid.String()) + s := testspecs.GenerateLegacyGasStationServerSpec(testspecs.LegacyGasStationServerSpecParams{ + JobID: jid.String(), + Name: jobName, + ForwarderAddress: forwarderAddress.Hex(), + EVMChainID: evmChainID, + CCIPChainSelector: ccipChainSelector, + FromAddresses: []string{fromKey.Address.String()}, + }).Toml() + jb, err := legacygasstation.ValidatedServerSpec(s) + require.NoError(t, err) + err = app.AddJobV2(testutils.Context(t), &jb) + require.NoError(t, err) + // Wait until all jobs are active and listening for logs + gomega.NewWithT(t).Eventually(func() bool { + jbs := app.JobSpawner().ActiveJobs() + for _, jb := range jbs { + return jb.Name.String == jobName + } + return false + }, testutils.WaitTimeout(t), 100*time.Millisecond).Should(gomega.BeTrue()) + return jb +} + +func createLegacyGasStationSidecarJob( + t *testing.T, + app chainlink.Application, + forwarderAddress, + offRampAddress common.Address, + ccipChainSelector uint64, + evmChainID uint64, + server lgslib.TestStatusUpdateServer, +) job.Job { + jid := uuid.New() + jobName := fmt.Sprintf("legacygasstationsidecar-%s", jid.String()) + s := testspecs.GenerateLegacyGasStationSidecarSpec(testspecs.LegacyGasStationSidecarSpecParams{ + JobID: jid.String(), + Name: jobName, + ForwarderAddress: forwarderAddress.Hex(), + OffRampAddress: offRampAddress.Hex(), + EVMChainID: evmChainID, + CCIPChainSelector: ccipChainSelector, + StatusUpdateURL: fmt.Sprintf("http://localhost:%d/return_success", server.Port), + }).Toml() + jb, err := legacygasstation.ValidatedSidecarSpec(s) + require.NoError(t, err) + err = app.AddJobV2(testutils.Context(t), &jb) + require.NoError(t, err) + // Wait until all jobs are active and listening for logs + gomega.NewWithT(t).Eventually(func() bool { + jbs := app.JobSpawner().ActiveJobs() + for _, jb := range jbs { + return jb.Name.String == jobName + } + return false + }, testutils.WaitTimeout(t), 100*time.Millisecond).Should(gomega.BeTrue()) + return jb +} + +func setUpForwarder(t *testing.T, owner *bind.TransactOpts, chain *backends.SimulatedBackend) (common.Address, *forwarder_wrapper.LegacyGasStationForwarder) { + // deploys EIP 2771 forwarder contract that verifies signatures from meta transaction and forwards the call to recipient contract (i.e BankERC20 token) + forwarderAddress, _, forwarder, err := forwarder_wrapper.DeployLegacyGasStationForwarder(owner, chain) + require.NoError(t, err) + chain.Commit() + // registers EIP712-compliant domain separator for BankERC20 token + _, err = forwarder.RegisterDomainSeparator(owner, BankERC20TokenName, BankERC20TokenVersion) + require.NoError(t, err) + chain.Commit() + + return forwarderAddress, forwarder +} + +func setUpBankERC20(t *testing.T, owner *bind.TransactOpts, chain *backends.SimulatedBackend, forwarderAddress, routerAddress, ccipFeeProvider common.Address, totalSupply *big.Int, chainID uint64) (common.Address, *bank_erc20.BankERC20) { + // deploys BankERC20 token that enables meta transactions for same-chain and cross-chain token transfers + tokenAddress, _, token, err := bank_erc20.DeployBankERC20( + owner, chain, "BankToken", "BANK", big.NewInt(0).Mul(totalSupply, big.NewInt(1e18)), forwarderAddress, routerAddress, ccipFeeProvider, chainID) + require.NoError(t, err) + chain.Commit() + return tokenAddress, token +} + +func transferToken(t *testing.T, token *bank_erc20.BankERC20, sender, receiver *bind.TransactOpts, amount *big.Int, chain *backends.SimulatedBackend) { + senderBalanceBefore, err := token.BalanceOf(nil, sender.From) + require.NoError(t, err) + chain.Commit() + + _, err = token.Transfer(sender, receiver.From, amount) + require.NoError(t, err) + chain.Commit() + + receiverBal, err := token.BalanceOf(nil, receiver.From) + require.NoError(t, err) + require.Equal(t, amount, receiverBal) + + senderBal, err := token.BalanceOf(nil, sender.From) + require.NoError(t, err) + require.Equal(t, senderBalanceBefore.Sub(senderBalanceBefore, amount), senderBal) +} + +func setUpDB(t *testing.T) (cfg chainlink.GeneralConfig, db *sqlx.DB) { + cfg, db = heavyweight.FullTestDBV2(t, "legacy_gas_station_integration_test", func(c *chainlink.Config, s *chainlink.Secrets) { + require.Zero(t, testutils.SimulatedChainID.Cmp(c.EVM[0].ChainID.ToInt())) + c.EVM[0].GasEstimator.Mode = ptr("FixedPrice") + c.EVM[0].GasEstimator.LimitDefault = ptr[uint32](2_000_000) + + c.EVM[0].HeadTracker.MaxBufferSize = ptr[uint32](100) + c.EVM[0].HeadTracker.SamplingInterval = models.MustNewDuration(0) // Head sampling disabled + + c.EVM[0].Transactions.ResendAfterThreshold = models.MustNewDuration(0) + c.EVM[0].Transactions.ReaperThreshold = models.MustNewDuration(100 * time.Millisecond) + + c.EVM[0].FinalityDepth = ptr[uint32](1) + c.Feature.LogPoller = ptr(true) + c.Feature.EAL = ptr(true) + }) + return +} + +func sendTransaction(t *testing.T, req types.SendTransactionRequest, url string) string { + body, err := json.Marshal(req) + require.NoError(t, err) + + resp, err := http.Post(fmt.Sprintf("%s/%s", url, "gasstation/send_transaction"), + "application/json", + bytes.NewBuffer(body), + ) + require.NoError(t, err) + require.Equal(t, resp.StatusCode, http.StatusOK) + + var jsonResp map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&jsonResp) + require.NoError(t, err) + require.Equal(t, "2.0", jsonResp["jsonrpc"]) + require.NotNil(t, jsonResp["result"]) + decodedJsonResp, ok := jsonResp["result"].(map[string]interface{}) + require.True(t, ok) + require.NotNil(t, decodedJsonResp["request_id"]) + requestID, ok := decodedJsonResp["request_id"].(string) + require.True(t, ok) + return uuid.MustParse(requestID).String() +} + +func setupTokenAndForwarderContracts( + t *testing.T, + deployer *bind.TransactOpts, + backend *backends.SimulatedBackend, + ccipRouterAddress common.Address, + ccipChainSelector uint64, +) (*forwarder_wrapper.LegacyGasStationForwarder, *bank_erc20.BankERC20, common.Address, common.Address) { + forwarderAddress, forwarder := setUpForwarder(t, deployer, backend) + bankERC20Address, bankERC20 := setUpBankERC20(t, deployer, backend, forwarderAddress, ccipRouterAddress, deployer.From, big.NewInt(1e9), ccipChainSelector) + return forwarder, bankERC20, forwarderAddress, bankERC20Address +} + +func generateKeyAndTransactor(t *testing.T, chainID *big.Int) (key ethkey.KeyV2, transactor *bind.TransactOpts) { + key = cltest.MustGenerateRandomKey(t) + transactor, err := bind.NewKeyedTransactorWithChainID(key.ToEcdsaPrivKey(), chainID) + require.NoError(t, err) + return +} + +func generateRequest( + t *testing.T, + backend *backends.SimulatedBackend, + forwarder *forwarder_wrapper.LegacyGasStationForwarder, + bankERC20Address common.Address, + senderKey ethkey.KeyV2, + receiver common.Address, + amount *big.Int, + sourceChainSelector uint64, + destChainSelector uint64, +) types.SendTransactionRequest { + _, calldataHash, err := lgslib.GenerateMetaTransferCalldata(receiver, amount, destChainSelector) + require.NoError(t, err) + deadline := big.NewInt(int64(backend.Blockchain().CurrentHeader().Time + uint64(time.Hour))) + signature, _, _, forwarderNonce, err := lgslib.SignMetaTransfer(*forwarder, senderKey.ToEcdsaPrivKey(), senderKey.Address, bankERC20Address, calldataHash, deadline, BankERC20TokenName, BankERC20TokenVersion) + require.NoError(t, err) + + return types.SendTransactionRequest{ + From: senderKey.Address, + Target: bankERC20Address, + TargetName: BankERC20TokenName, + Version: BankERC20TokenVersion, + Nonce: forwarderNonce, + Receiver: receiver, + Amount: amount, + SourceChainID: sourceChainSelector, + DestinationChainID: destChainSelector, + ValidUntilTime: deadline, + Signature: signature, + } +} diff --git a/core/services/legacygasstation/log_poller_adapter.go b/core/services/legacygasstation/log_poller_adapter.go new file mode 100644 index 00000000000..af195b5683c --- /dev/null +++ b/core/services/legacygasstation/log_poller_adapter.go @@ -0,0 +1,50 @@ +package legacygasstation + +import ( + "context" + + "github.com/smartcontractkit/capital-markets-projects/lib/services/legacygasstation" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/services/pg" + + gethcommon "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" +) + +var _ legacygasstation.LogPoller = &logPollerAdapter{} + +type logPollerAdapter struct { + lp logpoller.LogPoller +} + +func NewLogPollerAdapter(lp logpoller.LogPoller) *logPollerAdapter { + return &logPollerAdapter{lp: lp} +} + +func (a *logPollerAdapter) LatestBlock(ctx context.Context) (int64, error) { + return a.lp.LatestBlock(pg.WithParentCtx(ctx)) +} + +func (a logPollerAdapter) FilterName(id string, args ...any) string { + return logpoller.FilterName(id, args) +} + +func (a *logPollerAdapter) RegisterFilter(ctx context.Context, name string, eventSigs []gethcommon.Hash, addresses []gethcommon.Address) error { + return a.lp.RegisterFilter(logpoller.Filter{ + Name: name, + EventSigs: eventSigs, + Addresses: addresses, + }, pg.WithParentCtx(ctx)) +} + +func (a *logPollerAdapter) IndexedLogsByBlockRange(ctx context.Context, start, end int64, eventSig gethcommon.Hash, address gethcommon.Address, topicIndex int, topicValues []gethcommon.Hash) ([]gethtypes.Log, error) { + logs, err := a.lp.IndexedLogsByBlockRange(start, end, eventSig, address, topicIndex, topicValues, pg.WithParentCtx(ctx)) + if err != nil { + return nil, err + } + var gethlogs []gethtypes.Log + for _, l := range logs { + gethlogs = append(gethlogs, l.ToGethLog()) + } + return gethlogs, nil +} diff --git a/core/services/legacygasstation/orm.go b/core/services/legacygasstation/orm.go new file mode 100644 index 00000000000..3a47128ca94 --- /dev/null +++ b/core/services/legacygasstation/orm.go @@ -0,0 +1,270 @@ +package legacygasstation + +import ( + "context" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/lib/pq" + "github.com/pkg/errors" + "github.com/smartcontractkit/sqlx" + + "github.com/smartcontractkit/capital-markets-projects/lib/services/legacygasstation" + "github.com/smartcontractkit/capital-markets-projects/lib/services/legacygasstation/types" + txmgrtypes "github.com/smartcontractkit/chainlink/v2/common/txmgr/types" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/pg" + "github.com/smartcontractkit/chainlink/v2/core/utils" +) + +var _ legacygasstation.ORM = &orm{} + +type orm struct { + q pg.Q + lggr logger.Logger +} + +const InsertLegacyGaslessTxQuery = `INSERT INTO legacy_gasless_txs (legacy_gasless_tx_id, forwarder_address, from_address, target_address, receiver_address, nonce, amount, source_chain_id, destination_chain_id, valid_until_time, tx_signature, tx_status, token_name, token_version, eth_tx_id, created_at, updated_at) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW(), NOW())` + +const UpdateLegacyGaslessTxQuery = `UPDATE legacy_gasless_txs SET +tx_status = $2, +ccip_message_id = $3, +failure_reason = $4, +tx_hash = $5, +updated_at = NOW() +WHERE legacy_gasless_tx_id = $1` + +// NewORM creates an ORM scoped to chainID. +// TODO: implement pruning logic if needed +func NewORM(db *sqlx.DB, lggr logger.Logger, cfg pg.QConfig) legacygasstation.ORM { + namedLogger := lggr.Named("LegacyGasStation") + q := pg.NewQ(db, namedLogger, cfg) + return &orm{ + q: q, + lggr: namedLogger, + } +} + +type DbLegacyGaslessTx struct { + ID string `db:"legacy_gasless_tx_id"` // UUID + Forwarder common.Address `db:"forwarder_address"` // forwarder contract + From common.Address `db:"from_address"` // token sender + Target common.Address `db:"target_address"` // token contract + Receiver common.Address `db:"receiver_address"` // token receiver + Nonce *utils.Big // forwarder nonce + Amount *utils.Big // token amount to be transferred + SourceChainID uint64 // meta-transaction source chain ID. This is CCIP chain selector instead of EVM chain ID. + DestinationChainID uint64 // meta-transaction destination chain ID. This is CCIP chain selector instead of EVM chain ID. + ValidUntilTime *utils.Big // unix timestamp of meta-transaction expiry in seconds + Signature []byte `db:"tx_signature"` // EIP712 signature + Status string `db:"tx_status"` // status of meta-transaction + FailureReason *string // failure reason of meta-transaction TODO: change this to sql.NullString + TokenName string // name of token used to generate EIP712 domain separator hash + TokenVersion string // version of token used to generate EIP712 domain separator hash + CCIPMessageID *common.Hash `db:"ccip_message_id"` // CCIP message ID + EthTxID string `db:"eth_tx_id"` // tx ID in transaction manager + TxHash *common.Hash `db:"tx_hash"` // transaction hash on source chain + CreatedAt time.Time + UpdatedAt time.Time +} + +func toDbLegacyGaslessTx(tx types.LegacyGaslessTx) DbLegacyGaslessTx { + return DbLegacyGaslessTx{ + ID: tx.ID, + Forwarder: tx.Forwarder, + From: tx.From, + Target: tx.Target, + Receiver: tx.Receiver, + Nonce: utils.NewBig(tx.Nonce), + Amount: utils.NewBig(tx.Amount), + SourceChainID: tx.SourceChainID, + DestinationChainID: tx.DestinationChainID, + ValidUntilTime: utils.NewBig(tx.ValidUntilTime), + Signature: tx.Signature[:], + Status: tx.Status.String(), + FailureReason: tx.FailureReason, + TokenName: tx.TokenName, + TokenVersion: tx.TokenVersion, + CCIPMessageID: tx.CCIPMessageID, + EthTxID: tx.EthTxID, + TxHash: tx.TxHash, + CreatedAt: tx.CreatedAt, + UpdatedAt: tx.UpdatedAt, + } +} + +func toLegacyGaslessTx(dbTx DbLegacyGaslessTx) (*types.LegacyGaslessTx, error) { + var status types.Status + err := status.Scan(dbTx.Status) + if err != nil { + return nil, err + } + return &types.LegacyGaslessTx{ + ID: dbTx.ID, + Forwarder: dbTx.Forwarder, + From: dbTx.From, + Target: dbTx.Target, + Receiver: dbTx.Receiver, + Nonce: dbTx.Nonce.ToInt(), + Amount: dbTx.Amount.ToInt(), + SourceChainID: dbTx.SourceChainID, + DestinationChainID: dbTx.DestinationChainID, + ValidUntilTime: dbTx.ValidUntilTime.ToInt(), + Signature: dbTx.Signature, + Status: status, + FailureReason: dbTx.FailureReason, + TokenName: dbTx.TokenName, + TokenVersion: dbTx.TokenVersion, + CCIPMessageID: dbTx.CCIPMessageID, + EthTxID: dbTx.EthTxID, + TxHash: dbTx.TxHash, + CreatedAt: dbTx.CreatedAt, + UpdatedAt: dbTx.UpdatedAt, + }, nil +} + +// DbLegacyGaslessTxPlus has additional fieds from evm.txes and evm.tx_attempts table +type DbLegacyGaslessTxPlus struct { + DbLegacyGaslessTx + EthTxStatus txmgrtypes.TxState `db:"etx_state"` + EthTxHash *common.Hash `db:"etx_hash"` + EthTxError *string `db:"etx_error"` + Receipt *evmtypes.Receipt `db:"etx_receipt"` +} + +func (o *orm) SelectBySourceChainIDAndStatus(ctx context.Context, sourceChainID uint64, status types.Status) (txs []types.LegacyGaslessTx, err error) { + q := o.q.WithOpts(pg.WithParentCtx(ctx)) + var dbTxs []DbLegacyGaslessTx + err = q.Select(&dbTxs, ` + SELECT * FROM legacy_gasless_txs + WHERE legacy_gasless_txs.source_chain_id = $1 + AND legacy_gasless_txs.tx_status = $2 + `, sourceChainID, status.String()) + for _, dbTx := range dbTxs { + tx, err := toLegacyGaslessTx(dbTx) + if err != nil { + return nil, err + } + txs = append(txs, *tx) + } + return +} + +func (o *orm) SelectByDestChainIDAndStatus(ctx context.Context, destChainID uint64, status types.Status) (txs []types.LegacyGaslessTx, err error) { + q := o.q.WithOpts(pg.WithParentCtx(ctx)) + var dbTxs []DbLegacyGaslessTx + err = q.Select(&dbTxs, ` + SELECT * FROM legacy_gasless_txs + WHERE legacy_gasless_txs.destination_chain_id = $1 + AND legacy_gasless_txs.tx_status = $2 + `, destChainID, status.String()) + for _, dbTx := range dbTxs { + tx, err := toLegacyGaslessTx(dbTx) + if err != nil { + return nil, err + } + txs = append(txs, *tx) + } + return +} + +// InsertLegacyGaslessTx is idempotent +func (o *orm) InsertLegacyGaslessTx(ctx context.Context, lgsTx types.LegacyGaslessTx) error { + q := o.q.WithOpts(pg.WithParentCtx(ctx)) + dbTx := toDbLegacyGaslessTx(lgsTx) + return q.ExecQ(InsertLegacyGaslessTxQuery, + dbTx.ID, + dbTx.Forwarder, + dbTx.From, + dbTx.Target, + dbTx.Receiver, + dbTx.Nonce, + dbTx.Amount, + dbTx.SourceChainID, + dbTx.DestinationChainID, + dbTx.ValidUntilTime, + dbTx.Signature[:], + dbTx.Status, + dbTx.TokenName, + dbTx.TokenVersion, + dbTx.EthTxID, + ) +} + +// UpdateLegacyGaslessTx updates legacy gasless transaction with status, ccip message ID (optional), failure reason (optional) +func (o *orm) UpdateLegacyGaslessTx(ctx context.Context, lgsTx types.LegacyGaslessTx) error { + q := o.q.WithOpts(pg.WithParentCtx(ctx)) + dbTx := toDbLegacyGaslessTx(lgsTx) + return q.ExecQ(UpdateLegacyGaslessTxQuery, + dbTx.ID, + dbTx.Status, + dbTx.CCIPMessageID, + dbTx.FailureReason, + dbTx.TxHash, + ) +} + +func (o *orm) SelectBySourceChainIDAndEthTxStates(ctx context.Context, sourceChainID uint64, states []legacygasstation.EtxStatus) ([]types.LegacyGaslessTxPlus, error) { + var dbLgps []DbLegacyGaslessTxPlus + q := o.q.WithOpts(pg.WithParentCtx(ctx)) + err := q.Select(&dbLgps, `SELECT + lgt.*, + etx.state as etx_state, + eta.hash as etx_hash, + etx.error as etx_error, + etr.receipt as etx_receipt + FROM legacy_gasless_txs lgt + LEFT JOIN evm.txes etx ON etx.id = lgt.eth_tx_id + LEFT JOIN evm.tx_attempts eta ON etx.id = eta.eth_tx_id + LEFT JOIN evm.receipts etr ON eta.hash = etr.tx_hash + WHERE lgt.source_chain_id = $1 + AND etx.state = any($2) + ORDER BY eta.broadcast_before_block_num ASC, etr.block_number ASC + `, sourceChainID, pq.Array(states)) + if err != nil { + return nil, errors.Wrap(err, "select eth txs by source chain id and states") + } + + // result of the query above is sorted by broadcast_before_block_num in ascending order + // this map de-duplicates tx attempts so that only most recent attempt tx attempt for the given tx remains + // TODO: current implementation does not guarantee that the tx hash is on the canonical chain + recentLgps := make(map[string]DbLegacyGaslessTxPlus) + for _, dbLgp := range dbLgps { + if _, ok := recentLgps[dbLgp.ID]; ok { + // found a transaction with multiple attempts + o.lggr.Debugw("found a gasless transaction with multiple attempts", "RequestID", dbLgp.ID, "txHash", dbLgp.TxHash) + } + recentLgps[dbLgp.ID] = dbLgp + } + + var dedupedLgps []types.LegacyGaslessTxPlus + for _, dbLgp := range recentLgps { + lgp, err := toLegacyGaslessTxPlus(dbLgp) + if err != nil { + return nil, err + } + dedupedLgps = append(dedupedLgps, *lgp) + } + + return dedupedLgps, nil +} + +func toLegacyGaslessTxPlus(dbLgp DbLegacyGaslessTxPlus) (*types.LegacyGaslessTxPlus, error) { + lgTx, err := toLegacyGaslessTx(dbLgp.DbLegacyGaslessTx) + if err != nil { + return nil, err + } + var status *uint64 + if dbLgp.Receipt != nil { + status = &dbLgp.Receipt.Status + } + return &types.LegacyGaslessTxPlus{ + LegacyGaslessTx: *lgTx, + EthTxStatus: string(dbLgp.EthTxStatus), + EthTxHash: dbLgp.EthTxHash, + EthTxError: dbLgp.EthTxError, + ReceiptStatus: status, + }, nil +} diff --git a/core/services/legacygasstation/orm_test.go b/core/services/legacygasstation/orm_test.go new file mode 100644 index 00000000000..1eb3ae99775 --- /dev/null +++ b/core/services/legacygasstation/orm_test.go @@ -0,0 +1,160 @@ +package legacygasstation_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/sqlx" + "github.com/test-go/testify/require" + "gopkg.in/guregu/null.v4" + + "github.com/smartcontractkit/capital-markets-projects/lib/services/legacygasstation" + "github.com/smartcontractkit/capital-markets-projects/lib/services/legacygasstation/types" + txmgrstate "github.com/smartcontractkit/chainlink/v2/common/txmgr" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" + "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + configtest "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/configtest/v2" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/evmtest" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore" + lgsservice "github.com/smartcontractkit/chainlink/v2/core/services/legacygasstation" +) + +func TestORM_Insert(t *testing.T) { + orm, _, txStore, ethKeyStore := setup(t) + _, fromAddress := cltest.MustInsertRandomKeyReturningState(t, ethKeyStore, 0) + etx := cltest.MustInsertInProgressEthTxWithAttempt(t, txStore, 13, fromAddress) + + tx := legacygasstation.LegacyGaslessTx(t, legacygasstation.TestLegacyGaslessTx{ + EthTxID: etx.GetID(), + }) + err := orm.InsertLegacyGaslessTx(testutils.Context(t), tx) + require.NoError(t, err) + + txs, err := orm.SelectBySourceChainIDAndStatus(testutils.Context(t), tx.SourceChainID, tx.Status) + require.NoError(t, err) + require.Equal(t, 1, len(txs)) + legacygasstation.AssertTxEquals(t, tx, txs[0]) + + txs, err = orm.SelectByDestChainIDAndStatus(testutils.Context(t), tx.DestinationChainID, tx.Status) + require.NoError(t, err) + require.Equal(t, 1, len(txs)) + legacygasstation.AssertTxEquals(t, tx, txs[0]) +} + +func TestORM_MultipleInserts(t *testing.T) { + orm, _, txStore, ethKeyStore := setup(t) + _, fromAddress := cltest.MustInsertRandomKeyReturningState(t, ethKeyStore, 0) + etx := cltest.MustInsertInProgressEthTxWithAttempt(t, txStore, 13, fromAddress) + + nonce1, ok := new(big.Int).SetString("1", 10) + require.True(t, ok) + nonce2, ok := new(big.Int).SetString("2", 10) + require.True(t, ok) + + tx1 := legacygasstation.LegacyGaslessTx(t, legacygasstation.TestLegacyGaslessTx{ + EthTxID: etx.GetID(), + Nonce: nonce1, + }) + tx2 := legacygasstation.LegacyGaslessTx(t, legacygasstation.TestLegacyGaslessTx{ + EthTxID: etx.GetID(), + Nonce: nonce2, + }) + err := orm.InsertLegacyGaslessTx(testutils.Context(t), tx1) + require.NoError(t, err) + err = orm.InsertLegacyGaslessTx(testutils.Context(t), tx2) + require.NoError(t, err) + + txs, err := orm.SelectBySourceChainIDAndStatus(testutils.Context(t), tx1.SourceChainID, tx1.Status) + require.NoError(t, err) + require.Equal(t, 2, len(txs)) + + txs, err = orm.SelectByDestChainIDAndStatus(testutils.Context(t), tx1.DestinationChainID, tx1.Status) + require.NoError(t, err) + require.Equal(t, 2, len(txs)) +} + +func TestORM_Update(t *testing.T) { + orm, _, txStore, ethKeyStore := setup(t) + _, fromAddress := cltest.MustInsertRandomKeyReturningState(t, ethKeyStore, 0) + etx := cltest.MustInsertInProgressEthTxWithAttempt(t, txStore, 13, fromAddress) + + tx := legacygasstation.LegacyGaslessTx(t, legacygasstation.TestLegacyGaslessTx{ + EthTxID: etx.GetID(), + }) + err := orm.InsertLegacyGaslessTx(testutils.Context(t), tx) + require.NoError(t, err) + + tx.Status = types.SourceFinalized + ccipMessageID := common.HexToHash("1") + tx.CCIPMessageID = &ccipMessageID + + err = orm.UpdateLegacyGaslessTx(testutils.Context(t), tx) + require.NoError(t, err) + + txs, err := orm.SelectBySourceChainIDAndStatus(testutils.Context(t), tx.SourceChainID, tx.Status) + require.NoError(t, err) + require.Equal(t, 1, len(txs)) + legacygasstation.AssertTxEquals(t, tx, txs[0]) + + txs, err = orm.SelectByDestChainIDAndStatus(testutils.Context(t), tx.DestinationChainID, tx.Status) + require.NoError(t, err) + require.Equal(t, 1, len(txs)) + legacygasstation.AssertTxEquals(t, tx, txs[0]) + + tx.Status = types.Failure + failureReason := "executionReverted" + tx.FailureReason = &failureReason + + err = orm.UpdateLegacyGaslessTx(testutils.Context(t), tx) + require.NoError(t, err) + + txs, err = orm.SelectBySourceChainIDAndStatus(testutils.Context(t), tx.SourceChainID, tx.Status) + require.NoError(t, err) + require.Equal(t, 1, len(txs)) + legacygasstation.AssertTxEquals(t, tx, txs[0]) + + txs, err = orm.SelectByDestChainIDAndStatus(testutils.Context(t), tx.DestinationChainID, tx.Status) + require.NoError(t, err) + require.Equal(t, 1, len(txs)) + legacygasstation.AssertTxEquals(t, tx, txs[0]) +} + +func TestORM_FailedEthTx(t *testing.T) { + orm, _, txStore, ethKeyStore := setup(t) + _, fromAddress := cltest.MustInsertRandomKeyReturningState(t, ethKeyStore, 0) + etx := cltest.MustInsertInProgressEthTxWithAttempt(t, txStore, 13, fromAddress) + errorMsg := "execution reverted" + etx.Error = null.StringFrom(errorMsg) + err := txStore.UpdateTxFatalError(&etx) + require.NoError(t, err) + + tx := legacygasstation.LegacyGaslessTx(t, legacygasstation.TestLegacyGaslessTx{ + EthTxID: etx.GetID(), + }) + err = orm.InsertLegacyGaslessTx(testutils.Context(t), tx) + require.NoError(t, err) + + txs, err := orm.SelectBySourceChainIDAndEthTxStates(testutils.Context(t), tx.SourceChainID, []legacygasstation.EtxStatus{legacygasstation.EtxInProgress}) + require.NoError(t, err) + require.Equal(t, 0, len(txs)) + + txs, err = orm.SelectBySourceChainIDAndEthTxStates(testutils.Context(t), tx.SourceChainID, []legacygasstation.EtxStatus{legacygasstation.EtxFatalError}) + require.NoError(t, err) + require.Equal(t, 1, len(txs)) + require.Equal(t, txs[0].EthTxStatus, string(txmgrstate.TxFatalError)) + require.Equal(t, *txs[0].EthTxError, errorMsg) +} + +func setup(t *testing.T) (legacygasstation.ORM, *sqlx.DB, txmgr.TestEvmTxStore, keystore.Eth) { + cfg := configtest.NewTestGeneralConfig(t) + db := pgtest.NewSqlxDB(t) + evmtest.NewChainScopedConfig(t, cfg) + txStore := cltest.NewTestTxStore(t, db, cfg.Database()) + ethKeyStore := cltest.NewKeyStore(t, db, cfg.Database()).Eth() + orm := lgsservice.NewORM(db, logger.TestLogger(t), cfg.Database()) + return orm, db, txStore, ethKeyStore +} diff --git a/core/services/legacygasstation/server_delegate.go b/core/services/legacygasstation/server_delegate.go new file mode 100644 index 00000000000..434c1542ec7 --- /dev/null +++ b/core/services/legacygasstation/server_delegate.go @@ -0,0 +1,167 @@ +package legacygasstation + +import ( + "context" + + "github.com/gin-gonic/gin" + "github.com/pkg/errors" + "github.com/smartcontractkit/sqlx" + "go.uber.org/multierr" + + forwarder "github.com/smartcontractkit/capital-markets-projects/core/gethwrappers/legacygasstation/generated/legacy_gas_station_forwarder" + "github.com/smartcontractkit/capital-markets-projects/lib/services/legacygasstation" + "github.com/smartcontractkit/capital-markets-projects/lib/services/legacygasstation/types" + "github.com/smartcontractkit/capital-markets-projects/lib/web/jsonrpc" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/eal" + "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore" + "github.com/smartcontractkit/chainlink/v2/core/services/pg" +) + +type ( + Delegate struct { + lggr logger.Logger + cc evm.LegacyChainContainer + ks keystore.Eth + q pg.Q + db *sqlx.DB + rr *legacygasstation.RequestRouter + } + + RequestRouter interface { + SendTransaction(*gin.Context, types.SendTransactionRequest) (*types.SendTransactionResponse, *jsonrpc.Error) + } +) + +func NewServerDelegate(lggr logger.Logger, cc evm.LegacyChainContainer, ks keystore.Eth, db *sqlx.DB, cfg pg.QConfig) *Delegate { + return &Delegate{ + lggr: lggr, + cc: cc, + ks: ks, + q: pg.NewQ(db, lggr, cfg), + db: db, + rr: legacygasstation.NewRequestRouter(lggr), + } +} + +func (d *Delegate) JobType() job.Type { + return job.LegacyGasStationServer +} + +func (d *Delegate) BeforeJobCreated(spec job.Job) {} +func (d *Delegate) AfterJobCreated(spec job.Job) {} +func (d *Delegate) BeforeJobDeleted(spec job.Job) {} +func (d *Delegate) OnDeleteJob(spec job.Job, q pg.Queryer) error { return nil } + +func (d *Delegate) RequestRouter() RequestRouter { + return d.rr +} + +func (d *Delegate) ServicesForSpec(jb job.Job, qopts ...pg.QOpt) ([]job.ServiceCtx, error) { + if jb.LegacyGasStationServerSpec == nil { + return nil, errors.Errorf("ServicesForSpec expects a LegacyGasStationServerSpec, got %+v", jb) + } + service := &gasStationService{ + spec: jb, + rr: d.rr, + cc: d.cc, + ks: d.ks, + q: d.q, + db: d.db, + lggr: d.lggr, + } + return []job.ServiceCtx{service}, nil +} + +type gasStationService struct { + spec job.Job + rr *legacygasstation.RequestRouter + cc evm.LegacyChainContainer + ks keystore.Eth + q pg.Q + db *sqlx.DB + lggr logger.Logger +} + +// Start starts gasStationService. +func (s *gasStationService) Start(context.Context) error { + l := s.lggr.Named("Legacy Gas Station Server").With( + "jobID", s.spec.ID, + "externalJobID", s.spec.ExternalJobID, + "chainID", s.spec.LegacyGasStationServerSpec.EVMChainID.ToInt().Uint64(), + "ccipChainSelector", s.spec.LegacyGasStationServerSpec.CCIPChainSelector.ToInt().Uint64(), + ) + chain, err := s.cc.Get(s.spec.LegacyGasStationServerSpec.EVMChainID.String()) + if err != nil { + return err + } + forwarder, err := forwarder.NewLegacyGasStationForwarder(s.spec.LegacyGasStationServerSpec.ForwarderAddress.Address(), chain.Client()) + if err != nil { + return errors.Wrap(err, "initializing forwarder") + } + if err = checkFromAddressesExist(s.spec, s.ks); err != nil { + return err + } + + orm := NewORM(s.db, l, chain.Config().Database()) + + transactor, err := NewBlockchainTransactor( + s.lggr, + s.db, + chain.TxManager(), + s.ks, + s.spec.LegacyGasStationServerSpec.FromAddresses, + s.spec.LegacyGasStationServerSpec.EVMChainID.ToInt().Uint64(), + orm, + ) + if err != nil { + return err + } + + client, err := eal.NewBlockchainClient( + s.lggr, + chain.TxManager(), + s.ks, + s.spec.LegacyGasStationServerSpec.FromAddresses, + s.spec.LegacyGasStationServerSpec.EVMChainID.ToInt().Uint64(), + chain.Config().EVM(), + chain.Client(), + ) + if err != nil { + return err + } + + reqHandler, err := legacygasstation.NewRequestHandler( + l, + forwarder, + chain.ID().Uint64(), + s.spec.LegacyGasStationServerSpec.CCIPChainSelector.ToInt().Uint64(), + client, + transactor, + ) + if err != nil { + return err + } + err = s.rr.RegisterHandler(reqHandler) + if err != nil { + return err + } + return err +} + +func (s *gasStationService) Close() error { + s.rr.DeregisterHandler(s.spec.LegacyGasStationServerSpec.CCIPChainSelector.ToInt()) + return nil +} + +// CheckFromAddressesExist returns an error if and only if one of the addresses +// in the LegacyGasStationServerSpec spec's fromAddresses field does not exist in the keystore. +func checkFromAddressesExist(jb job.Job, gethks keystore.Eth) (err error) { + for _, a := range jb.LegacyGasStationServerSpec.FromAddresses { + _, err2 := gethks.Get(a.Hex()) + err = multierr.Append(err, err2) + } + return +} diff --git a/core/services/legacygasstation/sidecar_delegate.go b/core/services/legacygasstation/sidecar_delegate.go new file mode 100644 index 00000000000..1ff43851bf9 --- /dev/null +++ b/core/services/legacygasstation/sidecar_delegate.go @@ -0,0 +1,193 @@ +package legacygasstation + +import ( + "context" + "fmt" + "time" + + "github.com/pkg/errors" + "github.com/smartcontractkit/sqlx" + + "github.com/smartcontractkit/capital-markets-projects/core/gethwrappers/ccip/generated/evm_2_evm_off_ramp" + forwarder "github.com/smartcontractkit/capital-markets-projects/core/gethwrappers/legacygasstation/generated/legacy_gas_station_forwarder" + "github.com/smartcontractkit/capital-markets-projects/lib/services/legacygasstation" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore" + "github.com/smartcontractkit/chainlink/v2/core/services/pg" + "github.com/smartcontractkit/chainlink/v2/core/utils" +) + +type SidecarDelegate struct { + logger logger.Logger + chains evm.LegacyChainContainer + ks keystore.Eth + db *sqlx.DB +} + +// JobType satisfies the job.Delegate interface. +func (d *SidecarDelegate) JobType() job.Type { + return job.LegacyGasStationSidecar +} + +// NewDelegate creates a new Delegate. +func NewSidecarDelegate( + logger logger.Logger, + chains evm.LegacyChainContainer, + ks keystore.Eth, + db *sqlx.DB, +) *SidecarDelegate { + return &SidecarDelegate{ + logger: logger, + chains: chains, + ks: ks, + db: db, + } +} + +// ServicesForSpec satisfies the job.Delegate interface. +func (d *SidecarDelegate) ServicesForSpec(jb job.Job, qopts ...pg.QOpt) ([]job.ServiceCtx, error) { + if jb.LegacyGasStationSidecarSpec == nil { + return nil, errors.Errorf( + "legacygasstation.Delegate expects a LegacyGasStationSidecarSpec to be present, got %+v", jb) + } + + chain, err := d.chains.Get(jb.LegacyGasStationSidecarSpec.EVMChainID.String()) + if err != nil { + return nil, err + } + + log := d.logger.Named("Legacy Gas Station Sidecar").With("jobID", jb.ID, "externalJobID", jb.ExternalJobID) + + forwarder, err := forwarder.NewLegacyGasStationForwarder(jb.LegacyGasStationSidecarSpec.ForwarderAddress.Address(), chain.Client()) + if err != nil { + return nil, errors.Wrap(err, "initializing forwarder") + } + + offramp, err := evm_2_evm_off_ramp.NewEVM2EVMOffRamp(jb.LegacyGasStationSidecarSpec.OffRampAddress.Address(), chain.Client()) + if err != nil { + return nil, errors.Wrap(err, "initializing off ramp") + } + + if jb.LegacyGasStationSidecarSpec.LookbackBlocks < int32(chain.Config().EVM().FinalityDepth()) { + return nil, fmt.Errorf( + "waitBlocks must be greater than or equal to chain's finality depth (%d), currently %d", + chain.Config().EVM().FinalityDepth(), jb.LegacyGasStationSidecarSpec.LookbackBlocks) + } + + orm := NewORM(d.db, d.logger, chain.Config().Database()) + + var ( + mtlsCertificate string + mtlsKey string + ) + + if jb.LegacyGasStationSidecarSpec.ClientCertificate != nil && jb.LegacyGasStationSidecarSpec.ClientKey != nil { + // temporary workaround until mtls auth config can be fetched inside nodes + mtlsCertificate = *jb.LegacyGasStationSidecarSpec.ClientCertificate + mtlsKey = *jb.LegacyGasStationSidecarSpec.ClientKey + } + + su, err := legacygasstation.NewStatusUpdater( + jb.LegacyGasStationSidecarSpec.StatusUpdateURL, + mtlsCertificate, + mtlsKey, + log, + ) + if err != nil { + return nil, errors.Wrap(err, "new status updater") + } + + sidecar, err := legacygasstation.NewSidecar( + log, + NewLogPollerAdapter(chain.LogPoller()), + forwarder, + offramp, + jb.LegacyGasStationSidecarSpec.CCIPChainSelector.ToInt().Uint64(), + chain.Config().EVM().FinalityDepth(), + uint32(jb.LegacyGasStationSidecarSpec.LookbackBlocks), + orm, + su, + ) + if err != nil { + return nil, err + } + + return []job.ServiceCtx{&service{ + sidecar: sidecar, + pollPeriod: jb.LegacyGasStationSidecarSpec.PollPeriod, + runTimeout: jb.LegacyGasStationSidecarSpec.RunTimeout, + logger: log, + done: make(chan struct{}), + }}, nil +} + +// AfterJobCreated satisfies the job.Delegate interface. +func (d *SidecarDelegate) AfterJobCreated(spec job.Job) {} + +// AfterJobCreated satisfies the job.Delegate interface. +func (d *SidecarDelegate) BeforeJobCreated(spec job.Job) {} + +// AfterJobCreated satisfies the job.Delegate interface. +func (d *SidecarDelegate) BeforeJobDeleted(spec job.Job) {} + +// OnDeleteJob satisfies the job.Delegate interface. +func (d *SidecarDelegate) OnDeleteJob(spec job.Job, q pg.Queryer) error { return nil } + +// service is a job.Service that runs the Gasless Transaction Sidecar every pollPeriod. +type service struct { + utils.StartStopOnce + sidecar *legacygasstation.Sidecar + done chan struct{} + pollPeriod time.Duration + runTimeout time.Duration + logger logger.Logger + parentCtx context.Context + cancel context.CancelFunc +} + +// Start the Gasless Transaction Sidecar, satisfying the job.Service interface. +func (s *service) Start(context.Context) error { + return s.StartOnce("Gasless Transaction Sidecar", func() error { + s.logger.Infow("Gasless Transaction Sidecar") + ticker := time.NewTicker(utils.WithJitter(s.pollPeriod)) + s.parentCtx, s.cancel = context.WithCancel(context.Background()) + go func() { + defer close(s.done) + defer ticker.Stop() + for { + select { + case <-ticker.C: + s.runSidecar() + case <-s.parentCtx.Done(): + return + } + } + }() + return nil + }) +} + +// Close the gasless transaction sidecar, satisfying the job.Service interface. +func (s *service) Close() error { + return s.StopOnce("Gasless Transaction Sidecar", func() error { + s.logger.Infow("Stopping Gasless Transaction Sidecar") + s.cancel() + <-s.done + return nil + }) +} + +func (s *service) runSidecar() { + s.logger.Debugw("Running Gasless Transaction Sidecar") + ctx, cancel := context.WithTimeout(s.parentCtx, s.runTimeout) + defer cancel() + err := s.sidecar.Run(ctx) + if err == nil { + s.logger.Debugw("Gasless Transaction Sidecar run completed successfully") + } else { + s.logger.Errorw("Gasless Transaction Sidecar run was at least partially unsuccessful", + "error", err) + } +} diff --git a/core/services/legacygasstation/sidecar_test.go b/core/services/legacygasstation/sidecar_test.go new file mode 100644 index 00000000000..9f8cd018956 --- /dev/null +++ b/core/services/legacygasstation/sidecar_test.go @@ -0,0 +1,550 @@ +package legacygasstation_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + geth_types "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/ethconfig" + "github.com/test-go/testify/mock" + "github.com/test-go/testify/require" + + gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + "github.com/smartcontractkit/chainlink/v2/core/utils" + + "github.com/smartcontractkit/capital-markets-projects/core/gethwrappers/ccip/generated/evm_2_evm_off_ramp" + mock_contracts "github.com/smartcontractkit/capital-markets-projects/core/gethwrappers/ccip/mocks" + forwarder_wrapper "github.com/smartcontractkit/capital-markets-projects/core/gethwrappers/legacygasstation/generated/legacy_gas_station_forwarder" + forwarder_mocks "github.com/smartcontractkit/capital-markets-projects/core/gethwrappers/legacygasstation/mocks" + "github.com/smartcontractkit/capital-markets-projects/lib/services/legacygasstation" + lgsmocks "github.com/smartcontractkit/capital-markets-projects/lib/services/legacygasstation/mocks" + "github.com/smartcontractkit/capital-markets-projects/lib/services/legacygasstation/types" + "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" + "github.com/smartcontractkit/chainlink/v2/core/internal/cltest/heavyweight" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" + lgsservice "github.com/smartcontractkit/chainlink/v2/core/services/legacygasstation" +) + +type request struct { + tx legacygasstation.TestLegacyGaslessTx + confirmed bool + failed bool +} + +type testcase struct { + name string + latestBlock int64 + lookbackBlock int64 + chainID uint64 + requestData []request + forwardSucceededLogs []*forwarder_wrapper.LegacyGasStationForwarderForwardSucceeded + offrampExecutionLogs []*evm_2_evm_off_ramp.EVM2EVMOffRampExecutionStateChanged + resultData []legacygasstation.TestLegacyGaslessTx +} + +type testStatusUpdater struct { + statusCounter map[string]int +} + +func newTestStatusUpdater() *testStatusUpdater { + return &testStatusUpdater{ + statusCounter: make(map[string]int), + } +} + +func (s *testStatusUpdater) Update(tx types.LegacyGaslessTx) error { + s.statusCounter[tx.Status.String()]++ + return nil +} + +var ( + tests = []testcase{ + { + name: "submitted transaction confirmed", + latestBlock: 100, + lookbackBlock: 50, + chainID: testutils.SimulatedChainID.Uint64(), + requestData: []request{ + { + tx: legacygasstation.TestLegacyGaslessTx{ + ID: "4877f0a6-4b05-49d9-8776-4c50c24bed03", + Nonce: big.NewInt(0), + Amount: big.NewInt(1e18), + SourceChainID: testutils.SimulatedChainID.Uint64(), + DestinationChainID: testutils.SimulatedChainID.Uint64(), + }, + confirmed: true, + }, + }, + resultData: []legacygasstation.TestLegacyGaslessTx{ + { + ID: "4877f0a6-4b05-49d9-8776-4c50c24bed03", + Nonce: big.NewInt(0), + Amount: big.NewInt(1e18), + SourceChainID: testutils.SimulatedChainID.Uint64(), + DestinationChainID: testutils.SimulatedChainID.Uint64(), + Status: types.Confirmed, + }, + }, + }, + { + name: "submitted transaction failed", + latestBlock: 100, + lookbackBlock: 50, + chainID: testutils.SimulatedChainID.Uint64(), + requestData: []request{ + { + tx: legacygasstation.TestLegacyGaslessTx{ + ID: "4877f0a6-4b05-49d9-8776-4c50c24bed03", + Nonce: big.NewInt(0), + Amount: big.NewInt(1e18), + SourceChainID: testutils.SimulatedChainID.Uint64(), + DestinationChainID: testutils.SimulatedChainID.Uint64(), + }, + failed: true, + }, + }, + resultData: []legacygasstation.TestLegacyGaslessTx{ + { + ID: "4877f0a6-4b05-49d9-8776-4c50c24bed03", + Nonce: big.NewInt(0), + Amount: big.NewInt(1e18), + SourceChainID: testutils.SimulatedChainID.Uint64(), + DestinationChainID: testutils.SimulatedChainID.Uint64(), + Status: types.Failure, + }, + }, + }, + { + name: "confirmed transaction finalized", + latestBlock: 100, + lookbackBlock: 50, + chainID: testutils.SimulatedChainID.Uint64(), + requestData: []request{ + { + tx: legacygasstation.TestLegacyGaslessTx{ + ID: "4877f0a6-4b05-49d9-8776-4c50c24bed03", + Nonce: big.NewInt(0), + Amount: big.NewInt(1e18), + SourceChainID: testutils.SimulatedChainID.Uint64(), + DestinationChainID: testutils.SimulatedChainID.Uint64(), + Status: types.Confirmed, + }, + }, + }, + forwardSucceededLogs: []*forwarder_wrapper.LegacyGasStationForwarderForwardSucceeded{ + { + From: legacygasstation.FromAddress, + Nonce: big.NewInt(0), + Raw: geth_types.Log{ + Address: legacygasstation.ForwarderAddress, + }, + }, + }, + resultData: []legacygasstation.TestLegacyGaslessTx{ + { + ID: "4877f0a6-4b05-49d9-8776-4c50c24bed03", + Nonce: big.NewInt(0), + Amount: big.NewInt(1e18), + SourceChainID: testutils.SimulatedChainID.Uint64(), + DestinationChainID: testutils.SimulatedChainID.Uint64(), + Status: types.Finalized, + }, + }, + }, + { + name: "confirmed transaction failed", + latestBlock: 100, + lookbackBlock: 50, + chainID: testutils.SimulatedChainID.Uint64(), + requestData: []request{ + { + tx: legacygasstation.TestLegacyGaslessTx{ + ID: "4877f0a6-4b05-49d9-8776-4c50c24bed03", + Nonce: big.NewInt(0), + Amount: big.NewInt(1e18), + SourceChainID: testutils.SimulatedChainID.Uint64(), + DestinationChainID: testutils.SimulatedChainID.Uint64(), + Status: types.Confirmed, + }, + failed: true, + }, + }, + resultData: []legacygasstation.TestLegacyGaslessTx{ + { + ID: "4877f0a6-4b05-49d9-8776-4c50c24bed03", + Nonce: big.NewInt(0), + Amount: big.NewInt(1e18), + SourceChainID: testutils.SimulatedChainID.Uint64(), + DestinationChainID: testutils.SimulatedChainID.Uint64(), + Status: types.Failure, + }, + }, + }, + { + name: "multiple submitted txs finalized", + latestBlock: 100, + lookbackBlock: 50, + chainID: testutils.SimulatedChainID.Uint64(), + requestData: []request{ + { + tx: legacygasstation.TestLegacyGaslessTx{ + ID: "4877f0a6-4b05-49d9-8776-4c50c24bed03", + Nonce: big.NewInt(0), + Amount: big.NewInt(1e18), + SourceChainID: testutils.SimulatedChainID.Uint64(), + DestinationChainID: testutils.SimulatedChainID.Uint64(), + Status: types.Confirmed, + }, + }, + { + tx: legacygasstation.TestLegacyGaslessTx{ + ID: "a4efbb8b-ac67-46fb-8ded-c883f7f5fcab", + From: common.HexToAddress("0x780b3102c62d5DfDCc658B3480B93041Ba46F499"), + Nonce: big.NewInt(0), + Amount: big.NewInt(1e18), + SourceChainID: testutils.SimulatedChainID.Uint64(), + DestinationChainID: testutils.SimulatedChainID.Uint64(), + Status: types.Confirmed, + }, + }, + }, + forwardSucceededLogs: []*forwarder_wrapper.LegacyGasStationForwarderForwardSucceeded{ + { + From: legacygasstation.FromAddress, + Nonce: big.NewInt(0), + Raw: geth_types.Log{ + Address: legacygasstation.ForwarderAddress, + }, + }, + { + From: common.HexToAddress("0x780b3102c62d5DfDCc658B3480B93041Ba46F499"), + Nonce: big.NewInt(0), + Raw: geth_types.Log{ + Address: legacygasstation.ForwarderAddress, + }, + }, + }, + resultData: []legacygasstation.TestLegacyGaslessTx{ + { + ID: "4877f0a6-4b05-49d9-8776-4c50c24bed03", + Nonce: big.NewInt(0), + Amount: big.NewInt(1e18), + SourceChainID: testutils.SimulatedChainID.Uint64(), + DestinationChainID: testutils.SimulatedChainID.Uint64(), + Status: types.Finalized, + }, + { + ID: "a4efbb8b-ac67-46fb-8ded-c883f7f5fcab", + From: common.HexToAddress("0x780b3102c62d5DfDCc658B3480B93041Ba46F499"), + Nonce: big.NewInt(0), + Amount: big.NewInt(1e18), + SourceChainID: testutils.SimulatedChainID.Uint64(), + DestinationChainID: testutils.SimulatedChainID.Uint64(), + Status: types.Finalized, + }, + }, + }, + { + name: "no forwarder logs", + latestBlock: 100, + lookbackBlock: 50, + chainID: testutils.SimulatedChainID.Uint64(), + requestData: []request{ + { + tx: legacygasstation.TestLegacyGaslessTx{ + ID: "4877f0a6-4b05-49d9-8776-4c50c24bed03", + Nonce: big.NewInt(0), + Amount: big.NewInt(1e18), + SourceChainID: testutils.SimulatedChainID.Uint64(), + DestinationChainID: testutils.SimulatedChainID.Uint64(), + }, + }, + { + tx: legacygasstation.TestLegacyGaslessTx{ + ID: "a4efbb8b-ac67-46fb-8ded-c883f7f5fcab", + From: common.HexToAddress("0x780b3102c62d5DfDCc658B3480B93041Ba46F499"), + Nonce: big.NewInt(0), + Amount: big.NewInt(1e18), + SourceChainID: testutils.SimulatedChainID.Uint64(), + DestinationChainID: testutils.SimulatedChainID.Uint64(), + }, + }, + }, + forwardSucceededLogs: []*forwarder_wrapper.LegacyGasStationForwarderForwardSucceeded{}, + resultData: []legacygasstation.TestLegacyGaslessTx{ + { + ID: "4877f0a6-4b05-49d9-8776-4c50c24bed03", + Nonce: big.NewInt(0), + Amount: big.NewInt(1e18), + SourceChainID: testutils.SimulatedChainID.Uint64(), + DestinationChainID: testutils.SimulatedChainID.Uint64(), + }, + { + ID: "a4efbb8b-ac67-46fb-8ded-c883f7f5fcab", + From: common.HexToAddress("0x780b3102c62d5DfDCc658B3480B93041Ba46F499"), + Nonce: big.NewInt(0), + Amount: big.NewInt(1e18), + SourceChainID: testutils.SimulatedChainID.Uint64(), + DestinationChainID: testutils.SimulatedChainID.Uint64(), + }, + }, + }, + { + name: "cross chain submitted to source finalized log", + latestBlock: 100, + lookbackBlock: 50, + chainID: testutils.SimulatedChainID.Uint64(), + requestData: []request{ + { + tx: legacygasstation.TestLegacyGaslessTx{ + ID: "4877f0a6-4b05-49d9-8776-4c50c24bed03", + Nonce: big.NewInt(0), + Amount: big.NewInt(1e18), + SourceChainID: testutils.SimulatedChainID.Uint64(), + DestinationChainID: 1000, + Status: types.Confirmed, + }, + }, + }, + forwardSucceededLogs: []*forwarder_wrapper.LegacyGasStationForwarderForwardSucceeded{ + { + From: legacygasstation.FromAddress, + Nonce: big.NewInt(0), + Raw: geth_types.Log{ + Address: legacygasstation.ForwarderAddress, + }, + ReturnValue: common.HexToHash("0x30").Bytes(), + }, + }, + resultData: []legacygasstation.TestLegacyGaslessTx{ + { + ID: "4877f0a6-4b05-49d9-8776-4c50c24bed03", + Nonce: big.NewInt(0), + Amount: big.NewInt(1e18), + SourceChainID: testutils.SimulatedChainID.Uint64(), + DestinationChainID: 1000, + Status: types.SourceFinalized, + }, + }, + }, + { + name: "cross chain source finalized to finalized", + latestBlock: 100, + lookbackBlock: 50, + chainID: 1000, + requestData: []request{ + { + tx: legacygasstation.TestLegacyGaslessTx{ + ID: "4877f0a6-4b05-49d9-8776-4c50c24bed03", + Nonce: big.NewInt(0), + Amount: big.NewInt(1e18), + SourceChainID: 1000, + DestinationChainID: testutils.SimulatedChainID.Uint64(), + CCIPMessageID: ptr(common.HexToHash("0x30")), + Status: types.SourceFinalized, + }, + }, + }, + offrampExecutionLogs: []*evm_2_evm_off_ramp.EVM2EVMOffRampExecutionStateChanged{ + { + MessageId: common.HexToHash("0x30"), + Raw: geth_types.Log{ + Address: legacygasstation.OfframpAddress, + }, + }, + }, + resultData: []legacygasstation.TestLegacyGaslessTx{ + { + ID: "4877f0a6-4b05-49d9-8776-4c50c24bed03", + Nonce: big.NewInt(0), + Amount: big.NewInt(1e18), + SourceChainID: 1000, + DestinationChainID: testutils.SimulatedChainID.Uint64(), + Status: types.Finalized, + }, + }, + }, + } +) + +func TestSidecar(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + sc, orm, su := setUp(t, test) + err := sc.Run(testutils.Context(t)) + require.NoError(t, err) + assertAfterSidecarRun(t, test, orm, su) + }) + } +} + +func setUp(t *testing.T, test testcase) (*legacygasstation.Sidecar, legacygasstation.ORM, *testStatusUpdater) { + cfg, db := heavyweight.FullTestDBV2(t, "legacy_gas_station_sidecar_test", func(c *chainlink.Config, s *chainlink.Secrets) { + require.Zero(t, testutils.SimulatedChainID.Cmp(c.EVM[0].ChainID.ToInt())) + c.Feature.LogPoller = ptr(true) + }) + backend := cltest.NewSimulatedBackend(t, core.GenesisAlloc{}, uint32(ethconfig.Defaults.Miner.GasCeil)) + app := cltest.NewApplicationWithConfigV2AndKeyOnSimulatedBlockchain(t, cfg, backend) + forwarder := forwarder_mocks.NewLegacyGasStationForwarderInterface(t) + lggr := logger.TestLogger(t) + offramp := mock_contracts.NewEVM2EVMOffRampInterface(t) + orm := lgsservice.NewORM(db, lggr, cfg.Database()) + chain, err := app.GetRelayers().LegacyEVMChains().Get(testutils.SimulatedChainID.String()) + require.NoError(t, err) + lp := lgsmocks.NewLogPoller(t) + lp.On("FilterName", mock.Anything, mock.Anything, mock.Anything).Return("filterName") + lp.On("RegisterFilter", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + lp.On("LatestBlock", mock.Anything).Return(test.latestBlock, nil) + forwarder.On("Address").Return(legacygasstation.ForwarderAddress) + offramp.On("Address").Return(legacygasstation.OfframpAddress) + var ( + fsLpLogs []gethtypes.Log + oelLpLogs []gethtypes.Log + ) + + for _, fl := range test.forwardSucceededLogs { + forwarder.On("ParseLog", mock.Anything).Return(fl, nil).Once() + fsLpLogs = append(fsLpLogs, gethtypes.Log{ + Topics: []common.Hash{forwarder_wrapper.LegacyGasStationForwarderForwardSucceeded{}.Topic()}, + }) + } + for _, oel := range test.offrampExecutionLogs { + offramp.On("ParseLog", mock.Anything).Return(oel, nil).Once() + oelLpLogs = append(oelLpLogs, gethtypes.Log{ + Topics: []common.Hash{evm_2_evm_off_ramp.EVM2EVMOffRampExecutionStateChanged{}.Topic()}, + }) + } + lp.On("IndexedLogsByBlockRange", + mock.Anything, + mock.Anything, + mock.Anything, + forwarder_wrapper.LegacyGasStationForwarderForwardSucceeded{}.Topic(), + legacygasstation.ForwarderAddress, + 1, + mock.Anything, + ).Return(fsLpLogs, nil).Maybe() + lp.On("IndexedLogsByBlockRange", + mock.Anything, + mock.Anything, + mock.Anything, + evm_2_evm_off_ramp.EVM2EVMOffRampExecutionStateChanged{}.Topic(), + legacygasstation.OfframpAddress, + 2, + mock.Anything, + ).Return(oelLpLogs, nil).Maybe() + + su := newTestStatusUpdater() + sc, err := legacygasstation.NewSidecar( + lggr, + lp, + forwarder, + offramp, + testutils.SimulatedChainID.Uint64(), + chain.Config().EVM().FinalityDepth(), + uint32(test.lookbackBlock), + orm, + su, + ) + require.NoError(t, err) + for i, r := range test.requestData { + chainID := cltest.FixtureChainID + blockNumber := int64(75) + _, fromAddress := cltest.MustInsertRandomKeyReturningState(t, app.KeyStore.Eth(), chainID) + txStore := cltest.NewTestTxStore(t, app.GetSqlxDB(), app.Config.Database()) + var ethTx txmgr.Tx + if r.confirmed { + ethTx = cltest.MustInsertConfirmedEthTxWithLegacyAttempt(t, txStore, int64(i), blockNumber, fromAddress) + blockHash := utils.NewHash() + receipt := evmtypes.Receipt{ + TxHash: ethTx.TxAttempts[0].Hash, + BlockHash: blockHash, + BlockNumber: big.NewInt(int64(i)), + TransactionIndex: uint(1), + Status: uint64(1), // reverted txs have 0 as status. non-zero other wise + } + err := app.TxmStorageService().SaveFetchedReceipts([]*evmtypes.Receipt{&receipt}, &chainID) + require.NoError(t, err) + } else if r.failed { + ethTx = cltest.MustInsertFatalErrorEthTx(t, txStore, fromAddress) + } else { + ethTx = cltest.MustInsertInProgressEthTxWithAttempt(t, txStore, evmtypes.Nonce(int64(i)), fromAddress) + } + r.tx.EthTxID = ethTx.GetID() + tx := legacygasstation.LegacyGaslessTx(t, r.tx) + err = orm.InsertLegacyGaslessTx(testutils.Context(t), tx) + require.NoError(t, err) + err = orm.UpdateLegacyGaslessTx(testutils.Context(t), tx) // update populates ccipMessageID and failureReason + require.NoError(t, err) + } + return sc, orm, su +} + +func assertAfterSidecarRun(t *testing.T, test testcase, orm legacygasstation.ORM, su *testStatusUpdater) { + confirmedTxs, submittedTxs, finalizedTxs, sourceFinalizedTxs, failedTxs := categorizeTestTxs(t, test.resultData) + + txs, err := orm.SelectBySourceChainIDAndStatus(testutils.Context(t), test.chainID, types.Confirmed) + require.NoError(t, err) + require.Equal(t, len(confirmedTxs), len(txs)) + + txs, err = orm.SelectBySourceChainIDAndStatus(testutils.Context(t), test.chainID, types.Submitted) + require.NoError(t, err) + require.Equal(t, len(submittedTxs), len(txs)) + + txs, err = orm.SelectBySourceChainIDAndStatus(testutils.Context(t), test.chainID, types.Finalized) + require.NoError(t, err) + require.Equal(t, len(finalizedTxs), len(txs)) + + txs, err = orm.SelectBySourceChainIDAndStatus(testutils.Context(t), test.chainID, types.SourceFinalized) + require.NoError(t, err) + require.Equal(t, len(sourceFinalizedTxs), len(txs)) + + txs, err = orm.SelectBySourceChainIDAndStatus(testutils.Context(t), test.chainID, types.Failure) + require.NoError(t, err) + require.Equal(t, len(failedTxs), len(txs)) + + expectedStatusUpdates := make(map[string]int) + for i, req := range test.requestData { + resultStatus := test.resultData[i].Status.String() + if req.tx.Status != test.resultData[i].Status { + expectedStatusUpdates[resultStatus]++ + } + } + require.Equal(t, expectedStatusUpdates[types.Confirmed.String()], su.statusCounter[types.Confirmed.String()]) + require.Equal(t, expectedStatusUpdates[types.Failure.String()], su.statusCounter[types.Failure.String()]) + require.Equal(t, expectedStatusUpdates[types.SourceFinalized.String()], su.statusCounter[types.SourceFinalized.String()]) + require.Equal(t, expectedStatusUpdates[types.Finalized.String()], su.statusCounter[types.Finalized.String()]) +} + +func categorizeTestTxs(t *testing.T, testTxs []legacygasstation.TestLegacyGaslessTx) ( + confirmedTxs, + submittedTxs, + finalizedTxs, + sourceFinalizedTxs, + failedTxs []types.LegacyGaslessTx, +) { + for _, testTx := range testTxs { + tx := legacygasstation.LegacyGaslessTx(t, testTx) + switch tx.Status { + case types.Confirmed: + confirmedTxs = append(confirmedTxs, tx) + case types.Submitted: + submittedTxs = append(submittedTxs, tx) + case types.SourceFinalized: + sourceFinalizedTxs = append(sourceFinalizedTxs, tx) + case types.Finalized: + finalizedTxs = append(finalizedTxs, tx) + case types.Failure: + failedTxs = append(failedTxs, tx) + default: + t.Errorf("unexpected status: %s", tx.Status) + } + } + return +} diff --git a/core/services/legacygasstation/validate.go b/core/services/legacygasstation/validate.go new file mode 100644 index 00000000000..1575fe82f74 --- /dev/null +++ b/core/services/legacygasstation/validate.go @@ -0,0 +1,122 @@ +package legacygasstation + +import ( + "time" + + "github.com/google/uuid" + "github.com/pelletier/go-toml" + "github.com/pkg/errors" + + "github.com/smartcontractkit/chainlink/v2/core/services/job" +) + +// ValidatedServerSpec validates and converts the given toml string to a job.Job. +func ValidatedServerSpec(tomlString string) (job.Job, error) { + jb := job.Job{ + // Default to generating a UUID, can be overwritten by the specified one in tomlString. + ExternalJobID: uuid.New(), + } + + tree, err := toml.Load(tomlString) + if err != nil { + return jb, errors.Wrap(err, "loading toml") + } + + err = tree.Unmarshal(&jb) + if err != nil { + return jb, errors.Wrap(err, "unmarshalling toml spec") + } + + if jb.Type != job.LegacyGasStationServer { + return jb, errors.Errorf("unsupported type %s", jb.Type) + } + + var spec job.LegacyGasStationServerSpec + err = tree.Unmarshal(&spec) + if err != nil { + return jb, errors.Wrap(err, "unmarshalling toml job") + } + + // Required fields + if spec.ForwarderAddress == "" { + return jb, notSet("forwarderAddress") + } + if spec.EVMChainID == nil { + return jb, notSet("evmChainID") + } + if spec.CCIPChainSelector == nil { + return jb, notSet("ccipChainSelector") + } + if spec.FromAddresses == nil { + return jb, notSet("fromAddresses") + } + + jb.LegacyGasStationServerSpec = &spec + + return jb, nil +} + +// ValidatedSidecarSpec validates and converts the given toml string to a job.Job. +func ValidatedSidecarSpec(tomlString string) (job.Job, error) { + jb := job.Job{ + // Default to generating a UUID, can be overwritten by the specified one in tomlString. + ExternalJobID: uuid.New(), + } + + tree, err := toml.Load(tomlString) + if err != nil { + return jb, errors.Wrap(err, "loading toml") + } + + err = tree.Unmarshal(&jb) + if err != nil { + return jb, errors.Wrap(err, "unmarshalling toml spec") + } + + if jb.Type != job.LegacyGasStationSidecar { + return jb, errors.Errorf("unsupported type %s", jb.Type) + } + + var spec job.LegacyGasStationSidecarSpec + err = tree.Unmarshal(&spec) + if err != nil { + return jb, errors.Wrap(err, "unmarshalling toml job") + } + + // Required fields + if spec.ForwarderAddress == "" { + return jb, notSet("forwarderAddress") + } + if spec.OffRampAddress == "" { + return jb, notSet("offRampAddress") + } + if spec.EVMChainID == nil { + return jb, notSet("evmChainID") + } + if spec.CCIPChainSelector == nil { + return jb, notSet("ccipChainSelector") + } + + if spec.StatusUpdateURL == "" { + return jb, notSet("statusUpdateURL") + } + + // Defaults + if spec.LookbackBlocks == 0 { + spec.LookbackBlocks = 10000 + } + if spec.PollPeriod == 0 { + spec.PollPeriod = 15 * time.Second + } + if spec.RunTimeout == 0 { + spec.RunTimeout = 30 * time.Second + } + + jb.LegacyGasStationSidecarSpec = &spec + + return jb, nil +} + +func notSet(field string) error { + return errors.Errorf("%q must be set", field) +} diff --git a/core/store/migrate/migrations/0198_additional_legacy_gas_station_fields.sql b/core/store/migrate/migrations/0198_additional_legacy_gas_station_fields.sql new file mode 100644 index 00000000000..feedd63163b --- /dev/null +++ b/core/store/migrate/migrations/0198_additional_legacy_gas_station_fields.sql @@ -0,0 +1,7 @@ +-- +goose Up +ALTER TABLE legacy_gas_station_sidecar_specs ADD client_certificate text; +ALTER TABLE legacy_gas_station_sidecar_specs ADD client_key text; + +-- +goose Down +ALTER TABLE legacy_gas_station_sidecar_specs DROP client_certificate; +ALTER TABLE legacy_gas_station_sidecar_specs DROP client_key; \ No newline at end of file diff --git a/core/testdata/testspecs/v2_specs.go b/core/testdata/testspecs/v2_specs.go index 3dd7c675d50..c1b74010130 100644 --- a/core/testdata/testspecs/v2_specs.go +++ b/core/testdata/testspecs/v2_specs.go @@ -836,3 +836,158 @@ storeBlockhashesBatchSize = %d return BlockHeaderFeederSpec{BlockHeaderFeederSpecParams: params, toml: toml} } + +// LegacyGasStationServerSpecParams defines params for building a legacy gas station server job spec. +type LegacyGasStationServerSpecParams struct { + JobID string + Name string + ForwarderAddress string + EVMChainID uint64 + CCIPChainSelector uint64 + FromAddresses []string +} + +// LegacyGasStationServerSpec defines a legacy gas station server job spec. +type LegacyGasStationServerSpec struct { + LegacyGasStationServerSpecParams + toml string +} + +// Toml returns the LegacyGasStationServerSpec in TOML string form. +func (l LegacyGasStationServerSpec) Toml() string { + return l.toml +} + +// GenerateLegacyGasStationServerSpec creates a LegacyGasStationServerSpec from the given params. +func GenerateLegacyGasStationServerSpec(params LegacyGasStationServerSpecParams) LegacyGasStationServerSpec { + if params.JobID == "" { + params.JobID = "123e4567-e89b-12d3-a456-426655442211" + } + + if params.Name == "" { + params.Name = "legacygasstationserver" + } + + if params.ForwarderAddress == "" { + params.ForwarderAddress = "0xb078DA5cDa45Ee0f5D2007C03fCf8d9461395e6c" + } + + if params.CCIPChainSelector == 0 { + params.CCIPChainSelector = 125500 + } + + var formattedFromAddresses string + if params.FromAddresses == nil { + formattedFromAddresses = `["0xBe0b739f841bC113D4F4e4CdD16086ffAbB5f39f"]` + } else { + var addresses []string + for _, address := range params.FromAddresses { + addresses = append(addresses, fmt.Sprintf("%q", address)) + } + formattedFromAddresses = fmt.Sprintf("[%s]", strings.Join(addresses, ", ")) + } + + template := ` +type = "legacygasstationserver" +schemaVersion = 1 +name = "%s" +forwarderAddress = "%s" +evmChainID = "%d" +ccipChainSelector = "%d" +fromAddresses = %s +` + toml := fmt.Sprintf(template, params.Name, params.ForwarderAddress, params.EVMChainID, params.CCIPChainSelector, + formattedFromAddresses) + + return LegacyGasStationServerSpec{LegacyGasStationServerSpecParams: params, toml: toml} +} + +// LegacyGasStationSidecarSpecParams defines params for building a legacy gas station sidecar job spec. +type LegacyGasStationSidecarSpecParams struct { + JobID string + Name string + ForwarderAddress string + OffRampAddress string + LookbackBlocks int + PollPeriod time.Duration + RunTimeout time.Duration + EVMChainID uint64 + CCIPChainSelector uint64 + StatusUpdateURL string + ClientCertificate *string + ClientKey *string +} + +// LegacyGasStationSidecarSpec defines a legacy gas station sidecar job spec. +type LegacyGasStationSidecarSpec struct { + LegacyGasStationSidecarSpecParams + toml string +} + +// Toml returns the LegacyGasStationSidecarSpec in TOML string form. +func (l LegacyGasStationSidecarSpec) Toml() string { + return l.toml +} + +// GenerateLegacyGasStationSidecarSpec creates a LegacyGasStationSidecarSpec from the given params. +func GenerateLegacyGasStationSidecarSpec(params LegacyGasStationSidecarSpecParams) LegacyGasStationSidecarSpec { + if params.JobID == "" { + params.JobID = "123e4567-e89b-12d3-a456-426655442211" + } + + if params.Name == "" { + params.Name = "legacygasstationsidecar" + } + + if params.ForwarderAddress == "" { + params.ForwarderAddress = "0x2d7F888fE0dD469bd81A12f77e6291508f714d4B" + } + + if params.OffRampAddress == "" { + params.OffRampAddress = "0x016D54091ee83D42aF46e4F2d7177D0A232D2bDa" + } + + if params.CCIPChainSelector == 0 { + params.CCIPChainSelector = 125500 + } + + if params.LookbackBlocks == 0 { + params.LookbackBlocks = 10000 + } + + if params.PollPeriod == 0 { + params.PollPeriod = 10 * time.Second + } + + if params.RunTimeout == 0 { + params.RunTimeout = 10 * time.Second + } + + if params.StatusUpdateURL == "" { + params.StatusUpdateURL = "http://testurl.com" + } + + template := ` +type = "legacygasstationsidecar" +schemaVersion = 1 +name = "%s" +forwarderAddress = "%s" +offRampAddress = "%s" +lookbackBlocks = %d +pollPeriod = "%s" +runTimeout = "%s" +evmChainID = "%d" +ccipChainSelector = "%d" +statusUpdateURL = "%s" +` + toml := fmt.Sprintf(template, params.Name, params.ForwarderAddress, + params.OffRampAddress, params.LookbackBlocks, params.PollPeriod, + params.RunTimeout, params.EVMChainID, params.CCIPChainSelector, params.StatusUpdateURL) + + if params.ClientKey != nil && params.ClientCertificate != nil { + toml = fmt.Sprintf(toml+"\nclientCertificate: %s"+"\nclientKey: %s", + params.ClientCertificate, params.ClientCertificate) + } + + return LegacyGasStationSidecarSpec{LegacyGasStationSidecarSpecParams: params, toml: toml} +} diff --git a/core/web/jobs_controller.go b/core/web/jobs_controller.go index 4c8a77d370e..47a2ea5a552 100644 --- a/core/web/jobs_controller.go +++ b/core/web/jobs_controller.go @@ -23,6 +23,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/job" "github.com/smartcontractkit/chainlink/v2/core/services/keeper" "github.com/smartcontractkit/chainlink/v2/core/services/keystore" + "github.com/smartcontractkit/chainlink/v2/core/services/legacygasstation" "github.com/smartcontractkit/chainlink/v2/core/services/ocr" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/validate" "github.com/smartcontractkit/chainlink/v2/core/services/ocrbootstrap" @@ -248,6 +249,10 @@ func (jc *JobsController) validateJobSpec(tomlString string) (jb job.Job, status jb, err = blockheaderfeeder.ValidatedSpec(tomlString) case job.Bootstrap: jb, err = ocrbootstrap.ValidatedBootstrapSpecToml(tomlString) + case job.LegacyGasStationServer: + jb, err = legacygasstation.ValidatedServerSpec(tomlString) + case job.LegacyGasStationSidecar: + jb, err = legacygasstation.ValidatedSidecarSpec(tomlString) case job.Gateway: jb, err = gateway.ValidatedGatewaySpec(tomlString) default: diff --git a/core/web/legacy_gas_station_controller.go b/core/web/legacy_gas_station_controller.go new file mode 100644 index 00000000000..5b33410eaa9 --- /dev/null +++ b/core/web/legacy_gas_station_controller.go @@ -0,0 +1,39 @@ +package web + +import ( + "fmt" + + "github.com/gin-gonic/gin" + + "github.com/smartcontractkit/capital-markets-projects/lib/services/legacygasstation/types" + "github.com/smartcontractkit/capital-markets-projects/lib/web/jsonrpc" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/legacygasstation" +) + +type LegacyGasStationController struct { + requestRouter legacygasstation.RequestRouter + lggr logger.Logger +} + +// SendTransaction is a JSON RPC endpoint that submits meta transaction on-chain +func (c *LegacyGasStationController) SendTransaction(ctx *gin.Context) { + var req types.SendTransactionRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + c.lggr.Warnw("parse error", "err", err) + jsonrpc.JsonRpcError(ctx, jsonrpc.ParseError, fmt.Sprintf("Parse error: %s", err.Error())) + return + } + + resp, jsonRPCError := c.requestRouter.SendTransaction(ctx, req) + if jsonRPCError != nil { + jsonrpc.JsonRpcError(ctx, jsonRPCError.Code, jsonRPCError.Message) + return + } + // should not happen + if resp == nil { + jsonrpc.JsonRpcError(ctx, jsonrpc.InternalError, jsonrpc.InternalServerErrorMsg) + return + } + jsonrpc.JsonRpcResponse(ctx, *resp) +} diff --git a/core/web/resolver/mutation.go b/core/web/resolver/mutation.go index 68cbb0b7896..8ea9ecb3313 100644 --- a/core/web/resolver/mutation.go +++ b/core/web/resolver/mutation.go @@ -33,6 +33,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocrkey" "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/vrfkey" + "github.com/smartcontractkit/chainlink/v2/core/services/legacygasstation" "github.com/smartcontractkit/chainlink/v2/core/services/ocr" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/validate" "github.com/smartcontractkit/chainlink/v2/core/services/ocrbootstrap" @@ -1042,6 +1043,10 @@ func (r *Resolver) CreateJob(ctx context.Context, args struct { jb, err = blockheaderfeeder.ValidatedSpec(args.Input.TOML) case job.Bootstrap: jb, err = ocrbootstrap.ValidatedBootstrapSpecToml(args.Input.TOML) + case job.LegacyGasStationServer: + jb, err = legacygasstation.ValidatedServerSpec(args.Input.TOML) + case job.LegacyGasStationSidecar: + jb, err = legacygasstation.ValidatedSidecarSpec(args.Input.TOML) case job.Gateway: jb, err = gateway.ValidatedGatewaySpec(args.Input.TOML) default: diff --git a/core/web/resolver/testdata/config-empty-effective.toml b/core/web/resolver/testdata/config-empty-effective.toml index 45e92a147d3..e3f6bc12ec8 100644 --- a/core/web/resolver/testdata/config-empty-effective.toml +++ b/core/web/resolver/testdata/config-empty-effective.toml @@ -6,6 +6,7 @@ ShutdownGracePeriod = '5s' FeedsManager = true LogPoller = false UICSAKeys = false +EAL = false [Database] DefaultIdleInTxSessionTimeout = '1h0m0s' diff --git a/core/web/resolver/testdata/config-full.toml b/core/web/resolver/testdata/config-full.toml index ff7eb832c9c..7891054ad16 100644 --- a/core/web/resolver/testdata/config-full.toml +++ b/core/web/resolver/testdata/config-full.toml @@ -6,6 +6,7 @@ ShutdownGracePeriod = '10s' FeedsManager = true LogPoller = true UICSAKeys = true +EAL = true [Database] DefaultIdleInTxSessionTimeout = '1m0s' diff --git a/core/web/resolver/testdata/config-multi-chain-effective.toml b/core/web/resolver/testdata/config-multi-chain-effective.toml index 665de9be8cb..38c60d10cf7 100644 --- a/core/web/resolver/testdata/config-multi-chain-effective.toml +++ b/core/web/resolver/testdata/config-multi-chain-effective.toml @@ -6,6 +6,7 @@ ShutdownGracePeriod = '5s' FeedsManager = true LogPoller = false UICSAKeys = false +EAL = false [Database] DefaultIdleInTxSessionTimeout = '1h0m0s' diff --git a/core/web/router.go b/core/web/router.go index a873f14b708..3547309c39b 100644 --- a/core/web/router.go +++ b/core/web/router.go @@ -86,6 +86,7 @@ func NewRouter(app chainlink.Application, prometheus *ginprom.Prometheus) (*gin. sessionRoutes(app, api) v2Routes(app, api) loopRoutes(app, api) + legacyGasStationRoutes(app, api) guiAssetRoutes(engine, config.Insecure().DisableRateLimiting(), app.GetLogger()) @@ -678,3 +679,14 @@ func prometheusHandler(token string, h http.Handler) gin.HandlerFunc { h.ServeHTTP(c.Writer, c.Request) } } + +func legacyGasStationRoutes(app chainlink.Application, r *gin.RouterGroup) { + if app.GetConfig().Feature().EAL() { + group := r.Group("/gasstation") + lgsc := LegacyGasStationController{ + requestRouter: app.LegacyGasStationRequestRouter(), + lggr: app.GetLogger(), + } + group.Any("send_transaction", lgsc.SendTransaction) + } +} diff --git a/docs/CONFIG.md b/docs/CONFIG.md index ceb3d3dfe08..378a5bfab8a 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -51,6 +51,7 @@ ShutdownGracePeriod is the maximum time allowed to shut down gracefully. If exce FeedsManager = true # Default LogPoller = false # Default UICSAKeys = false # Default +EAL = false # Default ``` @@ -72,6 +73,12 @@ UICSAKeys = false # Default ``` UICSAKeys enables CSA Keys in the UI. +### EAL +```toml +EAL = false # Default +``` +EAL enables API endpoints for connecting to blockchain. + ## Database ```toml [Database] diff --git a/go.mod b/go.mod index d0fd99a8287..d5b43bac109 100644 --- a/go.mod +++ b/go.mod @@ -66,6 +66,8 @@ require ( github.com/shirou/gopsutil/v3 v3.23.8 github.com/shopspring/decimal v1.3.1 github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 + github.com/smartcontractkit/capital-markets-projects v0.0.0-20230926155417-a3ac6c7f5bea + github.com/smartcontractkit/capital-markets-projects/lib v0.0.0-20230926155417-a3ac6c7f5bea github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230913032705-f924d753cc47 github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230926113942-a871b2976dc1 github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230831134610-680240b97aca @@ -79,6 +81,7 @@ require ( github.com/smartcontractkit/wsrpc v0.7.2 github.com/spf13/cast v1.5.1 github.com/stretchr/testify v1.8.4 + github.com/test-go/testify v1.1.4 github.com/theodesp/go-heaps v0.0.0-20190520121037-88e35354fe0a github.com/tidwall/gjson v1.16.0 github.com/ugorji/go/codec v1.2.11 @@ -90,14 +93,14 @@ require ( go.dedis.ch/kyber/v3 v3.1.0 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.24.0 - golang.org/x/crypto v0.11.0 + golang.org/x/crypto v0.12.0 golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 - golang.org/x/net v0.12.0 + golang.org/x/net v0.14.0 golang.org/x/sync v0.3.0 - golang.org/x/term v0.10.0 - golang.org/x/text v0.11.0 + golang.org/x/term v0.11.0 + golang.org/x/text v0.12.0 golang.org/x/time v0.3.0 - golang.org/x/tools v0.11.0 + golang.org/x/tools v0.12.0 gonum.org/v1/gonum v0.13.0 google.golang.org/protobuf v1.31.0 gopkg.in/guregu/null.v2 v2.1.2 @@ -212,7 +215,7 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/yamux v0.0.0-20200609203250-aecfd211c9ce // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect - github.com/holiman/uint256 v1.2.2 // indirect + github.com/holiman/uint256 v1.2.3 // indirect github.com/huandu/skiplist v1.2.0 // indirect github.com/huandu/xstrings v1.4.0 // indirect github.com/huin/goupnp v1.0.3 // indirect diff --git a/go.sum b/go.sum index 59356146049..4ccd21ae3f1 100644 --- a/go.sum +++ b/go.sum @@ -705,8 +705,8 @@ github.com/hdevalence/ed25519consensus v0.1.0 h1:jtBwzzcHuTmFrQN6xQZn6CQEO/V9f7H github.com/hdevalence/ed25519consensus v0.1.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= -github.com/holiman/uint256 v1.2.2 h1:TXKcSGc2WaxPD2+bmzAsVthL4+pEN0YwXcL5qED83vk= -github.com/holiman/uint256 v1.2.2/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= +github.com/holiman/uint256 v1.2.3 h1:K8UWO1HUJpRMXBxbmaY1Y8IAMZC/RsKB+ArEnnK4l5o= +github.com/holiman/uint256 v1.2.3/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c= github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= @@ -1455,6 +1455,10 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 h1:T3lFWumvbfM1u/etVq42Afwq/jtNSBSOA8n5jntnNPo= github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704/go.mod h1:2QuJdEouTWjh5BDy5o/vgGXQtR4Gz8yH1IYB5eT7u4M= +github.com/smartcontractkit/capital-markets-projects v0.0.0-20230926155417-a3ac6c7f5bea h1:20YSrEi88NimVAKuDiwmbI4xbcGEvBb4MlW1vig0euU= +github.com/smartcontractkit/capital-markets-projects v0.0.0-20230926155417-a3ac6c7f5bea/go.mod h1:5G+3CGpX2DYoVOfcrp9IZy7CPTCpL0+pCOmB/3R7t7k= +github.com/smartcontractkit/capital-markets-projects/lib v0.0.0-20230926155417-a3ac6c7f5bea h1:R/e1SuWpEooOxr8F+1kA6PK5vZ9mW3F3BNwg2W6aREE= +github.com/smartcontractkit/capital-markets-projects/lib v0.0.0-20230926155417-a3ac6c7f5bea/go.mod h1:6sAI/Wb424wYzwCa2tn+r+eup2JtxZvY/On3Ct38xp0= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230913032705-f924d753cc47 h1:vdieOW3CZGdD2R5zvCSMS+0vksyExPN3/Fa1uVfld/A= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20230913032705-f924d753cc47/go.mod h1:xMwqRdj5vqYhCJXgKVqvyAwdcqM6ZAEhnwEQ4Khsop8= github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230926113942-a871b2976dc1 h1:Db333w+fSm2e18LMikcIQHIZqgxZruW9uCUGJLUC9mI= @@ -1739,8 +1743,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1841,8 +1845,8 @@ golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1969,8 +1973,8 @@ golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= -golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1982,8 +1986,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -2057,8 +2061,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8= -golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= +golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/integration-tests/types/config/node/core.go b/integration-tests/types/config/node/core.go index 3702d11745f..d06acb1f8e1 100644 --- a/integration-tests/types/config/node/core.go +++ b/integration-tests/types/config/node/core.go @@ -56,6 +56,7 @@ func NewBaseConfig() *chainlink.Config { LogPoller: utils2.Ptr(true), FeedsManager: utils2.Ptr(true), UICSAKeys: utils2.Ptr(true), + EAL: utils2.Ptr(true), }, P2P: toml.P2P{}, }, diff --git a/testdata/scripts/node/validate/default.txtar b/testdata/scripts/node/validate/default.txtar index 85b16edaa27..5d87882f0a6 100644 --- a/testdata/scripts/node/validate/default.txtar +++ b/testdata/scripts/node/validate/default.txtar @@ -18,6 +18,7 @@ ShutdownGracePeriod = '5s' FeedsManager = true LogPoller = false UICSAKeys = false +EAL = false [Database] DefaultIdleInTxSessionTimeout = '1h0m0s' diff --git a/testdata/scripts/node/validate/disk-based-logging-disabled.txtar b/testdata/scripts/node/validate/disk-based-logging-disabled.txtar index 5f02793ff57..00c84813adf 100644 --- a/testdata/scripts/node/validate/disk-based-logging-disabled.txtar +++ b/testdata/scripts/node/validate/disk-based-logging-disabled.txtar @@ -62,6 +62,7 @@ ShutdownGracePeriod = '5s' FeedsManager = true LogPoller = false UICSAKeys = false +EAL = false [Database] DefaultIdleInTxSessionTimeout = '1h0m0s' diff --git a/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar b/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar index 527a739f7ca..824807ad568 100644 --- a/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar +++ b/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar @@ -62,6 +62,7 @@ ShutdownGracePeriod = '5s' FeedsManager = true LogPoller = false UICSAKeys = false +EAL = false [Database] DefaultIdleInTxSessionTimeout = '1h0m0s' diff --git a/testdata/scripts/node/validate/disk-based-logging.txtar b/testdata/scripts/node/validate/disk-based-logging.txtar index 791a8aad076..cef8d664125 100644 --- a/testdata/scripts/node/validate/disk-based-logging.txtar +++ b/testdata/scripts/node/validate/disk-based-logging.txtar @@ -62,6 +62,7 @@ ShutdownGracePeriod = '5s' FeedsManager = true LogPoller = false UICSAKeys = false +EAL = false [Database] DefaultIdleInTxSessionTimeout = '1h0m0s' diff --git a/testdata/scripts/node/validate/invalid.txtar b/testdata/scripts/node/validate/invalid.txtar index e9db92fb8f7..62c5fc21147 100644 --- a/testdata/scripts/node/validate/invalid.txtar +++ b/testdata/scripts/node/validate/invalid.txtar @@ -52,6 +52,7 @@ ShutdownGracePeriod = '5s' FeedsManager = true LogPoller = false UICSAKeys = false +EAL = false [Database] DefaultIdleInTxSessionTimeout = '1h0m0s' diff --git a/testdata/scripts/node/validate/valid.txtar b/testdata/scripts/node/validate/valid.txtar index f48fa1926d8..19cfc623b82 100644 --- a/testdata/scripts/node/validate/valid.txtar +++ b/testdata/scripts/node/validate/valid.txtar @@ -59,6 +59,7 @@ ShutdownGracePeriod = '5s' FeedsManager = true LogPoller = false UICSAKeys = false +EAL = false [Database] DefaultIdleInTxSessionTimeout = '1h0m0s'