sdkWasm is the WebAssembly module of the transaction preparation library. noteSalt must stay the same for the life of every Plan you confirm. Store it with your Plans. Pass ensureNoteOwners to register the service’s private addresses before a Step spends to them.
Implement each store role over your database, then combine them with composePlanStore. This role keeps Plan documents in one table. planToJson turns each bigint into a string, and restorePlanValue turns it back.
import { planToJson, restorePlanValue, type Plan,} from '@arcanetech/privacy-sdk-automatization-plan';import { PlanNotFoundError, type PlanDocuments,} from '@arcanetech/privacy-sdk-automatization-service';type Query = (sql: string, params: unknown[]) => Promise<{ document: unknown }[]>;export function sqlPlanDocuments(query: Query): PlanDocuments { return { async savePlan(id, plan) { await query( `INSERT INTO plans (id, document) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET document = EXCLUDED.document`, [id, JSON.stringify(planToJson(plan))], ); }, async read(id) { const [row] = await query('SELECT document FROM plans WHERE id = $1', [id]); if (row === undefined) throw new PlanNotFoundError(); return restorePlanValue(row.document) as Plan; }, };}
Write the other roles the same way and pass all seven to composePlanStore:
import { composePlanStore, type PlanStore, type PlanStoreParts,} from '@arcanetech/privacy-sdk-automatization-service';export function createSqlPlanStore(parts: PlanStoreParts): PlanStore { return composePlanStore(parts);}
Deposit authorizations and the owner scalar of each Unspent note live in
DepositMaterial and UnspentNoteStore. Keep those tables on the service host.
depositToSign returns the invocation for a Deposit and the Soroban authorization entries the depositor signs. Pass a draft Plan for the Initial Deposit, or a Plan id for the next due Deposit.
import { confirm, depositToSign, type ConfirmDependencies, type DepositToSignDependencies,} from '@arcanetech/privacy-sdk-automatization-service';import type { Plan } from '@arcanetech/privacy-sdk-automatization-plan';export async function initialSigning(plan: Plan, deps: DepositToSignDependencies) { return depositToSign(plan, deps);}export async function confirmSigned( plan: Plan, sorobanAuthorization: string, deps: DepositToSignDependencies & ConfirmDependencies,) { const { invocation } = await depositToSign(plan, deps); return confirm(plan, { invocation, sorobanAuthorization }, deps);}
depositToSign throws DepositStepNotDueError when the Plan has no Deposit waiting, and DepositRefusedError when the Fee quote is above the Step’s Free tokens or the compliance check refuses it. For a later Deposit, pass the same invocation and the signed entries to authorizeDeposit.
earliestOpenStep returns the first Step that is not executed. Check it against the pool, the fee quote, and the Deposit signature.
import { isBlockedStep } from '@arcanetech/privacy-sdk-automatization-plan';import { earliestOpenStep, locateSteps, quoteRequest, type StepDependencies,} from '@arcanetech/privacy-sdk-automatization-service';export async function nextStepStatus(id: string, deps: StepDependencies) { if (await deps.store.isCancelled(id)) return { status: 'cancelled' } as const; const report = await deps.presence.report(); const steps = locateSteps(await deps.store.read(id)); const next = earliestOpenStep(steps, report); if (next === undefined) return { status: 'complete' } as const; const { place, step } = next; if (isBlockedStep(step, report)) return { status: 'blocked', place } as const; if (step.transactionId) return { status: 'submitted', place, transactionId: step.transactionId } as const; const quote = await deps.feeQuotes.quote( quoteRequest(step, next.publicDeposit, next.publicWithdrawal), ); if (quote.amount > step.freeTokens) return { status: 'feeAboveFreeTokens', place, quote } as const; const prepared = await deps.store.preparedDeposit(id, place); if ( place.role === 'deposit' && prepared?.invocation.root !== (await deps.currentRoot.current()) ) { return { status: 'awaitingSignature', place } as const; } const previous = steps[next.index - 1]; const startedAt = previous ? await deps.store.executedAt( id, `${previous.place.rowIndex}:${previous.place.role}`, ) : await deps.store.confirmedAt(id); return { status: 'scheduled', place, dueAt: startedAt === undefined ? undefined : startedAt + step.interval, } as const;}
awaitingSignature means the Deposit has no signature for the current pool root. scheduled has no dueAt until the service records when the previous Step executed. runDueSteps records it on its next pass.
Build the transaction for a scheduled Step without submitting it. A Deposit reuses the invocation the depositor signed. Any other Step asks your proof port.
Do not submit this preview yourself. maskedOutputs can take a new Unspent note
scalar on every call, and the store keeps only the scalar submitNextStep or
runDueSteps used.
runDueSteps submits the next due Step of every Plan and honors intervals. submitNextStep submits one Plan’s next Step and ignores its interval. Both store the transaction id and the Unspent note.
import { runDueSteps, type StepDependencies,} from '@arcanetech/privacy-sdk-automatization-service';export function runOnSchedule(deps: StepDependencies, everyMs: number) { let running = Promise.resolve(); return setInterval(() => { running = running.then(() => runDueSteps(deps)).catch(console.error); }, everyMs);}
Run one pass at a time, so two passes never submit the same Step. A Step whose submission returns ok: false stays unsubmitted, and the next run tries it again.