From a9ea8fda44fb51f1fb24c2cd2ef794abee353d79 Mon Sep 17 00:00:00 2001 From: Awbrey Hughlett Date: Wed, 11 Dec 2024 12:51:07 -0500 Subject: [PATCH] ContractReader Interface Tests (#945) * simple interface tests implementation * ContractWriter interface updates * add relay interface tests to relay workflow * move interface test to rust workflow and clean up Anchor.toml * add comment for rpc client wrapper * install solana CLI path fix * remove cd contracts * add env var to e2e test --- .github/workflows/e2e_custom_cl.yml | 1 + .github/workflows/rust.yml | 54 +++ Makefile | 4 + contracts/Anchor.toml | 1 + contracts/Cargo.lock | 7 + .../contract_reader_interface/Initialize.go | 188 ++++++++ .../Initialize_test.go | 32 ++ .../contract_reader_interface/accounts.go | 83 ++++ .../contract_reader_interface/instructions.go | 117 +++++ .../testing_utils.go | 20 + .../contract_reader_interface/types.go | 3 + .../contract-reader-interface/Cargo.toml | 19 + .../contract-reader-interface/Xargo.toml | 2 + .../contract-reader-interface/src/lib.rs | 46 ++ integration-tests/go.mod | 5 + integration-tests/go.sum | 12 + .../relayinterface/chain_components_test.go | 405 ++++++++++++++++++ integration-tests/utils/project_path.go | 1 + pkg/solana/chainreader/client_wrapper.go | 45 ++ 19 files changed, 1045 insertions(+) create mode 100644 contracts/generated/contract_reader_interface/Initialize.go create mode 100644 contracts/generated/contract_reader_interface/Initialize_test.go create mode 100644 contracts/generated/contract_reader_interface/accounts.go create mode 100644 contracts/generated/contract_reader_interface/instructions.go create mode 100644 contracts/generated/contract_reader_interface/testing_utils.go create mode 100644 contracts/generated/contract_reader_interface/types.go create mode 100644 contracts/programs/contract-reader-interface/Cargo.toml create mode 100644 contracts/programs/contract-reader-interface/Xargo.toml create mode 100644 contracts/programs/contract-reader-interface/src/lib.rs create mode 100644 integration-tests/relayinterface/chain_components_test.go create mode 100644 pkg/solana/chainreader/client_wrapper.go diff --git a/.github/workflows/e2e_custom_cl.yml b/.github/workflows/e2e_custom_cl.yml index a32244438..0ceeaf5f2 100644 --- a/.github/workflows/e2e_custom_cl.yml +++ b/.github/workflows/e2e_custom_cl.yml @@ -423,4 +423,5 @@ jobs: env: E2E_TEST_CHAINLINK_IMAGE: ${{ env.CL_ECR }} E2E_TEST_SOLANA_SECRET: thisisatestingonlysecret + CHAINLINK_USER_TEAM: ${{ github.event.inputs.team || 'BIX' }} diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e79467188..74afc825e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -123,3 +123,57 @@ jobs: cd /repo/contracts &&\ cargo check &&\ cargo clippy -- -D warnings" + + relay_run_interface_tests: + name: Relay Run Interface Tests + runs-on: ubuntu-latest-8cores-32GB + needs: [get_projectserum_version, build_wrapped_anchor_image] + steps: + - name: Checkout sources + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + + - name: Cache cargo target dir + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + with: + path: contracts/target + key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + + - name: cache docker build image + id: cache-image + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + with: + fail-on-cache-miss: true + path: contracts/docker-build.tar + key: ${{ runner.os }}-docker-pnpm-build-${{ needs.get_projectserum_version.outputs.projectserum_version }}-${{ hashFiles('**/Cargo.lock') }} + + - name: load cached image + run: | + docker load --input docker-build.tar + + - name: Setup go + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + with: + go-version-file: "./integration-tests/go.mod" + check-latest: true + cache-dependency-path: "./integration-tests/go.sum" + + - name: Install gotestloghelper + run: go install github.com/smartcontractkit/chainlink-testing-framework/tools/gotestloghelper@latest + + - name: Install Solana CLI + run: ../scripts/install-solana-ci.sh + + - name: Test Relay Interfaces + run: | + set -eoux pipefail + # compile artifacts + solana-keygen new -o id.json --no-bip39-passphrase + docker run -v "$(pwd)/../":/repo chainlink-solana:build bash -c "\ + set -eoux pipefail &&\ + RUSTUP_HOME=\"/root/.rustup\" &&\ + FORCE_COLOR=1 &&\ + cd /repo/contracts &&\ + anchor build" + + cd .. + make test_relay_integration \ No newline at end of file diff --git a/Makefile b/Makefile index 0f3fd8843..74ff0753c 100644 --- a/Makefile +++ b/Makefile @@ -92,6 +92,10 @@ test_smoke: cd ./integration-tests &&\ SELECTED_NETWORKS=SIMULATED go test -timeout 24h -count=1 -json $(args) -run TestSolanaOCRV2Smoke ./smoke 2>&1 | tee /tmp/gotest.log | gotestloghelper -json -tlogprefix -singlepackage -color +test_relay_integration: + cd ./integration-tests &&\ + go test -timeout 20m -count=1 -json $(args) ./relayinterface/... 2>&1 | tee /tmp/gotest.log | gotestloghelper -json -tlogprefix -singlepackage -color + .PHONY: gomods gomods: ## Install gomods go install github.com/jmank88/gomods@v0.1.3 diff --git a/contracts/Anchor.toml b/contracts/Anchor.toml index 78a2222ad..30b788caa 100644 --- a/contracts/Anchor.toml +++ b/contracts/Anchor.toml @@ -27,6 +27,7 @@ test = "pnpm run test" [programs.localnet] access_controller = "9xi644bRR8birboDGdTiwBq3C7VEeR7VuamRYYXCubUW" +contract-reader-interface = "6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE" log-read-test = "J1zQwrBNBngz26jRPNWsUSZMHJwBwpkoDitXRV95LdK4" ocr_2 = "cjg3oHmg9uuPsP8D6g29NWvhySJkdYdAo9D25PRbKXJ" # need to rename the idl to satisfy anchor.js... store = "HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny" \ No newline at end of file diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 953b2b81e..25e8b1f5c 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -710,6 +710,13 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +[[package]] +name = "contract-reader-interface" +version = "0.1.0" +dependencies = [ + "anchor-lang", +] + [[package]] name = "cpufeatures" version = "0.2.12" diff --git a/contracts/generated/contract_reader_interface/Initialize.go b/contracts/generated/contract_reader_interface/Initialize.go new file mode 100644 index 000000000..ee6fa51f8 --- /dev/null +++ b/contracts/generated/contract_reader_interface/Initialize.go @@ -0,0 +1,188 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. + +package contract_reader_interface + +import ( + "errors" + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// Initialize is the `initialize` instruction. +type Initialize struct { + TestIdx *uint64 + Value *uint64 + + // [0] = [WRITE] data + // + // [1] = [WRITE, SIGNER] signer + // + // [2] = [] systemProgram + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +// NewInitializeInstructionBuilder creates a new `Initialize` instruction builder. +func NewInitializeInstructionBuilder() *Initialize { + nd := &Initialize{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 3), + } + return nd +} + +// SetTestIdx sets the "testIdx" parameter. +func (inst *Initialize) SetTestIdx(testIdx uint64) *Initialize { + inst.TestIdx = &testIdx + return inst +} + +// SetValue sets the "value" parameter. +func (inst *Initialize) SetValue(value uint64) *Initialize { + inst.Value = &value + return inst +} + +// SetDataAccount sets the "data" account. +func (inst *Initialize) SetDataAccount(data ag_solanago.PublicKey) *Initialize { + inst.AccountMetaSlice[0] = ag_solanago.Meta(data).WRITE() + return inst +} + +// GetDataAccount gets the "data" account. +func (inst *Initialize) GetDataAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[0] +} + +// SetSignerAccount sets the "signer" account. +func (inst *Initialize) SetSignerAccount(signer ag_solanago.PublicKey) *Initialize { + inst.AccountMetaSlice[1] = ag_solanago.Meta(signer).WRITE().SIGNER() + return inst +} + +// GetSignerAccount gets the "signer" account. +func (inst *Initialize) GetSignerAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[1] +} + +// SetSystemProgramAccount sets the "systemProgram" account. +func (inst *Initialize) SetSystemProgramAccount(systemProgram ag_solanago.PublicKey) *Initialize { + inst.AccountMetaSlice[2] = ag_solanago.Meta(systemProgram) + return inst +} + +// GetSystemProgramAccount gets the "systemProgram" account. +func (inst *Initialize) GetSystemProgramAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[2] +} + +func (inst Initialize) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: Instruction_Initialize, + }} +} + +// ValidateAndBuild validates the instruction parameters and accounts; +// if there is a validation error, it returns the error. +// Otherwise, it builds and returns the instruction. +func (inst Initialize) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *Initialize) Validate() error { + // Check whether all (required) parameters are set: + { + if inst.TestIdx == nil { + return errors.New("TestIdx parameter is not set") + } + if inst.Value == nil { + return errors.New("Value parameter is not set") + } + } + + // Check whether all (required) accounts are set: + { + if inst.AccountMetaSlice[0] == nil { + return errors.New("accounts.Data is not set") + } + if inst.AccountMetaSlice[1] == nil { + return errors.New("accounts.Signer is not set") + } + if inst.AccountMetaSlice[2] == nil { + return errors.New("accounts.SystemProgram is not set") + } + } + return nil +} + +func (inst *Initialize) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + // + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("Initialize")). + // + ParentFunc(func(instructionBranch ag_treeout.Branches) { + + // Parameters of the instruction: + instructionBranch.Child("Params[len=2]").ParentFunc(func(paramsBranch ag_treeout.Branches) { + paramsBranch.Child(ag_format.Param("TestIdx", *inst.TestIdx)) + paramsBranch.Child(ag_format.Param(" Value", *inst.Value)) + }) + + // Accounts of the instruction: + instructionBranch.Child("Accounts[len=3]").ParentFunc(func(accountsBranch ag_treeout.Branches) { + accountsBranch.Child(ag_format.Meta(" data", inst.AccountMetaSlice[0])) + accountsBranch.Child(ag_format.Meta(" signer", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta("systemProgram", inst.AccountMetaSlice[2])) + }) + }) + }) +} + +func (obj Initialize) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + // Serialize `TestIdx` param: + err = encoder.Encode(obj.TestIdx) + if err != nil { + return err + } + // Serialize `Value` param: + err = encoder.Encode(obj.Value) + if err != nil { + return err + } + return nil +} +func (obj *Initialize) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + // Deserialize `TestIdx`: + err = decoder.Decode(&obj.TestIdx) + if err != nil { + return err + } + // Deserialize `Value`: + err = decoder.Decode(&obj.Value) + if err != nil { + return err + } + return nil +} + +// NewInitializeInstruction declares a new Initialize instruction with the provided parameters and accounts. +func NewInitializeInstruction( + // Parameters: + testIdx uint64, + value uint64, + // Accounts: + data ag_solanago.PublicKey, + signer ag_solanago.PublicKey, + systemProgram ag_solanago.PublicKey) *Initialize { + return NewInitializeInstructionBuilder(). + SetTestIdx(testIdx). + SetValue(value). + SetDataAccount(data). + SetSignerAccount(signer). + SetSystemProgramAccount(systemProgram) +} diff --git a/contracts/generated/contract_reader_interface/Initialize_test.go b/contracts/generated/contract_reader_interface/Initialize_test.go new file mode 100644 index 000000000..80cc9106a --- /dev/null +++ b/contracts/generated/contract_reader_interface/Initialize_test.go @@ -0,0 +1,32 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. + +package contract_reader_interface + +import ( + "bytes" + ag_gofuzz "github.com/gagliardetto/gofuzz" + ag_require "github.com/stretchr/testify/require" + "strconv" + "testing" +) + +func TestEncodeDecode_Initialize(t *testing.T) { + fu := ag_gofuzz.New().NilChance(0) + for i := 0; i < 1; i++ { + t.Run("Initialize"+strconv.Itoa(i), func(t *testing.T) { + { + params := new(Initialize) + fu.Fuzz(params) + params.AccountMetaSlice = nil + buf := new(bytes.Buffer) + err := encodeT(*params, buf) + ag_require.NoError(t, err) + got := new(Initialize) + err = decodeT(got, buf.Bytes()) + got.AccountMetaSlice = nil + ag_require.NoError(t, err) + ag_require.Equal(t, params, got) + } + }) + } +} diff --git a/contracts/generated/contract_reader_interface/accounts.go b/contracts/generated/contract_reader_interface/accounts.go new file mode 100644 index 000000000..edf383de1 --- /dev/null +++ b/contracts/generated/contract_reader_interface/accounts.go @@ -0,0 +1,83 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. + +package contract_reader_interface + +import ( + "fmt" + ag_binary "github.com/gagliardetto/binary" +) + +type DataAccount struct { + Idx uint64 + Bump uint8 + U64Value uint64 + U64Slice []uint64 +} + +var DataAccountDiscriminator = [8]byte{85, 240, 182, 158, 76, 7, 18, 233} + +func (obj DataAccount) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + // Write account discriminator: + err = encoder.WriteBytes(DataAccountDiscriminator[:], false) + if err != nil { + return err + } + // Serialize `Idx` param: + err = encoder.Encode(obj.Idx) + if err != nil { + return err + } + // Serialize `Bump` param: + err = encoder.Encode(obj.Bump) + if err != nil { + return err + } + // Serialize `U64Value` param: + err = encoder.Encode(obj.U64Value) + if err != nil { + return err + } + // Serialize `U64Slice` param: + err = encoder.Encode(obj.U64Slice) + if err != nil { + return err + } + return nil +} + +func (obj *DataAccount) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + // Read and check account discriminator: + { + discriminator, err := decoder.ReadTypeID() + if err != nil { + return err + } + if !discriminator.Equal(DataAccountDiscriminator[:]) { + return fmt.Errorf( + "wrong discriminator: wanted %s, got %s", + "[85 240 182 158 76 7 18 233]", + fmt.Sprint(discriminator[:])) + } + } + // Deserialize `Idx`: + err = decoder.Decode(&obj.Idx) + if err != nil { + return err + } + // Deserialize `Bump`: + err = decoder.Decode(&obj.Bump) + if err != nil { + return err + } + // Deserialize `U64Value`: + err = decoder.Decode(&obj.U64Value) + if err != nil { + return err + } + // Deserialize `U64Slice`: + err = decoder.Decode(&obj.U64Slice) + if err != nil { + return err + } + return nil +} diff --git a/contracts/generated/contract_reader_interface/instructions.go b/contracts/generated/contract_reader_interface/instructions.go new file mode 100644 index 000000000..7eee5c9a6 --- /dev/null +++ b/contracts/generated/contract_reader_interface/instructions.go @@ -0,0 +1,117 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. + +package contract_reader_interface + +import ( + "bytes" + "fmt" + ag_spew "github.com/davecgh/go-spew/spew" + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_text "github.com/gagliardetto/solana-go/text" + ag_treeout "github.com/gagliardetto/treeout" +) + +var ProgramID ag_solanago.PublicKey + +func SetProgramID(pubkey ag_solanago.PublicKey) { + ProgramID = pubkey + ag_solanago.RegisterInstructionDecoder(ProgramID, registryDecodeInstruction) +} + +const ProgramName = "ContractReaderInterface" + +func init() { + if !ProgramID.IsZero() { + ag_solanago.RegisterInstructionDecoder(ProgramID, registryDecodeInstruction) + } +} + +var ( + Instruction_Initialize = ag_binary.TypeID([8]byte{175, 175, 109, 31, 13, 152, 155, 237}) +) + +// InstructionIDToName returns the name of the instruction given its ID. +func InstructionIDToName(id ag_binary.TypeID) string { + switch id { + case Instruction_Initialize: + return "Initialize" + default: + return "" + } +} + +type Instruction struct { + ag_binary.BaseVariant +} + +func (inst *Instruction) EncodeToTree(parent ag_treeout.Branches) { + if enToTree, ok := inst.Impl.(ag_text.EncodableToTree); ok { + enToTree.EncodeToTree(parent) + } else { + parent.Child(ag_spew.Sdump(inst)) + } +} + +var InstructionImplDef = ag_binary.NewVariantDefinition( + ag_binary.AnchorTypeIDEncoding, + []ag_binary.VariantType{ + { + "initialize", (*Initialize)(nil), + }, + }, +) + +func (inst *Instruction) ProgramID() ag_solanago.PublicKey { + return ProgramID +} + +func (inst *Instruction) Accounts() (out []*ag_solanago.AccountMeta) { + return inst.Impl.(ag_solanago.AccountsGettable).GetAccounts() +} + +func (inst *Instruction) Data() ([]byte, error) { + buf := new(bytes.Buffer) + if err := ag_binary.NewBorshEncoder(buf).Encode(inst); err != nil { + return nil, fmt.Errorf("unable to encode instruction: %w", err) + } + return buf.Bytes(), nil +} + +func (inst *Instruction) TextEncode(encoder *ag_text.Encoder, option *ag_text.Option) error { + return encoder.Encode(inst.Impl, option) +} + +func (inst *Instruction) UnmarshalWithDecoder(decoder *ag_binary.Decoder) error { + return inst.BaseVariant.UnmarshalBinaryVariant(decoder, InstructionImplDef) +} + +func (inst *Instruction) MarshalWithEncoder(encoder *ag_binary.Encoder) error { + err := encoder.WriteBytes(inst.TypeID.Bytes(), false) + if err != nil { + return fmt.Errorf("unable to write variant type: %w", err) + } + return encoder.Encode(inst.Impl) +} + +func registryDecodeInstruction(accounts []*ag_solanago.AccountMeta, data []byte) (interface{}, error) { + inst, err := DecodeInstruction(accounts, data) + if err != nil { + return nil, err + } + return inst, nil +} + +func DecodeInstruction(accounts []*ag_solanago.AccountMeta, data []byte) (*Instruction, error) { + inst := new(Instruction) + if err := ag_binary.NewBorshDecoder(data).Decode(inst); err != nil { + return nil, fmt.Errorf("unable to decode instruction: %w", err) + } + if v, ok := inst.Impl.(ag_solanago.AccountsSettable); ok { + err := v.SetAccounts(accounts) + if err != nil { + return nil, fmt.Errorf("unable to set accounts for instruction: %w", err) + } + } + return inst, nil +} diff --git a/contracts/generated/contract_reader_interface/testing_utils.go b/contracts/generated/contract_reader_interface/testing_utils.go new file mode 100644 index 000000000..3fca2bdc1 --- /dev/null +++ b/contracts/generated/contract_reader_interface/testing_utils.go @@ -0,0 +1,20 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. + +package contract_reader_interface + +import ( + "bytes" + "fmt" + ag_binary "github.com/gagliardetto/binary" +) + +func encodeT(data interface{}, buf *bytes.Buffer) error { + if err := ag_binary.NewBorshEncoder(buf).Encode(data); err != nil { + return fmt.Errorf("unable to encode instruction: %w", err) + } + return nil +} + +func decodeT(dst interface{}, data []byte) error { + return ag_binary.NewBorshDecoder(data).Decode(dst) +} diff --git a/contracts/generated/contract_reader_interface/types.go b/contracts/generated/contract_reader_interface/types.go new file mode 100644 index 000000000..afece0551 --- /dev/null +++ b/contracts/generated/contract_reader_interface/types.go @@ -0,0 +1,3 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. + +package contract_reader_interface diff --git a/contracts/programs/contract-reader-interface/Cargo.toml b/contracts/programs/contract-reader-interface/Cargo.toml new file mode 100644 index 000000000..f422a9f15 --- /dev/null +++ b/contracts/programs/contract-reader-interface/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "contract-reader-interface" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "contract_reader_interface" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = "0.29.0" diff --git a/contracts/programs/contract-reader-interface/Xargo.toml b/contracts/programs/contract-reader-interface/Xargo.toml new file mode 100644 index 000000000..475fb71ed --- /dev/null +++ b/contracts/programs/contract-reader-interface/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/contracts/programs/contract-reader-interface/src/lib.rs b/contracts/programs/contract-reader-interface/src/lib.rs new file mode 100644 index 000000000..838190fe9 --- /dev/null +++ b/contracts/programs/contract-reader-interface/src/lib.rs @@ -0,0 +1,46 @@ +use anchor_lang::prelude::*; +use std::mem::size_of; + +declare_id!("6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE"); + +#[program] +pub mod contract_reader_interface { + use super::*; + + pub fn initialize(ctx: Context, test_idx: u64, value: u64) -> Result<()> { + let account = &mut ctx.accounts.data; + + account.u64_value = value; + account.u64_slice = [3, 4].to_vec(); + account.idx = test_idx; + account.bump = ctx.bumps.data; + + Ok(()) + } +} + +#[derive(Accounts)] +#[instruction(test_idx: u64)] +pub struct Initialize<'info> { + // derived test PDA + #[account( + init, + payer = signer, + space = size_of::() + 8, + seeds=[b"data".as_ref(), test_idx.to_le_bytes().as_ref()], + bump)] + pub data: Account<'info, DataAccount>, + + #[account(mut)] + pub signer: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +#[account] +pub struct DataAccount { + pub idx: u64, + pub bump: u8, + pub u64_value: u64, + pub u64_slice: Vec, +} diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 0c5efbd92..0a0d552b0 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -87,6 +87,7 @@ require ( github.com/aws/constructs-go/constructs/v10 v10.4.2 // indirect github.com/aws/jsii-runtime-go v1.104.0 // indirect github.com/aws/smithy-go v1.22.0 // indirect + github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 // indirect github.com/benbjohnson/clock v1.3.5 // indirect @@ -96,6 +97,7 @@ require ( github.com/blang/semver/v4 v4.0.0 // indirect github.com/blendle/zapdriver v1.3.1 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect + github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/bytecodealliance/wasmtime-go/v23 v23.0.0 // indirect github.com/bytedance/sonic v1.11.6 // indirect @@ -173,6 +175,7 @@ require ( github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gagliardetto/treeout v0.1.4 // indirect + github.com/gagliardetto/utilz v0.1.1 // indirect github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect github.com/getsentry/sentry-go v0.27.0 // indirect github.com/gin-contrib/sessions v0.0.5 // indirect @@ -249,6 +252,7 @@ require ( github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/gtank/merlin v0.1.1 // indirect github.com/gtank/ristretto255 v0.1.2 // indirect + github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026 // indirect github.com/hashicorp/consul/api v1.29.2 // indirect github.com/hashicorp/consul/sdk v0.16.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -375,6 +379,7 @@ require ( github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/rs/cors v1.10.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sanity-io/litter v1.5.5 // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 2dda62636..8bd407485 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -239,6 +239,8 @@ github.com/aws/jsii-runtime-go v1.104.0 h1:651Sh6J2FtatfnVzlOQ3/Ye1WWPAseZ6E/tSQ github.com/aws/jsii-runtime-go v1.104.0/go.mod h1:7ZmQXxV0AAhhvv/GaHX4n6zbgA1tSRVdnQYAJbIhXHk= github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 h1:WWB576BN5zNSZc/M9d/10pqEx5VHNhaQ/yOVAkmj5Yo= +github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0= @@ -274,6 +276,8 @@ github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6 github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 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 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bxcodec/faker v2.0.1+incompatible h1:P0KUpUw5w6WJXwrPfv35oc91i4d8nf40Nwln+M/+faA= @@ -529,10 +533,13 @@ github.com/gagliardetto/binary v0.7.7 h1:QZpT38+sgoPg+TIQjH94sLbl/vX+nlIRA37pEyO github.com/gagliardetto/binary v0.7.7/go.mod h1:mUuay5LL8wFVnIlecHakSZMvcdqfs+CsotR5n77kyjM= 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.8.4 h1:vmD/JmTlonyXGy39bAo0inMhmbdAwV7rXZtLDMZeodE= github.com/gagliardetto/solana-go v1.8.4/go.mod h1:i+7aAyNDTHG0jK8GZIBSI4OVvDqkt2Qx+LklYclRNG8= 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/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/gedex/inflector v0.0.0-20170307190818-16278e9db813 h1:Uc+IZ7gYqAf/rSGFplbWBSHaGolEQlNLgMgSE3ccnIQ= @@ -827,6 +834,8 @@ github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= +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/api v1.29.2 h1:aYyRn8EdE2mSfG14S1+L9Qkjtz8RzmaWh6AcNGRNwPw= github.com/hashicorp/consul/api v1.29.2/go.mod h1:0YObcaLNDSbtlgzIRtmRXI1ZkeuK0trCBxwZQ4MYnIk= @@ -1117,6 +1126,7 @@ github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6B github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs= github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ= @@ -1356,6 +1366,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/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/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= diff --git a/integration-tests/relayinterface/chain_components_test.go b/integration-tests/relayinterface/chain_components_test.go new file mode 100644 index 000000000..d3ccf13a5 --- /dev/null +++ b/integration-tests/relayinterface/chain_components_test.go @@ -0,0 +1,405 @@ +/* +Package relayinterface contains the interface tests for chain components. +*/ +package relayinterface + +import ( + "context" + "encoding/binary" + "io" + "os" + "path/filepath" + "testing" + "time" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + "github.com/gagliardetto/solana-go/rpc/ws" + "github.com/gagliardetto/solana-go/text" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/codec" + "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/types/query/primitives" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + contract "github.com/smartcontractkit/chainlink-solana/contracts/generated/contract_reader_interface" + "github.com/smartcontractkit/chainlink-solana/integration-tests/solclient" + "github.com/smartcontractkit/chainlink-solana/integration-tests/utils" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainreader" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" +) + +func TestChainComponents(t *testing.T) { + t.Parallel() + it := &SolanaChainComponentsInterfaceTester[*testing.T]{Helper: &helper{}} + it.Init(t) + + it.DisableTests([]string{ + // disable tests that set values + ContractReaderGetLatestValueBasedOnConfidenceLevel, + // disable anything returning a struct or requiring input params for now + ContractReaderGetLatestValueAsValuesDotValue, + ContractReaderGetLatestValue, + ContractReaderGetLatestValueWithModifiersUsingOwnMapstrctureOverrides, + // events not yet supported + ContractReaderGetLatestValueGetsLatestForEvent, + ContractReaderGetLatestValueBasedOnConfidenceLevelForEvent, + ContractReaderGetLatestValueReturnsNotFoundWhenNotTriggeredForEvent, + ContractReaderGetLatestValueWithFilteringForEvent, + // disable anything in batch relating to input params or structs for now + ContractReaderBatchGetLatestValue, + ContractReaderBatchGetLatestValueWithModifiersOwnMapstructureOverride, + ContractReaderBatchGetLatestValueDifferentParamsResultsRetainOrder, + ContractReaderBatchGetLatestValueDifferentParamsResultsRetainOrderMultipleContracts, + ContractReaderBatchGetLatestValueSetsErrorsProperly, + // query key not implemented yet + ContractReaderQueryKeyNotFound, + ContractReaderQueryKeyReturnsData, + ContractReaderQueryKeyReturnsDataAsValuesDotValue, + ContractReaderQueryKeyCanFilterWithValueComparator, + ContractReaderQueryKeyCanLimitResultsWithCursor, + }) + + RunChainComponentsSolanaTests(t, it) + RunChainComponentsInLoopSolanaTests(t, commontestutils.WrapContractReaderTesterForLoop(it)) +} + +func RunChainComponentsSolanaTests[T TestingT[T]](t T, it *SolanaChainComponentsInterfaceTester[T]) { + RunContractReaderSolanaTests(t, it) + // Add ChainWriter tests here +} + +func RunChainComponentsInLoopSolanaTests[T TestingT[T]](t T, it ChainComponentsInterfaceTester[T]) { + RunContractReaderInLoopTests(t, it) + // Add ChainWriter tests here +} + +func RunContractReaderSolanaTests[T TestingT[T]](t T, it *SolanaChainComponentsInterfaceTester[T]) { + RunContractReaderInterfaceTests(t, it, false) + + testCases := []Testcase[T]{} + + RunTests(t, it, testCases) +} + +func RunContractReaderInLoopTests[T TestingT[T]](t T, it ChainComponentsInterfaceTester[T]) { + RunContractReaderInterfaceTests(t, it, false) + + testCases := []Testcase[T]{} + + RunTests(t, it, testCases) +} + +type SolanaChainComponentsInterfaceTesterHelper[T TestingT[T]] interface { + Init(t T) + RPCClient() *chainreader.RPCClientWrapper + Context(t T) context.Context + Logger(t T) logger.Logger + GetJSONEncodedIDL(t T) []byte + CreateAccount(t T, value uint64) solana.PublicKey +} + +type SolanaChainComponentsInterfaceTester[T TestingT[T]] struct { + TestSelectionSupport + Helper SolanaChainComponentsInterfaceTesterHelper[T] + cr *chainreader.SolanaChainReaderService + chainReaderConfig config.ChainReader + accountPubKey solana.PublicKey + secondAccountPubKey solana.PublicKey +} + +func (it *SolanaChainComponentsInterfaceTester[T]) Setup(t T) { + t.Cleanup(func() {}) + + it.chainReaderConfig = config.ChainReader{ + Namespaces: map[string]config.ChainReaderMethods{ + AnyContractName: { + Methods: map[string]config.ChainDataReader{ + MethodReturningUint64: { + AnchorIDL: string(it.Helper.GetJSONEncodedIDL(t)), + Encoding: config.EncodingTypeBorsh, + Procedure: config.ChainReaderProcedure{ + IDLAccount: "DataAccount", + OutputModifications: codec.ModifiersConfig{ + &codec.PropertyExtractorConfig{FieldName: "U64value"}, + }, + }, + }, + MethodReturningUint64Slice: { + AnchorIDL: string(it.Helper.GetJSONEncodedIDL(t)), + Encoding: config.EncodingTypeBorsh, + Procedure: config.ChainReaderProcedure{ + IDLAccount: "DataAccount", + OutputModifications: codec.ModifiersConfig{ + &codec.PropertyExtractorConfig{FieldName: "U64slice"}, + }, + }, + }, + }, + }, + AnySecondContractName: { + Methods: map[string]config.ChainDataReader{ + MethodReturningUint64: { + AnchorIDL: string(it.Helper.GetJSONEncodedIDL(t)), + Encoding: config.EncodingTypeBorsh, + Procedure: config.ChainReaderProcedure{ + IDLAccount: "DataAccount", + OutputModifications: codec.ModifiersConfig{ + &codec.PropertyExtractorConfig{FieldName: "U64value"}, + }, + }, + }, + }, + }, + }, + } + + it.accountPubKey = it.Helper.CreateAccount(t, AnyValueToReadWithoutAnArgument) + it.secondAccountPubKey = it.Helper.CreateAccount(t, AnyDifferentValueToReadWithoutAnArgument) +} + +func (it *SolanaChainComponentsInterfaceTester[T]) Name() string { + return "" +} + +func (it *SolanaChainComponentsInterfaceTester[T]) GetAccountBytes(i int) []byte { + return nil +} + +func (it *SolanaChainComponentsInterfaceTester[T]) GetAccountString(i int) string { + return "" +} + +func (it *SolanaChainComponentsInterfaceTester[T]) GetContractReader(t T) types.ContractReader { + ctx := it.Helper.Context(t) + if it.cr != nil { + return it.cr + } + + svc, err := chainreader.NewChainReaderService(it.Helper.Logger(t), it.Helper.RPCClient(), it.chainReaderConfig) + + require.NoError(t, err) + require.NoError(t, svc.Start(ctx)) + + it.cr = svc + + return svc +} + +func (it *SolanaChainComponentsInterfaceTester[T]) GetContractWriter(t T) types.ContractWriter { + return nil +} + +func (it *SolanaChainComponentsInterfaceTester[T]) GetBindings(t T) []types.BoundContract { + // at the moment, use only a single account address for everything + return []types.BoundContract{ + {Name: AnyContractName, Address: it.accountPubKey.String()}, + {Name: AnySecondContractName, Address: it.secondAccountPubKey.String()}, + } +} + +func (it *SolanaChainComponentsInterfaceTester[T]) DirtyContracts() {} + +func (it *SolanaChainComponentsInterfaceTester[T]) MaxWaitTimeForEvents() time.Duration { + return time.Second +} + +func (it *SolanaChainComponentsInterfaceTester[T]) GenerateBlocksTillConfidenceLevel(t T, contractName, readName string, confidenceLevel primitives.ConfidenceLevel) { + +} + +func (it *SolanaChainComponentsInterfaceTester[T]) Init(t T) { + it.Helper.Init(t) +} + +type helper struct { + programID solana.PublicKey + rpcURL string + wsURL string + rpcClient *rpc.Client + wsClient *ws.Client + idlBts []byte + nonce uint64 +} + +func (h *helper) Init(t *testing.T) { + t.Helper() + + privateKey, err := solana.PrivateKeyFromBase58(solclient.DefaultPrivateKeysSolValidator[1]) + require.NoError(t, err) + + h.rpcURL, h.wsURL = setupTestValidator(t, privateKey.PublicKey().String()) + h.wsClient, err = ws.Connect(tests.Context(t), h.wsURL) + h.rpcClient = rpc.New(h.rpcURL) + + require.NoError(t, err) + + client.FundTestAccounts(t, []solana.PublicKey{privateKey.PublicKey()}, h.rpcURL) + + pubkey, err := solana.PublicKeyFromBase58(programPubKey) + require.NoError(t, err) + + contract.SetProgramID(pubkey) + h.programID = pubkey +} + +func (h *helper) RPCClient() *chainreader.RPCClientWrapper { + return &chainreader.RPCClientWrapper{Client: h.rpcClient} +} + +func (h *helper) Context(t *testing.T) context.Context { + return tests.Context(t) +} + +func (h *helper) Logger(t *testing.T) logger.Logger { + return logger.Test(t) +} + +func (h *helper) GetJSONEncodedIDL(t *testing.T) []byte { + t.Helper() + + if h.idlBts != nil { + return h.idlBts + } + + soPath := filepath.Join(utils.IDLDir, "contract_reader_interface.json") + + _, err := os.Stat(soPath) + if err != nil { + t.Log(err.Error()) + t.FailNow() + } + + bts, err := os.ReadFile(soPath) + require.NoError(t, err) + + h.idlBts = bts + + return h.idlBts +} + +func (h *helper) CreateAccount(t *testing.T, value uint64) solana.PublicKey { + t.Helper() + + h.nonce++ + + bts := make([]byte, 8) + binary.LittleEndian.PutUint64(bts, h.nonce*value) + + pubKey, _, err := solana.FindProgramAddress([][]byte{[]byte("data"), bts}, h.programID) + require.NoError(t, err) + + // Getting the default localnet private key + privateKey, err := solana.PrivateKeyFromBase58(solclient.DefaultPrivateKeysSolValidator[1]) + require.NoError(t, err) + + h.runInitialize(t, value, pubKey, func(key solana.PublicKey) *solana.PrivateKey { + return &privateKey + }, privateKey.PublicKey()) + + return pubKey +} + +func (h *helper) runInitialize( + t *testing.T, + value uint64, + data solana.PublicKey, + signerFunc func(key solana.PublicKey) *solana.PrivateKey, + payer solana.PublicKey, +) { + t.Helper() + + inst, err := contract.NewInitializeInstruction(h.nonce*value, value, data, payer, solana.SystemProgramID).ValidateAndBuild() + require.NoError(t, err) + + h.sendInstruction(t, inst, signerFunc, payer) +} + +func (h *helper) sendInstruction( + t *testing.T, + inst *contract.Instruction, + signerFunc func(key solana.PublicKey) *solana.PrivateKey, + payer solana.PublicKey, +) { + t.Helper() + + ctx := tests.Context(t) + + recent, err := h.rpcClient.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) + require.NoError(t, err) + + tx, err := solana.NewTransaction( + []solana.Instruction{ + inst, + }, + recent.Value.Blockhash, + solana.TransactionPayer(payer), + ) + require.NoError(t, err) + + _, err = tx.EncodeTree(text.NewTreeEncoder(io.Discard, "Initialize")) + require.NoError(t, err) + + _, err = tx.Sign(signerFunc) + require.NoError(t, err) + + sig, err := h.rpcClient.SendTransactionWithOpts( + ctx, tx, + rpc.TransactionOpts{ + PreflightCommitment: rpc.CommitmentConfirmed, + }, + ) + require.NoError(t, err) + + h.waitForTX(t, sig, rpc.CommitmentFinalized) +} + +func (h *helper) waitForTX(t *testing.T, sig solana.Signature, commitment rpc.CommitmentType) { + t.Helper() + + sub, err := h.wsClient.SignatureSubscribe( + sig, + commitment, + ) + require.NoError(t, err) + + defer sub.Unsubscribe() + + res, err := sub.Recv() + require.NoError(t, err) + + if res.Value.Err != nil { + t.Logf("transaction confirmation failed: %v", res.Value.Err) + t.FailNow() + } +} + +const programPubKey = "6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE" + +// upgradeAuthority is admin solana.PrivateKey as string +func setupTestValidator(t *testing.T, upgradeAuthority string) (string, string) { + t.Helper() + + soPath := filepath.Join(utils.ContractsDir, "contract_reader_interface.so") + + _, err := os.Stat(soPath) + if err != nil { + t.Log(err.Error()) + t.FailNow() + } + + flags := []string{ + "--warp-slot", "42", + "--upgradeable-program", + programPubKey, + soPath, + upgradeAuthority, + } + + return client.SetupLocalSolNodeWithFlags(t, flags...) +} diff --git a/integration-tests/utils/project_path.go b/integration-tests/utils/project_path.go index e048fc191..814e10b6a 100644 --- a/integration-tests/utils/project_path.go +++ b/integration-tests/utils/project_path.go @@ -11,4 +11,5 @@ var ( ProjectRoot = filepath.Join(filepath.Dir(b), "/../..") // ContractsDir path to our contracts ContractsDir = filepath.Join(ProjectRoot, "contracts", "target", "deploy") + IDLDir = filepath.Join(ProjectRoot, "contracts", "target", "idl") ) diff --git a/pkg/solana/chainreader/client_wrapper.go b/pkg/solana/chainreader/client_wrapper.go new file mode 100644 index 000000000..c08ba1031 --- /dev/null +++ b/pkg/solana/chainreader/client_wrapper.go @@ -0,0 +1,45 @@ +package chainreader + +import ( + "context" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" +) + +// RPCClientWrapper is a wrapper for an RPC client. This was necessary due to the solana RPC interface not +// providing directly mockable components in the GetMultipleAccounts response. +type RPCClientWrapper struct { + *rpc.Client +} + +// GetMultipleAccountData is a helper function that extracts byte data from a GetMultipleAccounts rpc call. +func (w *RPCClientWrapper) GetMultipleAccountData(ctx context.Context, keys ...solana.PublicKey) ([][]byte, error) { + result, err := w.Client.GetMultipleAccountsWithOpts(ctx, keys, &rpc.GetMultipleAccountsOpts{ + Encoding: solana.EncodingBase64, + Commitment: rpc.CommitmentFinalized, + }) + if err != nil { + return nil, err + } + + bts := make([][]byte, len(result.Value)) + + for idx, result := range result.Value { + if result == nil { + return nil, rpc.ErrNotFound + } + + if result.Data == nil { + return nil, rpc.ErrNotFound + } + + if result.Data.GetBinary() == nil { + return nil, rpc.ErrNotFound + } + + bts[idx] = result.Data.GetBinary() + } + + return bts, nil +}