Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 29 additions & 16 deletions .github/workflows/fork-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,11 @@ permissions:
contents: read

jobs:
fork-test:
name: Fork Tests
fork-test-baseline:
name: Fork Tests Baseline
runs-on: ubuntu-latest
timeout-minutes: 30
# On push: only run when commit message contains [fork-test]
# On workflow_dispatch: always run
if: >-
github.event_name == 'workflow_dispatch' ||
contains(github.event.head_commit.message, '[fork-test]')
# Advisory only — Citrea public RPC instability shouldn't block PRs.
continue-on-error: true
continue-on-error: false

steps:
- uses: actions/checkout@v4
Expand All @@ -37,16 +31,35 @@ jobs:
- run: yarn install --frozen-lockfile
- uses: foundry-rs/foundry-toolchain@v1

- name: Fork tests (dev)
- name: Fork tests (prod)
env:
FOUNDRY_PROFILE: fork
FORK_ENV: dev
DEV_RPC_URL: ${{ secrets.DEV_RPC_URL }}
run: forge test -vvv
FORK_ENV: prod
PROD_RPC_URL: ${{ secrets.PROD_RPC_URL || 'https://rpc.mainnet.citrea.xyz' }}
run: forge test --match-path 'test/foundry/fork/**/*.sol' --no-match-path 'test/foundry/fork/live/**' -vvv

- name: Fork tests (prod)
fork-test-advisory-live:
name: Fork Tests Advisory Live
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 30
continue-on-error: true

steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/setup-node@v4
with:
node-version: 20
cache: yarn
cache-dependency-path: yarn.lock
- run: yarn install --frozen-lockfile
- uses: foundry-rs/foundry-toolchain@v1

- name: Advisory live fork tests (prod)
env:
FOUNDRY_PROFILE: fork
FORK_ENV: prod
PROD_RPC_URL: ${{ secrets.PROD_RPC_URL }}
run: forge test -vvv
PROD_RPC_URL: ${{ secrets.PROD_RPC_URL || 'https://rpc.mainnet.citrea.xyz' }}
run: forge test --match-path 'test/foundry/fork/live/**' -vvv
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
"test": "forge test",
"snapshot": "forge snapshot --no-match-test 'testFuzz|invariant_'",
"snapshot:check": "forge snapshot --no-match-test 'testFuzz|invariant_' --check --tolerance 15",
"test:fork:dev": "FOUNDRY_PROFILE=fork FORK_ENV=dev forge test -vvv",
"test:fork:prod": "FOUNDRY_PROFILE=fork FORK_ENV=prod forge test -vvv",
"test:fork:baseline": "FOUNDRY_PROFILE=fork FORK_ENV=prod forge test --match-path 'test/foundry/fork/**/*.sol' --no-match-path 'test/foundry/fork/live/**' -vvv",
"test:fork:advisory:prod": "FOUNDRY_PROFILE=fork FORK_ENV=prod forge test --match-path 'test/foundry/fork/live/**' -vvv",
"test:fork:dev": "FOUNDRY_PROFILE=fork FORK_ENV=dev forge test --match-path 'test/foundry/fork/**/*.sol' --no-match-path 'test/foundry/fork/live/**' -vvv",
"test:fork:prod": "yarn test:fork:baseline",
"test:cov": "FOUNDRY_PROFILE=coverage forge coverage --report lcov",
"lint": "tsc --noEmit --noUnusedLocals --noUnusedParameters",
"format": "prettier --write \"contracts/**/*.sol\" \"scripts/**/*.ts\"",
Expand Down
143 changes: 143 additions & 0 deletions scripts/fork/build-redstone-submit-calldata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { Contract, providers, utils } from 'ethers';
import { WrapperBuilder } from '@redstone-finance/evm-connector';
import {
getSignersForDataServiceId,
type DataServiceIds,
} from '@redstone-finance/sdk';
import { loadEnvironment, normalizeEnvironment } from '../utils/environment';
import type { Environment, EnvironmentFile } from '../types/environment';

const BUILD_PREFIX = '[build-redstone-submit-calldata]';
const REDSTONE_DATA_SERVICE_ID: DataServiceIds = 'redstone-primary-prod';
const REDSTONE_UNIQUE_SIGNERS = 3;
const CORE_ABI = [
'function submitSettlementSample(uint256)',
'function redstoneFeedId() view returns (bytes32)',
'function maxSampleDistance() view returns (uint64)',
] as const;

type WrappedSubmitContract = Contract & {
populateTransaction: {
submitSettlementSample(marketId: string): Promise<{ data?: string }>;
};
};

function log(message: string) {
console.error(`${BUILD_PREFIX} ${message}`);
}

function fail(message: string): never {
throw new Error(message);
}

function resolveRpcUrl(env: Environment): string {
const envVar =
env === 'dev'
? 'DEV_RPC_URL'
: env === 'prod'
? 'PROD_RPC_URL'
: 'LOCAL_RPC_URL';
const value = process.env[envVar];
if (!value) {
fail(`Missing ${envVar}`);
}
return value;
}

function resolveCoreAddress(
passedCoreAddress: string | undefined,
envData: EnvironmentFile,
): string {
if (passedCoreAddress) {
return passedCoreAddress;
}
const fromEnvFile = envData.contracts.SignalsCoreProxy;
if (!fromEnvFile) {
fail('Missing SignalsCoreProxy in environment file');
}
return fromEnvFile;
}

function resolveFeedIdFallback(envData: EnvironmentFile): string {
const configuredFeedId = envData.config?.redstoneFeedId;
if (!configuredFeedId) {
fail('Missing redstoneFeedId in environment file');
}
return configuredFeedId;
}

async function resolveFeedId(
contract: Contract,
envData: EnvironmentFile,
): Promise<string> {
try {
const onchainFeedId = (await contract.redstoneFeedId()) as string;
return utils.parseBytes32String(onchainFeedId);
} catch {
return resolveFeedIdFallback(envData);
}
}

async function resolveMaxTimestampDeviationMs(
contract: Contract,
envData: EnvironmentFile,
): Promise<number | undefined> {
try {
const onchainMaxDistance = (await contract.maxSampleDistance()) as {
toNumber(): number;
};
return onchainMaxDistance.toNumber() * 1000;
} catch {
const configured = envData.config?.redstoneMaxSampleDistance;
if (!configured) return undefined;
const parsed = Number(configured);
return Number.isFinite(parsed) ? parsed * 1000 : undefined;
}
}

async function main() {
const [envArg, coreAddressArg, marketIdArg] = process.argv.slice(2);
if (!envArg || !marketIdArg) {
fail(
'Usage: tsx scripts/fork/build-redstone-submit-calldata.ts <env> <core-address> <market-id>',
);
}
if (!/^\d+$/.test(marketIdArg)) {
fail(`Invalid market ID: ${marketIdArg}`);
}

const env = normalizeEnvironment(envArg) as Environment;
const envData = loadEnvironment(env);
const coreAddress = resolveCoreAddress(coreAddressArg, envData);
const provider = new providers.JsonRpcProvider(resolveRpcUrl(env));
const core = new Contract(coreAddress, CORE_ABI, provider);

const feedId = await resolveFeedId(core, envData);
const maxTimestampDeviationMS = await resolveMaxTimestampDeviationMs(
core,
envData,
);

const wrapped = WrapperBuilder.wrap(core).usingDataService({
dataServiceId: REDSTONE_DATA_SERVICE_ID,
dataPackagesIds: [feedId],
uniqueSignersCount: REDSTONE_UNIQUE_SIGNERS,
authorizedSigners: getSignersForDataServiceId(REDSTONE_DATA_SERVICE_ID),
...(maxTimestampDeviationMS ? { maxTimestampDeviationMS } : {}),
}) as WrappedSubmitContract;

const tx =
await wrapped.populateTransaction.submitSettlementSample(marketIdArg);
if (!tx.data || !tx.data.startsWith('0x')) {
fail('Failed to build calldata');
}

process.stdout.write(`${tx.data}\n`);
}

main().catch((error: unknown) => {
const message =
error instanceof Error ? error.message : 'Unknown error building calldata';
log(message);
process.exit(1);
});
Loading
Loading