Skip to content
Open
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,5 @@ coverage/

# Vitest
coverage.data
coverage/
coverage/package-lock.json
package-lock.json
Comment on lines +176 to +177
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The original file ended with coverage/ and no trailing newline. When package-lock.json was appended, it got concatenated onto that line, turning coverage/ into coverage/package-lock.json. No functional impact since coverage/ is already ignored on line 171, but this wasn't the intended result.

Suggested change
coverage/package-lock.json
package-lock.json
coverage/
package-lock.json

Fix it with Roo Code or mention @roomote and request a fix.

13 changes: 13 additions & 0 deletions examples/node/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "node",
"version": "1.0.0",
"description": "This directory contains Node.js examples demonstrating the Lumera SDK capabilities.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs"
}
271 changes: 80 additions & 191 deletions examples/node/upload.ts
Original file line number Diff line number Diff line change
@@ -1,234 +1,123 @@
/**
* Advanced Upload Example: Direct Message Composition
*
* This example demonstrates how to use the Telescope-generated message composers
* directly for more granular control over the upload workflow:
*
* 1. Build file metadata manually
* 2. Create messages using generated MessageComposer
* 3. Simulate, sign, and broadcast transactions
* 4. Complete the action with finalization
*
* Cascade Upload Example: LumeraClient Facade
*
* This example demonstrates the high-level upload workflow using the
* LumeraClient facade, which handles RaptorQ encoding, supernode
* communication, and on-chain action registration automatically:
*
* 1. Initialize wallets (Direct + Amino for ADR-036 signing)
* 2. Create a LumeraClient with testnet preset
* 3. Upload a file via the Cascade uploader
*
* Prerequisites:
* - Set the MNEMONIC environment variable with a valid 24-word mnemonic
* - Set the MNEMONIC environment variable with a valid mnemonic
* - Ensure the account has sufficient balance for transaction fees
* - Have a file to upload (e.g., example.bin)
*
*
* Usage:
* MNEMONIC="your mnemonic here" npx tsx examples/node/upload.ts
*/

import { makeBlockchainClient } from "../../src/blockchain/client";
import { createLumeraClient } from "../../src";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import { lumera } from "../../src/codegen";
import { calculateCascadeFee } from "../../src/blockchain/messages";
import fs from "node:fs";
import crypto from "node:crypto";
import { Secp256k1HdWallet, makeSignDoc as makeAminoSignDoc } from "@cosmjs/amino";

async function main() {
console.log("=".repeat(60));
console.log("Lumera SDK - Direct Message Composition Example");
console.log("Lumera SDK - Cascade Upload Example");
console.log("=".repeat(60));

// ============================================================================
// STEP 1: Initialize wallet and blockchain client
// STEP 1: Initialize wallets
// ============================================================================
console.log("\n[Step 1] Setting up wallet and blockchain client...");
console.log("\n[Step 1] Setting up wallets...");

if (!process.env.MNEMONIC) {
throw new Error("Set MNEMONIC environment variable");
}

const wallet = await DirectSecp256k1HdWallet.fromMnemonic(process.env.MNEMONIC, {
prefix: "lumera"
const directWallet = await DirectSecp256k1HdWallet.fromMnemonic(process.env.MNEMONIC, {
prefix: "lumera",
});
const [account] = await wallet.getAccounts();
console.log(`✓ Using address: ${account.address}`);

const chain = await makeBlockchainClient({
rpcUrl: "https://rpc.testnet.lumera.io",
lcdUrl: "https://lcd.testnet.lumera.io",
chainId: "lumera-testnet-2",
signer: wallet,
address: account.address,
gasPrice: "0.025ulume"
const aminoWallet = await Secp256k1HdWallet.fromMnemonic(process.env.MNEMONIC, {
prefix: "lumera",
});

console.log(`✓ Connected to chain: ${await chain.getChainId()}`);

// ============================================================================
// STEP 2: Query blockchain parameters
// ============================================================================
console.log("\n[Step 2] Querying action module parameters...");

const params = await chain.Action.getParams();
console.log(` rq_ids_max: ${params.rq_ids_max}`);
console.log(` rq_ids_ic: ${params.rq_ids_ic}`);
console.log(` fee_base: ${params.fee_base}`);
console.log(` fee_per_kb: ${params.fee_per_kb}`);

// ============================================================================
// STEP 3: Prepare file and metadata
// ============================================================================
console.log("\n[Step 3] Preparing file metadata...");

// For this example, create a sample file if it doesn't exist
const filePath = "./example.bin";
let fileData: Buffer;

if (fs.existsSync(filePath)) {
fileData = fs.readFileSync(filePath);
console.log(`✓ Loaded existing file: ${filePath} (${fileData.length} bytes)`);
} else {
// Create a sample file
fileData = crypto.randomBytes(1024); // 1KB sample file
fs.writeFileSync(filePath, fileData);
console.log(`✓ Created sample file: ${filePath} (${fileData.length} bytes)`);
}

// Calculate file hash using SHA-256
const fileHash = crypto.createHash("sha256").update(fileData).digest("hex");
console.log(` File hash: ${fileHash}`);

// Generate action ID
const actionId = `example-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
console.log(` Action ID: ${actionId}`);

// Build metadata for Cascade action
const metadata = {
data_hash: fileHash,
file_name: "example.bin",
rq_ids_ic: params.rq_ids_ic,
signatures: "", // Would be populated with RaptorQ signatures in real use
public: false,
const [account] = await directWallet.getAccounts();
console.log(` Address: ${account.address}`);

// Combine direct + amino wallets and add signArbitrary (ADR-036) for Cascade
const signer = {
getAccounts: () => directWallet.getAccounts(),
signDirect: (addr: string, doc: any) => directWallet.signDirect(addr, doc),
signAmino: (addr: string, doc: any) => aminoWallet.signAmino(addr, doc),
async signArbitrary(_chainId: string, signerAddress: string, data: string) {
const signDoc = makeAminoSignDoc(
[
{
type: "sign/MsgSignData",
value: {
signer: signerAddress,
data: Buffer.from(data).toString("base64"),
},
},
],
{ gas: "0", amount: [] },
"", // ADR-036 requires empty chain_id
"",
0,
0
);
const { signature } = await aminoWallet.signAmino(signerAddress, signDoc);
return {
signed: data,
signature: signature.signature,
pub_key: signature.pub_key,
};
},
};

// Calculate price based on file size
const price = calculateCascadeFee(fileData.length, params.fee_base, params.fee_per_kb);
console.log(` Calculated fee: ${price} ulume`);

// Set expiration time (24 hours from now)
const expirationTime = Math.floor(Date.now() / 1000 + 86400).toString();
console.log(" Wallets ready");

// ============================================================================
// STEP 4: Build and broadcast MsgRequestAction using generated composer
// STEP 2: Create LumeraClient
// ============================================================================
console.log("\n[Step 4] Building MsgRequestAction with generated composer...");

// Use the Telescope-generated message composer
const msgRequestAction = lumera.action.v1.MessageComposer.withTypeUrl.requestAction({
creator: account.address,
actionType: "cascade",
metadata: JSON.stringify(metadata),
price,
expirationTime,
});
console.log("\n[Step 2] Creating LumeraClient...");

console.log(`✓ Message created with typeUrl: ${msgRequestAction.typeUrl}`);
console.log(` Action type: ${msgRequestAction.value.actionType}`);
console.log(` Price: ${msgRequestAction.value.price}`);

// Simulate transaction to estimate gas
console.log("\n Simulating transaction...");
const gasEstimate = await chain.Tx.simulate(account.address, [msgRequestAction]);
console.log(` Estimated gas: ${gasEstimate}`);

// Broadcast the transaction
console.log("\n Broadcasting transaction...");
const result = await chain.Tx.signAndBroadcast(
account.address,
[msgRequestAction],
{ amount: [{ denom: "ulume", amount: "10000" }], gas: gasEstimate.toString() },
"Request Cascade action"
);

if (result.code !== 0) {
console.error(`✗ Transaction failed: ${result.rawLog}`);
process.exit(1);
}
const client = await createLumeraClient({
preset: "testnet",
signer: signer as any,
address: account.address,
gasPrice: "0.025ulume",
});

console.log(`✓ Transaction successful!`);
console.log(` TX Hash: ${result.transactionHash}`);
console.log(` Block Height: ${result.height}`);
console.log(" Connected to testnet");

// ============================================================================
// STEP 5: Build and broadcast MsgFinalizeAction
// STEP 3: Upload a file via Cascade
// ============================================================================
console.log("\n[Step 5] Building MsgFinalizeAction with generated composer...");
console.log("\n[Step 3] Uploading file via Cascade...");

// In a real scenario, you would:
// 1. Upload the file data to supernodes
// 2. Get the RaptorQ IDs from supernodes
// 3. Include those in the finalize metadata
const file = new TextEncoder().encode("Hello, Lumera!");
// Expiration must be at least 86400s from now; add buffer to avoid race with block time
const expirationTime = String(Math.floor(Date.now() / 1000) + 86400 + 600);

const finalizeMetadata = {
...metadata,
rq_ids_max: params.rq_ids_max,
rq_ids: ["id1", "id2", "id3"], // Would be real RaptorQ IDs
};
console.log(` File size: ${file.length} bytes`);
console.log(` Expiration: ${expirationTime}`);

// Use the Telescope-generated message composer
const msgFinalizeAction = lumera.action.v1.MessageComposer.withTypeUrl.finalizeAction({
creator: account.address,
actionId,
actionType: "cascade",
metadata: JSON.stringify(finalizeMetadata),
const result = await client.Cascade.uploader.uploadFile(file, {
fileName: "hello.txt",
isPublic: true,
expirationTime,
taskOptions: { pollInterval: 2000, timeout: 300000 },
});

console.log(`✓ Message created with typeUrl: ${msgFinalizeAction.typeUrl}`);
console.log(` Action ID: ${msgFinalizeAction.value.actionId}`);

// Simulate and broadcast finalize transaction
console.log("\n Simulating finalize transaction...");
const finalizeGas = await chain.Tx.simulate(account.address, [msgFinalizeAction]);
console.log(` Estimated gas: ${finalizeGas}`);

console.log("\n Broadcasting finalize transaction...");
const finalizeResult = await chain.Tx.signAndBroadcast(
account.address,
[msgFinalizeAction],
{ amount: [{ denom: "ulume", amount: "10000" }], gas: finalizeGas.toString() },
"Finalize Cascade action"
);

if (finalizeResult.code !== 0) {
console.error(`✗ Finalize transaction failed: ${finalizeResult.rawLog}`);
process.exit(1);
}

console.log(`✓ Finalize transaction successful!`);
console.log(` TX Hash: ${finalizeResult.transactionHash}`);
console.log(` Block Height: ${finalizeResult.height}`);

// ============================================================================
// STEP 6: Verify action on blockchain
// ============================================================================
console.log("\n[Step 6] Verifying action on blockchain...");

try {
const action = await chain.Action.getAction(actionId);
console.log(`✓ Action found on chain:`);
console.log(` Action ID: ${action.actionId}`);
console.log(` Action Type: ${action.actionType}`);
console.log(` Creator: ${action.creator}`);
console.log(` State: ${action.state}`);
} catch (error) {
console.log(` Note: Action query may fail if not yet indexed`);
}

console.log("\n" + "=".repeat(60));
console.log("SUCCESS! Demonstrated direct message composition workflow");
console.log("Upload complete!");
console.log("=".repeat(60));
console.log("\nKey takeaways:");
console.log(" - Used lumera.action.v1.MessageComposer.withTypeUrl for messages");
console.log(" - Demonstrated both RequestAction and FinalizeAction");
console.log(" - Showed gas estimation and transaction broadcasting");
console.log(" - This is the low-level approach for advanced use cases");
console.log("\nFor simpler workflows, use the LumeraClient facade instead!");
console.log(result);
}

main().catch((error) => {
console.error("\n✗ Fatal error:");
console.error("\nFatal error:");
console.error(error);
process.exit(1);
});
});
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
"LICENSE"
],
"type": "module",
"main": "dist/cjs/index.cjs",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/types/index.d.ts",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.cjs",
"require": "./dist/cjs/index.js",
"types": "./dist/types/index.d.ts"
},
"./compat/blake3": {
Expand All @@ -37,7 +37,7 @@
"clean": "rm -rf dist",
"build:esm": "tsc -p tsconfig.json",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build": "npm run clean && npm run build:esm && npm run build:cjs",
"build": "npm run clean && npm run build:esm && npm run build:cjs && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
"gen:snapi": "openapi-typescript docs/snapi-swagger.json -o src/types/snapi.gen.ts",
"lint": "eslint 'src/**/*.{ts,tsx}'",
"test": "vitest run",
Expand Down
Loading