From 3e5545938c026f0f4f76fdff6f5281e9521b1aed Mon Sep 17 00:00:00 2001 From: Gorka Date: Fri, 15 May 2026 15:40:13 -0300 Subject: [PATCH 1/3] feat(client): submitBundle/deposit/send/withdraw accept optional from/to jurisdictions --- lib/client/bundle.ts | 12 ++++++++++++ lib/client/deposit.ts | 8 +++++++- lib/client/send.ts | 8 +++++++- lib/client/withdraw.ts | 8 +++++++- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/lib/client/bundle.ts b/lib/client/bundle.ts index ae6cbe9..70cd27a 100644 --- a/lib/client/bundle.ts +++ b/lib/client/bundle.ts @@ -1,10 +1,16 @@ import type { Config } from "./config.ts"; import { withE2ESpan } from "./tracer.ts"; +export interface SubmitBundleOptions { + jurisdictionFrom?: string; + jurisdictionTo?: string; +} + export function submitBundle( jwt: string, operationsMLXDR: string[], config: Config, + options: SubmitBundleOptions = {}, ): Promise { return withE2ESpan("bundle.submit", async () => { const maxRetries = 10; @@ -20,6 +26,12 @@ export function submitBundle( body: JSON.stringify({ operationsMLXDR, channelContractId: config.channelContractId, + ...(options.jurisdictionFrom !== undefined + ? { jurisdictionFrom: options.jurisdictionFrom } + : {}), + ...(options.jurisdictionTo !== undefined + ? { jurisdictionTo: options.jurisdictionTo } + : {}), }), }); diff --git a/lib/client/deposit.ts b/lib/client/deposit.ts index 24c3f04..0934bdd 100644 --- a/lib/client/deposit.ts +++ b/lib/client/deposit.ts @@ -10,12 +10,18 @@ import { submitBundle, waitForBundle } from "./bundle.ts"; const DEPOSIT_FEE = 0.05; // LOW entropy fee +export interface DepositOptions { + jurisdictionFrom?: string; + jurisdictionTo?: string; +} + export async function deposit( secretKey: string, amount: number, jwt: string, config: Config, tracer?: MoonlightTracer, + options: DepositOptions = {}, ): Promise { const keypair = Keypair.fromSecret(secretKey); const totalAmount = fromDecimals(amount + DEPOSIT_FEE, 7); @@ -54,7 +60,7 @@ export async function deposit( // 5. Submit bundle const operationsMLXDR = [depositOp.toMLXDR(), createOp.toMLXDR()]; - const bundleId = await submitBundle(jwt, operationsMLXDR, config); + const bundleId = await submitBundle(jwt, operationsMLXDR, config, options); console.log(` Bundle submitted: ${bundleId}`); await waitForBundle(jwt, bundleId, config); diff --git a/lib/client/send.ts b/lib/client/send.ts index 9f3318c..14465fd 100644 --- a/lib/client/send.ts +++ b/lib/client/send.ts @@ -9,6 +9,11 @@ import { submitBundle, waitForBundle } from "./bundle.ts"; const SEND_FEE = 0.1; // LOW entropy fee +export interface SendOptions { + jurisdictionFrom?: string; + jurisdictionTo?: string; +} + export async function send( secretKey: string, receiverOperationsMLXDR: string[], @@ -16,6 +21,7 @@ export async function send( jwt: string, config: Config, tracer?: MoonlightTracer, + options: SendOptions = {}, ): Promise { const feeBigInt = fromDecimals(SEND_FEE, 7); const amountBigInt = fromDecimals(amount, 7); @@ -86,7 +92,7 @@ export async function send( ...createOps.map((op) => op.toMLXDR()), ...spendOps.map((op) => op.toMLXDR()), ]; - const bundleId = await submitBundle(jwt, operationsMLXDR, config); + const bundleId = await submitBundle(jwt, operationsMLXDR, config, options); console.log(` Bundle submitted: ${bundleId}`); await waitForBundle(jwt, bundleId, config); diff --git a/lib/client/withdraw.ts b/lib/client/withdraw.ts index 9f32526..bab4365 100644 --- a/lib/client/withdraw.ts +++ b/lib/client/withdraw.ts @@ -9,6 +9,11 @@ import { submitBundle, waitForBundle } from "./bundle.ts"; const WITHDRAW_FEE = 0.1; // LOW entropy fee +export interface WithdrawOptions { + jurisdictionFrom?: string; + jurisdictionTo?: string; +} + export async function withdraw( secretKey: string, destinationAddress: string, @@ -16,6 +21,7 @@ export async function withdraw( jwt: string, config: Config, tracer?: MoonlightTracer, + options: WithdrawOptions = {}, ): Promise { const feeBigInt = fromDecimals(WITHDRAW_FEE, 7); const amountBigInt = fromDecimals(amount, 7); @@ -83,7 +89,7 @@ export async function withdraw( ...changeCreateOps.map((op) => op.toMLXDR()), ...spendOps.map((op) => op.toMLXDR()), ]; - const bundleId = await submitBundle(jwt, operationsMLXDR, config); + const bundleId = await submitBundle(jwt, operationsMLXDR, config, options); console.log(` Bundle submitted: ${bundleId}`); await waitForBundle(jwt, bundleId, config); From ed5d83326c93ed85cc9a5514bc65f0a408df8871 Mon Sep 17 00:00:00 2001 From: Gorka Date: Fri, 15 May 2026 15:41:53 -0300 Subject: [PATCH 2/3] feat(send-loop): deposit + N sends + withdraw cycle, random jurisdictions from council's accepted set --- send-loop.ts | 163 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 125 insertions(+), 38 deletions(-) diff --git a/send-loop.ts b/send-loop.ts index 9ef0e57..1e23daf 100644 --- a/send-loop.ts +++ b/send-loop.ts @@ -1,12 +1,16 @@ /** - * Local Dev — Alice→Bob send loop + * Local Dev — Alice→Bob send loop with deposit + withdraw * - * Fires N (default 5) Alice→Bob privacy-channel sends through the running PP, - * sleeping `INTERVAL_MS` between each. Useful for watching the provider-console - * events tail update in real time. + * Each run does one full lifecycle: Alice deposits into the channel, performs + * N sends to Bob (sleeping INTERVAL_MS between each), then Bob withdraws back + * to his real Stellar address. Useful for watching the provider-console + * dashboard fill up across all event kinds. * - * Reuses the lib/client helpers so the bundles travel the same code path as the - * e2e suite. + * Each bundle is tagged with a random from/to pair drawn from the council's + * accepted jurisdictions so the dashboard has flag data to render. + * + * Reuses the lib/client helpers so the bundles travel the same code path as + * the e2e suite. * * Prereqs: ./up.sh → ./setup-c.sh → ./setup-pp.sh has run. * @@ -17,9 +21,9 @@ * deno run --allow-all send-loop.ts * * Env overrides: - * COUNT default 5 - * INTERVAL_MS default 1000 - * SEND_AMOUNT default 1 (XLM per send) + * COUNT default 5 number of sends in the cycle + * INTERVAL_MS default 1000 pause between sends + * SEND_AMOUNT default 1 XLM per send * STATE_FILE default ./.local-dev-state */ import { Keypair } from "stellar-sdk"; @@ -28,36 +32,81 @@ import { loadConfig } from "./lib/client/config.ts"; import { deposit } from "./lib/client/deposit.ts"; import { prepareReceive } from "./lib/client/receive.ts"; import { send } from "./lib/client/send.ts"; +import { withdraw } from "./lib/client/withdraw.ts"; const STATE_FILE = Deno.env.get("STATE_FILE") ?? new URL("./.local-dev-state", import.meta.url).pathname; const COUNT = Number(Deno.env.get("COUNT") ?? "5"); const INTERVAL_MS = Number(Deno.env.get("INTERVAL_MS") ?? "1000"); const SEND_AMOUNT = Number(Deno.env.get("SEND_AMOUNT") ?? "1"); +const WITHDRAW_AMOUNT = 0.5; // less than SEND_AMOUNT so it fits in one UTXO + fee const DEPOSIT_BUFFER = 2; // headroom for per-send fees -function loadStateEnvVars(): void { +type ParsedState = { + councilId: string; + councilUrl: string; +}; + +function loadState(): ParsedState { const content = Deno.readTextFileSync(STATE_FILE); + const map: Record = {}; for (const line of content.split("\n")) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const eq = trimmed.indexOf("="); if (eq === -1) continue; - const key = trimmed.slice(0, eq); - const value = trimmed.slice(eq + 1); - if (key === "CHANNEL_ID") Deno.env.set("E2E_CHANNEL_CONTRACT_ID", value); - if (key === "COUNCIL_ID") Deno.env.set("E2E_CHANNEL_AUTH_ID", value); - if (key === "ASSET_ID") { - Deno.env.set("E2E_CHANNEL_ASSET_CONTRACT_ID", value); - } - if (key === "PROVIDER_URL") Deno.env.set("PROVIDER_URL", value); - if (key === "NETWORK_PASSPHRASE") { - Deno.env.set("STELLAR_NETWORK_PASSPHRASE", value); - } - if (key === "RPC_URL") Deno.env.set("STELLAR_RPC_URL", value); - if (key === "FRIENDBOT_URL") Deno.env.set("FRIENDBOT_URL", value); + map[trimmed.slice(0, eq)] = trimmed.slice(eq + 1); + } + // Set env vars the SDK config loader expects. + if (map.CHANNEL_ID) Deno.env.set("E2E_CHANNEL_CONTRACT_ID", map.CHANNEL_ID); + if (map.COUNCIL_ID) Deno.env.set("E2E_CHANNEL_AUTH_ID", map.COUNCIL_ID); + if (map.ASSET_ID) Deno.env.set("E2E_CHANNEL_ASSET_CONTRACT_ID", map.ASSET_ID); + if (map.PROVIDER_URL) Deno.env.set("PROVIDER_URL", map.PROVIDER_URL); + if (map.NETWORK_PASSPHRASE) { + Deno.env.set("STELLAR_NETWORK_PASSPHRASE", map.NETWORK_PASSPHRASE); } + if (map.RPC_URL) Deno.env.set("STELLAR_RPC_URL", map.RPC_URL); + if (map.FRIENDBOT_URL) Deno.env.set("FRIENDBOT_URL", map.FRIENDBOT_URL); + return { councilId: map.COUNCIL_ID, councilUrl: map.COUNCIL_URL }; +} + +async function fetchCouncilJurisdictions( + state: ParsedState, +): Promise { + const url = `${state.councilUrl}/api/v1/public/council?councilId=${ + encodeURIComponent(state.councilId) + }`; + const res = await fetch(url); + if (!res.ok) { + throw new Error( + `Council summary fetch failed: ${res.status} ${await res.text()}`, + ); + } + const { data } = await res.json(); + const codes: string[] = (data?.jurisdictions ?? []).map((j: { + countryCode: string; + }) => j.countryCode); + if (codes.length === 0) { + throw new Error( + "Council has no jurisdictions; setup-c.sh seeds US — re-run it.", + ); + } + return codes; +} + +function pickRandom(arr: T[]): T { + return arr[Math.floor(Math.random() * arr.length)]; +} + +function jurisdictionsFor(accepted: string[]): { + jurisdictionFrom: string; + jurisdictionTo: string; +} { + return { + jurisdictionFrom: pickRandom(accepted), + jurisdictionTo: pickRandom(accepted), + }; } async function fund(friendbotUrl: string, publicKey: string): Promise { @@ -70,45 +119,83 @@ async function fund(friendbotUrl: string, publicKey: string): Promise { } async function main(): Promise { - loadStateEnvVars(); + const state = loadState(); const config = loadConfig(); + const accepted = await fetchCouncilJurisdictions(state); console.log("\n=== local-dev — Alice→Bob send loop ===\n"); - console.log(` Count: ${COUNT}`); - console.log(` Interval: ${INTERVAL_MS}ms`); - console.log(` Send amount: ${SEND_AMOUNT} XLM`); - console.log(` Provider: ${config.providerUrl}`); + console.log(` Count: ${COUNT}`); + console.log(` Interval: ${INTERVAL_MS}ms`); + console.log(` Send amount: ${SEND_AMOUNT} XLM`); + console.log(` Withdraw amount: ${WITHDRAW_AMOUNT} XLM`); + console.log(` Provider: ${config.providerUrl}`); + console.log(` Jurisdictions: ${accepted.join(", ")}`); const alice = Keypair.random(); const bob = Keypair.random(); - console.log(` Alice: ${alice.publicKey()}`); - console.log(` Bob: ${bob.publicKey()}\n`); + console.log(` Alice: ${alice.publicKey()}`); + console.log(` Bob: ${bob.publicKey()}\n`); - console.log("[1/4] Funding Alice + Bob via Friendbot"); + console.log("[1/5] Funding Alice + Bob via Friendbot"); await fund(config.friendbotUrl, alice.publicKey()); await fund(config.friendbotUrl, bob.publicKey()); - console.log("[2/4] Authenticating both with provider"); + console.log("[2/5] Authenticating both with provider"); const aliceJwt = await authenticate(alice, config); - await authenticate(bob, config); + const bobJwt = await authenticate(bob, config); const depositAmount = SEND_AMOUNT * COUNT + DEPOSIT_BUFFER; - console.log(`[3/4] Alice depositing ${depositAmount} XLM into channel`); - await deposit(alice.secret(), depositAmount, aliceJwt, config); + const depositJurisdictions = jurisdictionsFor(accepted); + console.log( + `[3/5] Alice depositing ${depositAmount} XLM (${depositJurisdictions.jurisdictionFrom}→${depositJurisdictions.jurisdictionTo})`, + ); + await deposit( + alice.secret(), + depositAmount, + aliceJwt, + config, + undefined, + depositJurisdictions, + ); - console.log(`[4/4] Sending ${COUNT} bundles, ${INTERVAL_MS}ms apart\n`); + console.log(`[4/5] Sending ${COUNT} bundles, ${INTERVAL_MS}ms apart\n`); for (let i = 1; i <= COUNT; i++) { const startedAt = Date.now(); const receiverOps = await prepareReceive(bob.secret(), SEND_AMOUNT, config); - await send(alice.secret(), receiverOps, SEND_AMOUNT, aliceJwt, config); + const j = jurisdictionsFor(accepted); + await send( + alice.secret(), + receiverOps, + SEND_AMOUNT, + aliceJwt, + config, + undefined, + j, + ); console.log( - ` ${i}/${COUNT} sent ${SEND_AMOUNT} XLM (${Date.now() - startedAt}ms)`, + ` ${i}/${COUNT} sent ${SEND_AMOUNT} XLM (${j.jurisdictionFrom}→${j.jurisdictionTo}) ${ + Date.now() - startedAt + }ms`, ); if (i < COUNT) { await new Promise((r) => setTimeout(r, INTERVAL_MS)); } } + const withdrawJurisdictions = jurisdictionsFor(accepted); + console.log( + `\n[5/5] Bob withdrawing ${WITHDRAW_AMOUNT} XLM to ${bob.publicKey()} (${withdrawJurisdictions.jurisdictionFrom}→${withdrawJurisdictions.jurisdictionTo})`, + ); + await withdraw( + bob.secret(), + bob.publicKey(), + WITHDRAW_AMOUNT, + bobJwt, + config, + undefined, + withdrawJurisdictions, + ); + console.log("\n=== Done ===\n"); } From f97cbe3d719b904faf942f3c804c38da71066feb Mon Sep 17 00:00:00 2001 From: Gorka Date: Fri, 15 May 2026 15:52:13 -0300 Subject: [PATCH 3/3] feat(send-loop): pick jurisdictions from merged council + PP claimed set --- send-loop.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/send-loop.ts b/send-loop.ts index 1e23daf..d4eec76 100644 --- a/send-loop.ts +++ b/send-loop.ts @@ -71,7 +71,7 @@ function loadState(): ParsedState { return { councilId: map.COUNCIL_ID, councilUrl: map.COUNCIL_URL }; } -async function fetchCouncilJurisdictions( +async function fetchAcceptedJurisdictions( state: ParsedState, ): Promise { const url = `${state.councilUrl}/api/v1/public/council?councilId=${ @@ -84,15 +84,21 @@ async function fetchCouncilJurisdictions( ); } const { data } = await res.json(); - const codes: string[] = (data?.jurisdictions ?? []).map((j: { - countryCode: string; - }) => j.countryCode); - if (codes.length === 0) { + const councilCodes: string[] = (data?.jurisdictions ?? []).map( + (j: { countryCode: string }) => j.countryCode, + ); + const providerCodes: string[] = (data?.providers ?? []).flatMap(( + p: { jurisdictions: string[] | null }, + ) => p.jurisdictions ?? []); + const merged = Array.from( + new Set([...councilCodes, ...providerCodes].map((c) => c.toUpperCase())), + ); + if (merged.length === 0) { throw new Error( - "Council has no jurisdictions; setup-c.sh seeds US — re-run it.", + "No jurisdictions known; setup-c.sh seeds US and setup-pp.sh claims UY — re-run them.", ); } - return codes; + return merged; } function pickRandom(arr: T[]): T { @@ -121,7 +127,7 @@ async function fund(friendbotUrl: string, publicKey: string): Promise { async function main(): Promise { const state = loadState(); const config = loadConfig(); - const accepted = await fetchCouncilJurisdictions(state); + const accepted = await fetchAcceptedJurisdictions(state); console.log("\n=== local-dev — Alice→Bob send loop ===\n"); console.log(` Count: ${COUNT}`);