This guide shows the simplest way to start using the Bloxchain protocol: by connecting to an Account‑based contract that already combines all core components (SecureOwnable, RuntimeRBAC, GuardController) behind a single address.
For a deeper explanation of the pattern itself, see the Account Pattern doc.
- Node.js 18+
- TypeScript 4.5+
- npm or yarn
- Basic knowledge of Ethereum and smart contracts
npm install @bloxchain/sdk
# Or with yarn
yarn add @bloxchain/sdkimport {
SecureOwnable,
RuntimeRBAC,
GuardController,
} from '@bloxchain/sdk';
import { createPublicClient, createWalletClient, http } from 'viem';
import { sepolia } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';const rpcUrl = process.env.RPC_URL!; // e.g. https://sepolia.infura.io/v3/...
const privateKey = process.env.PRIVATE_KEY!; // never hardcode; use env vars
const account = privateKeyToAccount(privateKey);
// Public client for reads
const publicClient = createPublicClient({
chain: sepolia,
transport: http(rpcUrl),
});
// Wallet client for writes
const walletClient = createWalletClient({
account,
chain: sepolia,
transport: http(rpcUrl),
});Use a deployed Account implementation (for example AccountBlox) from deployed-addresses.json:
// Example shape – adjust to your deployed-addresses.json
import deployed from '../../deployed-addresses.json';
const network = 'sepolia' as const;
const accountAddress = deployed[network].AccountBlox.address as `0x${string}`;
// All three wrappers point to the SAME address
const secureOwnable = new SecureOwnable(publicClient, walletClient, accountAddress, sepolia);
const runtimeRBAC = new RuntimeRBAC(publicClient, walletClient, accountAddress, sepolia);
const guardController = new GuardController(publicClient, walletClient, accountAddress, sepolia);// SecureOwnable – core security state
const owner = await secureOwnable.owner();
const broadcasters = await secureOwnable.getBroadcasters();
const recovery = await secureOwnable.getRecovery();
const timeLockPeriod = await secureOwnable.getTimeLockPeriodSec();
console.log({ owner, broadcasters, recovery, timeLockPeriod });
// RuntimeRBAC – roles and permissions
const supportedRoles = await runtimeRBAC.getSupportedRoles();
const firstRole = supportedRoles[0];
const roleInfo = await runtimeRBAC.getRole(firstRole);
console.log('First role info:', roleInfo);// 1) Owner (or recovery) requests a transfer (new owner is encoded in the state machine)
const txRequest = await secureOwnable.transferOwnershipRequest({
from: account.address,
});
await publicClient.waitForTransactionReceipt({ hash: txRequest.hash });
// 2) After the timelock expires, approve the pending transaction (txId from BaseStateMachine.getPendingTransactions / getTransaction)
const baseStateMachine = new BaseStateMachine(publicClient, walletClient, accountAddress, sepolia);
const pendingTxIds = await baseStateMachine.getPendingTransactions();
const txId = pendingTxIds[0];
const txApprove = await secureOwnable.transferOwnershipDelayedApproval(txId, {
from: account.address,
});
await publicClient.waitForTransactionReceipt({ hash: txApprove.hash });Use the GuardController wrapper to execute a time‑locked call to a whitelisted target:
import { EngineBlox } from '@bloxchain/sdk/lib/EngineBlox';
// Assume target is a whitelisted contract for a registered function selector
const target = '0x...'; // e.g. ERC20 token
const functionSelector = '0xa9059cbb' as `0x${string}`; // transfer(address,uint256)
const params = '0x...' as `0x${string}`; // abi-encoded params
const gasLimit = 300_000n;
const operationType = EngineBlox.NATIVE_TRANSFER_OPERATION; // or custom op type
const txResult = await guardController.executeWithTimeLock(
target,
0n, // value
functionSelector,
params,
gasLimit,
operationType,
{ from: account.address },
);
console.log('Requested guarded execution tx hash:', txResult.hash);(Approvals, cancellations, and meta‑tx flows use the same patterns as described in the component‑specific docs.)
Account‑style contracts use OpenZeppelin Initializable semantics: there is no constructor state on the implementation; a single correct initialize(...) (or your product’s chained initializer) must run on the proxy (or minimal proxy). Treat initialization as part of deployment: the address end users call should not appear as a public, uninitialized proxy across block boundaries—wire initialize in the same transaction that creates the proxy (factory, proxy constructor _data, or equivalent), then rely on ownership, RBAC, and guards.
To avoid “forgot to call initialize” or wrong ordering when spinning up many instances, prefer a factory that creates the proxy and calls initialize in the same transaction. The repo includes CopyBlox as a reference pattern (contracts/examples/applications/CopyBlox/CopyBlox.sol):
- Validates the implementation implements
IBaseStateMachine. Clones.clone(EIP‑1167) thencallsinitialize(address,address,address,uint256,address)on the new clone.- If initialization reverts, the whole transaction reverts—you do not end up with a live, uninitialized clone from that path.
Use the same initializer arity and argument order your concrete contract exposes (often the same five parameters as CopyBlox / BaseStateMachine).
If you deploy transparent / UUPS / ERC‑1967–style proxies yourself, you still must avoid a live, public proxy that is uninitialized between transactions. Prefer one atomic transaction that both creates the proxy and runs initialize—for example:
- OpenZeppelin proxy constructors that accept
_data: supply ABI‑encodedinitialize(...)calldata so the proxy’s constructor performs the initializer delegatecall before the deployment transaction ends. - A proxy factory (or deployer helper) whose single entrypoint
deploys the proxy andcallsinitializeon the new address in the same transaction (any revert aborts the whole deploy; no orphan uninitialized proxy from that path).
Explicit runbook:
- Deploy implementation (never call user‑facing
initializeon the implementation in production unless you mean to brick or document a pattern—follow OZ guidance). - Create the proxy using a pattern where
initializeruns in the same transaction as proxy creation, with owner, broadcaster, recovery, timelock, andeventForwarder(match your concrete contract’s arity and order). Do not rely on a follow‑up transaction to initialize a proxy that is already callable at its deployed address. - Only after that atomic creation+initialization transaction succeeds, run smoke‑reads on the proxy:
owner(),getRecovery(),getTimeLockPeriodSec(), and verifyeventForwardermatches your intent—before funding, granting roles, or sending production traffic.
More detail: Best Practices — Deployment (initializer subsection under Deployment) and Account Pattern.
Keep these minimum practices in your integration:
// 1) Environment-based secrets
const PRIVATE_KEY = process.env.PRIVATE_KEY;
if (!PRIVATE_KEY) throw new Error('PRIVATE_KEY env var is required');
// 2) Simple address validation
const isAddress = (value: string) => /^0x[a-fA-F0-9]{40}$/.test(value);
// 3) Error handling around writes
try {
const result = await secureOwnable.transferOwnershipRequest({ from: account.address });
console.log('Tx hash:', result.hash);
} catch (error) {
console.error('Tx failed:', error);
}For a full set of recommendations, see Best Practices.
- Learn more about the Account Pattern and how it composes the core components.
- Explore component‑level docs:
- Dive into architecture:
- Look at end‑to‑end flows in Basic Examples.
For detailed API signatures, see the API Reference.