Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
127abe3
added relay chainspec to smoldot while using parachain setup
x3c41a Jan 12, 2026
61b1cfd
added retries
x3c41a Jan 12, 2026
fdf53b5
extracted waitForChainReady to common
x3c41a Jan 12, 2026
7f476bf
changed function signature
x3c41a Jan 12, 2026
52a7003
refactor
x3c41a Jan 12, 2026
c70f80b
refactor
x3c41a Jan 12, 2026
7bbf142
Apply suggestions from code review
bkontur Jan 12, 2026
89b2543
Added WebSocket P2P transport support for smoldot client. Configured …
x3c41a Jan 12, 2026
febf06b
fixed formatting
x3c41a Jan 12, 2026
a9e328b
removed lastRuntimeUpgrade
x3c41a Jan 13, 2026
aa1d499
increased smoldot log level
x3c41a Jan 13, 2026
afb4d05
check runtime constants to verify chain is accessibl
x3c41a Jan 13, 2026
f8b807e
changed test order to speed up time to result
x3c41a Jan 13, 2026
72b6e8f
added transaction timeout
x3c41a Jan 13, 2026
daa9723
added transaction index feature
x3c41a Jan 13, 2026
f331210
refactored waitForTransaction
x3c41a Jan 13, 2026
b898eac
Revert "refactored waitForTransaction"
x3c41a Jan 13, 2026
d79ebf8
Reapply "refactored waitForTransaction"
x3c41a Jan 13, 2026
f16cc74
removed westend-spec
x3c41a Jan 13, 2026
1df17b9
revert
x3c41a Jan 13, 2026
2ce5c2b
GitHub-hosted runners that have pre-installed Microsoft repos which s…
x3c41a Jan 13, 2026
dd9dde8
simplified convertBootNode and readChainSpec, refactored code
x3c41a Jan 13, 2026
eb7aad9
refactor log level
x3c41a Jan 13, 2026
51b6165
Update examples/authorize_and_store_papi_smoldot.js
bkontur Jan 13, 2026
36c297d
reverted order back
x3c41a Jan 14, 2026
acdd70d
removed chainspec
x3c41a Jan 14, 2026
cd23abf
simplified westend chain_spec
x3c41a Jan 14, 2026
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
12 changes: 4 additions & 8 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,26 +154,22 @@ jobs:
echo "ZOMBIENET_BINARY=zombienet-linux-x64" >> "$GITHUB_ENV"

# Westend parachain
- name: Run authorize and store (PAPI, RPC node, Westend parachain)
- name: Run 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-authorize-and-store "bulletin-westend-runtime" "ws"
- name: Run authorize and store (PAPI, smoldot, Westend parachain)
just run-authorize-and-store "bulletin-westend-runtime" "smoldot"
- name: Run authorize and store (PAPI, RPC node, Westend parachain)
working-directory: examples
# TODO: remove when smoldot for para is fixed
continue-on-error: true
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"
just run-authorize-and-store "bulletin-westend-runtime" "ws"
- name: Run store chunked data + DAG-PB (PJS-API, RPC node, Westend parachain)
working-directory: examples
# TODO: tmp
continue-on-error: true
run: |
export TEST_DIR="$(mktemp -d $GITHUB_WORKSPACE/bulletin-tests-run-XXXXX)/test"
echo "TEST_DIR=$TEST_DIR" >> "$GITHUB_ENV"
Expand Down
108 changes: 108 additions & 0 deletions bulletin-westend-spec.json

Large diffs are not rendered by default.

49 changes: 39 additions & 10 deletions examples/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,31 +37,56 @@ 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";

function waitForTransaction(tx, signer, txName, txMode = TX_MODE_IN_BLOCK) {
const DEFAULT_TX_TIMEOUT_MS = 120_000; // 2 minutes default timeout

function waitForTransaction(tx, signer, txName, txMode = TX_MODE_IN_BLOCK, timeoutMs = DEFAULT_TX_TIMEOUT_MS) {
return new Promise((resolve, reject) => {
const sub = tx.signSubmitAndWatch(signer).subscribe({
let sub;
let resolved = false;

const timeoutId = setTimeout(() => {
if (!resolved) {
resolved = true;
if (sub) sub.unsubscribe();
reject(new Error(`${txName} transaction timed out after ${timeoutMs}ms waiting for ${txMode}`));
}
}, timeoutMs);

sub = tx.signSubmitAndWatch(signer).subscribe({
next: (ev) => {
console.log(`✅ ${txName} event:`, ev.type);
switch (txMode) {
case TX_MODE_IN_BLOCK:
if (ev.type === "txBestBlocksState" && ev.found) {
console.log(`📦 ${txName} included in block:`, ev.block.hash);
sub.unsubscribe();
resolve(ev);
if (!resolved) {
resolved = true;
clearTimeout(timeoutId);
sub.unsubscribe();
resolve(ev);
}
}
break;
case TX_MODE_IN_POOL:
if (ev.type === "broadcasted") {
console.log(`📦 ${txName} broadcasted with txHash:`, ev.txHash);
sub.unsubscribe();
resolve(ev);
if (!resolved) {
resolved = true;
clearTimeout(timeoutId);
sub.unsubscribe();
resolve(ev);
}
}
break;
case TX_MODE_FINALIZED_BLOCK:
if (ev.type === "finalized") {
console.log(`📦 ${txName} included in finalized block:`, ev.block.hash);
sub.unsubscribe();
resolve(ev);
if (!resolved) {
resolved = true;
clearTimeout(timeoutId);
sub.unsubscribe();
resolve(ev);
}
}
break;

Expand All @@ -71,8 +96,12 @@ function waitForTransaction(tx, signer, txName, txMode = TX_MODE_IN_BLOCK) {
},
error: (err) => {
console.error(`❌ ${txName} error:`, err);
sub.unsubscribe();
reject(err);
if (!resolved) {
resolved = true;
clearTimeout(timeoutId);
sub.unsubscribe();
reject(err);
}
},
complete: () => {
console.log(`✅ ${txName} complete!`);
Expand Down
94 changes: 82 additions & 12 deletions examples/authorize_and_store_papi_smoldot.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,68 @@ import { createClient } from 'polkadot-api';
import { getSmProvider } from 'polkadot-api/sm-provider';
import { cryptoWaitReady } from '@polkadot/util-crypto';
import { authorizeAccount, fetchCid, store } from './api.js';
import { setupKeyringAndSigners } from './common.js';
import { setupKeyringAndSigners, waitForChainReady } from './common.js';
import { cidFromBytes } from "./cid_dag_metadata.js";
import { bulletin } from './.papi/descriptors/dist/index.mjs';

// Constants
const SYNC_WAIT_SEC = 15;
const SMOLDOT_LOG_LEVEL = 3; // 0=off, 1=error, 2=warn, 3=info, 4=debug, 5=trace
const SMOLDOT_LOG_LEVEL = 4; // 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

/**
* Converts TCP bootnodes to WebSocket bootnodes for smoldot compatibility.
* Uses convention: WebSocket port = TCP p2p_port + 1
*
* Example: /ip4/127.0.0.1/tcp/30333/p2p/PEER_ID
* -> /ip4/127.0.0.1/tcp/30334/ws/p2p/PEER_ID
*/
function convertBootNodesToWebSocket(bootNodes) {
if (!bootNodes || bootNodes.length === 0) {
return [];
}

const wsBootNodes = [];
for (const addr of bootNodes) {
// Parse multiaddr: /ip4/HOST/tcp/PORT/p2p/PEER_ID or /ip4/HOST/tcp/PORT/ws/p2p/PEER_ID
const tcpMatch = addr.match(/^(\/ip[46]\/[^/]+)\/tcp\/(\d+)\/p2p\/(.+)$/);
if (tcpMatch) {
const [, hostPart, portStr, peerId] = tcpMatch;
const tcpPort = parseInt(portStr, 10);
const wsPort = tcpPort + 1; // Convention: WS port = TCP port + 1
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a convention that lives as a code comment here and in zombienet/bulletin-westend-local.toml implicitly.

Maybe it's worth extracting this convention somewhere? But where? @bkontur

const wsAddr = `${hostPart}/tcp/${wsPort}/ws/p2p/${peerId}`;
wsBootNodes.push(wsAddr);
console.log(` 📡 Converted: tcp/${tcpPort} -> tcp/${wsPort}/ws`);
}
// Check if already a WebSocket address
const wsMatch = addr.match(/\/tcp\/\d+\/ws\/p2p\//);
if (wsMatch) {
wsBootNodes.push(addr);
console.log(` ✅ Already WebSocket: ${addr.substring(0, 50)}...`);
}
}
return wsBootNodes;
}

function readChainSpec(chainspecPath) {
const chainSpecContent = readFileSync(chainspecPath, 'utf8');
const chainSpecObj = JSON.parse(chainSpecContent);
chainSpecObj.protocolId = null;

// Convert TCP bootnodes to WebSocket for smoldot
if (chainSpecObj.bootNodes && chainSpecObj.bootNodes.length > 0) {
console.log(`🔄 Converting ${chainSpecObj.bootNodes.length} bootnode(s) to WebSocket for smoldot...`);
const wsBootNodes = convertBootNodesToWebSocket(chainSpecObj.bootNodes);
if (wsBootNodes.length > 0) {
chainSpecObj.bootNodes = wsBootNodes;
console.log(`✅ Using ${wsBootNodes.length} WebSocket bootnode(s)`);
} else {
console.log(`⚠️ No bootnodes could be converted to WebSocket`);
}
} else {
console.log(`⚠️ No bootnodes found in chain spec: ${chainspecPath}`);
}

return JSON.stringify(chainSpecObj);
}

Expand All @@ -33,34 +82,55 @@ function initSmoldot() {
return sd;
}

async function createSmoldotClient(chainspecPath) {
const chainSpec = readChainSpec(chainspecPath);
async function createSmoldotClient(chainSpecPath, parachainSpecPath = null) {
const sd = initSmoldot();
const chain = await sd.addChain({ chainSpec });
const client = createClient(getSmProvider(chain));

const chainSpec = readChainSpec(chainSpecPath);
const mainChain = await sd.addChain({ chainSpec });
console.log(`✅ Added main chain: ${chainSpecPath}`);

if (parachainSpecPath) {
const parachainSpec = readChainSpec(parachainSpecPath);
const parachain = await sd.addChain({
chainSpec: parachainSpec,
potentialRelayChains: [mainChain]
});
console.log(`✅ Added parachain: ${parachainSpecPath}`);
const client = createClient(getSmProvider(parachain));
return { client, sd };
}

const client = createClient(getSmProvider(mainChain));
return { client, sd };
}

async function main() {
await cryptoWaitReady();

// Get chainspec path from command line argument
const chainspecPath = process.argv[2];
if (!chainspecPath) {
console.error('❌ Error: Chainspec path is required as first argument');
console.error('Usage: node authorize_and_store_papi_smoldot.js <chainspec-path>');
// Get chainspec path from command line argument (required - main chain: relay for para, or solo)
const chainSpecPath = process.argv[2];
if (!chainSpecPath) {
console.error('❌ Error: Chain spec path is required as first argument');
console.error('Usage: node authorize_and_store_papi_smoldot.js <chain-spec-path> [parachain-spec-path]');
console.error(' For parachains: <relay-chain-spec-path> <parachain-spec-path>');
console.error(' For solochains: <solo-chain-spec-path>');
process.exit(1);
}

// Optional parachain chainspec path (only needed for parachains)
const parachainSpecPath = process.argv[3] || null;

let sd, client, resultCode;
try {
// Init Smoldot PAPI client and typed api.
({ client, sd } = await createSmoldotClient(chainspecPath));
({ client, sd } = await createSmoldotClient(chainSpecPath, parachainSpecPath));
console.log(`⏭️ Waiting ${SYNC_WAIT_SEC} seconds for smoldot to sync...`);
// TODO: check better way, when smoldot is synced, maybe some RPC/runtime api that checks best vs finalized block?
await new Promise(resolve => setTimeout(resolve, SYNC_WAIT_SEC * 1000));

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

// Signers.
const { sudoSigner, whoSigner, whoAddress } = setupKeyringAndSigners('//Alice', '//Alice');
Expand Down
25 changes: 25 additions & 0 deletions examples/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,28 @@ export class NonceManager {
return current;
}
}

/**
* 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) {

for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// Check runtime constants to verify chain is accessible
const version = typedApi.constants.System.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}`);
return false;
}
}
}
return false;
}
9 changes: 6 additions & 3 deletions examples/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -387,11 +387,14 @@ run-authorize-and-store runtime mode="ws": npm-install
if [ "{{ mode }}" = "smoldot" ]; then
# Set chainspec path based on runtime
if [ "{{ runtime }}" = "bulletin-westend-runtime" ]; then
CHAINSPEC_PATH="$TEST_DIR/bulletin-westend-collator-1/cfg/westend-local-1006.json"
else # bulletin-polkadot-runtime
# Parachain: relay chain (required) + parachain spec (optional)
RELAY_CHAINSPEC_PATH="$TEST_DIR/bob/cfg/westend-local.json"
PARACHAIN_CHAINSPEC_PATH="$TEST_DIR/bulletin-westend-collator-2/cfg/westend-local-1006.json"
node $SCRIPT_NAME "$RELAY_CHAINSPEC_PATH" "$PARACHAIN_CHAINSPEC_PATH"
else # bulletin-polkadot-runtime (solochain)
CHAINSPEC_PATH="$TEST_DIR/bob/cfg/bulletin-polkadot-local.json"
node $SCRIPT_NAME "$CHAINSPEC_PATH"
fi
node $SCRIPT_NAME "$CHAINSPEC_PATH"
else
node $SCRIPT_NAME
fi
Expand Down
2 changes: 1 addition & 1 deletion runtimes/bulletin-westend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ xcm-runtime-apis = { workspace = true }

# Cumulus
cumulus-pallet-aura-ext = { workspace = true }
cumulus-pallet-parachain-system = { workspace = true }
cumulus-pallet-parachain-system = { features = ["transaction-index"], workspace = true }
cumulus-pallet-session-benchmarking = { workspace = true }
cumulus-pallet-weight-reclaim = { workspace = true }
cumulus-pallet-xcm = { workspace = true }
Expand Down
12 changes: 12 additions & 0 deletions zombienet/bulletin-westend-local.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,20 @@ chain = "westend-local"
[[relaychain.nodes]]
name = "alice"
validator = true
p2p_port = 30333
rpc_port = 9942
balance = 2000000000000
# WebSocket P2P on p2p_port + 1 for smoldot light client support
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comments to be moved or removed

args = ["--listen-addr", "/ip4/0.0.0.0/tcp/30334/ws"]

[[relaychain.nodes]]
name = "bob"
validator = true
p2p_port = 30433
rpc_port = 9943
balance = 2000000000000
# WebSocket P2P on p2p_port + 1 for smoldot light client support
args = ["--listen-addr", "/ip4/0.0.0.0/tcp/30434/ws"]

[[parachains]]
id = 1006
Expand All @@ -38,6 +44,9 @@ rpc_port = 10000
args = [
"--ipfs-server",
"-lparachain=debug,runtime=trace,xcm=trace,bitswap=trace,sub-libp2p::bitswap=trace",
# WebSocket P2P on p2p_port + 1 for smoldot light client support
"--listen-addr",
"/ip4/0.0.0.0/tcp/10002/ws",
]

[[parachains.collators]]
Expand All @@ -49,4 +58,7 @@ rpc_port = 12346
args = [
"--ipfs-server",
"-lparachain=debug,runtime=trace,xcm=trace,bitswap=trace,sub-libp2p::bitswap=trace",
# WebSocket P2P on p2p_port + 1 for smoldot light client support
"--listen-addr",
"/ip4/0.0.0.0/tcp/12348/ws",
]