Skip to content
Closed
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
84 changes: 39 additions & 45 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,74 +155,68 @@ jobs:
echo "${ZOMBIENET_BIN_DIR}" >> "$GITHUB_PATH"
echo "ZOMBIENET_BINARY=zombienet-linux-x64" >> "$GITHUB_ENV"

# Westend parachain
- name: Run authorize and store (PAPI, RPC node, Westend parachain)
# Westend parachain tests
- name: Start services (Westend parachain)
working-directory: examples
run: |
export TEST_DIR="$(mktemp -d $GITHUB_WORKSPACE/bulletin-tests-run-XXXXX)/test"
echo "TEST_DIR=$TEST_DIR" >> "$GITHUB_ENV"
mkdir -p "$TEST_DIR"
just run-authorize-and-store "bulletin-westend-runtime" "ws"
- name: Run authorize and store (PAPI, smoldot, Westend parachain)
just start-services "bulletin-westend-runtime"
- name: Test authorize and store (PAPI, RPC node, Westend parachain)
working-directory: examples
run: |
export TEST_DIR="$(mktemp -d $GITHUB_WORKSPACE/bulletin-tests-run-XXXXX)/test"
echo "TEST_DIR=$TEST_DIR" >> "$GITHUB_ENV"
mkdir -p "$TEST_DIR"
just run-authorize-and-store "bulletin-westend-runtime" "smoldot"
- name: Run store chunked data + DAG-PB (PJS-API, RPC node, Westend parachain)
run: just test-authorize-and-store "bulletin-westend-runtime" "ws"
- name: Test authorize and store (PAPI, smoldot, Westend parachain)
working-directory: examples
run: |
export TEST_DIR="$(mktemp -d $GITHUB_WORKSPACE/bulletin-tests-run-XXXXX)/test"
echo "TEST_DIR=$TEST_DIR" >> "$GITHUB_ENV"
mkdir -p "$TEST_DIR"
just run-store-chunked-data "bulletin-westend-runtime"
run: just test-authorize-and-store "bulletin-westend-runtime" "smoldot"
- name: Test store chunked data + DAG-PB (PJS-API, RPC node, Westend parachain)
working-directory: examples
run: just test-store-chunked-data "bulletin-westend-runtime"
- name: Run store big data (PJS-API, RPC node, Westend parachain)
working-directory: examples
run: |
export TEST_DIR="$(mktemp -d $GITHUB_WORKSPACE/bulletin-tests-run-XXXXX)/test"
echo "TEST_DIR=$TEST_DIR" >> $GITHUB_ENV
mkdir -p "$TEST_DIR"
just run-store-big-data "bulletin-westend-runtime"
run: just run-store-big-data "bulletin-westend-runtime"
- name: Stop services (Westend parachain)
if: always()
working-directory: examples
run: just stop-services
- name: Upload Westend logs (on failure)
if: failure()
uses: actions/upload-artifact@v4
with:
name: westend-failed-logs
path: |
${{ env.TEST_DIR }}/*.log

# TODO: Polkadot parachain

# Polkadot solochain
- name: Run authorize and store (PAPI, RPC node, Polkadot solochain)
# Polkadot solochain tests
- name: Start services (Polkadot solochain)
working-directory: examples
run: |
export TEST_DIR="$(mktemp -d $GITHUB_WORKSPACE/bulletin-tests-run-XXXXX)/test"
echo "TEST_DIR=$TEST_DIR" >> "$GITHUB_ENV"
mkdir -p "$TEST_DIR"
just run-authorize-and-store "bulletin-polkadot-runtime" "ws"
- name: Run authorize and store (PAPI, smoldot, Polkadot solochain)
just start-services "bulletin-polkadot-runtime"
- name: Test authorize and store (PAPI, RPC node, Polkadot solochain)
working-directory: examples
run: |
export TEST_DIR="$(mktemp -d $GITHUB_WORKSPACE/bulletin-tests-run-XXXXX)/test"
echo "TEST_DIR=$TEST_DIR" >> "$GITHUB_ENV"
mkdir -p "$TEST_DIR"
just run-authorize-and-store "bulletin-polkadot-runtime" "smoldot"
- name: Run store chunked data + DAG-PB (PJS-API, RPC node, Polkadot solochain)
run: just test-authorize-and-store "bulletin-polkadot-runtime" "ws"
- name: Test authorize and store (PAPI, smoldot, Polkadot solochain)
working-directory: examples
run: |
export TEST_DIR="$(mktemp -d $GITHUB_WORKSPACE/bulletin-tests-run-XXXXX)/test"
echo "TEST_DIR=$TEST_DIR" >> "$GITHUB_ENV"
mkdir -p "$TEST_DIR"
just run-store-chunked-data "bulletin-polkadot-runtime"

run: just test-authorize-and-store "bulletin-polkadot-runtime" "smoldot"
- name: Test store chunked data + DAG-PB (PJS-API, RPC node, Polkadot solochain)
working-directory: examples
run: just test-store-chunked-data "bulletin-polkadot-runtime"
- name: Run store big data (PJS-API, RPC node, Polkadot solochain)
working-directory: examples
run: |
export TEST_DIR="$(mktemp -d $GITHUB_WORKSPACE/bulletin-tests-run-XXXXX)/test"
echo "TEST_DIR=$TEST_DIR" >> $GITHUB_ENV
mkdir -p "$TEST_DIR"
just run-store-big-data "bulletin-polkadot-runtime"

# Collects logs from the last failed zombienet run.
- name: Upload Zombienet logs (on failure)
run: just run-store-big-data "bulletin-polkadot-runtime"
- name: Stop services (Polkadot solochain)
if: always()
working-directory: examples
run: just stop-services
- name: Upload Polkadot logs (on failure)
if: failure()
uses: actions/upload-artifact@v4
with:
name: failed-zombienet-logs
name: polkadot-failed-logs
path: |
${{ env.TEST_DIR }}/*.log
49 changes: 29 additions & 20 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,35 @@ Install just with:
- `brew install just`, if you're on Mac OS and have `brew` package manager installed,
- `sudo apt install just`, if you're using a Linux distribution.

### Run prerequisites

It's only needed once after checkout or when dependencies change:
- `just build`
- `just npm-install`

### Run full workflow example
- `just run-authorize-and-store papi` - for PAPI,
- `just run-authorize-and-store pjs` - for PJS.

#### Run individual commands for manual testing
- `just setup-services papi` - Setup all services (IPFS, zombienet, reconnect, PAPI descriptors),
- `just ipfs-init` - Initialize IPFS (if needed),
- `just ipfs-start` - Start IPFS daemon,
- `just bulletin-solo-zombienet-start` - Start zombienet,
- `just ipfs-connect` - Connect to IPFS nodes,
- `just ipfs-reconnect-start` - Start IPFS reconnect script,
- `just papi-generate` - Generate PAPI descriptors,
- `just run-example papi` - Run example with PAPI or PJS,
- `just teardown-services` - Stop all services
### Quick Start - Run Tests

Start services once, run tests, then tear down:

```bash
# 1. Start services for your runtime (only needed once)
just start-services bulletin-westend-runtime
# or
just start-services bulletin-polkadot-runtime

# 2. Run tests (as many as you want)
just test-authorize-and-store bulletin-westend-runtime ws
just test-authorize-and-store bulletin-westend-runtime smoldot
just test-store-chunked-data bulletin-westend-runtime

# 3. Stop services when done
just stop-services
```

### Available Runtimes

- `bulletin-westend-runtime` - Westend parachain configuration
- `bulletin-polkadot-runtime` - Polkadot solochain configuration

### Available Test Commands

- `test-authorize-and-store <runtime> <mode>` - Test authorization and storage workflow
- `mode` can be: `ws` (WebSocket RPC) or `smoldot` (light client)
- `test-store-chunked-data <runtime>` - Test chunked data storage with DAG-PB

## Manually

Expand Down
23 changes: 20 additions & 3 deletions examples/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,13 @@ export const TX_MODE_IN_BLOCK = "in-block";
export const TX_MODE_FINALIZED_BLOCK = "finalized-block";
export const TX_MODE_IN_POOL = "in-tx-pool";

const DEFAULT_TX_TIMEOUT_MS = 60_000; // 60 seconds or 10 blocks
const DEFAULT_TX_TIMEOUT_MS = 120_000; // 120 seconds - includes time for in-block inclusion + stability delay

const TX_MODE_CONFIG = {
[TX_MODE_IN_BLOCK]: {
match: (ev) => ev.type === "txBestBlocksState" && ev.found,
log: (txName, ev) => `📦 ${txName} included in block: ${ev.block.hash}`,
stabilityDelayMs: 6000, // Wait 6s for block to stabilize (helps with light client reorgs)
},
[TX_MODE_IN_POOL]: {
match: (ev) => ev.type === "broadcasted",
Expand Down Expand Up @@ -113,8 +114,24 @@ function waitForTransaction(tx, signer, txName, txMode = TX_MODE_IN_BLOCK, timeo
console.log(`✅ ${txName} event:`, ev.type);
if (!resolved && config.match(ev)) {
console.log(config.log(txName, ev));
cleanup();
resolve(ev);

// Mark as resolved immediately to prevent error handler from rejecting
resolved = true;

// Unsubscribe to stop receiving events
if (sub) sub.unsubscribe();

// If config specifies a stability delay, wait before resolving
if (config.stabilityDelayMs) {
console.log(`⏳ Waiting ${config.stabilityDelayMs}ms for block stability...`);
setTimeout(() => {
clearTimeout(timeoutId);
resolve(ev);
}, config.stabilityDelayMs);
} else {
clearTimeout(timeoutId);
resolve(ev);
}
}
},
error: (err) => {
Expand Down
26 changes: 23 additions & 3 deletions examples/authorize_and_store_papi_smoldot.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { cidFromBytes } from "./cid_dag_metadata.js";
import { bulletin } from './.papi/descriptors/dist/index.mjs';

// Constants
const SYNC_WAIT_SEC = 15;
const SYNC_WAIT_SEC = 30;
const OVERALL_TIMEOUT_SEC = 180; // 3 minutes total timeout for the test
const SMOLDOT_LOG_LEVEL = 3; // 0=off, 1=error, 2=warn, 3=info, 4=debug, 5=trace
const HTTP_IPFS_API = 'http://127.0.0.1:8080' // Local IPFS HTTP gateway

Expand Down Expand Up @@ -92,7 +93,7 @@ async function createSmoldotClient(chainSpecPath, parachainSpecPath = null) {
return { client: createClient(getSmProvider(targetChain)), sd };
}

async function main() {
async function runTestWithTimeout() {
await cryptoWaitReady();

// Get chainspec path from command line argument (required - main chain: relay for para, or solo)
Expand All @@ -118,7 +119,11 @@ async function main() {

console.log('🔍 Checking if chain is ready...');
const bulletinAPI = client.getTypedApi(bulletin);
await waitForChainReady(bulletinAPI);
const isReady = await waitForChainReady(bulletinAPI);

if (!isReady) {
throw new Error('Chain failed to become ready after maximum retries');
}

// Signers.
const { sudoSigner, whoSigner, whoAddress } = setupKeyringAndSigners('//Alice', '//Alice');
Expand Down Expand Up @@ -167,4 +172,19 @@ async function main() {
}
}

async function main() {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`❌ Test timeout: exceeded ${OVERALL_TIMEOUT_SEC} seconds`));
}, OVERALL_TIMEOUT_SEC * 1000);
});

try {
await Promise.race([runTestWithTimeout(), timeoutPromise]);
} catch (error) {
console.error(error.message || error);
process.exit(1);
}
}

await main();
10 changes: 8 additions & 2 deletions examples/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,20 +207,26 @@ export class NonceManager {
* Wait for a PAPI typed API chain to be ready by checking runtime constants.
* Retries until the chain is ready or max retries reached.
*/
export async function waitForChainReady(typedApi, maxRetries = 10, retryDelayMs = 2000) {
export async function waitForChainReady(typedApi, maxRetries = 20, retryDelayMs = 2000) {

for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// Check runtime constants to verify chain is accessible
const version = typedApi.constants.System.Version;

// Validate that we got real values, not undefined
if (!version || !version.spec_name || !version.spec_version) {
throw new Error(`Invalid version data: spec_name=${version?.spec_name}, spec_version=${version?.spec_version}`);
}

console.log(`✅ Chain is ready! Runtime: ${version.spec_name} v${version.spec_version}`);
return true;
} catch (error) {
if (attempt < maxRetries) {
console.log(`⏳ Chain not ready yet (attempt ${attempt}/${maxRetries}), retrying in ${retryDelayMs/1000}s... Error: ${error.message}`);
await new Promise(resolve => setTimeout(resolve, retryDelayMs));
} else {
console.log(`⚠️ Chain readiness check failed after ${maxRetries} attempts. Proceeding anyway... Error: ${error.message}`);
console.error(`❌ Chain readiness check failed after ${maxRetries} attempts (${maxRetries * retryDelayMs / 1000}s total). Error: ${error.message}`);
return false;
}
}
Expand Down
Loading
Loading