Skip to content

Exact stroop conversion and per-test service isolation (closes #88, #91) - #127

Open
Spagero763 wants to merge 3 commits into
Stellar-Deejah:mainfrom
Spagero763:fix/stroops-precision-service-isolation
Open

Exact stroop conversion and per-test service isolation (closes #88, #91)#127
Spagero763 wants to merge 3 commits into
Stellar-Deejah:mainfrom
Spagero763:fix/stroops-precision-service-isolation

Conversation

@Spagero763

Copy link
Copy Markdown
Contributor

Summary

Closes #88
Closes #91

Two self-contained fixes: exact stroop conversion in the SDK, and real
per-test isolation for the backend services.

Scope note: #83 and #86 are not in this PR. Both require
cargo test --workspace to pass, and the contracts do not compile on main
at all (details at the bottom). That is a much larger repair than either issue
describes and deserves its own review, so it is coming as a separate PR.


#88toStroops() floating-point precision

toStroops was BigInt(Math.round(amount * 10_000_000)).

The concrete problem. A JS number has a 53-bit mantissa, and 0.1 is not
exactly representable in binary. Multiplying by 10_000_000 compounds that
representation error, and Math.round then hides it — the caller gets a value
that is silently off, with no error. It also accepted NaN/Infinity:
Math.round(NaN) is NaN, BigInt(NaN) throws a raw TypeError, so the
failure bypassed the SDK's SDKError classification entirely.

The fix — manual decimal parsing, no float math:

const [whole, fraction = ''] = text.split('.');
const stroops = BigInt(whole) * 10_000_000n
              + BigInt(fraction.padEnd(7, '0') || '0');
  • Accepts string | number. Strings are parsed exactly; numbers go through
    .toFixed(7) first so a float is never multiplied by 10^7.
  • SDKError('INVALID_AMOUNT') for NaN, Infinity, negatives, malformed
    input, >7 decimal places, and values exceeding i128 (Soroban's amount type).
  • fromStroops now 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')1n
  • toStroops(0.0000001)1n (no truncation)
  • toStroops(NaN) / toStroops(Infinity) throw SDKError('INVALID_AMOUNT')
  • accepts string and number
  • fromStroops(1n)'0.0000001', fromStroops(10_000_000n)'1'
  • round-trip fromStroops(toStroops('123.4567890')) === '123.456789'
  • sdk/tests/utils.test.ts27 passed

#91 — service test isolation

escrowService and enrollmentService read and wrote a shared module-level
store
, so state leaked between tests and results depended on execution order.

Why vi.resetModules() doesn't fix it: it only clears the module registry
for future dynamic import() calls. Bindings that were already imported at
the 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:

export function createEscrowService(store: MemoryAdapter = new MemoryAdapter()) {  }
export const escrowService = createEscrowService(defaultMemoryAdapter);
export const depositEscrow = escrowService.depositEscrow;   // unchanged import surface

Tests build a fresh service per test; production keeps the singleton, so route
handlers and every existing import are untouched.

beforeEach(() => { svc = createEscrowService(new MemoryAdapter()); });

Demonstrating the isolation: depositEscrow throws 409 Duplicate escrow record for a repeated queueId:identity. Previously, two tests using the same
pair 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 MemoryAdapter for a
Postgres-backed adapter is a constructor argument, not a source change — which is
exactly what the persistence work needs.

Acceptance

  • createEscrowService(store?) + default singleton
  • createEnrollmentService(store?) + default singleton
  • tests use the factory in beforeEach; no vi.resetModules()
  • order-independent results
  • route handlers unchanged
  • service tests 17 passed; full backend suite unchanged at 75 passed

Partially addressed: queueService also 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 the
two backend services + their tests. The following already fail on main:

  • sdk/src/client.ts has a syntax error (line 18: Unexpected "}"), so
    @lineproof/sdk cannot build. That is why tests/client.test.ts,
    integration.test.ts, sdk.test.ts and 6 backend test files fail to load.
  • The contracts do not compile at all. #[contract] is applied to a trait
    instead of a struct in all five crates, plus ~20 type errors and soroban-sdk 22
    API drift (deploy() returns Address, not BytesN<32>).
  • backend/src/index.ts has duplicate identifiers and config.ts fails
    exactOptionalPropertyTypes.

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 need
rewriting before cargo test --workspace can pass).

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
Copilot AI review requested due to automatic review settings July 21, 2026 12:03
@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

@Spagero763 is attempting to deploy a commit to the Deejah Team on Vercel.

A member of the Team first needs to authorize it.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Spagero763 and others added 2 commits July 22, 2026 13:09
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.
@Spagero763

Copy link
Copy Markdown
Contributor Author

CI status after the SDK repair

I 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 @lineproof/sdk) failed regardless of what this PR changed.

Now green: SDK Lint & Typecheck (was 20+ parse errors, now tsc --noEmit is clean), plus pnpm build succeeds for the first time.

The remaining red checks are pre-existing on main, not from this PR

This branch only touches sdk/src/utils.ts, sdk/tests/utils.test.ts, the two backend services + their tests, and the SDK syntax repair above.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants