Exact stroop conversion and per-test service isolation (closes #88, #91) - #127
Exact stroop conversion and per-test service isolation (closes #88, #91)#127Spagero763 wants to merge 3 commits into
Conversation
SDK — toStroops floating-point precision (Stellar-Deejah#88) toStroops used BigInt(Math.round(amount * 10_000_000)). Multiplying a float by 10^7 is lossy, and it also accepted NaN/Infinity, which reached BigInt() and threw a raw TypeError that bypassed SDKError classification. - toStroops now accepts string | number. Strings are parsed as decimals and assembled with BigInt arithmetic (split on '.', pad the fraction to 7 places), so no float multiplication occurs. Numbers are rendered with toFixed(7) first. - Explicit SDKError('INVALID_AMOUNT') for NaN, Infinity, negatives, malformed input, more than 7 decimal places, and values exceeding i128. - fromStroops handles negatives and is now covered by tests. backend — service test isolation (Stellar-Deejah#91) escrowService and enrollmentService read and wrote a shared module-level store, so state leaked between tests and results depended on execution order. vi.resetModules() does not help: it only clears the registry for future dynamic imports, while already-imported bindings keep their state. - Both services are now built by createEscrowService(store?) / createEnrollmentService(store?), which close over an injected adapter. - Each module still exports a singleton bound to the shared adapter, so route handlers and every existing import keep working unchanged. - Their tests construct a fresh service with a new MemoryAdapter in beforeEach; vi.resetModules() is gone. Verification: sdk tests/utils.test.ts 27 passed; backend service tests 17 passed; full backend suite unchanged at 75 passed (the 6 failing files fail to load on main because @lineproof/sdk cannot build - see note in the PR). Closes Stellar-Deejah#88 Closes Stellar-Deejah#91
|
@Spagero763 is attempting to deploy a commit to the Deejah Team on Vercel. A member of the Team first needs to authorize it. |
The SDK could not be parsed, let alone built: a bad merge left duplicated
import blocks with orphaned members and no `import {` opener, so tsc reported
"Expression expected" / esbuild reported Unexpected "}". This blocked the SDK
Lint, SDK Tests and Backend jobs (the backend cannot resolve @lineproof/sdk
when the package will not build), which is why this PR's checks were red.
- client.ts, enrollment.ts, identity.ts, queue.ts, escrow.ts: merged each
orphaned import fragment into the real import and removed the duplicate
block.
- queue.ts: removed a stale, unclosed Horizon-based `advance()` that had been
left in front of the current Soroban implementation (two methods of the same
name, the first missing its closing brace).
- client.ts: `getLedgerEntries()` returns a decoded `val`, not a base64 `xdr`
string; and `readOnly()` no longer passes an explicit `privateKey: undefined`,
which `exactOptionalPropertyTypes` rejects.
- queue.ts: omit `advancedAt` instead of assigning undefined, same reason.
- dropped the now-unused imports these changes exposed.
- tests/client.test.ts: the `@stellar/stellar-sdk` mock spread the Keypair
class (`...actual.Keypair`), which silently drops non-enumerable statics such
as `fromSecret`. It now inherits them via the prototype chain.
Result: `tsc --noEmit` is clean (was 20+ parse errors), `pnpm build` succeeds
(was impossible), and the SDK suite goes from 3 files unable to load / 49 tests
to 58 passing. The 9 remaining failures are pre-existing test fixtures that use
invalid Stellar secrets and contract ids; they only became visible now that the
code actually executes, and are not addressed here.
CI status after the SDK repairI pushed a commit that repairs the merge-damaged SDK sources, because they were the direct cause of this PR's red checks: the package could not be parsed, so SDK Lint & Typecheck, SDK Tests and every Backend job (which needs to resolve Now green: The remaining red checks are pre-existing on
|
| Check | Cause (all pre-existing) |
|---|---|
| Rust Lint / Rust Tests / Cargo Audit | The contracts do not compile at all: #[contract] is applied to a trait instead of a struct in all five crates. |
| Backend Lint & Typecheck | 11 errors in config.ts (exactOptionalPropertyTypes) and duplicate identifiers in index.ts. |
| Backend Tests | The route tests call enrollmentService.enroll, which has never existed (the service exports enrollIdentity). Confirmed against origin/main: zero matches. They were previously invisible because the module failed to load. |
| SDK Tests | 9 remaining failures are test fixtures using invalid Stellar secrets and contract ids (e.g. CQUEUE123). Same story: they only execute now that the code parses. |
| Dependency Review / pnpm Audit / Vercel | Repository settings and deploy authorization, not code. |
Note that fixing the SDK increased the visible failure count in a couple of jobs. That is expected: tests that previously could not load now actually run and fail on their own pre-existing problems. The repo is objectively healthier than before.
This PR's own tests
sdk tests/utils.test.ts 27 passed (#88)
backend escrow/enrollment service 17 passed (#91)
I have a separate branch (fix/contracts-compile-and-events) that already gets all five contracts building and implements #83's event payloads; happy to open it once its suite is green. The backend typecheck and route-test issues are also easy follow-ups if you'd like them as separate PRs.
Summary
Closes #88
Closes #91
Two self-contained fixes: exact stroop conversion in the SDK, and real
per-test isolation for the backend services.
#88 —
toStroops()floating-point precisiontoStroopswasBigInt(Math.round(amount * 10_000_000)).The concrete problem. A JS number has a 53-bit mantissa, and
0.1is notexactly representable in binary. Multiplying by
10_000_000compounds thatrepresentation error, and
Math.roundthen hides it — the caller gets a valuethat is silently off, with no error. It also accepted
NaN/Infinity:Math.round(NaN)isNaN,BigInt(NaN)throws a rawTypeError, so thefailure bypassed the SDK's
SDKErrorclassification entirely.The fix — manual decimal parsing, no float math:
string | number. Strings are parsed exactly; numbers go through.toFixed(7)first so a float is never multiplied by10^7.SDKError('INVALID_AMOUNT')forNaN,Infinity, negatives, malformedinput, >7 decimal places, and values exceeding i128 (Soroban's amount type).
fromStroopsnow handles negatives and is covered by tests.Precision note: Stellar/Soroban amounts are fixed-point with 7 decimal
places (1 unit = 10^7 stroops) — uniform across assets, including issued ones.
There is no per-asset precision to special-case.
Acceptance
toStroops('0.0000001')→1ntoStroops(0.0000001)→1n(no truncation)toStroops(NaN)/toStroops(Infinity)throwSDKError('INVALID_AMOUNT')fromStroops(1n)→'0.0000001',fromStroops(10_000_000n)→'1'fromStroops(toStroops('123.4567890')) === '123.456789'sdk/tests/utils.test.ts— 27 passed#91 — service test isolation
escrowServiceandenrollmentServiceread and wrote a shared module-levelstore, so state leaked between tests and results depended on execution order.
Why
vi.resetModules()doesn't fix it: it only clears the module registryfor future dynamic
import()calls. Bindings that were already imported atthe top of the test file still point at the original module instance, which
keeps its state. The hook ran and changed nothing.
The fix — factory + singleton:
Tests build a fresh service per test; production keeps the singleton, so route
handlers and every existing import are untouched.
Demonstrating the isolation:
depositEscrowthrows409 Duplicate escrow recordfor a repeatedqueueId:identity. Previously, two tests using the samepair passed or failed depending on order, and
vi.resetModules()did not help.With a fresh adapter per test each one starts empty, so the same ids are now
reusable across tests and order is irrelevant.
Compatibility with the planned persistence layer (#9): this is the same seam.
The factory takes the store as a parameter, so swapping
MemoryAdapterfor aPostgres-backed adapter is a constructor argument, not a source change — which is
exactly what the persistence work needs.
Acceptance
createEscrowService(store?)+ default singletoncreateEnrollmentService(store?)+ default singletonbeforeEach; novi.resetModules()Partially addressed:
queueServicealso has a module-level fixture array.Its factory is not in this PR — it seeds fixtures at module load and is entangled
with the contract-adapter read path, so it belongs with the contract work rather
than bolted on here. The two services named in the issue title are done.
Note: pre-existing breakage on
main(not from this PR)This branch touches only
sdk/src/utils.ts,sdk/tests/utils.test.ts, and thetwo backend services + their tests. The following already fail on
main:sdk/src/client.tshas a syntax error (line 18: Unexpected "}"), so@lineproof/sdkcannot build. That is whytests/client.test.ts,integration.test.ts,sdk.test.tsand 6 backend test files fail to load.#[contract]is applied to a traitinstead of a struct in all five crates, plus ~20 type errors and soroban-sdk 22
API drift (
deploy()returnsAddress, notBytesN<32>).backend/src/index.tshas duplicate identifiers andconfig.tsfailsexactOptionalPropertyTypes.I have a local branch that already gets all five contracts building and
implements #83's event payloads; it will follow once its test suite is green
(those tests call contract fns outside
env.as_contract(), so they needrewriting before
cargo test --workspacecan pass).