@zebec-network/aleo-stream-sdk
v2.0.2
Published
TypeScript client for Zebec payroll streams on Aleo. It wraps the on-chain program (`test_zebec_stream_v3.aleo` on testnet, `zebec_stream_v1.aleo` on mainnet) so a frontend or backend can create, fund, pause, withdraw, auto-withdraw, and cancel token stre
Keywords
Readme
Zebec Aleo Stream SDK
TypeScript client for Zebec payroll streams on Aleo. It wraps the on-chain program (test_zebec_stream_v3.aleo on testnet, zebec_stream_v1.aleo on mainnet) so a frontend or backend can create, fund, pause, withdraw, auto-withdraw, and cancel token streams without assembling Leo plaintext by hand.
Package: @zebec-network/aleo-stream-sdk
What it does
A stream pays a receiver a fixed amount of an IARC-22 stablecoin (usdcx or usad) linearly over a duration. The sender deposits tokens into the program; the receiver (or a designated withdrawer) pulls the vested portion over time.
The SDK is the off-chain counterpart of that program:
- Builds and submits the program transitions (create, topup, pause/resume, withdraw, cancel, admin config).
- Reads public mappings (
streams,stream_anchors,stream_configs, per-address registries, token whitelist). - Decrypts private records (sender / receiver / withdrawer tickets, token and credits records).
- Reproduces on-chain BHP256 hashing and Schnorr fee signatures so create-stream fees verify on-chain.
- Loads one
@provablehq/sdkWASM (testnet or mainnet), not both.
It does not ship a wallet. You pass an AleoWallet (Puzzle, a Provable Account + proving client, or your own adapter). The SDK only calls executeTransaction, decrypt, and requestRecords.
Public vs private streams
| | Public | Private |
| --- | --- | --- |
| Funding | Sender's public token balance (approve + transfer_from_public) | Private token record + Sealance freeze-list Merkle proofs |
| Visibility | Stream + StreamAnchor live in public mappings | Tickets are private records; StreamAnchor is still public |
| Who acts | Sender / receiver / config withdrawer by address | Same roles, authorized by the matching ticket |
| Listing | listPublicStreams(config) | listPrivateStreams() |
Every stream, public or private, has a public anchor: start time, duration, deposited/withdrawn amounts, paused/canceled flags. Follow-on transactions must prove against the current on-chain anchor. A proving service returning accepted is not the same as explorer confirmed / mapping update — wait for confirmation before the next write.
Configs (tenants)
Streams belong to a stream config stored in stream_configs. The config key is a field. Human-readable names such as Stream_Config_001 are hashed off-chain with BHP256 (configNameToField); the program only ever sees the field.
A config records:
admin— only this address mayupdateConfigandsetTokenWhitelisted.initializeConfigalways sets admin to the executing wallet.feeVault— receives the signed stream fee (paid in the stream token).withdrawer— address allowed to call auto-withdraw.baseFee/platformFee— credits fees used whenautoWithdrawableis true.
Tokens must be whitelisted per config before create. On testnet the usual token is test_usdcx_stablecoin.
Signed stream fees
Create-stream requires an admin-signed StreamTokenFee: config, token identifier, fee amount, stream amount, expiry, nonce. streamAmount must equal the stream's amount — it binds the signed fee to one stream size so it can't be replayed against a larger stream. On-chain verification is verify_schnorr(fee_signature, config.admin, BHP256(token_fee)). Use signStreamTokenFee with the admin private key; the signature is a sign1... literal passed into create.
Auto-withdraw
If autoWithdrawable is true, the config withdrawer can pull vested tokens on a fixed withdrawFrequency (must be one of 60, 120, 3600, 43200, 86400, 604800, 1209600, 2592000, 7776000, 15552000, 31536000 seconds). The auto-withdrawal fee in microcredits is platformFee + (duration * baseFee) / frequency.
Requirements
- Node.js 20+ (ESM). The package is
"type": "module". @provablehq/sdk^0.11.9(already a dependency).- An Aleo account with credits for fees.
- For public streams: public token balance plus an
approve_publicallowance to the stream program address. - For private streams: unspent token (and, if auto-withdraw is on, credits) records; a record scanner for
requestRecords. - Network access to an Aleo REST host (default
https://api.provable.com/v2) and, if you prove remotely, Provable DPS.
Install
npm install @zebec-network/aleo-stream-sdk
# or
yarn add @zebec-network/aleo-stream-sdkimport {
Network,
ZebecStreamService,
configNameToField,
signStreamTokenFee,
randomField,
loadProvableSdk,
} from "@zebec-network/aleo-stream-sdk";In a browser, construct the service for a single Network. loadProvableSdk(network) (and ZebecStreamService) dynamically import either @provablehq/sdk/testnet.js or mainnet.js. Do not statically import both, or the app will download two WASM blobs.
Setup
1. Provide a wallet
The service only needs this interface:
interface AleoWallet {
address: string;
decrypt: (cipherText: string) => Promise<string>;
requestRecords: (
program: string,
includePlaintext?: boolean,
) => Promise<unknown[]>;
executeTransaction: (options: {
program: string;
function: string;
inputs: string[];
fee?: number; // microcredits (SDK default 100_000)
privateFee?: boolean;
feeRecord?: string;
imports?: string[];
}) => Promise<{ transactionId: string }>;
}- Frontend: map Puzzle (or similar) connect/decrypt/records/execute onto this shape. Pass program imports through; the service fills IARC-22 nested imports for token dispatch.
- Backend / scripts: wrap
@provablehq/sdkAccount+ProgramManager+ DPS (submitProvingRequest). Seetests/setup.tsfor a complete adapter used by e2e (REST host, prover URI, record scanner, API key, consumer id).
priorityFee on service methods is microcredits. If your wallet's proving API expects credits, divide by 1_000_000 in the adapter (the e2e wallet does this).
2. Construct the service
const network = Network.TESTNET; // or Network.MAINNET
const service = await new ZebecStreamService(wallet, {
network,
// host: "https://api.provable.com/v2", // default
// programId: "test_zebec_stream_v3.aleo", // default per network
}).ready();ready() waits until that network's WASM is loaded. Async methods wait on it automatically; await ready() before reading service.networkClient.
Defaults:
| Network | Program | REST host |
| --- | --- | --- |
| testnet | test_zebec_stream_v3.aleo | https://api.provable.com/v2 |
| mainnet | zebec_stream_v1.aleo | same default, override host if needed |
Explorers: testnet · mainnet. Transaction URL: https://testnet.explorer.provable.com/transaction/<id>.
3. Admin: initialize a config and whitelist a token
Do this once per tenant. The executing wallet becomes admin.
const configName = await configNameToField("Acme_Payroll", network);
const config = {
configName,
admin: adminWallet.address,
feeVault: adminWallet.address,
withdrawer: adminWallet.address, // auto-withdraw signer
baseFee: 0, // credits
platformFee: 0, // credits
};
await admin.initializeConfig(config, { priorityFee: 100_000 });
const token = "test_usdcx_stablecoin"; // without .aleo
await admin.setTokenWhitelisted(configName, token, true, {
priorityFee: 100_000,
});isTokenWhitelisted(configName, token) and getStreamConfig(configName) are the corresponding reads. updateConfig may only be called by the existing admin.
4. Sign a stream fee and create a stream
Token argument to create methods is "usdcx_stablecoin" or "usad_stablecoin". On testnet the service prefixes test_.
const decimals = 6;
const streamId = `${await randomField(network)}field`;
const now = Math.floor(Date.now() / 1000);
const tokenFee = {
config: configName,
streamToken: "test_usdcx_stablecoin",
streamFeeAmount: 0,
streamAmount: "10", // must equal params.amount below
expiry: now + 3600,
nonce: `${await randomField(network)}field`,
};
const feeSignature = await signStreamTokenFee(
adminPrivateKey,
tokenFee,
network,
decimals,
);
const params = {
receiver: receiverAddress, // cannot be the sender
streamId,
amount: "10",
startTime: now,
duration: 86_400,
isCancelable: true,
isPausable: true,
autoWithdrawable: false,
withdrawFrequency: 60,
startNow: true, // on-chain start_time = create block time
canTopup: true,
// can_topup requires a buffer > 0. Deposit is this amount, not `amount`.
initialBufferAmount: "0.000001",
};Public create (approve the program, then create):
const payroll = await sender.programAddress();
await sender.approveTokenPublic(
"test_usdcx_stablecoin",
payroll,
"100", // allowance covering fee + deposit + later topups
decimals,
{ priorityFee: 100_000 },
);
const txId = await sender.createStreamPublic(
params,
"usdcx_stablecoin",
decimals,
config,
tokenFee,
feeSignature,
{ priorityFee: 100_000 },
);Private create finds a credits record (auto-withdraw fee) and a token record (deposit + stream fee), and attaches freeze-list Merkle proofs:
const txId = await sender.createStreamPrivate(
params,
"usdcx_stablecoin",
decimals,
config,
tokenFee,
feeSignature,
{ priorityFee: 100_000 },
);Wait until getStreamAnchor(streamId) returns before the next write.
Stream lifecycle
Pass a fresh unix timestamp on each call (StreamParams). Finalize checks it is close to chain time; for start_now streams it must not be older than on-chain start_time.
const op = { streamId, timestamp: Math.floor(Date.now() / 1000) };| Action | Public | Private | Who |
| --- | --- | --- | --- |
| Top up | topupStreamPublic({ ...op, amount, tokenDecimals }) | topupStreamPrivate(...) | Sender (needs canTopup) |
| Pause / resume | pauseResumeStreamPublic(op) | pauseResumeStreamPrivate(op) | Sender (isPausable) |
| Withdraw vested | withdrawStreamPublic(op) | withdrawStreamPrivate(op) | Receiver |
| Auto-withdraw | withdrawStreamAutoPublic(op, config) | withdrawStreamAutoPrivate(op, config) | Config withdrawer |
| Cancel | cancelStreamPublic(op) | cancelStreamPrivate(op) | Sender (isCancelable) |
Cancel requires withdrawable ≤ deposited. After a tiny create buffer, that usually means a successful topup first. Always wait for the previous transaction to confirm and for getStreamAnchor to show the new withdrawnAmount / paused / canceled before proving the next one.
Private methods take an optional ticket plaintext; otherwise the service scans records for the matching ticket type (0 sender, 1 receiver, 2 withdrawer).
Reads
await service.programAddress();
await service.getStream(streamId); // public Stream mapping
await service.getStreamAnchor(streamId); // public accounting
await service.getStreamConfig(configName);
await service.isTokenWhitelisted(configName, "test_usdcx_stablecoin");
await service.getOutgoingStreamCount(address, configName);
await service.listOutgoingStreamIds(address, configName);
await service.listIncomingStreamIds(address, configName);
await service.listPublicStreams(configName); // this wallet, both directions
await service.listPrivateStreams();
await service.decryptProgramRecords(service.programId);
await service.getPublicBalance();
await service.getPublicTokenBalance("test_usdcx_stablecoin.aleo", 6);
await service.getPrivateBalance();
await service.getPrivateTokenBalance("test_usdcx_stablecoin.aleo", 6);
await service.getComplianceProofs("usdcx", address, network);listPublicStreams hydrates each id with Stream + StreamAnchor (amounts in token units). Canceled and ended streams are included.
Token metadata: getTokenInfo(service.networkClient, "test_usdcx_stablecoin.aleo").
Helpers: approveTokenPublic, transferTokenPublic, transferTokenPrivateToPublic.
Withdrawable amount
computeWithdrawableAmount mirrors compute_withdrawable_amount in main.leo, so a UI can show the vested balance without sending a transaction. All arguments are bigint in the stream's micro units / unix seconds:
import { computeWithdrawableAmount } from "@zebec-network/aleo-stream-sdk";
const anchor = await service.getStreamAnchor(streamId);
const { totalWithdrawable, currentlyWithdrawable } = computeWithdrawableAmount(
BigInt(Math.floor(Date.now() / 1000)), // effectiveNow
BigInt(anchor.startTime),
BigInt(anchor.duration),
BigInt(anchor.pausedInterval),
fullAmount, // the stream's full amount, in micro units
withdrawnAmount, // anchor.withdrawnAmount, in micro units
);totalWithdrawable— vested up toeffectiveNow;currentlyWithdrawable— that minuswithdrawnAmount.effectiveNowis the timestamp to evaluate at.pausedIntervalonly covers pauses that have ended, so for a stream that is currently paused or canceled pass the timestamp at which accrual stopped (anchor.lastPausedTime/anchor.canceledAt) instead of the current time.- Returns zeros before
startTimeand clamps tofullAmountonceelapsed >= duration. - It does not consider
depositedAmount. A stream created with a small buffer and topped up later can vest more than it holds — capcurrentlyWithdrawableatdepositedAmount - withdrawnAmountbefore showing it as claimable. - Throws if
duration <= 0or if the computed total is belowwithdrawnAmount.
Local development
git clone https://github.com/Zebec-Fintech-Labs/aleo_payroll_sdk.git
cd aleo_payroll_sdk
yarn install
cp .env.example .envFill .env (never commit it):
| Variable | Purpose |
| --- | --- |
| NETWORK | testnet or mainnet |
| ADMIN_PRIVATE_KEY / SENDER_PRIVATE_KEY / RECEIVER_PRIVATE_KEY | Role keys for e2e. PRIVATE_KEY is a fallback for any role |
| ENDPOINT | Aleo REST host. Default in code is https://api.provable.com/v2 |
| EXPLORER_URL | Explorer origin for logged tx links |
| PROVER_URI | Delegated proving, e.g. https://api.provable.com/prove/testnet |
| PROVER_API_KEY / PROVER_CONSUMER_ID | DPS credentials. Register with POST https://api.provable.com/consumers {"username":"<handle>"} |
| E2E_TOKEN_PROGRAM | Default test_usdcx_stablecoin |
| E2E_PRIORITY_FEE | Microcredits, default 100000 |
| E2E_CONFIG_NAME | Reuse an existing config (hashed name or …field literal). If unset, e2e initializes a new config |
| E2E_STREAM_ID | Optional target for getter tests |
| RUN_E2E | Set to true by the yarn e2e scripts |
yarn test:unit
yarn test:e2e:getters # reads only
yarn test:e2e:public # create → topup → pause/resume → withdraw → cancel → auto-withdraw
yarn test:e2e:private
yarn test:e2e:initialize-configyarn test:e2e:public skips initialize_config when E2E_CONFIG_NAME is set.
Build:
yarn build # emits dist/ (also runs on npm prepare)API map
Admin: initializeConfig, updateConfig, setTokenWhitelisted
Public streams: createStreamPublic, topupStreamPublic, pauseResumeStreamPublic, withdrawStreamPublic, withdrawStreamAutoPublic, cancelStreamPublic
Private streams: createStreamPrivate, topupStreamPrivate, pauseResumeStreamPrivate, withdrawStreamPrivate, withdrawStreamAutoPrivate, cancelStreamPrivate
Tokens: approveTokenPublic, transferTokenPublic, transferTokenPrivateToPublic, getComplianceProofs
State: getStream, getStreamAnchor, getStreamConfig, isTokenWhitelisted, listPublicStreams, listPrivateStreams, balances, findCredits / findToken / findTokenRecords
Crypto / units: configNameToField, signStreamTokenFee, verifyStreamTokenFeeSignature, randomField, toMicroUnits / fromMicroUnits, computeAutoWithdrawalFee, computeWithdrawableAmount, WITHDRAW_FREQUENCIES
License
MIT. Copyright Zebec Network.
