Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/lifecycle-reusable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Lifecycle E2E (Reusable)

on:
workflow_call:
inputs:
contracts_version:
description: "soroban-core release tag (e.g. v0.1.0)"
type: string
default: "latest"
provider_version:
description: "provider-platform image tag (e.g. 0.2.0)"
type: string
default: "latest"
contracts_artifact:
description: "Name of a workflow artifact containing wasms (overrides release download)"
type: string
default: ""
provider_image_override:
description: "Full image ref override for provider-platform (e.g. ghcr.io/org/repo:sha-abc123)"
type: string
default: ""
secrets:
E2E_TRIGGER_TOKEN:
required: true

env:
REGISTRY: ghcr.io
ORG: moonlight-protocol

jobs:
lifecycle:
runs-on: ubuntu-latest
permissions:
packages: read
steps:
- uses: actions/checkout@v4
with:
repository: moonlight-protocol/local-dev
ref: main
token: ${{ secrets.E2E_TRIGGER_TOKEN }}

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.E2E_TRIGGER_TOKEN }}

- name: Download contract wasms from artifact
if: inputs.contracts_artifact != ''
uses: actions/download-artifact@v4
with:
name: ${{ inputs.contracts_artifact }}
path: e2e/wasms

- name: Download contract wasms from release
if: inputs.contracts_artifact == ''
env:
GH_TOKEN: ${{ secrets.E2E_TRIGGER_TOKEN }}
VERSION: ${{ inputs.contracts_version }}
run: |
mkdir -p e2e/wasms
if [ "$VERSION" = "latest" ]; then
gh release download --repo ${{ env.ORG }}/soroban-core -p '*.wasm' -D e2e/wasms/
else
gh release download "$VERSION" --repo ${{ env.ORG }}/soroban-core -p '*.wasm' -D e2e/wasms/
fi
ls -la e2e/wasms/

- name: Run Lifecycle E2E
env:
PROVIDER_IMAGE: ${{ inputs.provider_image_override != '' && inputs.provider_image_override || format('{0}/{1}/provider-platform:{2}', env.REGISTRY, env.ORG, inputs.provider_version) }}
working-directory: lifecycle
run: |
docker compose up -d
EXIT_CODE=$(docker wait lifecycle-test-runner-1)
echo "--- test-runner logs ---"
docker compose logs test-runner
echo "--- setup logs ---"
docker compose logs setup
echo "--- provider logs ---"
docker compose logs provider
docker compose down
exit $EXIT_CODE
2 changes: 2 additions & 0 deletions lifecycle/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
deployment.json
52 changes: 52 additions & 0 deletions lifecycle/admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Address, Contract, Keypair, rpc } from "stellar-sdk";
import { submitTx } from "./soroban.ts";

/**
* Register a Privacy Provider in the Channel Auth contract.
* Calls add_provider(provider) — requires admin authorization.
* Returns the transaction response for event extraction.
*/
export async function addProvider(
server: rpc.Server,
admin: Keypair,
networkPassphrase: string,
channelAuthId: string,
providerPublicKey: string,
): Promise<rpc.Api.GetSuccessfulTransactionResponse> {
console.log(` Registering provider ${providerPublicKey.slice(0, 8)}...`);

const contract = new Contract(channelAuthId);
const op = contract.call(
"add_provider",
new Address(providerPublicKey).toScVal(),
);

const result = await submitTx(server, admin, networkPassphrase, op);
console.log(" Provider registered");
return result;
}

/**
* Deregister a Privacy Provider from the Channel Auth contract.
* Calls remove_provider(provider) — requires admin authorization.
* Returns the transaction response for event extraction.
*/
export async function removeProvider(
server: rpc.Server,
admin: Keypair,
networkPassphrase: string,
channelAuthId: string,
providerPublicKey: string,
): Promise<rpc.Api.GetSuccessfulTransactionResponse> {
console.log(` Removing provider ${providerPublicKey.slice(0, 8)}...`);

const contract = new Contract(channelAuthId);
const op = contract.call(
"remove_provider",
new Address(providerPublicKey).toScVal(),
);

const result = await submitTx(server, admin, networkPassphrase, op);
console.log(" Provider removed");
return result;
}
134 changes: 134 additions & 0 deletions lifecycle/ci-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* CI Setup phase — runs inside docker-compose.
* Deploys contracts, registers provider, writes config for the provider
* and test-runner containers via the shared /config volume.
*/
import { Keypair } from "stellar-sdk";
import { createServer } from "./soroban.ts";
import {
deployChannelAuth,
deployPrivacyChannel,
getOrDeployNativeSac,
uploadWasm,
} from "./deploy.ts";
import { addProvider } from "./admin.ts";
import { extractEvents, verifyEvent } from "./events.ts";

const RPC_URL = Deno.env.get("STELLAR_RPC_URL")!;
const FRIENDBOT_URL = Deno.env.get("FRIENDBOT_URL")!;
const NETWORK_PASSPHRASE = Deno.env.get("STELLAR_NETWORK_PASSPHRASE") ??
"Standalone Network ; February 2017";
const CONFIG_DIR = Deno.env.get("CONFIG_DIR") ?? "/config";

async function fundAccount(publicKey: string): Promise<void> {
const res = await fetch(`${FRIENDBOT_URL}?addr=${publicKey}`);
if (!res.ok) {
throw new Error(
`Friendbot failed for ${publicKey}: ${res.status} ${await res.text()}`,
);
}
}

async function main() {
console.log("[ci-setup] Starting...");
const server = createServer(RPC_URL);

const admin = Keypair.random();
const provider = Keypair.random();
const treasury = Keypair.random();
console.log(` Admin: ${admin.publicKey()}`);
console.log(` Provider: ${provider.publicKey()}`);
console.log(` Treasury: ${treasury.publicKey()}`);

console.log("[ci-setup] Funding accounts...");
await fundAccount(admin.publicKey());
await fundAccount(treasury.publicKey());

// Step 1: Deploy Council (Channel Auth)
console.log("[ci-setup] Deploying Channel Auth...");
const channelAuthWasm = await Deno.readFile("/wasms/channel_auth_contract.wasm");
const channelAuthHash = await uploadWasm(
server, admin, NETWORK_PASSPHRASE, channelAuthWasm,
);
const { contractId: channelAuthId, txResponse: authDeployTx } =
await deployChannelAuth(server, admin, NETWORK_PASSPHRASE, channelAuthHash);

const deployEvents = extractEvents(authDeployTx);
const initResult = verifyEvent(deployEvents, "ContractInitialized", true);
if (initResult.found) console.log(" ContractInitialized event verified");

// Step 2: Deploy Channel (Privacy Channel)
console.log("[ci-setup] Deploying SAC + Privacy Channel...");
const assetContractId = await getOrDeployNativeSac(
server, admin, NETWORK_PASSPHRASE,
);

const privacyChannelWasm = await Deno.readFile("/wasms/privacy_channel.wasm");
const privacyChannelHash = await uploadWasm(
server, admin, NETWORK_PASSPHRASE, privacyChannelWasm,
);
const channelContractId = await deployPrivacyChannel(
server, admin, NETWORK_PASSPHRASE,
privacyChannelHash, channelAuthId, assetContractId,
);

// Step 3: Register provider
console.log("[ci-setup] Registering provider...");
const addTx = await addProvider(
server, admin, NETWORK_PASSPHRASE, channelAuthId, provider.publicKey(),
);
const addEvents = extractEvents(addTx);
const addResult = verifyEvent(addEvents, "ProviderAdded", true);
if (addResult.found) console.log(" ProviderAdded event verified");

// Write provider.env (read by provider-entrypoint.sh)
const providerEnv = `PORT=3000
MODE=development
LOG_LEVEL=TRACE
SERVICE_DOMAIN=localhost

STELLAR_RPC_URL=${RPC_URL}
NETWORK=local
NETWORK_FEE=1000000000
CHANNEL_CONTRACT_ID=${channelContractId}
CHANNEL_AUTH_ID=${channelAuthId}
CHANNEL_ASSET_CODE=XLM
CHANNEL_ASSET_CONTRACT_ID=${assetContractId}

PROVIDER_SK=${provider.secret()}
OPEX_PUBLIC=${treasury.publicKey()}
OPEX_SECRET=${treasury.secret()}

SERVICE_AUTH_SECRET=
SERVICE_FEE=100
CHALLENGE_TTL=900
SESSION_TTL=21600

MEMPOOL_SLOT_CAPACITY=100
MEMPOOL_EXPENSIVE_OP_WEIGHT=10
MEMPOOL_CHEAP_OP_WEIGHT=1
MEMPOOL_EXECUTOR_INTERVAL_MS=5000
MEMPOOL_VERIFIER_INTERVAL_MS=10000
MEMPOOL_TTL_CHECK_INTERVAL_MS=60000
`;
await Deno.writeTextFile(`${CONFIG_DIR}/provider.env`, providerEnv);

// Write contracts.env (read by test-runner)
const contractsEnv = `CHANNEL_CONTRACT_ID=${channelContractId}
CHANNEL_AUTH_ID=${channelAuthId}
CHANNEL_ASSET_CONTRACT_ID=${assetContractId}
PROVIDER_PK=${provider.publicKey()}
PROVIDER_SK=${provider.secret()}
TREASURY_PK=${treasury.publicKey()}
ADMIN_SK=${admin.secret()}
`;
await Deno.writeTextFile(`${CONFIG_DIR}/contracts.env`, contractsEnv);

console.log(`[ci-setup] Config written to ${CONFIG_DIR}/`);
console.log("[ci-setup] Done.");
}

main().catch((err) => {
console.error("[ci-setup] FAILED:", err);
Deno.exit(1);
});
Loading
Loading