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
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@
.direnv
.idea

**/node_modules/
# Node.js
**/node_modules/
package-lock.json

# Polkadot API generated files
.papi/
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,32 @@

The Bulletin chain consists of a customized node implementation and a single runtime.

## Quick Start with Just

This project includes a `justfile` for testing the complete PAPI workflow. Install [just](https://github.com/casey/just#installation) with `brew install just`, then:

```bash
# Run the complete PAPI workflow test (default command)
just

# Or explicitly:
just test-papi-flow

# Cleanup background processes after test
just cleanup
```

The `just` command automates the entire workflow:
1. ✅ Builds the bulletin chain
2. ✅ Installs npm dependencies
3. ✅ Starts IPFS daemon
4. ✅ Starts zombienet with the bulletin chain
5. ✅ Runs IPFS reconnect script
6. ✅ Generates PAPI descriptors
7. ✅ Runs the authorize and store example

This is perfect for testing the complete integration with a single command!

## Node implementation

The Bulletin chain node implements IPFS support on top of a regular Substrate node. Only work with `litep2p` network backend is supported (enabled by default), and in order to use IPFS functionality `--ipfs-server` flag must be passed to the node binary.
Expand Down Expand Up @@ -248,6 +274,34 @@ Run on the dedicated machine from the root directory:
python3 scripts/cmd/cmd.py bench bulletin-polkadot
```

# JavaScript Examples

The `examples/` directory contains Node.js scripts demonstrating how to interact with the Bulletin chain. For detailed setup and usage instructions, see [examples/README.md](./examples/README.md).

## Quick Start

1. **Install dependencies:**
```bash
npm install
```

2. **Generate PAPI descriptors (for type-safe API):**

Make sure your Bulletin node is running, then:
```bash
npm run papi:generate
```

This generates TypeScript type definitions in `.papi/` based on your chain's metadata. The `.papi/` folder is generated and should not be committed to git (it's listed in `.gitignore`).

3. **Run examples:**
```bash
cd examples
node authorize_and_store_papi.js
```

For more details on PAPI (Polkadot API), see [examples/README_PAPI.md](./examples/README_PAPI.md).

# Troubleshooting

## Build Bulletin Mac OS
Expand Down
43 changes: 43 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
# How to run

## Quick Test with Just (Recommended) ⚡

The easiest way to test the complete PAPI workflow:

```bash
# Install just (if not already installed)
brew install just

# Run the complete test workflow (builds everything, starts all services, runs example)
just test-papi-flow

# Cleanup when done
just cleanup
```

**That's it!** This single `just` command handles everything:
- ✅ Builds the bulletin chain
- ✅ Installs npm dependencies
- ✅ Starts IPFS daemon
- ✅ Starts zombienet with bulletin chain
- ✅ Connects IPFS to bulletin nodes
- ✅ Generates PAPI descriptors
- ✅ Runs the authorize and store example

## Manual Setup

### Build Bulletin
```
git clone https://github.com/paritytech/polkadot-bulletin-chain.git
Expand Down Expand Up @@ -88,10 +114,27 @@ npm install @polkadot/api @polkadot/keyring @polkadot/util-crypto @polkadot/util
```

### Example for simple authorizing and store

**Using legacy @polkadot/api:**
```
cd examples
node authorize_and_store.js
```

**Using modern PAPI (Polkadot API):**
```bash
# First, generate the PAPI descriptors (from the root of the repository)
npm run papi:generate
# or if you already have .papi folder you can always update it
npm run papi:update

# Then run the PAPI version (from the examples directory)
cd examples
node authorize_and_store_papi.js
```

See [README_PAPI.md](./README_PAPI.md) for more details on using PAPI.

### Example for multipart / chunked content / big files
The code stores one file, splits into chunks and then uploads those chunks to the Bulletin.
It collects all the partial CIDs for each chunk and saves them as a custom metadata JSON file in the Bulletin.
Expand Down
103 changes: 103 additions & 0 deletions examples/README_PAPI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Using PAPI with Polkadot Bulletin Chain

## Setup

1. **Install dependencies (from the root of the repository):**
```bash
npm install
```

2. **Generate PAPI descriptors:**

Make sure your Bulletin node is running, then generate the type-safe descriptors:

```bash
# From the root of the repository
npm run papi:generate
```

This will:
- Connect to your local bulletin chain at ws://localhost:10000
- Download the metadata
- Generate TypeScript types in `.papi/descriptors/`
- Create metadata files in `.papi/metadata/bulletin.scale`

> **Note:** The `.papi/` folder is generated and should not be committed to git. It's listed in `.gitignore`.

3. **Run the example:**
```bash
cd examples
node authorize_and_store_papi.js
```

## Key Differences from @polkadot/api

### 1. Client Creation
**Old (@polkadot/api):**
```javascript
const ws = new WsProvider('ws://localhost:10000');
const api = await ApiPromise.create({ provider: ws });
```

**New (PAPI):**
```javascript
import { createClient } from 'polkadot-api';
import { getWsProvider } from 'polkadot-api/ws-provider/node';

const wsProvider = getWsProvider('ws://localhost:10000');
const client = createClient(wsProvider);
const typedApi = client.getTypedApi(bulletin);
```

### 2. Transactions
**Old:**
```javascript
const tx = api.tx.transactionStorage.store(data);
const result = await tx.signAndSend(pair);
```

**New:**
```javascript
const tx = typedApi.tx.TransactionStorage.store({ data });
const result = await tx.signAndSubmit(pair);
```

### 3. Type Safety
PAPI provides full TypeScript type safety based on your chain's metadata:
- Transaction parameters are type-checked
- Query results have proper types
- Auto-completion in IDEs

### 4. Signing
PAPI uses a different signing interface. The `@polkadot-api/pjs-signer` package bridges between `@polkadot/keyring` and PAPI:

```javascript
import { getPolkadotSignerFromPjs } from '@polkadot-api/pjs-signer';
import { Keyring } from '@polkadot/keyring';

const keyring = new Keyring({ type: 'sr25519' });
const account = keyring.addFromUri('//Alice');

// Create PAPI-compatible signer (simple!)
const signer = getPolkadotSignerFromPjs(account);
```

## Benefits of PAPI

1. **Type Safety**: Full TypeScript support with generated types
2. **Light Client Support**: Can use smoldot for light client connections
3. **Better Performance**: More efficient serialization/deserialization
4. **Modern API**: Cleaner, more intuitive API design
5. **Better Developer Experience**: Auto-completion and type checking

## Troubleshooting

### Error: Cannot find module '@polkadot-api/descriptors'
Run: `npm run papi:generate` from the root of the repository

### Connection issues
Make sure your bulletin chain node is running on ws://localhost:10000

### Metadata errors
If metadata changes, regenerate descriptors: `npm run papi:update` from the root of the repository

143 changes: 143 additions & 0 deletions examples/authorize_and_store_papi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// npm install polkadot-api @polkadot-api/signer @polkadot/keyring @polkadot/util-crypto multiformats ipfs-http-client
// npx papi add -w ws://localhost:10000 bulletin
// ipfs daemon &
// ipfs swarm connect /ip4/127.0.0.1/tcp/10001/ws/p2p/12D3KooWQCkBm1BYtkHpocxCwMgR8yjitEeHGx8spzcDLGt2gkBm
// ipfs swarm connect /ip4/127.0.0.1/tcp/12347/ws/p2p/12D3KooWRkZhiRhsqmrQ28rt73K7V3aCBpqKugpsqfpggl4nzazpieyemw6xme
// ipfs swarm peers - should be there
// ipfs bitswap stat
// ipfs block get /ipfs/bafk2bzacebcnty2x5l3jr2sk5rvn7engdfkugpsqfpggl4nzazpieyemw6xme

import { createClient } from 'polkadot-api';
import { Binary } from '@polkadot-api/substrate-bindings';
import { getWsProvider } from 'polkadot-api/ws-provider/node';
import { getPolkadotSigner } from '@polkadot-api/signer';
import { Keyring } from '@polkadot/keyring';
import { cryptoWaitReady } from '@polkadot/util-crypto';
import { create } from 'ipfs-http-client';
import { waitForNewBlock, cidFromBytes } from './common.js';
import { bulletin } from '../.papi/descriptors/dist/index.mjs';

async function authorizeAccount(typedApi, sudoPair, who, transactions, bytes) {
console.log('Creating authorizeAccount transaction...');

const authorizeTx = typedApi.tx.TransactionStorage.authorize_account({
who,
transactions,
bytes
});

const sudoTx = typedApi.tx.Sudo.sudo({
call: authorizeTx.decodedCall
});

const result = await sudoTx.signAndSubmit(sudoPair);
console.log('Transaction authorizeAccount submitted:', result);
return result;
}

async function store(typedApi, pair, data) {
console.log('Storing data:', data);
const cid = cidFromBytes(data);

// Convert data to Uint8Array then wrap in Binary for PAPI typed API
const dataBytes = typeof data === 'string' ?
new Uint8Array(Buffer.from(data)) :
new Uint8Array(data);

// Wrap in Binary object for typed API - pass as object with 'data' property
const binaryData = Binary.fromBytes(dataBytes);
const tx = typedApi.tx.TransactionStorage.store({ data: binaryData });

const result = await tx.signAndSubmit(pair);
console.log('Transaction store submitted:', result);

return cid;
}

// Connect to a local IPFS gateway (e.g. Kubo)
const ipfs = create({
url: 'http://127.0.0.1:5001', // Local IPFS API
});

async function read_from_ipfs(cid) {
// Fetch the block (downloads via Bitswap if not local)
console.log('Trying to get cid: ', cid);
try {
const block = await ipfs.block.get(cid, {timeout: 10000});
console.log('Received block: ', block);
if (block.length !== 0) {
return block;
}
} catch (error) {
console.log('Block not found directly, trying cat...', error.message);
}

// Fetch the content from IPFS
console.log('Trying to chunk cid: ', cid);
const chunks = [];
for await (const chunk of ipfs.cat(cid)) {
chunks.push(chunk);
}

const content = Buffer.concat(chunks);
return content;
}

// Global client reference for cleanup
let client;

async function main() {
await cryptoWaitReady();

// Create PAPI client with WebSocket provider
client = createClient(getWsProvider('ws://localhost:10000'));

// Get typed API with generated descriptors
const typedApi = client.getTypedApi(bulletin);

// Create keyring and accounts
const keyring = new Keyring({ type: 'sr25519' });
const sudoAccount = keyring.addFromUri('//Alice');
const whoAccount = keyring.addFromUri('//Alice');

// Create PAPI-compatible signers using @polkadot-api/signer
// getPolkadotSigner expects (publicKey: Uint8Array, signingType, sign function)
const sudoSigner = getPolkadotSigner(
sudoAccount.publicKey,
'Sr25519',
(input) => sudoAccount.sign(input)
);
const whoSigner = getPolkadotSigner(
whoAccount.publicKey,
'Sr25519',
(input) => whoAccount.sign(input)
);

// Data
const who = whoAccount.address;
const transactions = 32; // u32 - regular number
const bytes = 64n * 1024n * 1024n; // u64 - BigInt for large numbers

console.log('Doing authorization...');
await authorizeAccount(typedApi, sudoSigner, who, transactions, bytes);
await waitForNewBlock();
console.log('Authorized!');

console.log('Storing data ...');
const dataToStore = "Hello, Bulletin with PAPI - " + new Date().toString();
let cid = await store(typedApi, whoSigner, dataToStore);
console.log('Stored data with CID: ', cid);
await waitForNewBlock();

console.log('Reading content... cid: ', cid);
let content = await read_from_ipfs(cid);
console.log('Content as bytes:', content);
console.log('Content as string:', content.toString());

client.destroy();
}

main().catch(console.error).finally(() => {
if (client) client.destroy();
});

Loading