Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
bcaea8d
state changes e2e tests
bogdan-rosianu Sep 9, 2025
d09845b
always run action
bogdan-rosianu Sep 9, 2025
3d42451
update script path
bogdan-rosianu Sep 9, 2025
285387e
dummy commit
bogdan-rosianu Sep 9, 2025
695b0f7
change script file perm
bogdan-rosianu Sep 10, 2025
41925dc
dummy run step
bogdan-rosianu Sep 10, 2025
0ab9b15
update awk
bogdan-rosianu Sep 10, 2025
f1f06a2
fix
bogdan-rosianu Sep 10, 2025
8ba0c79
logs
bogdan-rosianu Sep 10, 2025
0ca120f
start redis
bogdan-rosianu Sep 10, 2025
72ae638
start redis in workflow
bogdan-rosianu Sep 10, 2025
01c4f5d
check rabbit exchange
bogdan-rosianu Sep 10, 2025
6a3f044
start api and check balances
bogdan-rosianu Sep 10, 2025
e6b8ffd
start docker containers
bogdan-rosianu Sep 10, 2025
f530ea2
remove wait for rabbit mngmnt
bogdan-rosianu Sep 10, 2025
f1c4f32
move deps from compose to action
bogdan-rosianu Sep 10, 2025
9d4dba9
move deps from compose to action
bogdan-rosianu Sep 10, 2025
d09fb48
start redis separately
bogdan-rosianu Sep 10, 2025
2affd0b
yml fix
bogdan-rosianu Sep 10, 2025
09e41bb
back to compose
bogdan-rosianu Sep 10, 2025
bbe1490
versions fix
bogdan-rosianu Sep 10, 2025
4b7e645
disable websocket
bogdan-rosianu Sep 10, 2025
5682121
data preparation logs
bogdan-rosianu Sep 10, 2025
a51adbd
temp test
bogdan-rosianu Sep 10, 2025
98f0723
fixes
bogdan-rosianu Sep 10, 2025
f725887
fixes
bogdan-rosianu Sep 10, 2025
1523a6c
move to scripts
bogdan-rosianu Sep 16, 2025
82a3eae
state changes e2e tests
bogdan-rosianu Sep 9, 2025
b8f4a34
always run action
bogdan-rosianu Sep 9, 2025
ef55f24
update script path
bogdan-rosianu Sep 9, 2025
d68216a
dummy commit
bogdan-rosianu Sep 9, 2025
6ef3925
change script file perm
bogdan-rosianu Sep 10, 2025
44d658f
dummy run step
bogdan-rosianu Sep 10, 2025
1741bc1
update awk
bogdan-rosianu Sep 10, 2025
7de3788
fix
bogdan-rosianu Sep 10, 2025
be628f9
logs
bogdan-rosianu Sep 10, 2025
f50117f
start redis
bogdan-rosianu Sep 10, 2025
a2e6038
start redis in workflow
bogdan-rosianu Sep 10, 2025
94c47d7
check rabbit exchange
bogdan-rosianu Sep 10, 2025
2411b3e
start api and check balances
bogdan-rosianu Sep 10, 2025
cbde94c
start docker containers
bogdan-rosianu Sep 10, 2025
8adbf6e
remove wait for rabbit mngmnt
bogdan-rosianu Sep 10, 2025
2e35333
move deps from compose to action
bogdan-rosianu Sep 10, 2025
0a41470
move deps from compose to action
bogdan-rosianu Sep 10, 2025
8acf8f3
start redis separately
bogdan-rosianu Sep 10, 2025
34a91b4
yml fix
bogdan-rosianu Sep 10, 2025
81367c6
back to compose
bogdan-rosianu Sep 10, 2025
3b3bbf4
versions fix
bogdan-rosianu Sep 10, 2025
659cfa5
disable websocket
bogdan-rosianu Sep 10, 2025
796c675
data preparation logs
bogdan-rosianu Sep 10, 2025
7ca5ef2
temp test
bogdan-rosianu Sep 10, 2025
8c52c53
fixes
bogdan-rosianu Sep 10, 2025
713687f
fixes
bogdan-rosianu Sep 10, 2025
1209c39
move to scripts
bogdan-rosianu Sep 16, 2025
0676376
Merge remote-tracking branch 'origin/e2e-tests-state-changes' into e2…
stefangutica Sep 19, 2025
cdb3426
Merge branch 'state-changes' into e2e-tests-state-changes
bogdan-rosianu Nov 4, 2025
53c1607
fixes
bogdan-rosianu Nov 4, 2025
fa36acf
more tests
bogdan-rosianu Nov 4, 2025
e5e7b30
more tests
bogdan-rosianu Nov 4, 2025
dd64446
fix build
bogdan-rosianu Nov 4, 2025
127d06a
fixes
bogdan-rosianu Nov 4, 2025
10864f0
fixes
bogdan-rosianu Nov 4, 2025
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
94 changes: 94 additions & 0 deletions .github/workflows/e2e-state-accesses.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: E2E State Accesses

on:
pull_request:

jobs:
e2e:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Use Node.js 18.x
uses: actions/setup-node@v4
with:
node-version: '18.x'
cache: 'npm'

- name: Use Go 1.21
uses: actions/setup-go@v5
with:
go-version: '1.21'

- name: Install dependencies and init
run: |
set -euxo pipefail
npm ci
npm i --no-save mongoose@^8
npm run init

- name: Build and start chain simulator (state-changes stack)
run: npm run start:state-changes-cs

- name: Wait for services to be ready
run: |
echo "Waiting for services to be healthy..."
docker ps
sleep 20

- name: Print docker containers
run: docker ps

- name: Configure RabbitMQ exchange and queue
run: npm run rabbit:setup-state-changes

- name: Trigger block generation
run: npm run cs:generate-blocks

- name: Verify messages on queue
run: |
set -euxo pipefail
# Poll the queue until messages are received
for i in {1..60}; do
body=$(curl -s -u guest:guest -H "content-type: application/json" \
-X POST http://localhost:15672/api/queues/%2f/state_accesses_test/get \
-d '{"count":10,"ackmode":"ack_requeue_true","encoding":"auto","truncate":50000}')
# Non-empty array indicates at least one message
if [ "$body" != "[]" ] && [ -n "$body" ]; then
echo "Received messages on 'state_accesses_test' queue"
echo "$body" | head -c 2000
exit 0
fi
sleep 2
done
echo "No messages received on queue 'state_accesses_test'" >&2
exit 1

- name: Start API
run: |
npm run start:mainnet:e2e > api.out 2>&1 &
timeout=180; start=$(date +%s)
until [ "$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3001/about)" = "200" ]; do
now=$(date +%s); [ $((now-start)) -gt $timeout ] && { echo "API not up"; tail -n 200 api.out || true; exit 1; } || sleep 2;
done

- name: Prepare Test Data
run: npm run prepare:test-data

- name: Run state changes balances e2e
run: npm run test:state-changes-e2e

- name: Stop API after tests
if: always()
run: |
echo "Stopping the API..."
if [ -f api.pid ]; then
kill "$(cat api.pid)" || true
else
kill $(lsof -t -i:3001) || true
fi

- name: Stop state-changes stack
if: always()
run: npm run stop:state-changes-cs
4 changes: 2 additions & 2 deletions config/config.e2e.mainnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ api:
publicPort: 3001
private: true
privatePort: 4001
websocket: true
websocket: false
cron:
cacheWarmer: true
fastWarm: false
Expand All @@ -24,7 +24,7 @@ features:
enabled: false
port: 6002
stateChanges:
enabled: false
enabled: true
port: 5675
url: 'amqp://guest:[email protected]:5672'
exchange: 'state_accesses'
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,12 @@
"copy-e2e-mocked-mainnet-config:windows": "copy .\\config\\config.e2e-mocked.mainnet.yaml .\\config\\config.yaml",
"start-chain-simulator": "docker compose -f \"src/test/chain-simulator/docker/docker-compose.yml\" up -d --build",
"stop-chain-simulator": "docker compose -f \"src/test/chain-simulator/docker/docker-compose.yml\" down",
"start:state-changes-cs": "docker compose -f \"src/test/chain-simulator/docker/docker-compose.state-e2e.yml\" up -d && bash src/test/scripts/setup-simulator-and-notifier.sh",
"stop:state-changes-cs": "bash src/test/scripts/stop-custom-cs.sh && docker compose -f \"src/test/chain-simulator/docker/docker-compose.state-e2e.yml\" down -v",
"rabbit:setup-state-changes": "bash src/test/scripts/setup-rabbit-state.sh",
"cs:generate-blocks": "bash src/test/scripts/generate-blocks.sh",
"prepare:test-data": "ts-node src/test/chain-simulator/utils/prepare-test-data.ts",
"test:state-changes-e2e": "jest --config ./src/test/jest-state-changes-e2e.json --runInBand --detectOpenHandles --forceExit",
"test:ppu": "ts-node src/test/chain-simulator/utils/test-ppu-calculation.ts"
},
"dependencies": {
Expand Down
63 changes: 63 additions & 0 deletions src/test/chain-simulator/docker/docker-compose.state-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
version: '3.8'

services:
mongodb:
image: mongo:6
container_name: statee2e-mongodb
ports:
- "27017:27017"
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.runCommand({ ping: 1 })"]
interval: 10s
timeout: 5s
retries: 10
rabbitmq:
image: rabbitmq:3-management
container_name: statee2e-rabbitmq
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
ports:
- "5672:5672"
- "15672:15672"
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 10s
timeout: 5s
retries: 15

redis-master:
image: "bitnami/redis"
container_name: statee2e-redis-master
environment:
- REDIS_REPLICATION_MODE=master
- ALLOW_EMPTY_PASSWORD=yes
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 10

redis-sentinel:
image: "bitnami/redis-sentinel"
container_name: statee2e-redis-sentinel
depends_on:
- redis-master
environment:
- REDIS_MASTER_SET=mymaster
- REDIS_MASTER_HOST=redis-master
- ALLOW_EMPTY_PASSWORD=yes
- REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=10000
ports:
- "26379:26379"
healthcheck:
test: ["CMD-SHELL", "redis-cli -p 26379 ping | grep PONG"]
interval: 10s
timeout: 5s
retries: 10

networks:
default:
name: statee2e-net
8 changes: 8 additions & 0 deletions src/test/chain-simulator/docker/sentinel.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
port 26379
daemonize no
bind 0.0.0.0
sentinel monitor mymaster redis 6379 1
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1

Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import fetch from 'node-fetch';
import { config } from '../config/env.config';

const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms));

async function getJson(url: string): Promise<any | undefined> {
for (let i = 0; i < 30; i++) {
try {
const resp = await fetch(url);
if (resp.ok) {
return await resp.json();
}
} catch (_) {
// ignore and retry
}
await sleep(1000);
}
return undefined;
}

function pickBalance(payload: any): string | undefined {
if (!payload || typeof payload !== 'object') return undefined;
// Primary shape used by CI shell script: top-level balance
if (typeof payload.balance === 'string') return payload.balance;
if (typeof payload.balance === 'number') return String(payload.balance);
// Fallbacks in case the shape is wrapped
if (payload.data) {
if (typeof payload.data.balance === 'string') return payload.data.balance;
if (typeof payload.data.balance === 'number') return String(payload.data.balance);
if (payload.data.account && payload.data.account.balance) {
const b = payload.data.account.balance;
if (typeof b === 'string') return b;
if (typeof b === 'number') return String(b);
}
}
return undefined;
}

async function fetchBalance(baseUrl: string, address: string): Promise<string> {
const url = `${baseUrl}/accounts/${address}`;
const payload = await getJson(url);
if (!payload) throw new Error(`No payload from ${url}`);
const bal = pickBalance(payload);
if (!bal) throw new Error(`No balance field in response from ${url}`);
return bal;
}

async function fetchBalanceV2(baseUrl: string, address: string): Promise<string> {
const url = `${baseUrl}/v2/accounts/${address}`;
const payload = await getJson(url);
if (!payload) throw new Error(`No payload from ${url}`);
const bal = pickBalance(payload);
if (!bal) throw new Error(`No balance field in v2 response from ${url}`);
return bal;
}

describe('State changes: balances parity (v1 vs v2)', () => {
const base = config.apiServiceUrl;
const alice = config.aliceAddress;
const bob = config.bobAddress;

it('Alice balance matches between v1 and v2', async () => {
let v1 = await fetchBalance(base, alice);
let v2 = await fetchBalanceV2(base, alice);
for (let i = 0; i < 20 && v1 !== v2; i++) {
await sleep(1000);
v1 = await fetchBalance(base, alice);
v2 = await fetchBalanceV2(base, alice);
}
expect(v1).toBe(v2);
});

it('Bob balance matches between v1 and v2', async () => {
let v1 = await fetchBalance(base, bob);
let v2 = await fetchBalanceV2(base, bob);
for (let i = 0; i < 20 && v1 !== v2; i++) {
await sleep(1000);
v1 = await fetchBalance(base, bob);
v2 = await fetchBalanceV2(base, bob);
}
expect(v1).toBe(v2);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import axios from 'axios';
import { config } from '../config/env.config';
import { ChainSimulatorUtils } from '../utils/test.utils';

const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms));

async function fetchAccount(baseUrl: string, address: string): Promise<any> {
for (let i = 0; i < 45; i++) {
const resp = await axios.get(`${baseUrl}/accounts/${address}`).catch(() => undefined);
const acc = resp?.data;
if (acc) return acc;
await sleep(1000);
}
throw new Error(`Could not fetch account ${address}`);
}

async function fetchNonce(baseUrl: string, address: string): Promise<number> {
for (let i = 0; i < 45; i++) {
const resp = await axios.get(`${baseUrl}/proxy/address/${address}/nonce`).catch(() => undefined);
const n = resp?.data?.data?.nonce;
if (typeof n === 'number') return n;
await sleep(1000);
}
throw new Error(`Could not fetch nonce for ${address}`);
}

async function fetchMetaNonce(baseUrl: string): Promise<number> {
for (let i = 0; i < 45; i++) {
const resp = await axios.get(`${baseUrl}/network/status/4294967295`).catch(() => undefined);
const n = resp?.data?.data?.status?.erd_nonce;
if (typeof n === 'number') return n;
await sleep(1000);
}
throw new Error('Could not fetch meta-chain nonce');
}

describe('State changes: smart contract deploy visibility', () => {
const api = config.apiServiceUrl;
const deployer = config.aliceAddress;

it('Deploys ping-pong contract and exposes codeHash/rootHash; meta nonce increases', async () => {
const startMeta = await fetchMetaNonce(api);
const startNonce = await fetchNonce(api, deployer);

const scAddress = await ChainSimulatorUtils.deployPingPongSc(deployer);

// Wait until /accounts reflects deployment
let account: any = null;
for (let i = 0; i < 45; i++) {
account = await fetchAccount(api, scAddress).catch(() => undefined);
const codeHash = account?.codeHash ?? account?.data?.codeHash;
const rootHash = account?.rootHash ?? account?.data?.rootHash;
if (codeHash && codeHash !== '' && rootHash && rootHash !== '') break;
await sleep(1000);
}

const codeHash = account?.codeHash ?? account?.data?.codeHash;
const rootHash = account?.rootHash ?? account?.data?.rootHash;
expect(typeof codeHash).toBe('string');
expect(codeHash.length).toBeGreaterThan(0);
expect(typeof rootHash).toBe('string');
expect(rootHash.length).toBeGreaterThan(0);

// Nonce of deployer should increase
let endNonce = startNonce;
for (let i = 0; i < 30; i++) {
endNonce = await fetchNonce(api, deployer);
if (endNonce >= startNonce + 1) break;
await sleep(1000);
}
expect(endNonce).toBeGreaterThanOrEqual(startNonce + 1);

// Meta-chain nonce should advance as well
let endMeta = startMeta;
for (let i = 0; i < 30; i++) {
endMeta = await fetchMetaNonce(api);
if (endMeta > startMeta) break;
await sleep(1000);
}
expect(endMeta).toBeGreaterThan(startMeta);
});
});
Loading
Loading