Skip to content

feat(contract): add preview_payout_deep for recursive leaf resolution - #318

Open
MarcusDavidG wants to merge 4 commits into
tributary-protocol:mainfrom
MarcusDavidG:feat/preview-payout-deep-265
Open

feat(contract): add preview_payout_deep for recursive leaf resolution#318
MarcusDavidG wants to merge 4 commits into
tributary-protocol:mainfrom
MarcusDavidG:feat/preview-payout-deep-265

Conversation

@MarcusDavidG

Copy link
Copy Markdown
Contributor

Closes #265

What this does

Adds preview_payout_deep(id, amount) -> Result<Vec<(Address, i128)>, Error> to the splitter contract. This is the deep variant of preview_payout that resolves every Recipient::Split child recursively down to leaf Recipient::Account addresses.

Contract (contracts/splitter/src/lib.rs)

New public method: preview_payout_deep

pub fn preview_payout_deep(env: Env, id: u64, amount: i128) -> Result<Vec<(Address, i128)>, Error>
  • Returns (Address, i128) pairs — leaf address alongside its final amount. No more positional-zip against get_split.
  • Recurses through Recipient::Split children by loading each child split and applying amounts() at each level.
  • Same rounding as pay: uses the existing amounts() helper at every level, so dust-to-last-recipient behaviour is bit-for-bit identical to an actual payment.
  • Aggregates duplicates: if the same address appears as a leaf under multiple branches, amounts are summed into a single entry.
  • Depth-bounded: new constant MAX_PREVIEW_DEPTH = 8. Calls that would exceed the cap return Error::BadChildSplit. At 32 recipients per split that covers trees with millions of theoretical leaf slots — far more than any real-world routing tree.

New private helper: resolve_deep

Recursive implementation with an injected depth counter. Separated from the public entry point so the guard and accumulation logic stay readable.

SDK (sdk/src/index.ts)

  • preview_payout_deep added to the Client interface with return type Result<Array<readonly [string, i128]>>.
  • Added to the fromJSON map.

App (app/src/lib/tributary.ts)

  • previewPayoutDeep(id, amount) helper wraps the SDK call and returns { address: string; amount: bigint }[] for easy consumption by the Pay tab and the tree visualisation (Add a nested-split tree visualization #75).

Docs (README.md)

Added preview_payout_deep row to the Contract API table.

Tests (8 new, all pass — 49 total)

Test What it verifies
deep_preview_flat_split_matches_shallow_preview Flat split returns same numbers as preview_payout, paired with addresses
deep_preview_two_level_tree 2-level tree resolves correctly; preview matches actual pay + distribute
deep_preview_three_level_tree 3-level tree resolves correctly; preview matches actual pay + distribute chain
deep_preview_aggregates_shared_leaf_address Same address under two branches → single aggregated entry
deep_preview_dust_rounding_matches_actual_pay Tricky 3-way dust share at child level — preview matches real payout exactly
deep_preview_rejects_non_positive_amount InvalidAmount for 0 and negative inputs
deep_preview_rejects_unknown_split SplitNotFound for non-existent split id
deep_preview_depth_cap_is_enforced 9-deep chain returns BadChildSplit; 8-deep succeeds and sums to input amount

Acceptance criteria

Criteria Status
Deep preview resolves nested splits to leaf accounts with exact final amounts
Leaf amounts sum to the input amount ✅ tested at 2-level, 3-level, dust cases
Preview matches what an actual payment produces ✅ two-level and three-level verified
Recursion is depth-bounded and the bound is documented MAX_PREVIEW_DEPTH = 8 in lib.rs doc-comment
#69 and #75 can be built on it previewPayoutDeep app helper ready to use

@MarcusDavidG
MarcusDavidG requested a review from Spagero763 as a code owner July 20, 2026 10:57
@Spagero763

Copy link
Copy Markdown
Contributor

contract fails on cargo fmt --all --check (diff starting around contracts/splitter/src/test.rs:1466) — no logic issue, just needs a formatting pass. Run cargo fmt --all locally and push.

…utary-protocol#261)

- Add checkTrustlines() in trustlines.ts: checks every leaf Account
  recipient (including those in nested Recipient::Split children) for
  a trustline to the selected token via Horizon account lookups.
- XLM (native SAC) is always treated as receivable – no Horizon call.
- Results are cached in-memory for 30 s; the check is debounced 400 ms
  in the UI so it does not fire an RPC call on every keystroke.
- PaySplit shows a red blocking warning with the exact address(es) when
  a confirmed missing trustline is found, and disables the Pay button.
- When a Horizon lookup errors the check degrades gracefully: an
  inconclusive notice is shown but the Pay button stays enabled.
- Both warning and notice messages are fully translated in en/vi.
- 15 unit tests cover: XLM skip, all-ok, missing trustline, Horizon
  error, network error, caching, recursive nested split resolution,
  deduplication, and circular-split guard.
…tributary-protocol#265)

Adds preview_payout_deep(id, amount) -> Result<Vec<(Address, i128)>, Error>
to the splitter contract.

Contract changes (contracts/splitter/src/lib.rs):
- New public method preview_payout_deep(env, id, amount) on Splitter.
  Returns (Address, i128) pairs for every leaf account the payment would
  ultimately reach, resolving Recipient::Split children recursively.
- New private helper resolve_deep(env, id, amount, depth, out) does the
  recursive walk using the same amounts() helper as pay/distribute, so
  rounding (dust to last recipient) is bit-for-bit identical to an actual
  payment.
- Duplicate leaf addresses from different branches are aggregated into a
  single entry.
- New constant MAX_PREVIEW_DEPTH = 8. The guard fires when depth exceeds
  the cap and returns Error::BadChildSplit, matching the existing error
  used for invalid child split references.

SDK changes (sdk/src/index.ts):
- Added preview_payout_deep to the Client interface with return type
  Result<Array<readonly [string, i128]>>.
- Added corresponding fromJSON entry.

App changes (app/src/lib/tributary.ts):
- Added previewPayoutDeep() helper that calls preview_payout_deep via the
  SDK and returns { address, amount }[] for use in Pay tab / tree views.

Docs (README.md):
- Added preview_payout_deep row to the Contract API table.

Tests (contracts/splitter/src/test.rs) — 8 new tests, all pass:
- deep_preview_flat_split_matches_shallow_preview
- deep_preview_two_level_tree (preview matches actual pay+distribute)
- deep_preview_three_level_tree (preview matches actual pay+distribute)
- deep_preview_aggregates_shared_leaf_address
- deep_preview_dust_rounding_matches_actual_pay
- deep_preview_rejects_non_positive_amount
- deep_preview_rejects_unknown_split
- deep_preview_depth_cap_is_enforced (verifies both the cap and the boundary)
@MarcusDavidG
MarcusDavidG force-pushed the feat/preview-payout-deep-265 branch from 589f615 to 9816124 Compare July 22, 2026 05:15
@Spagero763
Spagero763 enabled auto-merge (squash) July 27, 2026 05:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extend preview_payout to resolve nested splits down to leaf recipients

2 participants