diff --git a/contracts/Anchor.toml b/contracts/Anchor.toml index 6371e3d9c..c12a8682f 100644 --- a/contracts/Anchor.toml +++ b/contracts/Anchor.toml @@ -1,11 +1,14 @@ -anchor_version = "0.29.0" +[toolchain] + +[features] +seeds = false +skip-lint = false [registry] url = "https://anchor.projectserum.com" [provider] -cluster = "localnet" -# wallet = "~/.config/solana/id.json" +cluster = "Localnet" wallet = "id.json" [scripts] @@ -21,6 +24,7 @@ test = "yarn run test" # TODO: add pubkeys [programs.localnet] +access_controller = "9xi644bRR8birboDGdTiwBq3C7VEeR7VuamRYYXCubUW" +keystone-forwarder = "6v9Lm94wiHXJf4HYoWoRj7JGb5YCDnsvybr9Y3seJ7po" ocr_2 = "cjg3oHmg9uuPsP8D6g29NWvhySJkdYdAo9D25PRbKXJ" # need to rename the idl to satisfy anchor.js... store = "HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny" -access_controller = "9xi644bRR8birboDGdTiwBq3C7VEeR7VuamRYYXCubUW" diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index ba4b5e1d7..385015c6b 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -1126,6 +1126,13 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "keystone-forwarder" +version = "0.1.0" +dependencies = [ + "anchor-lang", +] + [[package]] name = "lazy_static" version = "1.4.0" diff --git a/contracts/programs/keystone-forwarder/Cargo.toml b/contracts/programs/keystone-forwarder/Cargo.toml new file mode 100644 index 000000000..f9f492a3b --- /dev/null +++ b/contracts/programs/keystone-forwarder/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "keystone-forwarder" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "keystone_forwarder" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = "0.29.0" diff --git a/contracts/programs/keystone-forwarder/Xargo.toml b/contracts/programs/keystone-forwarder/Xargo.toml new file mode 100644 index 000000000..475fb71ed --- /dev/null +++ b/contracts/programs/keystone-forwarder/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/contracts/programs/keystone-forwarder/src/lib.rs b/contracts/programs/keystone-forwarder/src/lib.rs new file mode 100644 index 000000000..bc3d43781 --- /dev/null +++ b/contracts/programs/keystone-forwarder/src/lib.rs @@ -0,0 +1,148 @@ +use anchor_lang::prelude::*; + +declare_id!("6v9Lm94wiHXJf4HYoWoRj7JGb5YCDnsvybr9Y3seJ7po"); + +// TODO: ownable + +pub const STATE_VERSION: u8 = 1; + +#[account] +#[derive(Default)] +pub struct State { + version: u8, + authority_nonce: u8, + owner: Pubkey, +} + +#[account] +#[derive(Default)] +pub struct ExecutionState {} + +#[error_code] +pub enum ErrorCode { + #[msg("Unauthorized")] + Unauthorized = 0, + + #[msg("Invalid input")] + InvalidInput = 1, +} + +#[program] +pub mod keystone_forwarder { + use anchor_lang::solana_program::{instruction::Instruction, program::invoke_signed}; + + use super::*; + + pub fn initialize(ctx: Context) -> Result<()> { + // Precompute the authority PDA bump + let (_authority_pubkey, authority_nonce) = Pubkey::find_program_address( + &[b"forwarder", ctx.accounts.state.key().as_ref()], + &crate::ID, + ); + + let state = &mut ctx.accounts.state; + state.version = STATE_VERSION; + state.authority_nonce = authority_nonce; + state.owner = ctx.accounts.owner.key(); + Ok(()) + } + + // TODO: use raw &[u8] input to avoid serialization and encoding + pub fn report(ctx: Context, data: Vec) -> Result<()> { + const RAW_REPORT_LEN: usize = 1 + OFFSET; + require!(data.len() > RAW_REPORT_LEN, ErrorCode::InvalidInput); + let len = data[0] as usize; + let data = &data[1..]; + let (raw_signatures, raw_report) = data.split_at(32 * len); + + // TODO: a way to store context inside data without limiting the receiver + const OFFSET: usize = 32 + 32; + // meta = (workflowID, workflowExecutionID) + let (meta, data) = raw_report.split_at(OFFSET); + + // verify signature + use anchor_lang::solana_program::{hash, keccak, secp256k1_recover::*}; + + // 64 byte signature + 1 byte recovery id + const SIGNATURE_LEN: usize = SECP256K1_SIGNATURE_LENGTH + 1; + // raw_signatures is exactly sized + require!( + raw_signatures.len() % SIGNATURE_LEN == 0, + ErrorCode::InvalidInput + ); + // let signature_count = raw_signatures.len() / SIGNATURE_LEN; + // require!( + // signature_count == usize::from(config.f) + 1, + // ErrorCode::InvalidInput + // ); + + let hash = hash::hash(&raw_report).to_bytes(); + + let raw_signatures = raw_signatures.chunks(SIGNATURE_LEN); + for signature in raw_signatures { + // TODO: + } + + // check if PDA exists, if so terminate the call + + // invoke_signed with forwarder authority + let program_id = ctx.accounts.receiver_program.key(); + let accounts = vec![]; + let ix = Instruction::new_with_bytes(program_id, &raw_report, accounts); + let account_infos = &[]; + let state_pubkey = ctx.accounts.state.key(); + let signers_seeds = &[ + b"forwarder", + state_pubkey.as_ref(), + &[ctx.accounts.state.authority_nonce], + ]; + let _ = invoke_signed(&ix, account_infos, &[signers_seeds]); + + // mark tx as signed by initializing PDA via create_account instruction + Ok(()) + } +} + +#[derive(Accounts)] +pub struct Initialize<'info> { + // space: 8 discriminator + u8 authority_nonce + 1 bump + #[account( + init, + payer = owner, + space = 8 + 1 + 1 + 32 + )] + pub state: Account<'info, State>, + #[account(mut)] + pub owner: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct Report<'info> { + /// Forwarder state acccount + #[account(mut)] + pub state: Account<'info, State>, + + /// Transmitter, signing the current transaction call + pub authority: Signer<'info>, + + /// Authority used for signing the receiver invocation + /// CHECK: This is a PDA + #[account(seeds = [b"forwarder", state.key().as_ref()], bump = state.authority_nonce)] + pub forwarder_authority: AccountInfo<'info>, + + /// State PDA for the workflow execution represented by this report. + /// TODO: we need to manually verify that it's the correct PDA since we need to unpack meta to get the execution ID + #[account(mut)] + // pub execution_state: Account<'info, ExecutionState>, + /// CHECK: TODO: + pub execution_state: UncheckedAccount<'info>, + + #[account(executable)] + /// CHECK: We don't use Program<> here since it can be any program, "executable" is enough + pub receiver_program: UncheckedAccount<'info>, + // TODO: ensure receiver isn't forwarder itself? + + // remaining_accounts... get passed to receiver as is +} diff --git a/contracts/tests/forwarder.spec.ts b/contracts/tests/forwarder.spec.ts new file mode 100644 index 000000000..909b349f1 --- /dev/null +++ b/contracts/tests/forwarder.spec.ts @@ -0,0 +1,145 @@ +import * as anchor from "@coral-xyz/anchor"; +import { ProgramError, BN } from "@coral-xyz/anchor"; +import { Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js"; +import * as borsh from "borsh"; + +import { randomBytes, createHash } from "crypto"; +import * as secp256k1 from "secp256k1"; +import { keccak256 } from "ethereum-cryptography/keccak"; + +import { assert } from "chai"; +import { getOrCreateAssociatedTokenAccount } from "@solana/spl-token"; + +describe("ocr2", () => { + // Configure the client to use the local cluster. + const provider = anchor.AnchorProvider.local(); + anchor.setProvider(provider); + + const forwarderProgram = anchor.workspace.KeystoneForwarder; + + // Generate a new wallet keypair and airdrop SOL + const payer = Keypair.generate(); + + const owner = provider.wallet; + + const state = Keypair.generate(); + const transmitter = Keypair.generate(); + + let authorityNonce: number; + let authority: PublicKey; + + let oracles = []; + const f = 6; + // NOTE: 17 is the most we can fit into one proposeConfig if we use a different payer + // if the owner == payer then we can fit 19 + const n = 19; // min: 3 * f + 1; + + let generateOracle = async () => { + let secretKey = randomBytes(32); + let transmitter = Keypair.generate(); + return { + signer: { + secretKey, + publicKey: secp256k1.publicKeyCreate(secretKey, false).slice(1), // compressed = false, skip first byte (0x04) + }, + transmitter, + }; + }; + + it("Funds the payer", async () => { + await provider.connection.confirmTransaction( + await provider.connection.requestAirdrop(payer.publicKey, LAMPORTS_PER_SOL * 1000), + "confirmed" + ); + + await provider.connection.confirmTransaction( + await provider.connection.requestAirdrop(transmitter.publicKey, LAMPORTS_PER_SOL * 1000), + "confirmed" + ); + }); + + it("Initializes the forwarder", async() => { + await forwarderProgram.methods + .initialize() + .accounts({ + state: state.publicKey, + owner: owner.publicKey, + }) + .signers([state]) + // .preInstructions([await program.account.state.createInstruction(state)]) + .rpc(); + + let stateAccount = await forwarderProgram.account.state.fetch(state.publicKey); + authorityNonce = stateAccount.authorityNonce; + authority = PublicKey.createProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode("forwarder")), + state.publicKey.toBuffer(), + Buffer.from([authorityNonce]) + ], + forwarderProgram.programId + ); + + console.log(`Generating ${n} oracles...`); + let futures = []; + for (let i = 0; i < n; i++) { + futures.push(generateOracle()); + } + oracles = await Promise.all(futures); + }); + + // TODO: deploy mock receiver, forward the report there, assert on program log + + it("Successfully receives a new, valid report", async () => { + const rawReport = Buffer.from([ + // 32 byte workflow id + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + // 32 byte workflow execution id + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, + // report data + 0, 0, 0, 1 + ]); + + let hash = createHash("sha256") + .update(rawReport) + .digest(); + + let rawSignatures = []; + // for (let oracle of oracles.slice(0, f + 1)) { + // // sign with `f` + 1 oracles + // let { signature, recid } = secp256k1.ecdsaSign( + // hash, + // oracle.signer.secretKey + // ); + // rawSignatures.push(...signature); + // rawSignatures.push(recid); + // } + + let data = Buffer.concat([ + Buffer.from([rawSignatures.length]), + Buffer.from(rawSignatures), + rawReport, + ]); + + const executionState = Keypair.generate(); + + await forwarderProgram.methods + .report(data) + .accounts({ + state: state.publicKey, + authority: transmitter.publicKey, + forwarderAuthority: authority, + executionState: executionState.publicKey, // TODO: derive + receiverProgram: forwarderProgram.programId, // TODO: + }) + .signers([transmitter]) + .rpc(); + + // TODO: await until confirmation on all of these + + }); + + it("Doesn't retransmit the same report", async () => { + + }); +}) diff --git a/contracts/tests/ocr2.spec.ts b/contracts/tests/ocr2.spec.ts index 7452a4f71..09a3acc38 100644 --- a/contracts/tests/ocr2.spec.ts +++ b/contracts/tests/ocr2.spec.ts @@ -73,7 +73,7 @@ describe("ocr2", () => { const state = Keypair.generate(); // const stateSize = 8 + ; const feed = Keypair.generate(); - const payer = Keypair.generate(); + const payer = Keypair.generate(); // TODO: both payer and fromWallet seem redundant, use payer // const owner = Keypair.generate(); const owner = provider.wallet; const mintAuthority = Keypair.generate(); diff --git a/go.mod b/go.mod index 92e546feb..19e284ad1 100644 --- a/go.mod +++ b/go.mod @@ -9,11 +9,12 @@ require ( github.com/gagliardetto/solana-go v1.4.1-0.20220428092759-5250b4abbb27 github.com/gagliardetto/treeout v0.1.4 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.20240301141417-6e6e1dd8a6ea github.com/smartcontractkit/libocr v0.0.0-20240112202000-6359502d2ff1 github.com/stretchr/testify v1.8.4 github.com/test-go/testify v1.1.4 @@ -48,7 +49,7 @@ require ( 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/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 diff --git a/go.sum b/go.sum index ff155ba26..b4b12d794 100644 --- a/go.sum +++ b/go.sum @@ -267,8 +267,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= @@ -424,8 +424,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.20240301141417-6e6e1dd8a6ea h1:2keZr1X1bagXD3UvEX9kHgc+Bxuf3c9qsMRb3PGqXk0= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240301141417-6e6e1dd8a6ea/go.mod h1:6aXWSEQawX2oZXcPPOdxnEGufAhj7PqPKolXf6ijRGA= 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= diff --git a/pkg/monitoring/mocks/ChainReader.go b/pkg/monitoring/mocks/ChainReader.go index d1d9498d4..889fc7513 100644 --- a/pkg/monitoring/mocks/ChainReader.go +++ b/pkg/monitoring/mocks/ChainReader.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.12.0. DO NOT EDIT. +// Code generated by mockery v2.35.4. DO NOT EDIT. package mocks @@ -12,8 +12,6 @@ import ( rpc "github.com/gagliardetto/solana-go/rpc" solana "github.com/gagliardetto/solana-go" - - testing "testing" ) // ChainReader is an autogenerated mock type for the ChainReader type @@ -26,6 +24,10 @@ func (_m *ChainReader) GetBalance(ctx context.Context, account solana.PublicKey, ret := _m.Called(ctx, account, commitment) var r0 *rpc.GetBalanceResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) (*rpc.GetBalanceResult, error)); ok { + return rf(ctx, account, commitment) + } if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) *rpc.GetBalanceResult); ok { r0 = rf(ctx, account, commitment) } else { @@ -34,7 +36,6 @@ func (_m *ChainReader) GetBalance(ctx context.Context, account solana.PublicKey, } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, rpc.CommitmentType) error); ok { r1 = rf(ctx, account, commitment) } else { @@ -49,20 +50,23 @@ func (_m *ChainReader) GetLatestTransmission(ctx context.Context, account solana ret := _m.Called(ctx, account, commitment) var r0 pkgsolana.Answer + var r1 uint64 + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) (pkgsolana.Answer, uint64, error)); ok { + return rf(ctx, account, commitment) + } if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) pkgsolana.Answer); ok { r0 = rf(ctx, account, commitment) } else { r0 = ret.Get(0).(pkgsolana.Answer) } - var r1 uint64 if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, rpc.CommitmentType) uint64); ok { r1 = rf(ctx, account, commitment) } else { r1 = ret.Get(1).(uint64) } - var r2 error if rf, ok := ret.Get(2).(func(context.Context, solana.PublicKey, rpc.CommitmentType) error); ok { r2 = rf(ctx, account, commitment) } else { @@ -77,6 +81,10 @@ func (_m *ChainReader) GetSignaturesForAddressWithOpts(ctx context.Context, acco ret := _m.Called(ctx, account, opts) var r0 []*rpc.TransactionSignature + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) ([]*rpc.TransactionSignature, error)); ok { + return rf(ctx, account, opts) + } if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) []*rpc.TransactionSignature); ok { r0 = rf(ctx, account, opts) } else { @@ -85,7 +93,6 @@ func (_m *ChainReader) GetSignaturesForAddressWithOpts(ctx context.Context, acco } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) error); ok { r1 = rf(ctx, account, opts) } else { @@ -100,20 +107,23 @@ func (_m *ChainReader) GetState(ctx context.Context, account solana.PublicKey, c ret := _m.Called(ctx, account, commitment) var r0 pkgsolana.State + var r1 uint64 + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) (pkgsolana.State, uint64, error)); ok { + return rf(ctx, account, commitment) + } if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) pkgsolana.State); ok { r0 = rf(ctx, account, commitment) } else { r0 = ret.Get(0).(pkgsolana.State) } - var r1 uint64 if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, rpc.CommitmentType) uint64); ok { r1 = rf(ctx, account, commitment) } else { r1 = ret.Get(1).(uint64) } - var r2 error if rf, ok := ret.Get(2).(func(context.Context, solana.PublicKey, rpc.CommitmentType) error); ok { r2 = rf(ctx, account, commitment) } else { @@ -128,6 +138,10 @@ func (_m *ChainReader) GetTokenAccountBalance(ctx context.Context, account solan ret := _m.Called(ctx, account, commitment) var r0 *rpc.GetTokenAccountBalanceResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) (*rpc.GetTokenAccountBalanceResult, error)); ok { + return rf(ctx, account, commitment) + } if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) *rpc.GetTokenAccountBalanceResult); ok { r0 = rf(ctx, account, commitment) } else { @@ -136,7 +150,6 @@ func (_m *ChainReader) GetTokenAccountBalance(ctx context.Context, account solan } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, rpc.CommitmentType) error); ok { r1 = rf(ctx, account, commitment) } else { @@ -151,6 +164,10 @@ func (_m *ChainReader) GetTransaction(ctx context.Context, txSig solana.Signatur ret := _m.Called(ctx, txSig, opts) var r0 *rpc.GetTransactionResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.Signature, *rpc.GetTransactionOpts) (*rpc.GetTransactionResult, error)); ok { + return rf(ctx, txSig, opts) + } if rf, ok := ret.Get(0).(func(context.Context, solana.Signature, *rpc.GetTransactionOpts) *rpc.GetTransactionResult); ok { r0 = rf(ctx, txSig, opts) } else { @@ -159,7 +176,6 @@ func (_m *ChainReader) GetTransaction(ctx context.Context, txSig solana.Signatur } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, solana.Signature, *rpc.GetTransactionOpts) error); ok { r1 = rf(ctx, txSig, opts) } else { @@ -169,8 +185,12 @@ func (_m *ChainReader) GetTransaction(ctx context.Context, txSig solana.Signatur return r0, r1 } -// NewChainReader creates a new instance of ChainReader. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. -func NewChainReader(t testing.TB) *ChainReader { +// NewChainReader creates a new instance of ChainReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewChainReader(t interface { + mock.TestingT + Cleanup(func()) +}) *ChainReader { mock := &ChainReader{} mock.Mock.Test(t) diff --git a/pkg/monitoring/mocks/Metrics.go b/pkg/monitoring/mocks/Metrics.go index 3947c1db2..dd52a33c8 100644 --- a/pkg/monitoring/mocks/Metrics.go +++ b/pkg/monitoring/mocks/Metrics.go @@ -1,12 +1,8 @@ -// Code generated by mockery v2.12.0. DO NOT EDIT. +// Code generated by mockery v2.35.4. DO NOT EDIT. package mocks -import ( - mock "github.com/stretchr/testify/mock" - - testing "testing" -) +import mock "github.com/stretchr/testify/mock" // Metrics is an autogenerated mock type for the Metrics type type Metrics struct { @@ -23,8 +19,12 @@ func (_m *Metrics) SetBalance(balance uint64, balanceAccountName string, account _m.Called(balance, balanceAccountName, accountAddress, feedID, chainID, contractStatus, contractType, feedName, feedPath, networkID, networkName) } -// NewMetrics creates a new instance of Metrics. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. -func NewMetrics(t testing.TB) *Metrics { +// NewMetrics creates a new instance of Metrics. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMetrics(t interface { + mock.TestingT + Cleanup(func()) +}) *Metrics { mock := &Metrics{} mock.Mock.Test(t) diff --git a/pkg/solana/capabilities/targets/write_target.go b/pkg/solana/capabilities/targets/write_target.go new file mode 100644 index 000000000..c4d722deb --- /dev/null +++ b/pkg/solana/capabilities/targets/write_target.go @@ -0,0 +1,214 @@ +package targets + +import ( + "context" + "fmt" + + sdk "github.com/gagliardetto/solana-go" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/values" + "github.com/smartcontractkit/chainlink-solana/pkg/solana" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" +) + +// func InitializeWrite(registry commontypes.CapabilitiesRegistry, lggr logger.Logger) error { +// for _, chain := range legacyEVMChains.Slice() { +// capability := NewSolanaWrite(chain, lggr) +// if err := registry.Add(context.TODO(), capability); err != nil { +// return err +// } +// } +// return nil +// } + +var ( + _ capabilities.ActionCapability = &SolanaWrite{} +) + +type SolanaWrite struct { + chain solana.Chain + reader client.Reader + capabilities.CapabilityInfo + lggr logger.Logger +} + +func NewSolanaWrite(chain solana.Chain, lggr logger.Logger) (*SolanaWrite, error) { + // generate ID based on chain selector + name := fmt.Sprintf("write_solana_%v", chain.ID()) + + info := capabilities.MustNewCapabilityInfo( + name, + capabilities.CapabilityTypeTarget, + "Write target.", + "v1.0.0", + ) + + reader, err := chain.Reader() + if err != nil { + return nil, err + } + + return &SolanaWrite{ + chain, + reader, + info, + lggr, + }, nil +} + +type SolanaAccount struct { + PublicKey string `mapstructure:"public_key"` + IsWritable bool `mapstructure:"is_writable"` + IsSigner bool `mapstructure:"is_signer"` +} + +type SolanaConfig struct { + ChainID uint + ReceiverProgramID sdk.PublicKey + Params []any + Accounts []SolanaAccount + // ABI string TODO: +} + +// TODO: enforce required key presence + +func parseConfig(rawConfig *values.Map) (SolanaConfig, error) { + var config SolanaConfig + configAny, err := rawConfig.Unwrap() + if err != nil { + return config, err + } + err = mapstructure.Decode(configAny, &config) + return config, err +} + +// func evaluateParams(params []any, inputs map[string]any) ([]any, error) { +// vars := pipeline.NewVarsFrom(inputs) +// var args []any +// for _, param := range params { +// switch v := param.(type) { +// case string: +// val, err := pipeline.VarExpr(v, vars)() +// if err == nil { +// args = append(args, val) +// } else if errors.Is(errors.Cause(err), pipeline.ErrParameterEmpty) { +// args = append(args, param) +// } else { +// return args, err +// } +// default: +// args = append(args, param) +// } +// } + +// return args, nil +// } + +func (cap *SolanaWrite) Execute(ctx context.Context, callback chan<- capabilities.CapabilityResponse, request capabilities.CapabilityRequest) error { + cap.lggr.Debugw("Execute", "request", request) + + config := cap.chain.Config().ChainWriter() + + if config == nil { + return fmt.Errorf("ChainWriter config undefined") + } + + reqConfig, err := parseConfig(request.Config) + if err != nil { + return err + } + + inputsAny, err := request.Inputs.Unwrap() + if err != nil { + return err + } + inputs := inputsAny.(map[string]any) + + blockhash, err := cap.reader.LatestBlockhash() + + programID := config.ForwarderProgramID + state := config.ForwarderStateAccount + authority := config.FromAddress + + // Determine store authority + // TODO: compute this only once per capability init + seeds := [][]byte{[]byte("forwarder"), state.Bytes()} + forwarderAuthority, _, err := sdk.FindProgramAddress(seeds, programID) + if err != nil { + return errors.Wrap(err, "error on FindProgramAddress") + } + + data := []byte{} + + // No signature validation in the MVP demo + signatures := [][]byte{} // TODO: validate each sig is 64 bytes + + data = append(data, uint8(len(signatures))) // length prefix + for _, sig := range signatures { + data = append(data, sig...) + } + + // TODO: encode inputs into data + data = append(data, inputs["report"].([]byte)...) + + accounts := []*sdk.AccountMeta{ + // state + {PublicKey: state, IsWritable: false, IsSigner: false}, + // authority (node's transmitter key that's the tx signer) + {PublicKey: authority, IsWritable: false, IsSigner: true}, + // forwarder_authority + {PublicKey: forwarderAuthority, IsWritable: false, IsSigner: false}, + // receiver_program + {PublicKey: reqConfig.ReceiverProgramID, IsWritable: false, IsSigner: false}, + // ... rest passed from reqConfig + } + + for _, account := range reqConfig.Accounts { + publicKey, err := sdk.PublicKeyFromBase58(account.PublicKey) + if err != nil { + return err + } + accounts = append(accounts, &sdk.AccountMeta{ + PublicKey: publicKey, + IsWritable: account.IsWritable, + IsSigner: account.IsSigner, + }) + } + + tx, err := sdk.NewTransaction( + []sdk.Instruction{ + sdk.NewInstruction(config.ForwarderProgramID, accounts, data), + }, + blockhash.Value.Blockhash, + sdk.TransactionPayer(authority), + ) + if err != nil { + return errors.Wrap(err, "error on Transmit.NewTransaction") + } + + if err = cap.chain.TxManager().Enqueue(authority.String(), tx); err != nil { + return err + } + + go func() { + // TODO: cast tx.Error to Err (or Value to Value?) + callback <- capabilities.CapabilityResponse{ + Value: nil, + Err: nil, + } + close(callback) + }() + return nil +} + +func (cap *SolanaWrite) RegisterToWorkflow(ctx context.Context, request capabilities.RegisterToWorkflowRequest) error { + return nil +} + +func (cap *SolanaWrite) UnregisterFromWorkflow(ctx context.Context, request capabilities.UnregisterFromWorkflowRequest) error { + return nil +} diff --git a/pkg/solana/capabilities/targets/write_target_test.go b/pkg/solana/capabilities/targets/write_target_test.go new file mode 100644 index 000000000..ce946d825 --- /dev/null +++ b/pkg/solana/capabilities/targets/write_target_test.go @@ -0,0 +1,106 @@ +package targets_test + +import ( + "context" + "testing" + "time" + + sdk "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/values" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/capabilities/targets" + clientmocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/client/mocks" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" + mocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/mocks" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func ptr[T any](t T) *T { + return &t +} + +func TestSolanaWrite(t *testing.T) { + lggr := logger.Test(t) + cc := config.Chain{} + cc.SetDefaults() + cc.ChainWriter = &config.ChainWriter{ + FromAddress: sdk.MustPublicKeyFromBase58("SysvarS1otHashes111111111111111111111111111"), + ForwarderProgramID: sdk.MustPublicKeyFromBase58("SysvarC1ock11111111111111111111111111111111"), + ForwarderStateAccount: sdk.MustPublicKeyFromBase58("Vote111111111111111111111111111111111111111"), + } + tomlConfig := solana.TOMLConfig{ + ChainID: ptr("devnet"), + Enabled: ptr(true), + Chain: cc, + } + + chain := mocks.NewChain(t) + chain.On("ID").Return(*tomlConfig.ChainID) + chain.On("Config").Return(&tomlConfig) + txManager := mocks.NewTxManager(t) + chain.On("TxManager").Return(txManager) + client := clientmocks.NewReaderWriter(t) + client.On("LatestBlockhash").Return( + &rpc.GetLatestBlockhashResult{ + RPCContext: rpc.RPCContext{}, + Value: &rpc.LatestBlockhashResult{ + Blockhash: [32]byte{}, + LastValidBlockHeight: 0, + }, + }, + nil, + ) + chain.On("Reader").Return(client, nil) + + capability, err := targets.NewSolanaWrite(chain, lggr) + require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + config, err := values.NewMap(map[string]any{ + // "chain_id": , + // "receiver_program_id": sdk.MustPublicKeyFromBase58("11111111111111111111111111111111"), + // "abi": "receive(bytes report)", + "params": []any{"$(report)"}, + "accounts": []any{ + map[string]any{"public_key": "A9QnpgfhCkmiBSjgBuWk76Wo3HxzxvDopUq9x6UUMmjn", "is_writable": true, "is_signer": true}, + }, + }) + require.NoError(t, err) + + inputs, err := values.NewMap(map[string]any{ + "report": []byte{1, 2, 3}, + }) + require.NoError(t, err) + + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "hello", + }, + Config: config, + Inputs: inputs, + } + + txManager.On("Enqueue", mock.Anything, mock.Anything).Return(nil).Run(func(args mock.Arguments) { + // TODO: tx asserts + req := args.Get(1).(*sdk.Transaction) + msg := req.Message + ix := msg.Instructions[0] + require.Equal(t, len(ix.Accounts), 4+1) // 4 required by forwarder, 1 passed through from reqConfig + // TODO: assert on ix.Data + }) + + ch := make(chan capabilities.CapabilityResponse) + + err = capability.Execute(ctx, ch, req) + require.NoError(t, err) + + response := <-ch + require.Nil(t, response.Err) +} diff --git a/pkg/solana/chain.go b/pkg/solana/chain.go index f295bc086..6893a18d6 100644 --- a/pkg/solana/chain.go +++ b/pkg/solana/chain.go @@ -28,6 +28,7 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm" ) +//go:generate mockery --name Chain --output ./mocks/ --case=underscore --filename chain.go type Chain interface { types.ChainService diff --git a/pkg/solana/client/mocks/ReaderWriter.go b/pkg/solana/client/mocks/ReaderWriter.go index d53772769..24c0714fc 100644 --- a/pkg/solana/client/mocks/ReaderWriter.go +++ b/pkg/solana/client/mocks/ReaderWriter.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.20.0. DO NOT EDIT. +// Code generated by mockery v2.35.4. DO NOT EDIT. package mocks @@ -242,13 +242,12 @@ func (_m *ReaderWriter) SlotHeight() (uint64, error) { return r0, r1 } -type mockConstructorTestingTNewReaderWriter interface { +// NewReaderWriter creates a new instance of ReaderWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewReaderWriter(t interface { mock.TestingT Cleanup(func()) -} - -// NewReaderWriter creates a new instance of ReaderWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewReaderWriter(t mockConstructorTestingTNewReaderWriter) *ReaderWriter { +}) *ReaderWriter { mock := &ReaderWriter{} mock.Mock.Test(t) diff --git a/pkg/solana/config.go b/pkg/solana/config.go index 822064edd..588934d4a 100644 --- a/pkg/solana/config.go +++ b/pkg/solana/config.go @@ -178,6 +178,9 @@ func setFromChain(c, f *solcfg.Chain) { if f.MaxRetries != nil { c.MaxRetries = f.MaxRetries } + if f.ChainWriter != nil { + c.ChainWriter = f.ChainWriter + } } func (c *TOMLConfig) ValidateConfig() (err error) { @@ -267,6 +270,10 @@ func (c *TOMLConfig) FeeBumpPeriod() time.Duration { return c.Chain.FeeBumpPeriod.Duration() } +func (c *TOMLConfig) ChainWriter() *solcfg.ChainWriter { + return c.Chain.ChainWriter +} + func (c *TOMLConfig) ListNodes() ([]soldb.Node, error) { var allNodes []soldb.Node for _, n := range c.Nodes { diff --git a/pkg/solana/config/config.go b/pkg/solana/config/config.go index fd07bf57a..4253ff857 100644 --- a/pkg/solana/config/config.go +++ b/pkg/solana/config/config.go @@ -4,6 +4,7 @@ import ( "strings" "time" + "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" "go.uber.org/multierr" @@ -31,6 +32,7 @@ var defaultConfigSet = configSet{ ComputeUnitPriceMin: 0, ComputeUnitPriceDefault: 0, FeeBumpPeriod: 3 * time.Second, + ChainWriter: nil, } //go:generate mockery --name Config --output ./mocks/ --case=underscore --filename config.go @@ -52,6 +54,8 @@ type Config interface { ComputeUnitPriceMin() uint64 ComputeUnitPriceDefault() uint64 FeeBumpPeriod() time.Duration + + ChainWriter() *ChainWriter } // opt: remove @@ -72,6 +76,7 @@ type configSet struct { ComputeUnitPriceMin uint64 ComputeUnitPriceDefault uint64 FeeBumpPeriod time.Duration + ChainWriter *ChainWriter } var _ Config = (*cfg)(nil) @@ -240,6 +245,10 @@ func (c *cfg) FeeBumpPeriod() time.Duration { return c.defaults.FeeBumpPeriod } +func (c *cfg) ChainWriter() *ChainWriter { + return nil // TODO: should never get used +} + type Chain struct { BalancePollPeriod *config.Duration ConfirmPollPeriod *config.Duration @@ -256,6 +265,7 @@ type Chain struct { ComputeUnitPriceMin *uint64 ComputeUnitPriceDefault *uint64 FeeBumpPeriod *config.Duration + ChainWriter *ChainWriter } func (c *Chain) SetDefaults() { @@ -324,3 +334,9 @@ func (n *Node) ValidateConfig() (err error) { } return } + +type ChainWriter struct { + FromAddress solana.PublicKey + ForwarderProgramID solana.PublicKey + ForwarderStateAccount solana.PublicKey +} diff --git a/pkg/solana/config/mocks/config.go b/pkg/solana/config/mocks/config.go index f9f35c3a5..ec008e3ec 100644 --- a/pkg/solana/config/mocks/config.go +++ b/pkg/solana/config/mocks/config.go @@ -1,11 +1,13 @@ -// Code generated by mockery v2.20.0. DO NOT EDIT. +// Code generated by mockery v2.35.4. DO NOT EDIT. package mocks import ( - rpc "github.com/gagliardetto/solana-go/rpc" + config "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" mock "github.com/stretchr/testify/mock" + rpc "github.com/gagliardetto/solana-go/rpc" + time "time" ) @@ -28,6 +30,22 @@ func (_m *Config) BalancePollPeriod() time.Duration { return r0 } +// ChainWriter provides a mock function with given fields: +func (_m *Config) ChainWriter() *config.ChainWriter { + ret := _m.Called() + + var r0 *config.ChainWriter + if rf, ok := ret.Get(0).(func() *config.ChainWriter); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*config.ChainWriter) + } + } + + return r0 +} + // Commitment provides a mock function with given fields: func (_m *Config) Commitment() rpc.CommitmentType { ret := _m.Called() @@ -226,13 +244,12 @@ func (_m *Config) TxTimeout() time.Duration { return r0 } -type mockConstructorTestingTNewConfig interface { +// NewConfig creates a new instance of Config. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewConfig(t interface { mock.TestingT Cleanup(func()) -} - -// NewConfig creates a new instance of Config. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewConfig(t mockConstructorTestingTNewConfig) *Config { +}) *Config { mock := &Config{} mock.Mock.Test(t) diff --git a/pkg/solana/fees/mocks/Estimator.go b/pkg/solana/fees/mocks/Estimator.go index 8c8b04213..69c551827 100644 --- a/pkg/solana/fees/mocks/Estimator.go +++ b/pkg/solana/fees/mocks/Estimator.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.20.0. DO NOT EDIT. +// Code generated by mockery v2.35.4. DO NOT EDIT. package mocks @@ -55,13 +55,12 @@ func (_m *Estimator) Start(_a0 context.Context) error { return r0 } -type mockConstructorTestingTNewEstimator interface { +// NewEstimator creates a new instance of Estimator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewEstimator(t interface { mock.TestingT Cleanup(func()) -} - -// NewEstimator creates a new instance of Estimator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewEstimator(t mockConstructorTestingTNewEstimator) *Estimator { +}) *Estimator { mock := &Estimator{} mock.Mock.Test(t) diff --git a/pkg/solana/mocks/chain.go b/pkg/solana/mocks/chain.go new file mode 100644 index 000000000..5b39cef8a --- /dev/null +++ b/pkg/solana/mocks/chain.go @@ -0,0 +1,259 @@ +// Code generated by mockery v2.35.4. DO NOT EDIT. + +package mocks + +import ( + big "math/big" + + client "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" + config "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" + + context "context" + + mock "github.com/stretchr/testify/mock" + + solana "github.com/smartcontractkit/chainlink-solana/pkg/solana" + + types "github.com/smartcontractkit/chainlink-common/pkg/types" +) + +// Chain is an autogenerated mock type for the Chain type +type Chain struct { + mock.Mock +} + +// Close provides a mock function with given fields: +func (_m *Chain) Close() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Config provides a mock function with given fields: +func (_m *Chain) Config() config.Config { + ret := _m.Called() + + var r0 config.Config + if rf, ok := ret.Get(0).(func() config.Config); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(config.Config) + } + } + + return r0 +} + +// GetChainStatus provides a mock function with given fields: ctx +func (_m *Chain) GetChainStatus(ctx context.Context) (types.ChainStatus, error) { + ret := _m.Called(ctx) + + var r0 types.ChainStatus + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (types.ChainStatus, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) types.ChainStatus); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(types.ChainStatus) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// HealthReport provides a mock function with given fields: +func (_m *Chain) HealthReport() map[string]error { + ret := _m.Called() + + var r0 map[string]error + if rf, ok := ret.Get(0).(func() map[string]error); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]error) + } + } + + return r0 +} + +// ID provides a mock function with given fields: +func (_m *Chain) ID() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// ListNodeStatuses provides a mock function with given fields: ctx, pageSize, pageToken +func (_m *Chain) ListNodeStatuses(ctx context.Context, pageSize int32, pageToken string) ([]types.NodeStatus, string, int, error) { + ret := _m.Called(ctx, pageSize, pageToken) + + var r0 []types.NodeStatus + var r1 string + var r2 int + var r3 error + if rf, ok := ret.Get(0).(func(context.Context, int32, string) ([]types.NodeStatus, string, int, error)); ok { + return rf(ctx, pageSize, pageToken) + } + if rf, ok := ret.Get(0).(func(context.Context, int32, string) []types.NodeStatus); ok { + r0 = rf(ctx, pageSize, pageToken) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]types.NodeStatus) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int32, string) string); ok { + r1 = rf(ctx, pageSize, pageToken) + } else { + r1 = ret.Get(1).(string) + } + + if rf, ok := ret.Get(2).(func(context.Context, int32, string) int); ok { + r2 = rf(ctx, pageSize, pageToken) + } else { + r2 = ret.Get(2).(int) + } + + if rf, ok := ret.Get(3).(func(context.Context, int32, string) error); ok { + r3 = rf(ctx, pageSize, pageToken) + } else { + r3 = ret.Error(3) + } + + return r0, r1, r2, r3 +} + +// Name provides a mock function with given fields: +func (_m *Chain) Name() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Reader provides a mock function with given fields: +func (_m *Chain) Reader() (client.Reader, error) { + ret := _m.Called() + + var r0 client.Reader + var r1 error + if rf, ok := ret.Get(0).(func() (client.Reader, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() client.Reader); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(client.Reader) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Ready provides a mock function with given fields: +func (_m *Chain) Ready() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Start provides a mock function with given fields: _a0 +func (_m *Chain) Start(_a0 context.Context) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Transact provides a mock function with given fields: ctx, from, to, amount, balanceCheck +func (_m *Chain) Transact(ctx context.Context, from string, to string, amount *big.Int, balanceCheck bool) error { + ret := _m.Called(ctx, from, to, amount, balanceCheck) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, *big.Int, bool) error); ok { + r0 = rf(ctx, from, to, amount, balanceCheck) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TxManager provides a mock function with given fields: +func (_m *Chain) TxManager() solana.TxManager { + ret := _m.Called() + + var r0 solana.TxManager + if rf, ok := ret.Get(0).(func() solana.TxManager); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(solana.TxManager) + } + } + + return r0 +} + +// NewChain creates a new instance of Chain. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewChain(t interface { + mock.TestingT + Cleanup(func()) +}) *Chain { + mock := &Chain{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/solana/mocks/txm.go b/pkg/solana/mocks/txm.go new file mode 100644 index 000000000..11e73cc08 --- /dev/null +++ b/pkg/solana/mocks/txm.go @@ -0,0 +1,42 @@ +// Code generated by mockery v2.35.4. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + + solana "github.com/gagliardetto/solana-go" +) + +// TxManager is an autogenerated mock type for the TxManager type +type TxManager struct { + mock.Mock +} + +// Enqueue provides a mock function with given fields: accountID, msg +func (_m *TxManager) Enqueue(accountID string, msg *solana.Transaction) error { + ret := _m.Called(accountID, msg) + + var r0 error + if rf, ok := ret.Get(0).(func(string, *solana.Transaction) error); ok { + r0 = rf(accountID, msg) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewTxManager creates a new instance of TxManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewTxManager(t interface { + mock.TestingT + Cleanup(func()) +}) *TxManager { + mock := &TxManager{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/solana/relay.go b/pkg/solana/relay.go index 054663d62..04526e18e 100644 --- a/pkg/solana/relay.go +++ b/pkg/solana/relay.go @@ -21,6 +21,7 @@ import ( var _ TxManager = (*txm.Txm)(nil) +//go:generate mockery --name TxManager --output ./mocks/ --case=underscore --filename txm.go type TxManager interface { Enqueue(accountID string, msg *solana.Transaction) error } diff --git a/pkg/solana/txm/mocks/simple_keystore.go b/pkg/solana/txm/mocks/simple_keystore.go index b62403dad..528eb54e8 100644 --- a/pkg/solana/txm/mocks/simple_keystore.go +++ b/pkg/solana/txm/mocks/simple_keystore.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.20.0. DO NOT EDIT. +// Code generated by mockery v2.35.4. DO NOT EDIT. package mocks @@ -39,13 +39,12 @@ func (_m *SimpleKeystore) Sign(ctx context.Context, account string, data []byte) return r0, r1 } -type mockConstructorTestingTNewSimpleKeystore interface { +// NewSimpleKeystore creates a new instance of SimpleKeystore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSimpleKeystore(t interface { mock.TestingT Cleanup(func()) -} - -// NewSimpleKeystore creates a new instance of SimpleKeystore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewSimpleKeystore(t mockConstructorTestingTNewSimpleKeystore) *SimpleKeystore { +}) *SimpleKeystore { mock := &SimpleKeystore{} mock.Mock.Test(t) diff --git a/shell.nix b/shell.nix index 22261fb9d..592cdbddd 100644 --- a/shell.nix +++ b/shell.nix @@ -2,7 +2,7 @@ pkgs.mkShell { nativeBuildInputs = with pkgs; [ - (rust-bin.stable.latest.default.override { extensions = ["rust-src"]; }) + (rust-bin.stable.latest.default.override { extensions = ["rust-src" "rust-analyzer"]; }) # lld_11 llvm_11 stdenv.cc.cc.lib