From 41c3e12dc83d9759181d94c748544c80d33917e3 Mon Sep 17 00:00:00 2001 From: Gorka Date: Thu, 28 May 2026 14:59:22 -0300 Subject: [PATCH 1/5] fix(test): write E2E_PP_PUBLIC_KEY in the local e2e setup The CI flow's `e2e/setup.sh` was updated to include the URL-scoped PP key, but the local `./test.sh e2e` runner uses a separate `test/setup-e2e.ts` writer that produces /config/contracts.env from the setup container. Without E2E_PP_PUBLIC_KEY in that file, the test-runner's loadConfig() throws before any bundle is submitted. --- test/setup-e2e.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/setup-e2e.ts b/test/setup-e2e.ts index 7d6cfd3..85be850 100644 --- a/test/setup-e2e.ts +++ b/test/setup-e2e.ts @@ -244,6 +244,9 @@ E2E_CHANNEL_ASSET_CONTRACT_ID=${assetContractId} # Provider keypair (registered on-chain by setup) E2E_PROVIDER_PK=${provider.publicKey()} E2E_PROVIDER_SK=${provider.secret()} +# Bundles are URL-scoped to /providers/:ppPublicKey/bundles; in the single-PP +# e2e harness the PP key == the provider key. +E2E_PP_PUBLIC_KEY=${provider.publicKey()} # Admin/Council keypair (for governance tests) E2E_ADMIN_SK=${admin.secret()} From 2f84373023e3d4a9086a4df74a84d5f59e15b0e4 Mon Sep 17 00:00:00 2001 From: Gorka Date: Thu, 28 May 2026 17:02:23 -0300 Subject: [PATCH 2/5] fix(lifecycle): wire ppPublicKey + entity registration in CI runner lifecycle/ci-test.ts is the test runner invoked by lifecycle's docker compose (and by provider-platform's lifecycle-reusable CI). Two fields were missed when lifecycle/main.ts was updated for the URL-scoped bundles work: - e2eConfig was missing ppPublicKey, so lib/client/bundle.ts composed /providers/undefined/bundles, which the resolver couldn't service. - Alice and Bob weren't registered as APPROVED entities before deposit/send, tripping the SUBMITTER_NOT_APPROVED gate. Mirrors the equivalent fixes already in lifecycle/main.ts. --- lifecycle/ci-test.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/lifecycle/ci-test.ts b/lifecycle/ci-test.ts index 8e5adfd..9c027a5 100644 --- a/lifecycle/ci-test.ts +++ b/lifecycle/ci-test.ts @@ -60,6 +60,25 @@ function loadEnvFile(path: string): Record { return env; } +async function registerEntity( + providerUrl: string, + pubkey: string, + name: string, +): Promise { + const res = await fetch(`${providerUrl}/api/v1/entities`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ pubkey, name, jurisdictions: [] }), + }); + // 409 = already APPROVED; treat as success for idempotency. + if (!res.ok && res.status !== 409) { + throw new Error( + `Entity registration failed for ${pubkey}: ${res.status} ${await res + .text()}`, + ); + } +} + async function fundAccount(publicKey: string): Promise { const res = await fetch(`${FRIENDBOT_URL}?addr=${publicKey}`); if (!res.ok) { @@ -198,6 +217,9 @@ async function main() { horizonUrl, friendbotUrl: FRIENDBOT_URL, providerUrl: PROVIDER_URL, + // Bundles are URL-scoped to /providers/:ppPublicKey/bundles. Lifecycle + // CI runs against the single seeded PP, so ppPublicKey == ppOperator. + ppPublicKey: ppOperator.publicKey(), channelContractId: channelContractId as ContractId, channelAuthId: channelAuthId as ContractId, channelAssetContractId: assetContractId as ContractId, @@ -418,11 +440,15 @@ async function main() { const aliceJwt = await authenticate(alice, e2eConfig); console.log(" Alice authenticated"); + await registerEntity(PROVIDER_URL, alice.publicKey(), "Alice"); + console.log(" Alice approved as entity"); await deposit(alice.secret(), DEPOSIT_AMOUNT, aliceJwt, e2eConfig); console.log(` Deposit ${DEPOSIT_AMOUNT} XLM complete`); const bobJwt = await authenticate(bob, e2eConfig); console.log(" Bob authenticated"); + await registerEntity(PROVIDER_URL, bob.publicKey(), "Bob"); + console.log(" Bob approved as entity"); const receiverOps = await prepareReceive( bob.secret(), SEND_AMOUNT, From 3963530132865b7bba0e4608b27a01cb41b95d84 Mon Sep 17 00:00:00 2001 From: Gorka Date: Thu, 28 May 2026 17:02:57 -0300 Subject: [PATCH 3/5] fix(governance): read councilMemberships (plural) on PP list response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit provider-platform PR #103 (events dashboard redesign) renamed councilMembership (singular) → councilMemberships (plural array) on the dashboard /pp/list response. The governance e2e test was never updated to match. Now reads [0]?.status for the approved/rejected checks and .length === 0 for the no-council case. --- e2e/governance/uc2-approve-reject.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/e2e/governance/uc2-approve-reject.ts b/e2e/governance/uc2-approve-reject.ts index 49a3e06..d190785 100644 --- a/e2e/governance/uc2-approve-reject.ts +++ b/e2e/governance/uc2-approve-reject.ts @@ -377,9 +377,12 @@ check("3 PPs", list.data.data?.length, 3); const la = list.data.data?.find((p: any) => p.label === "PP-Approve"); const lr = list.data.data?.find((p: any) => p.label === "PP-Reject"); const ln = list.data.data?.find((p: any) => p.label === "PP-None"); -check("PP-Approve: ACTIVE", la?.councilMembership?.status, "ACTIVE"); -check("PP-Reject: REJECTED", lr?.councilMembership?.status, "REJECTED"); -check("PP-None: no council", ln?.councilMembership, null); +// provider-platform PR #103 renamed councilMembership (singular) → +// councilMemberships (plural array). A PP can be a member of multiple +// councils; this test creates exactly one membership per PP. +check("PP-Approve: ACTIVE", la?.councilMemberships?.[0]?.status, "ACTIVE"); +check("PP-Reject: REJECTED", lr?.councilMemberships?.[0]?.status, "REJECTED"); +check("PP-None: no council", ln?.councilMemberships?.length, 0); // --- Test 5: Council shows both --- console.log("\n\x1b[34m=== Test 5: Council requests list ===\x1b[0m"); From ab39b8001676147c89334550372b37b1ad0a1ca8 Mon Sep 17 00:00:00 2001 From: Gorka Date: Thu, 28 May 2026 17:05:08 -0300 Subject: [PATCH 4/5] fix(pos-instant): register pay-service as APPROVED entity Pay-platform submits bundles to provider-platform under its PAY_SERVICE_SK identity. provider-platform now gates bundle admission on the submitter being an APPROVED entity (SUBMITTER_NOT_APPROVED), so the pay-service keypair needs an entity record before the instant payment flow can run. New [1b/5] step calls POST /api/v1/entities with keys.payService right after funding. --- e2e/pos-instant/main.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/e2e/pos-instant/main.ts b/e2e/pos-instant/main.ts index 5c3525d..baf7354 100644 --- a/e2e/pos-instant/main.ts +++ b/e2e/pos-instant/main.ts @@ -82,6 +82,28 @@ await Promise.all([ ]); console.log(` Funded (${elapsed()})`); +// [1b] Register pay-service as an APPROVED entity on provider-platform. +// Pay-platform submits bundles to provider-platform under its PAY_SERVICE_SK +// identity; provider-platform now gates bundle admission on the submitter +// being an APPROVED entity, so this registration is required. +console.log("\n[1b/5] Registering pay-service as APPROVED entity..."); +const entityRes = await fetch(`${PROVIDER_URL}/api/v1/entities`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + pubkey: keys.payService.publicKey(), + name: "Pay Service", + jurisdictions: [], + }), +}); +// 409 = already APPROVED; treat as success for idempotency. +if (!entityRes.ok && entityRes.status !== 409) { + throw new Error( + `Entity registration failed: ${entityRes.status} ${await entityRes.text()}`, + ); +} +console.log(` Pay-service approved (${elapsed()})`); + // [2] Create merchant on pay-platform + store UTXOs console.log("\n[2/5] Creating merchant account + UTXOs..."); const merchantJwt = await getPayJwt( From 4a1b262377e9db9b85875d7e18f0f12ed320328f Mon Sep 17 00:00:00 2001 From: Gorka Date: Fri, 29 May 2026 15:30:24 -0300 Subject: [PATCH 5/5] fix(testnet): wire ppPublicKey + register entities before bundles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit testnet/main.ts and lifecycle/testnet-verify.ts are the suites driven by ./testnet/run-all.sh (and ./testnet/run-local.sh). Both were on the pre-URL-scoped client shape, so deno-check failed and a real run would have died at the first deposit: - e2eConfig was missing the now-required Config.ppPublicKey field (the resolver couldn't dispatch the bundle to a PP). - Alice and Bob were never registered as APPROVED entities, so even with the URL fixed the platform's SUBMITTER_NOT_APPROVED gate would have rejected every bundle. Adds a registerEntity helper to each file and seeds the PP key on Config. Mirrors the equivalent fixes already in lifecycle/main.ts and lifecycle/ci-test.ts. Verified locally against the running stack via: ./testnet/run-local.sh 1 # payment flow — 88.7s green ./testnet/run-local.sh 3 # lifecycle — 85.3s green --- lifecycle/testnet-verify.ts | 30 ++++++++++++++++++++++++++++++ testnet/main.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/lifecycle/testnet-verify.ts b/lifecycle/testnet-verify.ts index 449b345..ccc0ced 100644 --- a/lifecycle/testnet-verify.ts +++ b/lifecycle/testnet-verify.ts @@ -79,6 +79,25 @@ const SEND_AMOUNT = 5; const WITHDRAW_AMOUNT = 4; // ─── Helpers ────────────────────────────────────────────────────────── +async function registerEntity( + providerUrl: string, + pubkey: string, + name: string, +): Promise { + const res = await fetch(`${providerUrl}/api/v1/entities`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ pubkey, name, jurisdictions: [] }), + }); + // 409 = already APPROVED; idempotent. + if (!res.ok && res.status !== 409) { + throw new Error( + `Entity registration failed for ${pubkey}: ${res.status} ${await res + .text()}`, + ); + } +} + async function fundAccount(publicKey: string): Promise { const res = await fetch(`${FRIENDBOT_URL}?addr=${publicKey}`); if (!res.ok && res.status !== 400) { @@ -500,6 +519,10 @@ async function main() { horizonUrl, friendbotUrl: FRIENDBOT_URL, providerUrl: PROVIDER_URL, + // Bundles are URL-scoped: /providers/:ppPublicKey/bundles. testnet-verify + // runs against the single PP it just registered, so ppPublicKey == + // ppKeypair.publicKey(). + ppPublicKey: ppKeypair.publicKey(), channelContractId: channelContractId as ContractId, channelAuthId: channelAuthId as ContractId, channelAssetContractId: assetContractId as ContractId, @@ -530,6 +553,11 @@ async function main() { ); console.log(" Alice authenticated"); + // provider-platform now gates bundle admission on the submitter's entity + // being APPROVED. Register Alice (and Bob below) before any bundle. + await registerEntity(PROVIDER_URL, alice.publicKey(), "Alice"); + console.log(" Alice approved as entity"); + await withE2ESpan( "e2e.deposit", () => @@ -542,6 +570,8 @@ async function main() { () => authenticate(bob, e2eConfig), ); console.log(" Bob authenticated"); + await registerEntity(PROVIDER_URL, bob.publicKey(), "Bob"); + console.log(" Bob approved as entity"); const receiverOps = await withE2ESpan( "e2e.prepare_receive", diff --git a/testnet/main.ts b/testnet/main.ts index 998176c..4e180f5 100644 --- a/testnet/main.ts +++ b/testnet/main.ts @@ -61,6 +61,25 @@ const SEND_AMOUNT = 5; const WITHDRAW_AMOUNT = 4; // ─── Helpers ──────────────────────────────────────────────────────── +async function registerEntity( + providerUrl: string, + pubkey: string, + name: string, +): Promise { + const res = await fetch(`${providerUrl}/api/v1/entities`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ pubkey, name, jurisdictions: [] }), + }); + // 409 = already APPROVED; idempotent. + if (!res.ok && res.status !== 409) { + throw new Error( + `Entity registration failed for ${pubkey}: ${res.status} ${await res + .text()}`, + ); + } +} + async function fundAccount(publicKey: string): Promise { const res = await fetch(`${FRIENDBOT_URL}?addr=${publicKey}`); if (!res.ok && res.status !== 400) { @@ -455,6 +474,10 @@ async function main() { horizonUrl, friendbotUrl: FRIENDBOT_URL, providerUrl: PROVIDER_URL, + // Bundles are URL-scoped: /providers/:ppPublicKey/bundles. testnet/main + // runs against the single PP it registered above, so ppPublicKey == + // ppOperator.publicKey(). + ppPublicKey: ppOperator.publicKey(), channelContractId: channelContractId as ContractId, channelAuthId: channelAuthId as ContractId, channelAssetContractId: assetContractId as ContractId, @@ -491,6 +514,13 @@ async function main() { ); console.log(" Bob authenticated"); + // provider-platform now gates bundle admission on the submitter's entity + // being APPROVED. Register Alice and Bob via POST /api/v1/entities before + // any deposit / send / withdraw. + await registerEntity(PROVIDER_URL, alice.publicKey(), "Alice"); + await registerEntity(PROVIDER_URL, bob.publicKey(), "Bob"); + console.log(" Alice + Bob approved as entities"); + await withE2ESpan( "e2e.deposit", () =>