@octwa/sdk
v2.3.0
Published
Official TypeScript SDK for integrating dApps with OctWa Wallet Extension. RFC-O-1 compliant provider API with typed access to Octra RPC, transactions, contracts, and privacy methods.
Maintainers
Readme
@octwa/sdk
Official TypeScript SDK for integrating dApps with the OctWa wallet extension.
Implements RFC-O-1 — Octra Provider JavaScript API. The SDK is a thin, typed wrapper around window.octra.request(); every public method maps one-to-one onto a standard provider method, with a small number of conveniences (waitForConfirmation, native RPC pass-through) layered on top.
Installation
npm install @octwa/sdkThe SDK ships dual builds (CJS + ESM) plus full TypeScript declarations. Node >= 16 is required for the build toolchain; at runtime, the SDK targets modern browsers.
Quick start
import { OctraSDK } from '@octwa/sdk';
// Detect the in-page provider. Times out after 3 s by default.
const sdk = await OctraSDK.init();
if (!sdk.isInstalled()) {
throw new Error('OctWa wallet is not installed.');
}
// Ask the user to connect. Opens the wallet approval popup.
const accounts = await sdk.connect({
permissions: ['read_address', 'read_balance', 'send_transactions'],
});
// Read state through the native RPC pass-through.
const balance = await sdk.rpc('octra_balance', [accounts[0]]);
// Build, sign, and broadcast a transaction in one approval.
const tx = await sdk.sendTransaction({
to: 'oct...',
amount: '1000000', // raw OU; 1 OCT = 1_000_000 OU
fee: '1',
});
// Poll until the transaction reaches a terminal status.
const final = await sdk.waitForConfirmation(tx.hash);
console.log(final.status); // 'confirmed' | 'rejected' | 'dropped'All wallet write methods (sendTransaction, signMessage, encryptBalance, etc.) trigger the wallet popup; the user must explicitly approve every signed operation. Read methods (octra_accounts, callContract, native RPC pass-through) never open a popup.
API reference
Provider lifecycle
// Detect window.octra (uses an EIP-6963-style announce/request handshake).
const sdk = await OctraSDK.init({ timeout: 3_000 });
sdk.isInstalled(); // true once the provider has been detected
sdk.isConnected(); // true once accounts have been authorized
sdk.getProvider(); // raw OctraProvider | null — for advanced usage
sdk.getAccounts(); // string[] — locally cached, refreshed on accountsChangedAccounts, network, and permissions
await sdk.connect({ permissions, networkId? }); // octra_requestAccounts
await sdk.disconnect(); // octra_disconnect
await sdk.fetchAccounts(); // octra_accounts
await sdk.getNetworkId(); // octra_networkId
await sdk.getNetworkInfo(); // octra_networkInfo
await sdk.getPermissions(); // octra_permissions
await sdk.switchNetwork('octra:devnet'); // octra_switchNetworkdisconnect() revokes the dApp's session at the wallet. The next connect() call always opens a fresh approval popup, so the user can pick a different wallet if they want.
The full permission catalogue:
read_address read_balance read_public_key
sign_messages send_transactions contract_calls
view_encrypted_balance encrypt_balance decrypt_balance
private_transfers stealth_scan stealth_claimAlways request the smallest set of permissions your dApp actually needs. The wallet enforces every grant per origin, and unused grants enlarge your attack surface for nothing.
Transactions
await sdk.signMessage('hello octra'); // octra_signMessage
await sdk.sendTransaction({ to, amount, fee?, message? }); // octra_sendTransaction
await sdk.signTransaction({ to, amount, fee, nonce?, message? }); // octra_signTransaction
await sdk.submitTransaction(signedTx); // octra_submitTransaction
await sdk.waitForConfirmation(hash, { // polls octra_transaction
timeoutMs?: 120_000,
pollIntervalMs?: 3_000,
onTick?: (result) => { /* ... */ },
});signTransaction returns a fully canonicalized, Ed25519-signed payload that you can hold and submit later via submitTransaction. This is the recommended pattern when a dApp needs to inspect or store the signed transaction before broadcasting.
The OctraTransactionResult envelope returned by sendTransaction and submitTransaction carries everything a UI typically needs:
interface OctraTransactionResult {
hash: string;
accepted: boolean;
status: 'pending' | 'confirmed' | 'rejected' | 'dropped';
nonce?: number;
ouCost?: string; // total gas in raw OU
explorerUrl?: string; // pre-built explorer link
}Contracts
// Read-only call — no popup, no fee, no transaction.
const result = await sdk.callContract({
address: 'oct...',
method: 'get_owner',
params: [],
});
// State-changing call — opens the wallet popup.
const tx = await sdk.sendContractTransaction({
address: 'oct...',
method: 'transfer',
params: ['oct...', 1_000_000],
amount: '0',
});
const receipt = await sdk.getContractReceipt(tx.hash);A reverted contract call still confirms on-chain with status === 'confirmed'. Always inspect the receipt to verify success — octra_transaction only reports tx-level acceptance, not contract execution outcome.
Privacy
await sdk.getEncryptedBalance(); // returns cipher + decrypted amount
await sdk.encryptBalance('1000000'); // public → encrypted
await sdk.decryptBalance('1000000'); // encrypted → public
await sdk.sendPrivateTransfer({ to, amount, fee? }); // stealth send (v5 envelope)
await sdk.scanStealth(0); // scan from given epoch
await sdk.claimStealth(outputId); // claim a scanned outputAll HFHE primitives — Paillier-style additive ciphertexts, Pedersen commitments, zero/range proofs, and the v5 stealth envelope — run inside the wallet's PVAC WASM worker. The dApp never touches private key material.
Native RPC pass-through
Every documented Octra JSON-RPC method is reachable through sdk.rpc(method, params). Parameters are positional arrays per the Octra RPC convention.
await sdk.rpc('epoch_current');
await sdk.rpc('octra_balance', ['oct...']);
await sdk.rpc('octra_recommendedFee', ['standard']);
await sdk.rpc('octra_transaction', [hash]);
await sdk.rpc('octra_contractAbi', [contractAddr]);This is the escape hatch for any RPC method that has not yet been promoted into the typed SDK surface.
EVM bridge (sdk.evm.*)
OctWa derives a secp256k1 (EVM) address from the same BIP39 seed as the Octra wallet. The EVM bridge gives dApps typed access to Ethereum-compatible operations through the same window.octra provider — no MetaMask or separate wallet required.
// Same session, no re-connect
const evmAddr = await sdk.evm.getDerivedAddress(); // 0x...
const bal = await sdk.evm.getBalance(); // { balance, balanceWei, chainId }Network
await sdk.evm.getChainId(); // 1 (Ethereum mainnet)
await sdk.evm.getNetworkInfo(); // { id, name, chainId, symbol, explorerUrl }
await sdk.evm.switchChain(137); // switch to Polygon — opens popupTransactions
// Native ETH transfer — opens popup
await sdk.evm.sendTransaction({ to: '0x...', value: '0.1' });
// Contract interaction — attach calldata
await sdk.evm.sendTransaction({ to: '0x...', data: '0xa9059cbb...' });
// Sign a message (personal_sign)
await sdk.evm.signMessage('hello evm');
// EIP-712 typed data
await sdk.evm.signTypedData({ domain, types, value });ERC-20 tokens
await sdk.evm.getTokenBalance('0xA0b8...', '0xOwner...');
await sdk.evm.getTokenInfo('0xA0b8...'); // { name, symbol, decimals }
// Transfer tokens — opens popup
await sdk.evm.transferToken({ token: '0xA0b8...', to: '0x...', amount: '1000000' });
// Approve spender (e.g. bridge contract) — opens popup
await sdk.evm.approveToken({ token: '0xA0b8...', spender: '0xBridge...' });
// Check existing allowance
await sdk.evm.getAllowance({ token: '0xA0b8...', owner: '0x...', spender: '0xBridge...' });Low-level reads
await sdk.evm.call({ to: '0x...', data: '0x...' }); // eth_call
await sdk.evm.estimateGas({ to: '0x...', value: '0.01' });
await sdk.evm.getGasPrice(); // { gasPriceWei, gasPriceGwei }EVM events
sdk.on('evmChainChanged', (chainId: number) => { ... });
sdk.on('evmTransactionSent', ({ hash, chainId }) => { ... });Events
sdk.on('connect', ({ networkId, networkInfo }) => { /* ... */ });
sdk.on('disconnect', (error) => { /* ... */ });
sdk.on('networkChanged', (networkInfo) => { /* ... */ });
sdk.on('accountsChanged', (accounts) => { /* ... */ });
sdk.on('permissionsChanged', (permissions) => { /* ... */ });
sdk.on('balanceChanged', ({ address, public, encrypted }) => { /* ... */ });
sdk.on('transactionChanged', ({ hash, status }) => { /* ... */ });
sdk.on('message', (message) => { /* ... */ });
sdk.removeListener('accountsChanged', listener);accountsChanged is fired with an empty array when the user revokes access or locks the wallet — treat it as a soft disconnect and clear any cached account data.
Error handling
Every SDK method throws a typed subclass of OctraProviderError. The base class carries the RFC-O-1 standard code; subclasses let you discriminate without inspecting numeric codes.
import {
isUserRejection,
OctraProviderError,
UserRejectedError,
UnauthorizedError,
UnsupportedMethodError,
DisconnectedError,
NetworkUnavailableError,
} from '@octwa/sdk';
try {
await sdk.sendTransaction({ to, amount });
} catch (error) {
if (isUserRejection(error)) {
// user closed the popup or pressed reject
} else if (error instanceof UnauthorizedError) {
// missing permission — request it via connect()
} else if (error instanceof OctraProviderError) {
console.log(error.code, error.message, error.data?.reason);
}
}| Code | Class | Meaning |
| ---: | ------------------------- | ---------------------------------------- |
| 4001 | UserRejectedError | The user rejected the request |
| 4100 | UnauthorizedError | Method or account is not authorized |
| 4200 | UnsupportedMethodError | The provider does not implement the method |
| 4900 | DisconnectedError | The provider is disconnected from all networks |
| 4901 | NetworkUnavailableError | The requested network is unavailable |
The optional error.data.reason carries machine-readable detail for common cases such as insufficient_balance, nonce_conflict, or permission_denied.
Working with window.octra directly
For framework integrations or migration scenarios, the raw provider remains accessible:
const accounts = await window.octra.request({
method: 'octra_requestAccounts',
params: [{ permissions: ['read_address', 'send_transactions'] }],
});
const balance = await window.octra.request({
method: 'octra_balance',
params: [accounts[0]],
});The SDK provides better ergonomics, typed errors, and convenience helpers, but both surfaces interoperate cleanly — anything you do through window.octra.request is reflected in the SDK's cached state on the next event.
Browser support
The SDK runs anywhere window.octra is injected, which today means the OctWa Chrome / Edge extension. The wallet is built on Manifest V3 with WebAssembly threads (SharedArrayBuffer) and SIMD enabled in the wallet UI; the dApp side has no such requirements — it only speaks JSON-RPC over postMessage.
Specification
This SDK conforms to RFC-O-1: Octra Provider JavaScript API. Every method, event, and error code matches the standard.
