diff --git a/go.mod b/go.mod index 92e546feb..0d485b698 100644 --- a/go.mod +++ b/go.mod @@ -8,19 +8,21 @@ require ( github.com/gagliardetto/gofuzz v1.2.2 github.com/gagliardetto/solana-go v1.4.1-0.20220428092759-5250b4abbb27 github.com/gagliardetto/treeout v0.1.4 + github.com/gagliardetto/utilz v0.1.1 github.com/google/uuid v1.3.1 - github.com/hashicorp/go-plugin v1.5.2 + github.com/hashicorp/go-plugin v1.6.0 + github.com/mitchellh/mapstructure v1.5.0 github.com/pelletier/go-toml/v2 v2.1.1 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.17.0 - github.com/smartcontractkit/chainlink-common v0.1.7-0.20240213113935-001c2f4befd4 + github.com/smartcontractkit/chainlink-common v0.1.7-0.20240312172711-7ec0dab0a498 github.com/smartcontractkit/libocr v0.0.0-20240112202000-6359502d2ff1 - github.com/stretchr/testify v1.8.4 - github.com/test-go/testify v1.1.4 + github.com/stretchr/testify v1.9.0 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.26.0 golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa golang.org/x/sync v0.6.0 + golang.org/x/text v0.14.0 gopkg.in/guregu/null.v4 v4.0.0 ) @@ -32,6 +34,7 @@ require ( github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blendle/zapdriver v1.3.1 // indirect + github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 // indirect @@ -47,16 +50,20 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.0-rc.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect + github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect - github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect + github.com/hashicorp/yamux v0.1.1 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.15.15 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/linkedin/goavro/v2 v2.12.0 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/miekg/dns v1.1.35 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -70,9 +77,11 @@ require ( github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.11.1 // indirect github.com/riferrei/srclient v0.5.4 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.2.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect - github.com/stretchr/objx v0.5.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125 // indirect github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/match v1.1.1 // indirect @@ -93,11 +102,11 @@ require ( golang.org/x/net v0.20.0 // indirect golang.org/x/sys v0.16.0 // indirect golang.org/x/term v0.16.0 // indirect - golang.org/x/text v0.14.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect google.golang.org/grpc v1.58.3 // indirect google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ff155ba26..d3874d791 100644 --- a/go.sum +++ b/go.sum @@ -78,6 +78,8 @@ github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHf github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129 h1:gfAMKE626QEuKG3si0pdTRcr/YEbBoxY+3GOH3gWvl4= +github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= @@ -105,6 +107,7 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -142,10 +145,13 @@ github.com/gagliardetto/binary v0.7.1 h1:6ggDQ26vR+4xEvl/S13NcdLK3MUCi4oSy73pS9a github.com/gagliardetto/binary v0.7.1/go.mod h1:aOfYkc20U0deHaHn/LVZXiqlkDbFAX0FpTlDhsXa0S0= github.com/gagliardetto/gofuzz v1.2.2 h1:XL/8qDMzcgvR4+CyRQW9UGdwPRPMHVJfqQ/uMvSUuQw= github.com/gagliardetto/gofuzz v1.2.2/go.mod h1:bkH/3hYLZrMLbfYWA0pWzXmi5TTRZnu4pMGZBkqMKvY= +github.com/gagliardetto/hashsearch v0.0.0-20191005111333-09dd671e19f9/go.mod h1:513DXpQPzeRo7d4dsCP3xO3XI8hgvruMl9njxyQeraQ= github.com/gagliardetto/solana-go v1.4.1-0.20220428092759-5250b4abbb27 h1:q2IztKyRQUxJ6abXRsawaBtvDFvM+szj4jDqV4od1gs= github.com/gagliardetto/solana-go v1.4.1-0.20220428092759-5250b4abbb27/go.mod h1:NFuoDwHPvw858ZMHUJr6bkhN8qHt4x6e+U3EYHxAwNY= github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw= github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok= +github.com/gagliardetto/utilz v0.1.1 h1:/etW4hl607emKg6R6Lj9jRJ9d6ue2AQOyjhuAwjzs1U= +github.com/gagliardetto/utilz v0.1.1/go.mod h1:b+rGFkRHz3HWJD0RYMzat47JyvbTtpE0iEcYTRJTLLA= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -244,6 +250,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026 h1:BpJ2o0OR5FV7vrkDYfXYVJQeMNWa8RhklZOpW2ITAIQ= +github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026/go.mod h1:5Scbynm8dF1XAPwIwkGPqzkM/shndPm79Jd1003hTjE= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -267,8 +275,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= -github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -328,6 +336,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.35 h1:oTfOaDH+mZkdcgdIjH6yBajRGtIwcwcaR+rt23ZSrJs= +github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -380,6 +390,7 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -410,10 +421,13 @@ github.com/riferrei/srclient v0.5.4 h1:dfwyR5u23QF7beuVl2WemUY2KXh5+Sc4DHKyPXBNY github.com/riferrei/srclient v0.5.4/go.mod h1:vbkLmWcgYa7JgfPvuy/+K8fTS0p1bApqadxrxi/S1MI= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= github.com/santhosh-tekuri/jsonschema/v5 v5.2.0 h1:WCcC4vZDS1tYNxjWlwRJZQy28r8CMoggKnxNzxsVDMQ= github.com/santhosh-tekuri/jsonschema/v5 v5.2.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= @@ -424,8 +438,8 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240213113935-001c2f4befd4 h1:Yk0RK9WV59ISOZZMsdtxZBAKaBfdgb05oXyca/qSqcw= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240213113935-001c2f4befd4/go.mod h1:pRlQrvcizMmuHAUV4N96oO2e3XbA99JCQELLc6ES160= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240312172711-7ec0dab0a498 h1:WeLb67RVd0lqdbm8qJ/LqrcfoLr7PoMjzi9wGy7ocqc= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240312172711-7ec0dab0a498/go.mod h1:8sn4HEfG8lR/D1Ov0yPHsY4kOleDKZKh2r8Op4oaAFE= github.com/smartcontractkit/go-plugin v0.0.0-20231003134350-e49dad63b306 h1:ko88+ZznniNJZbZPWAvHQU8SwKAdHngdDZ+pvVgB5ss= github.com/smartcontractkit/go-plugin v0.0.0-20231003134350-e49dad63b306/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4= github.com/smartcontractkit/grpc-proxy v0.0.0-20230731113816-f1be6620749f h1:hgJif132UCdjo8u43i7iPN1/MFnu49hv7lFGFftCHKU= @@ -447,8 +461,9 @@ github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -459,8 +474,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125 h1:3SNcvBmEPE1YlB1JpVZouslJpI3GBNoiqW7+wb0Rz7w= @@ -540,6 +556,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= @@ -637,6 +654,7 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= @@ -658,6 +676,7 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -692,6 +711,7 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= @@ -879,6 +899,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index b9cdee810..9a57ee8f4 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -12,13 +12,13 @@ require ( github.com/lib/pq v1.10.9 github.com/onsi/gomega v1.30.0 github.com/rs/zerolog v1.30.0 - github.com/smartcontractkit/chainlink-common v0.1.7-0.20240219152510-85226a0fbdc1 + github.com/smartcontractkit/chainlink-common v0.1.7-0.20240312172711-7ec0dab0a498 github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240216142700-c5869534c19e github.com/smartcontractkit/chainlink-testing-framework v1.23.6 github.com/smartcontractkit/chainlink/integration-tests v0.0.0-20240221052856-2cd4bc5508e2 github.com/smartcontractkit/chainlink/v2 v2.9.0-beta0.0.20240221052856-2cd4bc5508e2 github.com/smartcontractkit/libocr v0.0.0-20240215150045-fe2ba71b2f0a - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/testcontainers/testcontainers-go v0.23.0 go.uber.org/zap v1.26.0 golang.org/x/crypto v0.19.0 @@ -372,7 +372,7 @@ require ( github.com/spf13/viper v1.16.0 // indirect github.com/status-im/keycard-go v0.2.0 // indirect github.com/streamingfast/logging v0.0.0-20220405224725-2755dab2ce75 // indirect - github.com/stretchr/objx v0.5.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/supranational/blst v0.3.11 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index e81748fbb..0208dcba4 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1399,8 +1399,8 @@ github.com/smartcontractkit/chain-selectors v1.0.10 h1:t9kJeE6B6G+hKD0GYR4kGJSCq github.com/smartcontractkit/chain-selectors v1.0.10/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.2-0.20240118014648-1ab6a88c9429 h1:xkejUBZhcBpBrTSfxc91Iwzadrb6SXw8ks69bHIQ9Ww= github.com/smartcontractkit/chainlink-automation v1.0.2-0.20240118014648-1ab6a88c9429/go.mod h1:wJmVvDf4XSjsahWtfUq3wvIAYEAuhr7oxmxYnEL/LGQ= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240219152510-85226a0fbdc1 h1:MNYkjakmoKxg7L1nmfAVeFOdONaLT7E62URBpmcTh84= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240219152510-85226a0fbdc1/go.mod h1:6aXWSEQawX2oZXcPPOdxnEGufAhj7PqPKolXf6ijRGA= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240312172711-7ec0dab0a498 h1:WeLb67RVd0lqdbm8qJ/LqrcfoLr7PoMjzi9wGy7ocqc= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240312172711-7ec0dab0a498/go.mod h1:8sn4HEfG8lR/D1Ov0yPHsY4kOleDKZKh2r8Op4oaAFE= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240213120401-01a23955f9f8 h1:I326nw5GwHQHsLKHwtu5Sb9EBLylC8CfUd7BFAS0jtg= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240213120401-01a23955f9f8/go.mod h1:a65NtrK4xZb01mf0dDNghPkN2wXgcqFQ55ADthVBgMc= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240214203158-47dae5de1336 h1:j00D0/EqE9HRu+63v7KwUOe4ZxLc4AN5SOJFiinkkH0= @@ -1473,8 +1473,9 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -1486,8 +1487,9 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= diff --git a/pkg/solana/chain_reader.go b/pkg/solana/chain_reader.go deleted file mode 100644 index 6642cf62a..000000000 --- a/pkg/solana/chain_reader.go +++ /dev/null @@ -1,74 +0,0 @@ -package solana - -import ( - "context" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/services" - "github.com/smartcontractkit/chainlink-common/pkg/types" -) - -const ServiceName = "SolanaChainReader" - -type SolanaChainReaderService struct { - lggr logger.Logger - services.StateMachine -} - -var ( - _ services.Service = &SolanaChainReaderService{} - _ types.ChainReader = &SolanaChainReaderService{} -) - -// NewChainReaderService is a constructor for a new ChainReaderService for Solana. Returns a nil service on error. -func NewChainReaderService(lggr logger.Logger) (*SolanaChainReaderService, error) { - return &SolanaChainReaderService{ - lggr: logger.Named(lggr, ServiceName), - }, nil -} - -// Name implements the services.ServiceCtx interface and returns the logger service name. -func (s *SolanaChainReaderService) Name() string { - return s.lggr.Name() -} - -// Start implements the services.ServiceCtx interface and starts necessary background services. -// An error is returned if starting any internal services fails. Subsequent calls to Start return -// and error. -func (s *SolanaChainReaderService) Start(_ context.Context) error { - return s.StartOnce(ServiceName, func() error { - return nil - }) -} - -// Close implements the services.ServiceCtx interface and stops all background services and cleans -// up used resources. Subsequent calls to Close return an error. -func (s *SolanaChainReaderService) Close() error { - return s.StopOnce(ServiceName, func() error { - return nil - }) -} - -// Ready implements the services.ServiceCtx interface and returns an error if starting the service -// encountered any errors or if the service is not ready to serve requests. -func (s *SolanaChainReaderService) Ready() error { - return s.StateMachine.Ready() -} - -// HealthReport implements the services.ServiceCtx interface and returns errors for any internal -// function or service that may have failed. -func (s *SolanaChainReaderService) HealthReport() map[string]error { - return map[string]error{s.Name(): s.Healthy()} -} - -// GetLatestValue implements the types.ChainReader interface and requests and parses on-chain -// data named by the provided contract, method, and params. -func (s *SolanaChainReaderService) GetLatestValue(_ context.Context, contractName, method string, params any, returnVal any) error { - return types.UnimplementedError("GetLatestValue not available") -} - -// Bind implements the types.ChainReader interface and allows new contract bindings to be added -// to the service. -func (s *SolanaChainReaderService) Bind(_ context.Context, bindings []types.BoundContract) error { - return types.UnimplementedError("Bind not available") -} diff --git a/pkg/solana/chain_reader_test.go b/pkg/solana/chain_reader_test.go deleted file mode 100644 index 9db536a6c..000000000 --- a/pkg/solana/chain_reader_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package solana_test - -import ( - "testing" - - "github.com/test-go/testify/require" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" - - "github.com/smartcontractkit/chainlink-solana/pkg/solana" -) - -func TestSolanaChainReaderService_ServiceCtx(t *testing.T) { - t.Parallel() - - ctx := tests.Context(t) - svc, err := solana.NewChainReaderService(logger.Test(t)) - - require.NoError(t, err) - require.NotNil(t, svc) - - require.Error(t, svc.Ready()) - require.Len(t, svc.HealthReport(), 1) - require.Contains(t, svc.HealthReport(), solana.ServiceName) - require.Error(t, svc.HealthReport()[solana.ServiceName]) - - require.NoError(t, svc.Start(ctx)) - require.NoError(t, svc.Ready()) - require.Equal(t, map[string]error{solana.ServiceName: nil}, svc.HealthReport()) - - require.Error(t, svc.Start(ctx)) - - require.NoError(t, svc.Close()) - require.Error(t, svc.Ready()) - require.Error(t, svc.Close()) -} diff --git a/pkg/solana/chainreader/account_read_binding.go b/pkg/solana/chainreader/account_read_binding.go new file mode 100644 index 000000000..6d44d1c6f --- /dev/null +++ b/pkg/solana/chainreader/account_read_binding.go @@ -0,0 +1,51 @@ +package chainreader + +import ( + "context" + "fmt" + + "github.com/gagliardetto/solana-go" + + "github.com/smartcontractkit/chainlink-common/pkg/types" +) + +// BinaryDataReader provides an interface for reading bytes from a source. This is likely a wrapper +// for a solana client. +type BinaryDataReader interface { + ReadAll(context.Context, solana.PublicKey) ([]byte, error) +} + +// accountReadBinding provides decoding and reading Solana Account data using a defined codec. The +// `idlAccount` refers to the account name in the IDL for which the codec has a type mapping. +type accountReadBinding struct { + idlAccount string + account solana.PublicKey + codec types.RemoteCodec + reader BinaryDataReader +} + +var _ readBinding = &accountReadBinding{} + +func (b *accountReadBinding) GetLatestValue(ctx context.Context, _ any, outVal any) error { + bts, err := b.reader.ReadAll(ctx, b.account) + if err != nil { + return fmt.Errorf("%w: failed to get binary data", err) + } + + return b.codec.Decode(ctx, bts, outVal, b.idlAccount) +} + +func (b *accountReadBinding) Bind(contract types.BoundContract) error { + account, err := solana.PublicKeyFromBase58(contract.Address) + if err != nil { + return err + } + + b.account = account + + return nil +} + +func (b *accountReadBinding) CreateType(_ bool) (any, error) { + return b.codec.CreateType(b.idlAccount, false) +} diff --git a/pkg/solana/chainreader/bindings.go b/pkg/solana/chainreader/bindings.go new file mode 100644 index 000000000..2125684b2 --- /dev/null +++ b/pkg/solana/chainreader/bindings.go @@ -0,0 +1,142 @@ +package chainreader + +import ( + "context" + "fmt" + "reflect" + "strconv" + "strings" + + "github.com/smartcontractkit/chainlink-common/pkg/types" +) + +type readBinding interface { + GetLatestValue(ctx context.Context, params, returnVal any) error + Bind(types.BoundContract) error + CreateType(bool) (any, error) +} + +// key is namespace +type namespaceBindings map[string]methodBindings + +// key is method name +type methodBindings map[string]readBindings + +// read bindings is a list of bindings by index +type readBindings []readBinding + +func (b namespaceBindings) AddReadBinding(namespace, methodName string, reader readBinding) { + nbs, nbsExists := b[namespace] + if !nbsExists { + nbs = methodBindings{} + b[namespace] = nbs + } + + rbs, rbsExists := nbs[methodName] + if !rbsExists { + rbs = []readBinding{} + } + + b[namespace][methodName] = append(rbs, reader) +} + +func (b namespaceBindings) GetReadBindings(namespace, methodName string) ([]readBinding, error) { + nbs, nbsExists := b[namespace] + if !nbsExists { + return nil, fmt.Errorf("%w: no read binding exists for %s", types.ErrInvalidConfig, namespace) + } + + rbs, rbsExists := nbs[methodName] + if !rbsExists { + return nil, fmt.Errorf("%w: no read binding exists for %s and %s", types.ErrInvalidConfig, namespace, methodName) + } + + return rbs, nil +} + +func (b namespaceBindings) CreateType(namespace, methodName string, forEncoding bool) (any, error) { + bindings, err := b.GetReadBindings(namespace, methodName) + if err != nil { + return nil, err + } + + if len(bindings) == 1 { + // get the item type from the binding codec + return bindings[0].CreateType(forEncoding) + } + + // build a merged struct from all bindings + fields := make([]reflect.StructField, 0) + var fieldIdx int + fieldNames := make(map[string]struct{}) + + for _, binding := range bindings { + bindingType, err := binding.CreateType(forEncoding) + if err != nil { + return nil, err + } + + tBinding := reflect.TypeOf(bindingType) + + // all bindings must be structs to allow multiple bindings + if tBinding.Kind() != reflect.Struct { + return nil, fmt.Errorf("%w: support for multiple bindings only applies to all bindings having the type struct", types.ErrInvalidType) + } + + for idx := 0; idx < tBinding.NumField(); idx++ { + value := tBinding.FieldByIndex([]int{idx}) + + _, exists := fieldNames[value.Name] + if exists { + return nil, fmt.Errorf("%w: field name overlap on %s", types.ErrInvalidConfig, value.Name) + } + + field := reflect.StructField{ + Name: value.Name, + Type: value.Type, + Index: []int{fieldIdx}, + } + + fields = append(fields, field) + + fieldIdx++ + fieldNames[value.Name] = struct{}{} + } + } + + return reflect.New(reflect.StructOf(fields)).Interface(), nil +} + +func (b namespaceBindings) Bind(boundContracts []types.BoundContract) error { + for _, bc := range boundContracts { + parts := strings.Split(bc.Name, ".") + if len(parts) != 3 { + return fmt.Errorf("%w: BoundContract.Name must follow pattern of [namespace.method.procedure_idx]", types.ErrInvalidConfig) + } + + nbs, nbsExist := b[parts[0]] + if !nbsExist { + return fmt.Errorf("%w: no namespace named %s for %s", types.ErrInvalidConfig, parts[0], bc.Name) + } + + mbs, mbsExists := nbs[parts[1]] + if !mbsExists { + return fmt.Errorf("%w: no method named %s for %s", types.ErrInvalidConfig, parts[1], bc.Name) + } + + val, err := strconv.Atoi(parts[2]) + if err != nil { + return fmt.Errorf("%w: procedure index not parsable for %s", types.ErrInvalidConfig, bc.Name) + } + + if len(mbs) <= val { + return fmt.Errorf("%w: no procedure for index %d for %s", types.ErrInvalidConfig, val, bc.Name) + } + + if err := mbs[val].Bind(bc); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/solana/chainreader/bindings_test.go b/pkg/solana/chainreader/bindings_test.go new file mode 100644 index 000000000..77befa06b --- /dev/null +++ b/pkg/solana/chainreader/bindings_test.go @@ -0,0 +1,120 @@ +package chainreader + +import ( + "context" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/types" +) + +func TestBindings_CreateType(t *testing.T) { + t.Parallel() + + t.Run("single binding returns type", func(t *testing.T) { + t.Parallel() + + expected := 8 + binding := new(mockBinding) + bindings := namespaceBindings{} + bindings.AddReadBinding("A", "B", binding) + + binding.On("CreateType", mock.Anything).Return(expected, nil) + + returned, err := bindings.CreateType("A", "B", true) + + require.NoError(t, err) + assert.Equal(t, expected, returned) + }) + + t.Run("multiple bindings return merged struct", func(t *testing.T) { + t.Parallel() + + bindingA := new(mockBinding) + bindingB := new(mockBinding) + bindings := namespaceBindings{} + + bindings.AddReadBinding("A", "B", bindingA) + bindings.AddReadBinding("A", "B", bindingB) + + bindingA.On("CreateType", mock.Anything).Return(struct{ A string }{A: "test"}, nil) + bindingB.On("CreateType", mock.Anything).Return(struct{ B int }{B: 8}, nil) + + result, err := bindings.CreateType("A", "B", true) + + expected := reflect.New(reflect.StructOf([]reflect.StructField{ + {Name: "A", Type: reflect.TypeOf("")}, + {Name: "B", Type: reflect.TypeOf(0)}, + })) + + require.NoError(t, err) + assert.Equal(t, expected.Type(), reflect.TypeOf(result)) + }) + + t.Run("multiple bindings fails when not a struct", func(t *testing.T) { + t.Parallel() + + bindingA := new(mockBinding) + bindingB := new(mockBinding) + bindings := namespaceBindings{} + + bindings.AddReadBinding("A", "B", bindingA) + bindings.AddReadBinding("A", "B", bindingB) + + bindingA.On("CreateType", mock.Anything).Return(8, nil) + bindingB.On("CreateType", mock.Anything).Return(struct{ A string }{A: "test"}, nil) + + _, err := bindings.CreateType("A", "B", true) + + require.ErrorIs(t, err, types.ErrInvalidType) + }) + + t.Run("multiple bindings errors when fields overlap", func(t *testing.T) { + t.Parallel() + + bindingA := new(mockBinding) + bindingB := new(mockBinding) + bindings := namespaceBindings{} + + bindings.AddReadBinding("A", "B", bindingA) + bindings.AddReadBinding("A", "B", bindingB) + + type A struct { + A string + B int + } + + type B struct { + A int + } + + bindingA.On("CreateType", mock.Anything).Return(A{A: ""}, nil) + bindingB.On("CreateType", mock.Anything).Return(B{A: 8}, nil) + + _, err := bindings.CreateType("A", "B", true) + + require.ErrorIs(t, err, types.ErrInvalidConfig) + }) +} + +type mockBinding struct { + mock.Mock +} + +func (_m *mockBinding) GetLatestValue(ctx context.Context, params, returnVal any) error { + return nil +} + +func (_m *mockBinding) Bind(types.BoundContract) error { + return nil +} + +func (_m *mockBinding) CreateType(b bool) (any, error) { + ret := _m.Called(b) + + return ret.Get(0), ret.Error(1) +} diff --git a/pkg/solana/chainreader/chain_reader.go b/pkg/solana/chainreader/chain_reader.go new file mode 100644 index 000000000..3418b59ff --- /dev/null +++ b/pkg/solana/chainreader/chain_reader.go @@ -0,0 +1,168 @@ +package chainreader + +import ( + "context" + "encoding/json" + + ag_solana "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/types" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" +) + +const ServiceName = "SolanaChainReader" + +type SolanaChainReaderService struct { + // provided values + lggr logger.Logger + client BinaryDataReader + + // internal values + bindings namespaceBindings + + // service state management + services.StateMachine +} + +var ( + _ services.Service = &SolanaChainReaderService{} + _ types.ChainReader = &SolanaChainReaderService{} +) + +// NewChainReaderService is a constructor for a new ChainReaderService for Solana. Returns a nil service on error. +func NewChainReaderService(lggr logger.Logger, dataReader BinaryDataReader, cfg config.ChainReader) (*SolanaChainReaderService, error) { + svc := &SolanaChainReaderService{ + lggr: logger.Named(lggr, ServiceName), + client: dataReader, + bindings: namespaceBindings{}, + } + + if err := svc.init(cfg.Namespaces); err != nil { + return nil, err + } + + return svc, nil +} + +// Name implements the services.ServiceCtx interface and returns the logger service name. +func (s *SolanaChainReaderService) Name() string { + return s.lggr.Name() +} + +// Start implements the services.ServiceCtx interface and starts necessary background services. +// An error is returned if starting any internal services fails. Subsequent calls to Start return +// and error. +func (s *SolanaChainReaderService) Start(_ context.Context) error { + return s.StartOnce(ServiceName, func() error { + return nil + }) +} + +// Close implements the services.ServiceCtx interface and stops all background services and cleans +// up used resources. Subsequent calls to Close return an error. +func (s *SolanaChainReaderService) Close() error { + return s.StopOnce(ServiceName, func() error { + return nil + }) +} + +// Ready implements the services.ServiceCtx interface and returns an error if starting the service +// encountered any errors or if the service is not ready to serve requests. +func (s *SolanaChainReaderService) Ready() error { + return s.StateMachine.Ready() +} + +// HealthReport implements the services.ServiceCtx interface and returns errors for any internal +// function or service that may have failed. +func (s *SolanaChainReaderService) HealthReport() map[string]error { + return map[string]error{s.Name(): s.Healthy()} +} + +// GetLatestValue implements the types.ChainReader interface and requests and parses on-chain +// data named by the provided contract, method, and params. +func (s *SolanaChainReaderService) GetLatestValue(ctx context.Context, contractName, method string, params any, returnVal any) error { + bindings, err := s.bindings.GetReadBindings(contractName, method) + if err != nil { + return err + } + + for _, binding := range bindings { + if err := binding.GetLatestValue(ctx, params, returnVal); err != nil { + return err + } + } + + return nil +} + +// Bind implements the types.ChainReader interface and allows new contract bindings to be added +// to the service. +func (s *SolanaChainReaderService) Bind(_ context.Context, bindings []types.BoundContract) error { + return s.bindings.Bind(bindings) +} + +// CreateContractType implements the ContractTypeProvider interface and allows the chain reader +// service to explicitly define the expected type for a grpc server to provide. +func (s *SolanaChainReaderService) CreateContractType(contractName, itemType string, forEncoding bool) (any, error) { + return s.bindings.CreateType(contractName, itemType, forEncoding) +} + +func (s *SolanaChainReaderService) init(namespaces map[string]config.ChainReaderMethods) error { + for namespace, methods := range namespaces { + for methodName, method := range methods.Methods { + var idl codec.IDL + if err := json.Unmarshal([]byte(method.AnchorIDL), &idl); err != nil { + return err + } + + idlCodec, err := codec.NewIDLCodec(idl) + if err != nil { + return err + } + + for _, procedure := range method.Procedures { + mod, err := procedure.OutputModifications.ToModifier(codec.DecoderHooks...) + if err != nil { + return err + } + + codecWithModifiers, err := codec.NewNamedModifierCodec(idlCodec, procedure.IDLAccount, mod) + if err != nil { + return err + } + + s.bindings.AddReadBinding(namespace, methodName, &accountReadBinding{ + idlAccount: procedure.IDLAccount, + codec: codecWithModifiers, + reader: s.client, + }) + } + } + } + + return nil +} + +type accountDataReader struct { + client *rpc.Client +} + +func NewAccountDataReader(client *rpc.Client) *accountDataReader { + return &accountDataReader{client: client} +} + +func (r *accountDataReader) ReadAll(ctx context.Context, pk ag_solana.PublicKey) ([]byte, error) { + result, err := r.client.GetAccountInfo(ctx, pk) + if err != nil { + return nil, err + } + + bts := result.Value.Data.GetBinary() + + return bts, nil +} diff --git a/pkg/solana/chainreader/chain_reader_test.go b/pkg/solana/chainreader/chain_reader_test.go new file mode 100644 index 000000000..2daa5fc12 --- /dev/null +++ b/pkg/solana/chainreader/chain_reader_test.go @@ -0,0 +1,662 @@ +package chainreader_test + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + "testing" + "time" + + ag_solana "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + codeccommon "github.com/smartcontractkit/chainlink-common/pkg/codec" + "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + commontestutils "github.com/smartcontractkit/chainlink-common/pkg/loop/testutils" + "github.com/smartcontractkit/chainlink-common/pkg/types" + . "github.com/smartcontractkit/chainlink-common/pkg/types/interfacetests" //nolint common practice to import test mods with . + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainreader" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec/testutils" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" +) + +const ( + Namespace = "NameSpace" + NamedMethod = "NamedMethod1" +) + +func TestSolanaChainReaderService_ReaderInterface(t *testing.T) { + t.Parallel() + + it := &chainReaderInterfaceTester{} + RunChainReaderInterfaceTests(t, it) + RunChainReaderInterfaceTests(t, commontestutils.WrapChainReaderTesterForLoop(it)) +} + +func TestSolanaChainReaderService_ServiceCtx(t *testing.T) { + t.Parallel() + + ctx := tests.Context(t) + svc, err := chainreader.NewChainReaderService(logger.Test(t), new(mockedRPCClient), config.ChainReader{}) + + require.NoError(t, err) + require.NotNil(t, svc) + + require.Error(t, svc.Ready()) + require.Len(t, svc.HealthReport(), 1) + require.Contains(t, svc.HealthReport(), chainreader.ServiceName) + require.Error(t, svc.HealthReport()[chainreader.ServiceName]) + + require.NoError(t, svc.Start(ctx)) + require.NoError(t, svc.Ready()) + require.Equal(t, map[string]error{chainreader.ServiceName: nil}, svc.HealthReport()) + + require.Error(t, svc.Start(ctx)) + + require.NoError(t, svc.Close()) + require.Error(t, svc.Ready()) + require.Error(t, svc.Close()) +} + +func TestSolanaChainReaderService_GetLatestValue(t *testing.T) { + t.Parallel() + + ctx := tests.Context(t) + + // encode values from unmodified test struct to be read and decoded + expected := testutils.DefaultTestStruct + + t.Run("Success", func(t *testing.T) { + t.Parallel() + + testCodec, conf := newTestConfAndCodec(t) + encoded, err := testCodec.Encode(ctx, expected, testutils.TestStructWithNestedStruct) + + require.NoError(t, err) + + client := new(mockedRPCClient) + svc, err := chainreader.NewChainReaderService(logger.Test(t), client, conf) + + require.NoError(t, err) + require.NotNil(t, svc) + + client.On("ReadAll", mock.Anything, mock.Anything).Return(encoded, nil) + + var result modifiedStructWithNestedStruct + + require.NoError(t, svc.GetLatestValue(ctx, Namespace, NamedMethod, nil, &result)) + assert.Equal(t, expected.InnerStruct, result.InnerStruct) + assert.Equal(t, expected.Value, result.V) + assert.Equal(t, expected.TimeVal, result.TimeVal) + assert.Equal(t, expected.DurationVal, result.DurationVal) + }) + + t.Run("Error Returned From Account Reader", func(t *testing.T) { + t.Parallel() + + _, conf := newTestConfAndCodec(t) + + client := new(mockedRPCClient) + expectedErr := fmt.Errorf("expected error") + svc, err := chainreader.NewChainReaderService(logger.Test(t), client, conf) + + require.NoError(t, err) + require.NotNil(t, svc) + + client.On("ReadAll", mock.Anything, mock.Anything).Return(nil, expectedErr) + + var result modifiedStructWithNestedStruct + + assert.ErrorIs(t, svc.GetLatestValue(ctx, Namespace, NamedMethod, nil, &result), expectedErr) + }) + + t.Run("Method Not Found", func(t *testing.T) { + t.Parallel() + + _, conf := newTestConfAndCodec(t) + + client := new(mockedRPCClient) + svc, err := chainreader.NewChainReaderService(logger.Test(t), client, conf) + + require.NoError(t, err) + require.NotNil(t, svc) + + var result modifiedStructWithNestedStruct + + assert.NotNil(t, svc.GetLatestValue(ctx, Namespace, "Unknown", nil, &result)) + }) + + t.Run("Namespace Not Found", func(t *testing.T) { + t.Parallel() + + _, conf := newTestConfAndCodec(t) + + client := new(mockedRPCClient) + svc, err := chainreader.NewChainReaderService(logger.Test(t), client, conf) + + require.NoError(t, err) + require.NotNil(t, svc) + + var result modifiedStructWithNestedStruct + + assert.NotNil(t, svc.GetLatestValue(ctx, "Unknown", "Unknown", nil, &result)) + }) + + t.Run("Bind Success", func(t *testing.T) { + t.Parallel() + + _, conf := newTestConfAndCodec(t) + + client := new(mockedRPCClient) + svc, err := chainreader.NewChainReaderService(logger.Test(t), client, conf) + + require.NoError(t, err) + require.NotNil(t, svc) + + pk := ag_solana.NewWallet().PublicKey() + err = svc.Bind(ctx, []types.BoundContract{ + { + Address: pk.String(), + Name: fmt.Sprintf("%s.%s.%d", Namespace, NamedMethod, 0), + }, + }) + + assert.NoError(t, err) + }) + + t.Run("Bind Errors", func(t *testing.T) { + t.Parallel() + + _, conf := newTestConfAndCodec(t) + + client := new(mockedRPCClient) + svc, err := chainreader.NewChainReaderService(logger.Test(t), client, conf) + + require.NoError(t, err) + require.NotNil(t, svc) + + pk := ag_solana.NewWallet().PublicKey() + + require.NotNil(t, svc.Bind(ctx, []types.BoundContract{ + { + Address: pk.String(), + Name: "incorrect format", + }, + })) + + require.NotNil(t, svc.Bind(ctx, []types.BoundContract{ + { + Address: pk.String(), + Name: fmt.Sprintf("%s.%s.%d", "Unknown", "Unknown", 0), + }, + })) + + require.NotNil(t, svc.Bind(ctx, []types.BoundContract{ + { + Address: pk.String(), + Name: fmt.Sprintf("%s.%s.%d", Namespace, "Unknown", 0), + }, + })) + + require.NotNil(t, svc.Bind(ctx, []types.BoundContract{ + { + Address: pk.String(), + Name: fmt.Sprintf("%s.%s.%d", Namespace, NamedMethod, 1), + }, + })) + + require.NotNil(t, svc.Bind(ctx, []types.BoundContract{ + { + Address: pk.String(), + Name: fmt.Sprintf("%s.%s.o", Namespace, NamedMethod), + }, + })) + + require.NotNil(t, svc.Bind(ctx, []types.BoundContract{ + { + Address: "invalid", + Name: fmt.Sprintf("%s.%s.%d", Namespace, NamedMethod, 0), + }, + })) + }) +} + +func newTestIDLAndCodec(t *testing.T) (string, codec.IDL, encodings.CodecFromTypeCodec) { + t.Helper() + + var idl codec.IDL + if err := json.Unmarshal([]byte(testutils.JSONIDLWithAllTypes), &idl); err != nil { + t.Logf("failed to unmarshal test IDL: %s", err.Error()) + t.FailNow() + } + + entry, err := codec.NewIDLCodec(idl) + if err != nil { + t.Logf("failed to create new codec from test IDL: %s", err.Error()) + t.FailNow() + } + + require.NotNil(t, entry) + + return testutils.JSONIDLWithAllTypes, idl, entry +} + +func newTestConfAndCodec(t *testing.T) (encodings.CodecFromTypeCodec, config.ChainReader) { + t.Helper() + + rawIDL, _, testCodec := newTestIDLAndCodec(t) + conf := config.ChainReader{ + Namespaces: map[string]config.ChainReaderMethods{ + Namespace: { + Methods: map[string]config.ChainDataReader{ + NamedMethod: { + AnchorIDL: rawIDL, + Procedures: []config.ChainReaderProcedure{ + { + IDLAccount: testutils.TestStructWithNestedStruct, + Type: config.ProcedureTypeAnchor, + OutputModifications: codeccommon.ModifiersConfig{ + &codeccommon.RenameModifierConfig{Fields: map[string]string{"Value": "V"}}, + }, + }, + }, + }, + }, + }, + }, + } + + return testCodec, conf +} + +type modifiedStructWithNestedStruct struct { + V uint8 + InnerStruct testutils.ObjectRef1 + BasicNestedArray [][]uint32 + Option *string + DefinedArray []testutils.ObjectRef2 + BasicVector []string + TimeVal int64 + DurationVal time.Duration + PublicKey ag_solana.PublicKey + EnumVal uint8 +} + +// TODO: BCF-3060 convert mock client to be instance of solana chain. +type mockedRPCClient struct { + mock.Mock +} + +func (_m *mockedRPCClient) ReadAll(ctx context.Context, pk ag_solana.PublicKey) ([]byte, error) { + ret := _m.Called(ctx, pk) + + var r0 []byte + + if val, ok := ret.Get(0).([]byte); ok { + r0 = val + } + + return r0, ret.Error(1) +} + +type chainReaderInterfaceTester struct { + conf config.ChainReader + address []string + reader *wrappedTestChainReader +} + +func (r *chainReaderInterfaceTester) GetAccountBytes(i int) []byte { + account := [20]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10} + account[i%20] += byte(i) + account[(i+3)%20] += byte(i + 3) + return account[:] +} + +func (r *chainReaderInterfaceTester) Name() string { + return "Solana" +} + +func (r *chainReaderInterfaceTester) Setup(t *testing.T) { + r.address = make([]string, 6) + for idx := range r.address { + r.address[idx] = ag_solana.NewWallet().PublicKey().String() + } + + r.conf = config.ChainReader{ + Namespaces: map[string]config.ChainReaderMethods{ + AnyContractName: { + Methods: map[string]config.ChainDataReader{ + MethodTakingLatestParamsReturningTestStruct: { + AnchorIDL: fmt.Sprintf(baseIDL, testStructIDL, strings.Join([]string{midLevelStructIDL, innerStructIDL}, ",")), + Procedures: []config.ChainReaderProcedure{ + { + IDLAccount: "TestStruct", + Type: config.ProcedureTypeAnchor, + }, + }, + }, + MethodReturningUint64: { + AnchorIDL: fmt.Sprintf(baseIDL, uint64BaseTypeIDL, ""), + Procedures: []config.ChainReaderProcedure{ + { + IDLAccount: "SimpleUint64Value", + Type: config.ProcedureTypeAnchor, + OutputModifications: codeccommon.ModifiersConfig{ + &codeccommon.PropertyExtractorConfig{FieldName: "I"}, + }, + }, + }, + }, + DifferentMethodReturningUint64: { + AnchorIDL: fmt.Sprintf(baseIDL, uint64BaseTypeIDL, ""), + Procedures: []config.ChainReaderProcedure{ + { + IDLAccount: "SimpleUint64Value", + Type: config.ProcedureTypeAnchor, + OutputModifications: codeccommon.ModifiersConfig{ + &codeccommon.PropertyExtractorConfig{FieldName: "I"}, + }, + }, + }, + }, + MethodReturningUint64Slice: { + AnchorIDL: fmt.Sprintf(baseIDL, uint64SliceBaseTypeIDL, ""), + Procedures: []config.ChainReaderProcedure{ + { + IDLAccount: "Uint64Slice", + Type: config.ProcedureTypeAnchor, + OutputModifications: codeccommon.ModifiersConfig{ + &codeccommon.PropertyExtractorConfig{FieldName: "Vals"}, + }, + }, + }, + }, + MethodReturningSeenStruct: { + AnchorIDL: fmt.Sprintf(baseIDL, testStructIDL, strings.Join([]string{midLevelStructIDL, innerStructIDL}, ",")), + Procedures: []config.ChainReaderProcedure{ + { + IDLAccount: "TestStruct", + Type: config.ProcedureTypeAnchor, + OutputModifications: codeccommon.ModifiersConfig{ + &codeccommon.HardCodeModifierConfig{OffChainValues: map[string]any{"ExtraField": AnyExtraValue}}, + // &codeccommon.RenameModifierConfig{Fields: map[string]string{"NestedStruct.Inner.IntVal": "I"}}, + }, + }, + }, + }, + }, + }, + AnySecondContractName: { + Methods: map[string]config.ChainDataReader{ + MethodReturningUint64: { + AnchorIDL: fmt.Sprintf(baseIDL, uint64BaseTypeIDL, ""), + Procedures: []config.ChainReaderProcedure{ + { + IDLAccount: "SimpleUint64Value", + Type: config.ProcedureTypeAnchor, + OutputModifications: codeccommon.ModifiersConfig{ + &codeccommon.PropertyExtractorConfig{FieldName: "I"}, + }, + }, + }, + }, + }, + }, + }, + } +} + +func (r *chainReaderInterfaceTester) GetChainReader(t *testing.T) types.ChainReader { + client := new(mockedRPCClient) + svc, err := chainreader.NewChainReaderService(logger.Test(t), client, r.conf) + if err != nil { + t.Logf("chain reader service was not able to start: %s", err.Error()) + t.FailNow() + } + + if r.reader == nil { + r.reader = &wrappedTestChainReader{ + test: t, + tester: r, + } + } + + r.reader.service = svc + r.reader.client = client + + return r.reader +} + +type wrappedTestChainReader struct { + test *testing.T + service *chainreader.SolanaChainReaderService + client *mockedRPCClient + tester ChainReaderInterfaceTester + testStructQueue []*TestStruct +} + +func (r *wrappedTestChainReader) GetLatestValue(ctx context.Context, contractName string, method string, params, returnVal any) error { + switch contractName + method { + case AnyContractName + EventName: + // t.Skip won't skip the test here + // returning the expected error to satisfy the test + return types.ErrNotFound + case AnyContractName + MethodReturningUint64: + cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, uint64BaseTypeIDL, "")) + onChainStruct := struct { + I uint64 + }{ + I: AnyValueToReadWithoutAnArgument, + } + + bts, err := cdc.Encode(ctx, onChainStruct, "SimpleUint64Value") + if err != nil { + r.test.Log(err.Error()) + r.test.FailNow() + } + + r.client.On("ReadAll", mock.Anything, mock.Anything).Return(bts, nil).Once() + case AnyContractName + MethodReturningUint64Slice: + cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, uint64SliceBaseTypeIDL, "")) + onChainStruct := struct { + Vals []uint64 + }{ + Vals: AnySliceToReadWithoutAnArgument, + } + + bts, err := cdc.Encode(ctx, onChainStruct, "Uint64Slice") + if err != nil { + r.test.FailNow() + } + + r.client.On("ReadAll", mock.Anything, mock.Anything).Return(bts, nil).Once() + case AnySecondContractName + MethodReturningUint64, AnyContractName + DifferentMethodReturningUint64: + cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, uint64BaseTypeIDL, "")) + onChainStruct := struct { + I uint64 + }{ + I: AnyDifferentValueToReadWithoutAnArgument, + } + + bts, err := cdc.Encode(ctx, onChainStruct, "SimpleUint64Value") + if err != nil { + r.test.FailNow() + } + + r.client.On("ReadAll", mock.Anything, mock.Anything).Return(bts, nil).Once() + case AnyContractName + MethodReturningSeenStruct: + nextStruct := CreateTestStruct(0, r.tester) + r.testStructQueue = append(r.testStructQueue, &nextStruct) + + fallthrough + default: + if r.testStructQueue == nil || len(r.testStructQueue) == 0 { + r.test.FailNow() + } + + nextTestStruct := r.testStructQueue[0] + r.testStructQueue = r.testStructQueue[1:len(r.testStructQueue)] + + cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, testStructIDL, strings.Join([]string{midLevelStructIDL, innerStructIDL}, ","))) + bts, err := cdc.Encode(ctx, nextTestStruct, "TestStruct") + if err != nil { + r.test.FailNow() + } + + r.client.On("ReadAll", mock.Anything, mock.Anything).Return(bts, nil).Once() + } + + return r.service.GetLatestValue(ctx, contractName, method, params, returnVal) +} + +func (r *wrappedTestChainReader) Bind(ctx context.Context, bindings []types.BoundContract) error { + return r.service.Bind(ctx, bindings) +} + +func (r *wrappedTestChainReader) CreateContractType(contractName, itemType string, forEncoding bool) (any, error) { + if AnyContractName+EventName == contractName+itemType { + // events are not supported, so just make the tests pass + return nil, types.ErrNotFound + } + + return r.service.CreateContractType(contractName, itemType, forEncoding) +} + +// SetLatestValue is expected to return the same bound contract and method in the same test +// Any setup required for this should be done in Setup. +// The contract should take a LatestParams as the params and return the nth TestStruct set +func (r *chainReaderInterfaceTester) SetLatestValue(t *testing.T, testStruct *TestStruct) { + if r.reader == nil { + r.reader = &wrappedTestChainReader{ + test: t, + tester: r, + } + } + + r.reader.testStructQueue = append(r.reader.testStructQueue, testStruct) +} + +func (r *chainReaderInterfaceTester) TriggerEvent(t *testing.T, testStruct *TestStruct) { + t.Skip("Events are not yet supported in Solana") +} + +func (r *chainReaderInterfaceTester) GetBindings(t *testing.T) []types.BoundContract { + return []types.BoundContract{ + {Name: strings.Join([]string{AnyContractName, MethodTakingLatestParamsReturningTestStruct, "0"}, "."), Address: r.address[0], Pending: true}, + {Name: strings.Join([]string{AnyContractName, MethodReturningUint64, "0"}, "."), Address: r.address[1], Pending: true}, + {Name: strings.Join([]string{AnyContractName, DifferentMethodReturningUint64, "0"}, "."), Address: r.address[2], Pending: true}, + {Name: strings.Join([]string{AnyContractName, MethodReturningUint64Slice, "0"}, "."), Address: r.address[3], Pending: true}, + {Name: strings.Join([]string{AnyContractName, MethodReturningSeenStruct, "0"}, "."), Address: r.address[4], Pending: true}, + {Name: strings.Join([]string{AnySecondContractName, MethodReturningUint64, "0"}, "."), Address: r.address[5], Pending: true}, + } +} + +func (r *chainReaderInterfaceTester) MaxWaitTimeForEvents() time.Duration { + // From trial and error, when running on CI, sometimes the boxes get slow + maxWaitTime := time.Second + maxWaitTimeStr, ok := os.LookupEnv("MAX_WAIT_TIME_FOR_EVENTS_S") + if ok { + wiatS, err := strconv.ParseInt(maxWaitTimeStr, 10, 64) + if err != nil { + fmt.Printf("Error parsing MAX_WAIT_TIME_FOR_EVENTS_S: %v, defaulting to %v\n", err, maxWaitTime) + } + maxWaitTime = time.Second * time.Duration(wiatS) + } + + return maxWaitTime +} + +func makeTestCodec(t *testing.T, rawIDL string) encodings.CodecFromTypeCodec { + t.Helper() + + var idl codec.IDL + if err := json.Unmarshal([]byte(rawIDL), &idl); err != nil { + t.Logf("failed to unmarshal test IDL: %s", err.Error()) + t.FailNow() + } + + testCodec, err := codec.NewIDLCodec(idl) + if err != nil { + t.Logf("failed to create new codec from test IDL: %s", err.Error()) + t.FailNow() + } + + return testCodec +} + +const ( + baseIDL = `{ + "version": "0.1.0", + "name": "some_test_idl", + "accounts": [%s], + "types": [%s] + }` + + testStructIDL = `{ + "name": "TestStruct", + "type": { + "kind": "struct", + "fields": [ + {"name": "field","type": {"option": "i32"}}, + {"name": "differentField","type": "string"}, + {"name": "oracleID","type": "u8"}, + {"name": "oracleIDs","type": {"array": ["u8",32]}}, + {"name": "account","type": "bytes"}, + {"name": "accounts","type": {"vec": "bytes"}}, + {"name": "bigField","type": "i128"}, + {"name": "nestedStruct","type": {"defined": "MidLevelStruct"}} + ] + } + }` + + midLevelStructIDL = `{ + "name": "MidLevelStruct", + "type": { + "kind": "struct", + "fields": [ + {"name": "fixedBytes", "type": {"array": ["u8",2]}}, + {"name": "inner", "type": {"defined": "InnerTestStruct"}} + ] + } + }` + + innerStructIDL = `{ + "name": "InnerTestStruct", + "type": { + "kind": "struct", + "fields": [ + {"name": "i", "type": "i32"}, + {"name": "s", "type": "string"} + ] + } + }` + + uint64BaseTypeIDL = `{ + "name": "SimpleUint64Value", + "type": { + "kind": "struct", + "fields": [ + {"name": "i", "type": "u64"} + ] + } + }` + + uint64SliceBaseTypeIDL = `{ + "name": "Uint64Slice", + "type": { + "kind": "struct", + "fields": [ + {"name": "vals", "type": {"vec": "u64"}} + ] + } + }` +) diff --git a/pkg/solana/codec/anchoridl.go b/pkg/solana/codec/anchoridl.go new file mode 100644 index 000000000..359409d5c --- /dev/null +++ b/pkg/solana/codec/anchoridl.go @@ -0,0 +1,403 @@ +package codec + +/* +copied from https://github.com/gagliardetto/anchor-go where the IDL definition is not importable due to being defined +in the `main` package. +*/ + +import ( + "encoding/json" + "fmt" + + "github.com/davecgh/go-spew/spew" + "github.com/gagliardetto/utilz" +) + +// https://github.com/project-serum/anchor/blob/97e9e03fb041b8b888a9876a7c0676d9bb4736f3/ts/src/idl.ts +type IDL struct { + Version string `json:"version"` + Name string `json:"name"` + Instructions []IdlInstruction `json:"instructions"` + Accounts IdlTypeDefSlice `json:"accounts,omitempty"` + Types IdlTypeDefSlice `json:"types,omitempty"` + Events []IdlEvent `json:"events,omitempty"` + Errors []IdlErrorCode `json:"errors,omitempty"` + Constants []IdlConstant `json:"constants,omitempty"` +} + +type IdlConstant struct { + Name string + Type IdlType + Value string +} + +type IdlTypeDefSlice []IdlTypeDef + +func (named IdlTypeDefSlice) GetByName(name string) *IdlTypeDef { + for i := range named { + v := named[i] + if v.Name == name { + return &v + } + } + return nil +} + +type IdlEvent struct { + Name string `json:"name"` + Fields []IdlEventField `json:"fields"` +} + +type IdlEventField struct { + Name string `json:"name"` + Type IdlType `json:"type"` + Index bool `json:"index"` +} + +type IdlInstruction struct { + Name string `json:"name"` + Docs []string `json:"docs"` // @custom + Accounts IdlAccountItemSlice `json:"accounts"` + Args []IdlField `json:"args"` +} + +type IdlAccountItemSlice []IdlAccountItem + +func (slice IdlAccountItemSlice) NumAccounts() (count int) { + + for _, item := range slice { + if item.IdlAccount != nil { + count++ + } + + if item.IdlAccounts != nil { + count += item.IdlAccounts.Accounts.NumAccounts() + } + } + + return count +} + +// type IdlAccountItem = IdlAccount | IdlAccounts; +type IdlAccountItem struct { + IdlAccount *IdlAccount + IdlAccounts *IdlAccounts +} + +func (env *IdlAccountItem) UnmarshalJSON(data []byte) error { + var temp interface{} + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + + if temp == nil { + return fmt.Errorf("envelope is nil: %v", env) + } + + switch v := temp.(type) { + case map[string]interface{}: + if len(v) == 0 { + return nil + } + + // Multiple accounts: + if _, ok := v["accounts"]; ok { + if err := utilz.TranscodeJSON(temp, &env.IdlAccounts); err != nil { + return err + } + } + // Single account: + // TODO: check both isMut and isSigner + if _, ok := v["isMut"]; ok { + if err := utilz.TranscodeJSON(temp, &env.IdlAccount); err != nil { + return err + } + } + default: + return fmt.Errorf("Unknown kind: %s", spew.Sdump(temp)) + } + + return nil +} + +type IdlAccount struct { + Docs []string `json:"docs"` // @custom + Name string `json:"name"` + IsMut bool `json:"isMut"` + IsSigner bool `json:"isSigner"` + Optional bool `json:"optional"` // @custom +} + +// A nested/recursive version of IdlAccount. +type IdlAccounts struct { + Name string `json:"name"` + Docs []string `json:"docs"` // @custom + Accounts IdlAccountItemSlice `json:"accounts"` +} + +type IdlField struct { + Name string `json:"name"` + Docs []string `json:"docs"` // @custom + Type IdlType `json:"type"` +} + +type IdlTypeAsString string + +const ( + IdlTypeBool IdlTypeAsString = "bool" + IdlTypeU8 IdlTypeAsString = "u8" + IdlTypeI8 IdlTypeAsString = "i8" + IdlTypeU16 IdlTypeAsString = "u16" + IdlTypeI16 IdlTypeAsString = "i16" + IdlTypeU32 IdlTypeAsString = "u32" + IdlTypeI32 IdlTypeAsString = "i32" + IdlTypeU64 IdlTypeAsString = "u64" + IdlTypeI64 IdlTypeAsString = "i64" + IdlTypeU128 IdlTypeAsString = "u128" + IdlTypeI128 IdlTypeAsString = "i128" + IdlTypeBytes IdlTypeAsString = "bytes" + IdlTypeString IdlTypeAsString = "string" + IdlTypePublicKey IdlTypeAsString = "publicKey" + + // Custom additions: + IdlTypeUnixTimestamp IdlTypeAsString = "unixTimestamp" + IdlTypeHash IdlTypeAsString = "hash" + IdlTypeDuration IdlTypeAsString = "duration" +) + +type IdlTypeVec struct { + Vec IdlType `json:"vec"` +} + +type IdlTypeOption struct { + Option IdlType `json:"option"` +} + +// User defined type. +type IdlTypeDefined struct { + Defined string `json:"defined"` +} + +// Wrapper type: +type IdlTypeArray struct { + Thing IdlType + Num int +} + +func (env *IdlType) UnmarshalJSON(data []byte) error { + var temp interface{} + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + + if temp == nil { + return fmt.Errorf("envelope is nil: %v", env) + } + + switch v := temp.(type) { + case string: + env.asString = IdlTypeAsString(v) + case map[string]interface{}: + if len(v) == 0 { + return nil + } + + if _, ok := v["vec"]; ok { + var target IdlTypeVec + if err := utilz.TranscodeJSON(temp, &target); err != nil { + return err + } + env.asIdlTypeVec = &target + } + if _, ok := v["option"]; ok { + var target IdlTypeOption + if err := utilz.TranscodeJSON(temp, &target); err != nil { + return err + } + env.asIdlTypeOption = &target + } + if _, ok := v["defined"]; ok { + var target IdlTypeDefined + if err := utilz.TranscodeJSON(temp, &target); err != nil { + return err + } + env.asIdlTypeDefined = &target + } + if got, ok := v["array"]; ok { + + if _, ok := got.([]interface{}); !ok { + panic(utilz.Sf("array is not in expected format:\n%s", spew.Sdump(got))) + } + arrVal := got.([]interface{}) + if len(arrVal) != 2 { + panic(utilz.Sf("array is not of expected length:\n%s", spew.Sdump(got))) + } + var target IdlTypeArray + if err := utilz.TranscodeJSON(arrVal[0], &target.Thing); err != nil { + return err + } + + target.Num = int(arrVal[1].(float64)) + + env.asIdlTypeArray = &target + } + default: + return fmt.Errorf("Unknown kind: %s", spew.Sdump(temp)) + } + + return nil +} + +// Wrapper type: +type IdlType struct { + asString IdlTypeAsString + asIdlTypeVec *IdlTypeVec + asIdlTypeOption *IdlTypeOption + asIdlTypeDefined *IdlTypeDefined + asIdlTypeArray *IdlTypeArray +} + +func (env *IdlType) IsString() bool { + return env.asString != "" +} +func (env *IdlType) IsIdlTypeVec() bool { + return env.asIdlTypeVec != nil +} +func (env *IdlType) IsIdlTypeOption() bool { + return env.asIdlTypeOption != nil +} +func (env *IdlType) IsIdlTypeDefined() bool { + return env.asIdlTypeDefined != nil +} +func (env *IdlType) IsArray() bool { + return env.asIdlTypeArray != nil +} + +// Getters: +func (env *IdlType) GetString() IdlTypeAsString { + return env.asString +} +func (env *IdlType) GetIdlTypeVec() *IdlTypeVec { + return env.asIdlTypeVec +} +func (env *IdlType) GetIdlTypeOption() *IdlTypeOption { + return env.asIdlTypeOption +} +func (env *IdlType) GetIdlTypeDefined() *IdlTypeDefined { + return env.asIdlTypeDefined +} +func (env *IdlType) GetArray() *IdlTypeArray { + return env.asIdlTypeArray +} + +type IdlTypeDef struct { + Name string `json:"name"` + Type IdlTypeDefTy `json:"type"` +} + +type IdlTypeDefTyKind string + +const ( + IdlTypeDefTyKindStruct IdlTypeDefTyKind = "struct" + IdlTypeDefTyKindEnum IdlTypeDefTyKind = "enum" +) + +type IdlTypeDefTyStruct struct { + Kind IdlTypeDefTyKind `json:"kind"` // == "struct" + + Fields *IdlTypeDefStruct `json:"fields,omitempty"` +} + +type IdlTypeDefTyEnum struct { + Kind IdlTypeDefTyKind `json:"kind"` // == "enum" + + Variants IdlEnumVariantSlice `json:"variants,omitempty"` +} + +type IdlTypeDefTy struct { + Kind IdlTypeDefTyKind `json:"kind"` + + Fields *IdlTypeDefStruct `json:"fields,omitempty"` + Variants IdlEnumVariantSlice `json:"variants,omitempty"` +} + +type IdlEnumVariantSlice []IdlEnumVariant + +func (slice IdlEnumVariantSlice) IsAllUint8() bool { + for _, elem := range slice { + if !elem.IsUint8() { + return false + } + } + return true +} + +func (slice IdlEnumVariantSlice) IsSimpleEnum() bool { + return slice.IsAllUint8() +} + +type IdlTypeDefStruct = []IdlField + +type IdlEnumVariant struct { + Name string `json:"name"` + Docs []string `json:"docs"` // @custom + Fields *IdlEnumFields `json:"fields,omitempty"` +} + +func (variant *IdlEnumVariant) IsUint8() bool { + // it's a simple uint8 if there is no fields data + return variant.Fields == nil +} + +// type IdlEnumFields = IdlEnumFieldsNamed | IdlEnumFieldsTuple; +type IdlEnumFields struct { + IdlEnumFieldsNamed *IdlEnumFieldsNamed + IdlEnumFieldsTuple *IdlEnumFieldsTuple +} + +type IdlEnumFieldsNamed []IdlField + +type IdlEnumFieldsTuple []IdlType + +// TODO: verify with examples +func (env *IdlEnumFields) UnmarshalJSON(data []byte) error { + var temp interface{} + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + + if temp == nil { + return fmt.Errorf("envelope is nil: %v", env) + } + + switch v := temp.(type) { + case []interface{}: + if len(v) == 0 { + return nil + } + + firstItem := v[0] + + if _, ok := firstItem.(map[string]interface{})["name"]; ok { + // TODO: + // If has `name` field, then it's most likely a IdlEnumFieldsNamed. + if err := utilz.TranscodeJSON(temp, &env.IdlEnumFieldsNamed); err != nil { + return err + } + } else { + if err := utilz.TranscodeJSON(temp, &env.IdlEnumFieldsTuple); err != nil { + return err + } + } + default: + return fmt.Errorf("Unknown kind: %s", spew.Sdump(temp)) + } + + return nil +} + +type IdlErrorCode struct { + Code int `json:"code"` + Name string `json:"name"` + Msg string `json:"msg,omitempty"` +} diff --git a/pkg/solana/codec/duration.go b/pkg/solana/codec/duration.go new file mode 100644 index 000000000..e6e540778 --- /dev/null +++ b/pkg/solana/codec/duration.go @@ -0,0 +1,54 @@ +package codec + +import ( + "fmt" + "reflect" + "time" + + "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings" + "github.com/smartcontractkit/chainlink-common/pkg/types" +) + +func NewDuration(builder encodings.Builder) encodings.TypeCodec { + return &duration{ + intEncoder: builder.Int64(), + } +} + +type duration struct { + intEncoder encodings.TypeCodec +} + +var _ encodings.TypeCodec = &duration{} + +func (d *duration) Encode(value any, into []byte) ([]byte, error) { + bi, ok := value.(time.Duration) + if !ok { + return nil, fmt.Errorf("%w: expected time.Duration, got %T", types.ErrInvalidType, value) + } + + return d.intEncoder.Encode(int64(bi), into) +} + +func (d *duration) Decode(encoded []byte) (any, []byte, error) { + value, bytes, err := d.intEncoder.Decode(encoded) + + bi, ok := value.(int64) + if !ok { + return value, bytes, err + } + + return time.Duration(bi), bytes, nil +} + +func (d *duration) GetType() reflect.Type { + return reflect.TypeOf(time.Duration(0)) +} + +func (d *duration) Size(val int) (int, error) { + return d.intEncoder.Size(val) +} + +func (d *duration) FixedSize() (int, error) { + return d.intEncoder.FixedSize() +} diff --git a/pkg/solana/codec/solana.go b/pkg/solana/codec/solana.go new file mode 100644 index 000000000..2df40529d --- /dev/null +++ b/pkg/solana/codec/solana.go @@ -0,0 +1,326 @@ +/* +Package codec provides functions to create a codec from an Anchor IDL. All Anchor primitives map to the following native +Go values: + +bool -> bool +string -> string +bytes -> []byte +[u|i][8-64] -> [u]int[8-64] +[u|i]128 -> *big.Int +duration -> time.Duration +unixTimestamp -> int64 +publicKey -> [32]byte +hash -> [32]byte + +Enums as an Anchor data structure are only supported in their basic form of uint8 values. Enums with variants are not +supported at this time. + +Modifiers can be provided to assist in modifying property names, adding properties, etc. +*/ +package codec + +import ( + "fmt" + "math" + + "github.com/mitchellh/mapstructure" + "golang.org/x/text/cases" + "golang.org/x/text/language" + + "github.com/smartcontractkit/chainlink-common/pkg/codec" + "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings" + "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary" + "github.com/smartcontractkit/chainlink-common/pkg/types" +) + +const ( + DefaultHashBitLength = 32 + unknownIDLFormat = "%w: unknown IDL type def %s" +) + +// BigIntHook allows *big.Int to be represented as any integer type or a string and to go back to them. +// Useful for config, or if when a model may use a go type that isn't a *big.Int when Pack expects one. +// Eg: int32 in a go struct from a plugin could require a *big.Int in Pack for int24, if it fits, we shouldn't care. +// SliceToArrayVerifySizeHook verifies that slices have the correct size when converting to an array +// EpochToTimeHook allows multiple conversions: time.Time -> int64; int64 -> time.Time; *big.Int -> time.Time; and more +var DecoderHooks = []mapstructure.DecodeHookFunc{codec.EpochToTimeHook, codec.BigIntHook, codec.SliceToArrayVerifySizeHook} + +func NewNamedModifierCodec(original types.RemoteCodec, itemType string, modifier codec.Modifier) (types.RemoteCodec, error) { + mod, err := codec.NewByItemTypeModifier(map[string]codec.Modifier{itemType: modifier}) + if err != nil { + return nil, err + } + + modCodec, err := codec.NewModifierCodec(original, mod, DecoderHooks...) + if err != nil { + return nil, err + } + + _, err = modCodec.CreateType(itemType, true) + + return modCodec, err +} + +// NewIDLCodec is for Anchor custom types +func NewIDLCodec(idl IDL) (encodings.CodecFromTypeCodec, error) { + accounts := make(map[string]encodings.TypeCodec) + + refs := &codecRefs{ + builder: binary.LittleEndian(), + codecs: make(map[string]encodings.TypeCodec), + typeDefs: idl.Types, + dependencies: make(map[string][]string), + } + + for _, account := range idl.Accounts { + var ( + name string + accCodec encodings.TypeCodec + err error + ) + + name, accCodec, err = createNamedCodec(account, refs) + if err != nil { + return nil, err + } + + accounts[name] = accCodec + } + + return encodings.CodecFromTypeCodec(accounts), nil +} + +type codecRefs struct { + builder encodings.Builder + codecs map[string]encodings.TypeCodec + typeDefs IdlTypeDefSlice + dependencies map[string][]string +} + +func createNamedCodec( + def IdlTypeDef, + refs *codecRefs, +) (string, encodings.TypeCodec, error) { + caser := cases.Title(language.English) + name := def.Name + + switch def.Type.Kind { + case IdlTypeDefTyKindStruct: + return asStruct(def, refs, name, caser) + case IdlTypeDefTyKindEnum: + variants := def.Type.Variants + if !variants.IsAllUint8() { + return name, nil, fmt.Errorf("%w: variants are not supported", types.ErrInvalidConfig) + } + + return name, refs.builder.Uint8(), nil + default: + return name, nil, fmt.Errorf(unknownIDLFormat, types.ErrInvalidConfig, def.Type.Kind) + } +} + +func asStruct( + def IdlTypeDef, + refs *codecRefs, + name string, // name is the struct name and can be used in dependency checks + caser cases.Caser, +) (string, encodings.TypeCodec, error) { + named := make([]encodings.NamedTypeCodec, len(*def.Type.Fields)) + + for idx, field := range *def.Type.Fields { + fieldName := field.Name + + typedCodec, err := processFieldType(name, field.Type, refs) + if err != nil { + return name, nil, err + } + + named[idx] = encodings.NamedTypeCodec{Name: caser.String(fieldName), Codec: typedCodec} + } + + structCodec, err := encodings.NewStructCodec(named) + if err != nil { + return name, nil, err + } + + return name, structCodec, nil +} + +func processFieldType(parentTypeName string, idlType IdlType, refs *codecRefs) (encodings.TypeCodec, error) { + switch true { + case idlType.IsString(): + return getCodecByStringType(idlType.GetString(), refs.builder) + case idlType.IsIdlTypeOption(): + // Go doesn't have an `Option` type; use pointer to type instead + // this should be automatic in the codec + return processFieldType(parentTypeName, idlType.GetIdlTypeOption().Option, refs) + case idlType.IsIdlTypeDefined(): + return asDefined(parentTypeName, idlType.GetIdlTypeDefined(), refs) + case idlType.IsArray(): + return asArray(parentTypeName, idlType.GetArray(), refs) + case idlType.IsIdlTypeVec(): + return asVec(parentTypeName, idlType.GetIdlTypeVec(), refs) + default: + return nil, fmt.Errorf("%w: unknown IDL type def", types.ErrInvalidConfig) + } +} + +func asDefined(parentTypeName string, definedName *IdlTypeDefined, refs *codecRefs) (encodings.TypeCodec, error) { + if definedName == nil { + return nil, fmt.Errorf("%w: defined type name should not be nil", types.ErrInvalidConfig) + } + + // already exists as a type in the typed codecs + if savedCodec, ok := refs.codecs[definedName.Defined]; ok { + return savedCodec, nil + } + + // nextDef should not have a dependency on definedName + if !validDependency(refs, parentTypeName, definedName.Defined) { + return nil, fmt.Errorf("%w: circular dependency detected on %s -> %s relation", types.ErrInvalidConfig, parentTypeName, definedName.Defined) + } + + // codec by defined type doesn't exist + // process it using the provided typeDefs + nextDef := refs.typeDefs.GetByName(definedName.Defined) + if nextDef == nil { + return nil, fmt.Errorf("%w: IDL type does not exist for name %s", types.ErrInvalidConfig, definedName.Defined) + } + + saveDependency(refs, parentTypeName, definedName.Defined) + + newTypeName, newTypeCodec, err := createNamedCodec(*nextDef, refs) + if err != nil { + return nil, err + } + + // we know that recursive found codecs are types so add them to the type lookup + refs.codecs[newTypeName] = newTypeCodec + + return newTypeCodec, nil +} + +func asArray(parentTypeName string, idlArray *IdlTypeArray, refs *codecRefs) (encodings.TypeCodec, error) { + codec, err := processFieldType(parentTypeName, idlArray.Thing, refs) + if err != nil { + return nil, err + } + + return encodings.NewArray(idlArray.Num, codec) +} + +func asVec(parentTypeName string, idlVec *IdlTypeVec, refs *codecRefs) (encodings.TypeCodec, error) { + codec, err := processFieldType(parentTypeName, idlVec.Vec, refs) + if err != nil { + return nil, err + } + + b, err := refs.builder.Int(4) + if err != nil { + return nil, err + } + + return encodings.NewSlice(codec, b) +} + +func getCodecByStringType(curType IdlTypeAsString, builder encodings.Builder) (encodings.TypeCodec, error) { + switch curType { + case IdlTypeBool: + return builder.Bool(), nil + case IdlTypeString: + return builder.String(math.MaxUint32) + case IdlTypeI8, IdlTypeI16, IdlTypeI32, IdlTypeI64, IdlTypeI128: + return getIntCodecByStringType(curType, builder) + case IdlTypeU8, IdlTypeU16, IdlTypeU32, IdlTypeU64, IdlTypeU128: + return getUIntCodecByStringType(curType, builder) + case IdlTypeUnixTimestamp, IdlTypeDuration: + return getTimeCodecByStringType(curType, builder) + case IdlTypeBytes, IdlTypePublicKey, IdlTypeHash: + return getByteCodecByStringType(curType, builder) + default: + return nil, fmt.Errorf(unknownIDLFormat, types.ErrInvalidConfig, curType) + } +} + +func getIntCodecByStringType(curType IdlTypeAsString, builder encodings.Builder) (encodings.TypeCodec, error) { + switch curType { + case IdlTypeI8: + return builder.Int8(), nil + case IdlTypeI16: + return builder.Int16(), nil + case IdlTypeI32: + return builder.Int32(), nil + case IdlTypeI64: + return builder.Int64(), nil + case IdlTypeI128: + return builder.BigInt(16, true) + default: + return nil, fmt.Errorf(unknownIDLFormat, types.ErrInvalidConfig, curType) + } +} + +func getUIntCodecByStringType(curType IdlTypeAsString, builder encodings.Builder) (encodings.TypeCodec, error) { + switch curType { + case IdlTypeU8: + return builder.Uint8(), nil + case IdlTypeU16: + return builder.Uint16(), nil + case IdlTypeU32: + return builder.Uint32(), nil + case IdlTypeU64: + return builder.Uint64(), nil + case IdlTypeU128: + return builder.BigInt(16, true) + default: + return nil, fmt.Errorf(unknownIDLFormat, types.ErrInvalidConfig, curType) + } +} + +func getTimeCodecByStringType(curType IdlTypeAsString, builder encodings.Builder) (encodings.TypeCodec, error) { + switch curType { + case IdlTypeUnixTimestamp: + return builder.Int64(), nil + case IdlTypeDuration: + return NewDuration(builder), nil + default: + return nil, fmt.Errorf(unknownIDLFormat, types.ErrInvalidConfig, curType) + } +} + +func getByteCodecByStringType(curType IdlTypeAsString, builder encodings.Builder) (encodings.TypeCodec, error) { + switch curType { + case IdlTypeBytes: + b, err := builder.Int(4) + if err != nil { + return nil, err + } + + return encodings.NewSlice(builder.Uint8(), b) + case IdlTypePublicKey, IdlTypeHash: + return encodings.NewArray(DefaultHashBitLength, builder.Uint8()) + default: + return nil, fmt.Errorf(unknownIDLFormat, types.ErrInvalidConfig, curType) + } +} + +func validDependency(refs *codecRefs, parent, child string) bool { + deps, ok := refs.dependencies[child] + if ok { + for _, dep := range deps { + if dep == parent { + return false + } + } + } + + return true +} + +func saveDependency(refs *codecRefs, parent, child string) { + deps, ok := refs.dependencies[parent] + if !ok { + deps = make([]string, 0) + } + + refs.dependencies[parent] = append(deps, child) +} diff --git a/pkg/solana/codec/solana_test.go b/pkg/solana/codec/solana_test.go new file mode 100644 index 000000000..48415dd07 --- /dev/null +++ b/pkg/solana/codec/solana_test.go @@ -0,0 +1,139 @@ +package codec_test + +import ( + "encoding/json" + "testing" + "time" + + ag_solana "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + codeccommon "github.com/smartcontractkit/chainlink-common/pkg/codec" + "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings" + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec/testutils" +) + +func TestNewIDLCodec(t *testing.T) { + t.Parallel() + + ctx := tests.Context(t) + _, _, entry := newTestIDLAndCodec(t) + + expected := testutils.DefaultTestStruct + bts, err := entry.Encode(ctx, expected, testutils.TestStructWithNestedStruct) + + require.NoError(t, err) + + var decoded testutils.StructWithNestedStruct + + require.NoError(t, entry.Decode(ctx, bts, &decoded, testutils.TestStructWithNestedStruct)) + require.Equal(t, expected, decoded) +} + +func TestNewIDLCodec_WithModifiers(t *testing.T) { + t.Parallel() + + ctx := tests.Context(t) + _, _, idlCodec := newTestIDLAndCodec(t) + modConfig := codeccommon.ModifiersConfig{ + &codeccommon.RenameModifierConfig{Fields: map[string]string{"Value": "V"}}, + } + + renameMod, err := modConfig.ToModifier(codec.DecoderHooks...) + require.NoError(t, err) + + idlCodecWithMods, err := codec.NewNamedModifierCodec(idlCodec, testutils.TestStructWithNestedStruct, renameMod) + require.NoError(t, err) + + type modifiedTestStruct struct { + V uint8 + InnerStruct testutils.ObjectRef1 + BasicNestedArray [][]uint32 + Option *string + DefinedArray []testutils.ObjectRef2 + BasicVector []string + TimeVal int64 + DurationVal time.Duration + PublicKey ag_solana.PublicKey + EnumVal uint8 + } + + expected := modifiedTestStruct{ + V: testutils.DefaultTestStruct.Value, + InnerStruct: testutils.DefaultTestStruct.InnerStruct, + BasicNestedArray: testutils.DefaultTestStruct.BasicNestedArray, + Option: testutils.DefaultTestStruct.Option, + DefinedArray: testutils.DefaultTestStruct.DefinedArray, + BasicVector: testutils.DefaultTestStruct.BasicVector, + TimeVal: testutils.DefaultTestStruct.TimeVal, + DurationVal: testutils.DefaultTestStruct.DurationVal, + PublicKey: testutils.DefaultTestStruct.PublicKey, + EnumVal: testutils.DefaultTestStruct.EnumVal, + } + + withModsBts, err := idlCodecWithMods.Encode(ctx, expected, testutils.TestStructWithNestedStruct) + require.NoError(t, err) + + noModsBts, err := idlCodec.Encode(ctx, testutils.DefaultTestStruct, testutils.TestStructWithNestedStruct) + + // the codec without modifiers should encode an unmodified struct to the same bytes + // as the codec with modifiers encodes a modified struct + require.NoError(t, err) + require.Equal(t, withModsBts, noModsBts) + + var decoded modifiedTestStruct + + // the codec with modifiers should decode from unmodified bytes into a modified struct + require.NoError(t, idlCodecWithMods.Decode(ctx, noModsBts, &decoded, testutils.TestStructWithNestedStruct)) + require.Equal(t, expected, decoded) + + var unmodifiedDecoded testutils.StructWithNestedStruct + + // the codec without modifiers should decode from unmodified bytes to the same values as + // modified struct + require.NoError(t, idlCodec.Decode(ctx, noModsBts, &unmodifiedDecoded, testutils.TestStructWithNestedStruct)) + require.Equal(t, expected.V, unmodifiedDecoded.Value) + require.Equal(t, expected.TimeVal, unmodifiedDecoded.TimeVal) + require.Equal(t, expected.DurationVal, unmodifiedDecoded.DurationVal) + require.Equal(t, expected.PublicKey, unmodifiedDecoded.PublicKey) + require.Equal(t, expected.EnumVal, unmodifiedDecoded.EnumVal) +} + +func TestNewIDLCodec_CircularDependency(t *testing.T) { + t.Parallel() + + var idl codec.IDL + if err := json.Unmarshal([]byte(testutils.CircularDepIDL), &idl); err != nil { + t.Logf("failed to unmarshal test IDL: %s", err.Error()) + t.FailNow() + } + + _, err := codec.NewIDLCodec(idl) + + assert.ErrorIs(t, err, types.ErrInvalidConfig) +} + +func newTestIDLAndCodec(t *testing.T) (string, codec.IDL, encodings.CodecFromTypeCodec) { + t.Helper() + + var idl codec.IDL + if err := json.Unmarshal([]byte(testutils.JSONIDLWithAllTypes), &idl); err != nil { + t.Logf("failed to unmarshal test IDL: %s", err.Error()) + t.FailNow() + } + + entry, err := codec.NewIDLCodec(idl) + if err != nil { + t.Logf("failed to create new codec from test IDL: %s", err.Error()) + t.FailNow() + } + + require.NotNil(t, entry) + + return testutils.JSONIDLWithAllTypes, idl, entry +} diff --git a/pkg/solana/codec/testutils/circularDepIDL.json b/pkg/solana/codec/testutils/circularDepIDL.json new file mode 100644 index 000000000..fb896f088 --- /dev/null +++ b/pkg/solana/codec/testutils/circularDepIDL.json @@ -0,0 +1,42 @@ +{ + "accounts": [{ + "name": "TopLevelStruct", + "type": { + "kind": "struct", + "fields": [{ + "name": "circularOne", + "type": { + "defined": "TypeOne" + } + }, { + "name": "circularTwo", + "type": { + "defined": "TypeTwo" + } + }] + } + }], + "types": [{ + "name": "TypeOne", + "type": { + "kind": "struct", + "fields": [{ + "name": "circular", + "type": { + "defined": "TypeTwo" + } + }] + } + }, { + "name": "TypeTwo", + "type": { + "kind": "struct", + "fields": [{ + "name": "circular", + "type": { + "defined": "TypeOne" + } + }] + } + }] +} \ No newline at end of file diff --git a/pkg/solana/codec/testutils/testIDL.json b/pkg/solana/codec/testutils/testIDL.json new file mode 100644 index 000000000..d05496ee5 --- /dev/null +++ b/pkg/solana/codec/testutils/testIDL.json @@ -0,0 +1,159 @@ +{ + "version": "0.1.0", + "name": "some_test_idl", + "accounts": [ + { + "name": "StructWithNestedStruct", + "type": { + "kind": "struct", + "fields": [ + { + "name": "value", + "type": "u8" + }, + { + "name": "innerStruct", + "type": { + "defined": "ObjectRef1" + } + }, + { + "name": "basicNestedArray", + "type": { + "array": [ + { + "array": [ + "u32", + 3 + ] + }, + 3 + ] + } + }, + { + "name": "option", + "type": { + "option": "string" + } + }, + { + "name": "definedArray", + "type": { + "array": [ + { + "defined": "ObjectRef2" + }, + 2 + ] + } + }, + { + "name": "basicVector", + "type": { + "vec": "string" + } + }, + { + "name": "timeVal", + "type": "unixTimestamp" + }, + { + "name": "durationVal", + "type": "duration" + }, + { + "name": "publicKey", + "type": "publicKey" + }, + { + "name": "enumVal", + "type": { + "defined": "SimpleEnum" + } + } + ] + } + } + ], + "types": [ + { + "name": "ObjectRef1", + "type": { + "kind": "struct", + "fields": [ + { + "name": "prop1", + "type": "i8" + }, + { + "name": "prop2", + "type": "string" + }, + { + "name": "prop3", + "type": "u128" + }, + { + "name": "prop4", + "type": "u16" + }, + { + "name": "prop5", + "type": "u64" + }, + { + "name": "prop6", + "type": "bool" + } + ] + } + }, + { + "name": "ObjectRef2", + "type": { + "kind": "struct", + "fields": [ + { + "name": "prop1", + "type": "u32" + }, + { + "name": "prop2", + "type": "i128" + }, + { + "name": "prop3", + "type": "i16" + }, + { + "name": "prop4", + "type": "i32" + }, + { + "name": "prop5", + "type": "i64" + }, + { + "name": "prop6", + "type": "bytes" + } + ] + } + }, + { + "name": "SimpleEnum", + "type": { + "kind": "enum", + "variants": [ + { + "name": "A" + }, + { + "name": "B" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/solana/codec/testutils/types.go b/pkg/solana/codec/testutils/types.go new file mode 100644 index 000000000..7c20762f7 --- /dev/null +++ b/pkg/solana/codec/testutils/types.go @@ -0,0 +1,87 @@ +package testutils + +import ( + _ "embed" + "math/big" + "time" + + ag_solana "github.com/gagliardetto/solana-go" +) + +var ( + TestStructWithNestedStruct = "StructWithNestedStruct" + DefaultStringRef = "test string" + DefaultTestStruct = StructWithNestedStruct{ + Value: 80, + InnerStruct: ObjectRef1{ + Prop1: 10, + Prop2: "some_val", + Prop3: new(big.Int).SetUint64(42), + Prop4: 42, + Prop5: 42, + Prop6: true, + }, + BasicNestedArray: [][]uint32{{5, 6, 7}, {0, 0, 0}, {0, 0, 0}}, + Option: &DefaultStringRef, + DefinedArray: []ObjectRef2{ + { + Prop1: 42, + Prop2: new(big.Int).SetInt64(42), + Prop3: 43, + Prop4: 44, + Prop5: 45, + Prop6: []byte{}, + }, + { + Prop1: 46, + Prop2: new(big.Int).SetInt64(46), + Prop3: 47, + Prop4: 48, + Prop5: 49, + Prop6: []byte{}, + }, + }, + BasicVector: []string{"some string", "another string"}, + TimeVal: 683_100_000, + DurationVal: 42 * time.Second, + PublicKey: ag_solana.NewWallet().PublicKey(), + EnumVal: 0, + } +) + +type StructWithNestedStruct struct { + Value uint8 + InnerStruct ObjectRef1 + BasicNestedArray [][]uint32 + Option *string + DefinedArray []ObjectRef2 + BasicVector []string + TimeVal int64 + DurationVal time.Duration + PublicKey ag_solana.PublicKey + EnumVal uint8 +} + +type ObjectRef1 struct { + Prop1 int8 + Prop2 string + Prop3 *big.Int + Prop4 uint16 + Prop5 uint64 + Prop6 bool +} + +type ObjectRef2 struct { + Prop1 uint32 + Prop2 *big.Int + Prop3 int16 + Prop4 int32 + Prop5 int64 + Prop6 []byte +} + +//go:embed testIDL.json +var JSONIDLWithAllTypes string + +//go:embed circularDepIDL.json +var CircularDepIDL string diff --git a/pkg/solana/config/chain_reader.go b/pkg/solana/config/chain_reader.go new file mode 100644 index 000000000..980f6c535 --- /dev/null +++ b/pkg/solana/config/chain_reader.go @@ -0,0 +1,39 @@ +package config + +import "github.com/smartcontractkit/chainlink-common/pkg/codec" + +type ChainReader struct { + Namespaces map[string]ChainReaderMethods `json:"namespaces" toml:"namespaces"` +} + +type ChainReaderMethods struct { + Methods map[string]ChainDataReader `json:"methods" toml:"methods"` +} + +type ChainDataReader struct { + AnchorIDL string `json:"anchorIDL" toml:"anchorIDL"` + Procedures []ChainReaderProcedure `json:"procedures" toml:"procedures"` +} + +type ProcedureType int + +const ( + ProcedureTypeInternal ProcedureType = iota + ProcedureTypeAnchor +) + +type ChainReaderProcedure chainDataProcedureFields + +type chainDataProcedureFields struct { + // IDLAccount refers to the account defined in the IDL. + IDLAccount string `json:"idlAccount"` + // Type describes the procedure type to use such as internal for static values, + // anchor-read for using an anchor generated IDL to read values from an account, + // or custom structure for reading from a native account. Currently, only anchor + // reads are supported, but the type is a placeholder to allow internal functions + // to be run apart from anchor reads. + Type ProcedureType `json:"type"` + // OutputModifications provides modifiers to convert chain data format to custom + // output formats. + OutputModifications codec.ModifiersConfig `json:"outputModifications,omitempty"` +} diff --git a/sonar-project.properties b/sonar-project.properties index 7f64da7db..0434465b5 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,7 +3,7 @@ sonar.projectKey=smartcontractkit_chainlink-solana sonar.sources=. # Full exclusions from the static analysis -sonar.exclusions=**/node_modules/**/*, **/contracts/artifacts/**/*, **/generated/**/*, **/docs/**/*, **/*.config.ts, **/*.config.js, **/*.txt +sonar.exclusions=**/node_modules/**/*, **/contracts/artifacts/**/*, **/generated/**/*, **/docs/**/*, **/*.config.ts, **/*.config.js, **/*.txt, pkg/solana/codec/anchoridl.go # Coverage exclusions sonar.coverage.exclusions=**/*.test.ts, **/*_test.go, **/contracts/tests/**/*, **/integration-tests/**/*