Skip to content
 
 

Latest commit

 

History

354 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ZK Payroll SDK

TypeScript SDK for interacting with the ZK Payroll smart contracts.

Installation

npm install @zk-payroll/sdk

Usage

The SDK provides configuration presets for common environments to simplify initialization:

import { PayrollService, ConfigPresets } from "@zk-payroll/sdk";

// Initialize config for a specific environment
const config = ConfigPresets.testnet()
  .withContractId("CCONTRACT_ID...")
  .withProofConfig({
    wasmUrl: "https://cdn.example.com/payroll_circuit.wasm",
    zkeyUrl: "https://cdn.example.com/payroll_circuit.zkey",
  })
  .build(); // Validates required fields

// Initialize service
const service = new PayrollService(config);

// Process a private payment
await service.processPayment(
  "G...", // Recipient Stellar address
  1000n   // Amount
);

Configuration & Schema Validation

The SDK enforces strict schema validation for all configuration parameters (network, rpcUrl/networkUrl, contractId/contractIds, proofConfig, retryPolicy, and featureFlags) before operations run.

Minimal Valid Config Example

import { ConfigPresets, validateConfig, assertValidConfig } from "@zk-payroll/sdk";

// Using ConfigPresets for a minimal valid setup:
const config = ConfigPresets.testnet()
  .withContractId("CAKZGMMMJOHMSZ5V3DYKCUDNTIWBG57MAMFJDSVICNWUNVXLX6EZN3NC")
  .build();

// Direct schema validation utility
const validation = validateConfig(config);
if (!validation.isValid) {
  console.error("Config errors:", validation.errors);
}

// Asserts configuration validity or throws structured ValidationError
assertValidConfig(config);

Invalid Configuration Handling

The ConfigBuilder and assertValidConfig fail fast with structured ValidationError (code: CONFIG_VALIDATION_ERROR) if required fields are missing or malformed:

// Throws ValidationError: "Configuration validation failed:\n- contractId is malformed: invalid_id"
ConfigPresets.testnet().withContractId("invalid_id").build();

Idempotent retries

For safe retries, pass an idempotencyKey when processing a payment.

import { PayrollService, createPaymentIdempotencyKey } from "@zk-payroll/sdk";

const idempotencyKey = createPaymentIdempotencyKey({
  recipient: "G...",
  amount: 1000n,
  asset: "native",
});

await service.processPayment({
  recipient: "G...",
  amount: 1000n,
  asset: "native",
  idempotencyKey,
});

Features

  • Typed Contract Clients: Fully typed client wrappers for PayrollRegistry, SalaryCommitment, ProofVerifier, and PaymentExecutor contracts.
  • ZK Proof Generation: Client-side proof generation using snarkjs for privacy.
  • Caching: Built-in caching for proofs and circuit artifacts.
  • Error Handling: Robust error typing and management.
  • Mock Testing Environment: Comprehensive testing utilities for unit tests without a live network.
  • Setup Checklist Generator: Generates a pre-payroll integration checklist covering config, network, contracts, treasury, proofs, wallet, and test fixtures.

Browser and Backend Usage

Use this section to pick the right environment before you wire the SDK into a product. The package supports browsers (wallets, dashboards) and Node.js backends (workers, automation), but secrets and signing paths differ.

Quick matrix

Concern Browser (frontend) Backend (Node worker / service)
Wallet signing User wallets (Freighter, Albedo) via adapters Server-held keys from a secrets manager — not browser extensions
Proof generation On-device with snarkjs; prefer Web Workers so the UI stays responsive On the worker/process for batch payroll; good for heavy or unattended jobs
Secrets / witnesses Never embed Stellar secret keys (S…) or long-lived note secrets in frontend code, env shipped to the client, or localStorage Load signer material from env / KMS / secrets manager; never log full witnesses
Next.js / SSR Import SDK only in Client Components ("use client") Do not import the browser wallet path in Server Components or Route Handlers for UI signing
Best fit Employee/employer dashboards, interactive connect + sign Payroll automation, queues, retries, multi-payment workers

Supported runtime versions: Runtime Support Matrix.

Browser (short guidance)

Interactive apps should connect a wallet, generate or request proofs without blocking the main thread when possible, and keep sensitive material off the client bundle.

// Frontend / Next.js Client Component — "use client"
import { FreighterAdapter } from "@zk-payroll/sdk";

const wallet = new FreighterAdapter();
if (wallet.isAvailable()) {
  const publicKey = await wallet.connect();
  // Build XDR with public flows; sign via the adapter — never with a hardcoded secret key
  // const signed = await wallet.signTransaction(xdr);
}

Backend (short guidance)

Backend services own automation: queue jobs, generate proofs on the server, sign with keys that never leave the host, and submit to RPC.

// Node worker — secrets from environment / secrets manager only
import { PayrollService, ConfigPresets } from "@zk-payroll/sdk";

const config = ConfigPresets.testnet()
  .withContractId(process.env.CONTRACT_ID!)
  .withProofConfig({
    wasmUrl: process.env.WASM_URL!,
    zkeyUrl: process.env.ZKEY_URL!,
  })
  .build();

const service = new PayrollService(config);
// Signer secret: process.env / KMS — never commit S… keys or put them in browser env

Secret handling (both environments)

Do Don't
Keep note secret / nullifier inputs in memory only as long as needed for proving Log witnesses, proofs with private inputs, or secret keys
Use HTTPS CDN or authenticated artifact hosts for .wasm / .zkey Ship production signing keys in frontend env (NEXT_PUBLIC_*, Vite VITE_*, etc.)
Rotate and scope backend signer keys; prefer HSM/KMS where possible Reuse a single hot key across untrusted multi-tenant frontends

Proof APIs and witness shapes: ZK Proof Generation.

Related docs

Zero-Knowledge Proof Generation

The SDK includes production-ready ZK proof generation using snarkjs:

import { SnarkjsProofGenerator, MemoryCacheProvider } from "@zk-payroll/sdk";

// Configure circuit artifacts
const config = {
  wasmUrl: "https://cdn.example.com/payroll_circuit.wasm",
  zkeyUrl: "https://cdn.example.com/payroll_circuit.zkey",
  artifactCacheTTL: 86400, // 24 hours
};

// Create generator with caching
const cache = new MemoryCacheProvider<string>();
const generator = new SnarkjsProofGenerator(config, cache);

// Generate proof
const witness = {
  recipient: "GDZQHV...",
  amount: 1000000n,
  nullifier: 123456789n,
  secret: 987654321n,
};

const proof = await generator.generateProof(witness);

See ZK Proof Generation Guide for detailed documentation.

Backend Worker Quickstart

Teams building internal payroll automation workers can follow the Backend Worker Quickstart for a practical end-to-end prototype covering setup, polling, retries, and event handling.

Testing

The SDK includes a powerful mock testing environment for writing unit tests:

import { MockContractEnvironment, MockPayrollContract } from "@zk-payroll/sdk";

const mockEnv = new MockContractEnvironment();
mockEnv.expectInvoke("deposit").toReturn("tx_hash_123");

const mockContract = new MockPayrollContract(mockEnv);
const txHash = await mockContract.deposit(1000n);

See the Testing Guide for complete documentation.

Examples

Runnable examples covering two core use cases are in the examples/ directory. Each example works out of the box in demo mode (no Stellar node required) and switches to a live network automatically when the relevant environment variables are set.

Employee Onboarding

examples/employee-onboarding.ts

Shows how to onboard a new employee: verify they have no existing payroll account, fund it with an initial allocation, and confirm the deposit was recorded.

npx tsx examples/employee-onboarding.ts

Payroll Execution

examples/payroll-execution.ts

Shows how to run a full private payroll batch: configure SnarkjsProofGenerator with circuit artifacts and caching, wire up PayrollService, process multiple payments, and report results.

npx tsx examples/payroll-execution.ts

Configuration

Copy the environment variable template and fill in your values to run against a live network:

cp examples/.env.example examples/.env
# edit examples/.env
source examples/.env && npx tsx examples/payroll-execution.ts

See examples/.env.example for all available variables.

Typed Contract Clients

The SDK provides typed client wrappers for the core ZK Payroll contracts. Each client exposes typed methods that encode arguments and decode responses automatically.

PayrollRegistryClient

import { PayrollRegistryClient, rpc } from "@zk-payroll/sdk";

const server = new rpc.Server("https://soroban-testnet.stellar.org");
const client = new PayrollRegistryClient(server, "CCONTRACT_ID...");

// Register a payroll relationship
await client.register(
  { employer: "G...", employee: "G...", salary: 1000n, token: "C...", metadata: "engineering" },
  signer
);

// Query a registry entry
const entry = await client.getRegistry("G...", "G...", signer);
console.log(entry.salary, entry.active);

// List employees
const employees = await client.getEmployees("G...", 0, 10, signer);

// Check if a registry exists
const exists = await client.registryExists("G...", "G...", signer);

SalaryCommitmentClient

import { SalaryCommitmentClient } from "@zk-payroll/sdk";

const client = new SalaryCommitmentClient(server, "CCONTRACT_ID...");

// Commit to a salary amount (hidden via hash)
await client.commit(
  { employer: "G...", employee: "G...", commitmentHash: "abcd...", cycleId: 1n },
  signer
);

// Retrieve a commitment
const commitment = await client.getCommitment("G...", "G...", 1n, signer);

// Batch commit multiple salaries
await client.batchCommit("G...", [
  { employee: "G...1", commitmentHash: "abcd", cycleId: 1n },
  { employee: "G...2", commitmentHash: "ef01", cycleId: 1n },
], signer);

// Verify a commitment against a ZK proof
const isValid = await client.verifyCommitment("G...", "G...", 1n, proof, signer);

// Reveal the actual salary
await client.revealSalary("G...", "G...", 1n, 1000n, signer);

Payload Normalization

Consumers rarely pass payroll data in exactly one shape (different key names, extra whitespace, comma-formatted amounts, mixed-case addresses). normalizePayrollPayload converts any of these variations into the SDK's canonical entry shape before validation, proof preparation, or transaction building:

import { normalizePayrollPayload } from "@zk-payroll/sdk";

const { entries, issues } = normalizePayrollPayload({
  entries: [
    { employee_id: "  E-42  ", wallet: "gabc...", asset: "xlm", amount: "1,000.50" },
  ],
});

// entries[0] => { employeeId: "E-42", walletAddress: "GABC...", asset: "native", amount: "1000.50", source: {...} }

// Required fields (employeeId, walletAddress, asset, amount) are never silently dropped —
// missing/unparseable data shows up as an indexed issue instead, with the original
// input still reachable via entries[issue.index].source.raw for clear validation errors.
if (issues.length > 0) {
  console.log(issues);
}

See the Payload Normalization Guide for the full field-by-field normalization rules.

Batch Payload Validation

The SDK automatically validates batch payroll payloads before submitting them to contracts, preventing empty batches, duplicate recipients, and invalid amounts:

import { BatchPayloadBuilder, validateBatchPayload, PayrollValidation } from "@zk-payroll/sdk";

const entries = [
  { recipient: "GABC...", amount: 1000n, asset: "native" },
  { recipient: "GDEF...", amount: 2000n, asset: "native" },
];

// Validate entries before building
const errors = validateBatchPayload(entries);
if (errors.length === 0) {
  const payload = new BatchPayloadBuilder().addMany(entries).build();
  // Safe to submit payload.entries
} else {
  // Structured errors returned for UI display (code, message, field, index)
  console.log(errors);
}

ProofVerifierClient

import { ProofVerifierClient } from "@zk-payroll/sdk";

const client = new ProofVerifierClient(server, "CCONTRACT_ID...");

// Verify a ZK proof on-chain
const valid = await client.verify(
  { pi_a: ["1","2"], pi_b: [["3","4"],["5","6"]], pi_c: ["7","8"], publicSignals: ["sig1"] },
  ["input1"],
  1, // verification key ID
  signer
);

// Add a new verification key
const vkId = await client.addVerificationKey("aabbcc...", "groth16 key", signer);

// Get active verification key
const activeId = await client.getActiveVerificationKeyId(signer);

// Get verification key info
const info = await client.getVerificationKeyInfo(1, signer);

PaymentExecutorClient

import { PaymentExecutorClient } from "@zk-payroll/sdk";

const client = new PaymentExecutorClient(server, "CCONTRACT_ID...");

// Execute an immediate payment
const result = await client.execute(
  { recipient: "G...", amount: 1000n, asset: "C...", memo: "salary" },
  signer
);
console.log("Transaction:", result.txHash);

// Schedule a future payment
const scheduled = await client.schedule(
  { recipient: "G...", amount: 500n, asset: "C...", executeAt: 1700000000, memo: "bonus" },
  signer
);
console.log("Payment ID:", scheduled.paymentId);

// Cancel a scheduled payment
await client.cancel(scheduled.paymentId, signer);

// Get pending payments
const payments = await client.getPendingPayments("G...", 0n, 20, signer);

Environment Sanity Checker

To catch configuration integration problems (such as misconfigured RPC endpoints, invalid contract IDs, or missing/unreachable circuit artifacts) before starting runtime work, the SDK provides the validateEnvironment helper:

import { validateEnvironment } from "@zk-payroll/sdk";

const clientConfig = {
  networkUrl: "https://soroban-testnet.stellar.org",
  contractId: "CCONTRACT_ID...",
};

const proofConfig = {
  wasmUrl: "https://cdn.example.com/payroll_circuit.wasm",
  zkeyUrl: "https://cdn.example.com/payroll_circuit.zkey",
};

const result = await validateEnvironment(clientConfig, proofConfig);

if (!result.isValid) {
  console.error("Environment check failed!");
  for (const diagnostic of result.diagnostics) {
    if (diagnostic.status === "error") {
      console.error(`- [${diagnostic.component}] ${diagnostic.message}`);
    }
  }
} else {
  console.log("Environment is ready!");
}

Diagnostic Result Structure

validateEnvironment returns a SanityCheckResult containing:

  • isValid: boolean - true if all validations pass with no errors.
  • diagnostics: DiagnosticEntry[] - List of diagnostics for each checked component.

Each DiagnosticEntry contains:

  • component: "rpc" | "contract" | "artifacts" - The checked component.
  • status: "success" | "warning" | "error" - The validation status.
  • message: string - Actionable diagnostic message explaining the result.
  • error?: Error - The caught error object, if any.
  • details?: Record<string, unknown> - Extra context (e.g. network passphrases or RPC response details).

Payroll Setup Checklist

Before running payroll, use generateSetupChecklist to generate an integration checklist covering config, network, contracts, treasury, proofs, wallet, and test fixtures:

import { generateSetupChecklist } from "@zk-payroll/sdk";

const checklist = generateSetupChecklist(config, {
  expectedNetworkPassphrase: "Test SDF Network ; September 2015",
  rpcReachable: true,             // result of validateEnvironment(config)
  networkPassphrase: "Test SDF Network ; September 2015",
  contractDeployed: true,         // result of validateEnvironment(config)
  treasury: { treasuryAddress: "G...", funded: true },
  wallet: { name: "Freighter", isAvailable: true, isConnected: true, network: "testnet" },
  testFixturesAvailable: true,
});

if (!checklist.isReady) {
  for (const blocker of checklist.blockers) {
    console.error(`[${blocker.category}] ${blocker.message}${blocker.remediation}`);
  }
}

Each check returns pass, warn, or fail with an actionable remediation. See the Setup Checklist Guide for full details.

Audit-Safe Debug Snapshot

To troubleshoot issues without exposing private payroll data, capture an audit-safe snapshot of SDK configuration and runtime state:

import { createDebugSnapshot } from "@zk-payroll/sdk";

const { snapshot, redactedFieldCount, redactedKeys } = await createDebugSnapshot({
  config: client.getConfig(),
  state: { pendingPayments, draft, signerSecret: signer.secret() },
});

console.log(`Redacted ${redactedFieldCount} fields: ${redactedKeys.join(", ")}`);
console.log(JSON.stringify(snapshot)); // safe to attach to a support ticket

The snapshot is always JSON-serializable (BigInt, dates, cycles handled), redacts sensitive payroll fields recursively, and carries an integrity hash verifiable via verifyDebugSnapshot. See the Audit-Safe Debug Snapshot guide.

Network Request Timing

To diagnose slow RPC or API paths, wrap your rpc.Server with createTimedRpcServer and read back the timing metadata — without changing any existing response behavior:

import { rpc } from "@stellar/stellar-sdk";
import { createTimedRpcServer } from "@zk-payroll/sdk";

const server = createTimedRpcServer(new rpc.Server(rpcUrl));
// Pass `server` to the SDK wherever it accepts an rpc.Server.

// Existing behavior is unchanged.
// ... run payroll operations ...

const stats = server.getNetworkTimingStats();
if (stats.byOperation.simulateTransaction?.avgDurationMs > 1500) {
  console.warn("simulateTransaction is slow; consider a closer RPC endpoint");
}

HTTP(S) artifact fetches can be timed per-request with timeAxiosRequest or globally (opt-in) with installAxiosTiming. Timing metadata is attached to responses as a non-enumerable symbol and never includes payloads or private payroll values. See the Network Request Timing guide.

Multi-Asset Support

The SDK provides a centralised AssetRegistry that maps asset identifiers to labels, decimal precision, and display behaviour. Applications can extend it with any custom Soroban token.

import { AssetRegistry, formatAmount, parseAmount } from "@zk-payroll/sdk";

// Use a built-in asset (native XLM, USDC, EUROC ship pre-registered)
const xlm = AssetRegistry.getOrThrow("native");
formatAmount(10_000_000n, xlm); // "1.0000000 XLM"

// Register a custom Soroban token
AssetRegistry.register({
  id: "CTOKEN_CORP123",
  symbol: "CORP",
  label: "Corp Company Token",
  decimals: 7,
});

const corp = AssetRegistry.getOrThrow("CTOKEN_CORP123");
parseAmount("500.00 CORP", corp); // 3_500_000_000n

Canonical amount normalization

Before submitting any amount to a contract, batch builder, or commitment hash, normalize it into the asset's canonical smallest-unit bigint. normalizeCanonicalAmount accepts any of the loose shapes payroll data arrives in (string, number, bigint) and either an asset id string (resolved via the registry) or an AssetMetadata object:

import {
  normalizeCanonicalAmount,
  tryNormalizeCanonicalAmount,
  RoundingMode,
} from "@zk-payroll/sdk";

// Throwing variant — canonical { amount, decimals, assetSymbol, assetId, wasRounded, original }
const { amount, assetSymbol, wasRounded } = normalizeCanonicalAmount(
  "  $1,000.50 XLM  ", // formatted string with currency symbol + whitespace
  "native",             // asset id resolved via AssetRegistry
  { rounding: RoundingMode.HALF_UP }
);
// amount      => 10_005_000_000n (canonical stroops)
// assetSymbol => "XLM"
// wasRounded  => false  (input has 2 decimals, XLM has 7 — no rounding)

// bigint inputs are already canonical (matches formatAmount) — no double-scaling
normalizeCanonicalAmount(10_005_000_000n, "native").amount; // 10_005_000_000n

// Non-throwing variant — discriminated { ok, value | error }
const result = tryNormalizeCanonicalAmount(input, "USDC");
if (!result.ok) {
  console.warn(result.error.code, result.error.message);
} else {
  submit(result.value.amount);
}

Use normalizeCanonicalAmount instead of manually scaling strings, so submissions always carry the canonical precision the contract expects.

See the Multi-Asset Guide for the full API reference, isolation patterns for tests, and rules for extending the registry in production.

Employee Eligibility & Reason Codes

The SDK includes a comprehensive, typed employee eligibility evaluation engine to inspect employee records before payroll resolution. It returns typed, machine-readable reason codes that make it easy for dashboards and backends to explain blocked recipients without exposing private payroll amounts.

import {
  evaluateEmployeeEligibility,
  evaluateBatchEligibility,
  filterEligibleEmployees,
  formatEligibilityReport,
  EligibilityReasonCode,
  EmployeeRegistry,
} from "@zk-payroll/sdk";

// Single employee evaluation
const result = evaluateEmployeeEligibility({
  employeeId: "EMP-001",
  recipient: "GA...",
  salary: 5000000000n,
  asset: "native",
  status: "suspended",
});

if (!result.isEligible) {
  console.log(result.primaryReasonCode); // "EMPLOYEE_SUSPENDED"
  console.log(result.reasons[0].action); // "Resolve the administrative suspension prior to releasing payroll disbursements."
}

// Batch evaluation with privacy-safe diagnostic summary for dashboards
const batch = evaluateBatchEligibility([
  { employeeId: "EMP-001", recipient: "GA...", salary: 5000000000n, asset: "native" },
  { employeeId: "EMP-002", recipient: "", salary: 2000000000n, asset: "native" },
]);

const report = formatEligibilityReport(batch);
// All salaries and secrets remain redacted in logs/telemetry:
console.log(report.reasonSummary); // { MISSING_RECIPIENT_ADDRESS: 1 }

Typed Reason Codes Catalog

Reason Code Category Description Suggested Remediation
MISSING_RECIPIENT_ADDRESS Identity Destination Stellar address is missing or empty Provide a valid Stellar G... address
INVALID_RECIPIENT_ADDRESS Identity Address format is malformed or invalid Correct public key address format
MISSING_EMPLOYEE_ID Identity Employee record identifier is missing Assign a unique employee ID
DUPLICATE_EMPLOYEE_ID Identity Duplicate employee ID within payroll batch Deduplicate employee records in batch
DUPLICATE_RECIPIENT_ADDRESS Identity Duplicate destination address within batch Ensure unique recipient per payout batch
INACTIVE_EMPLOYEE_STATUS Status Account is flagged as inactive Activate employee in HR/payroll system
EMPLOYEE_SUSPENDED Status Account is currently suspended Resolve administrative suspension
EMPLOYEE_TERMINATED Status Employee is terminated / offboarded Settle through severance workflows
EFFECTIVE_DATE_FUTURE Status Employment start date is in the future Schedule payout for on/after effective date
EFFECTIVE_DATE_EXPIRED Status Contract period ended before payroll cycle Renew contract or adjust cycle bounds
ZERO_OR_NEGATIVE_SALARY Compensation Payout amount is zero or negative Set positive non-zero payout amount
SALARY_EXCEEDS_MAX_LIMIT Compensation Salary exceeds configured maximum ceiling Split transaction or request limit elevation
SALARY_BELOW_MIN_LIMIT Compensation Salary is below minimum payout threshold Combine into next cycle or verify amount
MISSING_ASSET_IDENTIFIER Compensation Asset identifier is missing Specify token address or "native"
UNSUPPORTED_ASSET Compensation Asset is not permitted in configuration Use an approved asset token
COMPLIANCE_BLOCKED Compliance Failed KYC/AML verification or policy hold Complete identity verification
PAYROLL_LOCKED Compliance Administrative or security hold active Release lock in dashboard
SANCTION_LISTED Compliance Recipient matches prohibited sanctions list Escalate to compliance/legal team
REGISTRY_RECORD_NOT_FOUND Registry No on-chain registry record found Register employee with contract first
REGISTRY_RECORD_DEACTIVATED Registry On-chain registry record is deactivated Reactivate registry entry on-chain
REGISTRY_SALARY_MISMATCH Registry Resolution salary differs from on-chain Update registry salary on-chain
REGISTRY_TOKEN_MISMATCH Registry Payment token differs from on-chain token Use registered token contract
CUSTOM_INELIGIBILITY_RULE Custom Custom user-defined eligibility rule failed Review custom business policy criteria

Documentation

Development

# Install dependencies
npm install

# Build
npm run build

# Run tests
npm test

# Lint
npm run lint

Having trouble? See the Troubleshooting Guide.

About

No description, website, or topics provided.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages