From 1d1279073b75e80be2ae2fcf04b5476c5e112bc9 Mon Sep 17 00:00:00 2001 From: Collins C Augustine Date: Mon, 22 Jun 2026 18:39:09 +0100 Subject: [PATCH] feat: implement integration guide showing how to gate a Soroban contract using has_valid_credential --- Cargo.lock | 20 +++--- docs/INTEGRATION.md | 153 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 132 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33fd0d6..b3e2c50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,9 +124,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.64" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", "shlex", @@ -693,9 +693,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "log" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "memchr" @@ -851,9 +851,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -1393,9 +1393,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.49" +version = "0.3.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" dependencies = [ "deranged", "num-conv", @@ -1413,9 +1413,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.29" +version = "0.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" dependencies = [ "num-conv", "time-core", diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md index cf45866..375c82e 100644 --- a/docs/INTEGRATION.md +++ b/docs/INTEGRATION.md @@ -28,16 +28,19 @@ soroban-sdk = { version = "21.7.6", features = [] } [dev-dependencies] soroban-sdk = { version = "21.7.6", features = ["testutils"] } +stellar-id = { path = "../stellar-id" } ``` #### 2. src/lib.rs ```rust #![no_std] -use soroban_sdk::{ - contract, contractimpl, contractclient, Address, Env, String, Symbol, Vec, -}; +use soroban_sdk::{contract, contractimpl, contractclient, Address, Env}; +// ── External contract interface ───────────────────────────────────────── +// #[contractclient] generates `StellarIdClient` — a cross-contract caller +// that wraps env.invoke_contract(). The trait methods must match the exact +// signatures of the StellarID contract's public functions. #[contractclient(name = "StellarIdClient")] trait StellarId { fn has_valid_credential(env: Env, subject: Address, schema_id: u32) -> bool; @@ -45,6 +48,9 @@ trait StellarId { fn get_identity(env: Env, subject: Address) -> Identity; } +// The Identity type must be replicated so the generated client can decode +// the cross-contract return value. Keep this in sync with the StellarID +// contract definition. #[contracttype] #[derive(Clone, Debug)] pub struct Identity { @@ -54,6 +60,16 @@ pub struct Identity { pub created_at: u64, } +// ── Storage keys ──────────────────────────────────────────────────────── +#[contracttype] +enum DataKey { + Admin, + StellarIdContract, + KycSchemaId, + Balance(Address), +} + +// ── Gated Vault contract ──────────────────────────────────────────────── #[contract] pub struct GatedVault; @@ -90,6 +106,7 @@ impl GatedVault { fn assert_kyc_verified(env: &Env, user: &Address) { let stellar_id_contract: Address = env.storage().instance().get(&DataKey::StellarIdContract).expect("Not initialized"); let kyc_schema_id: u32 = env.storage().instance().get(&DataKey::KycSchemaId).expect("Not initialized"); + // Build a cross-contract client and call has_valid_credential let client = StellarIdClient::new(env, &stellar_id_contract); assert!( client.has_valid_credential(user, &kyc_schema_id), @@ -98,14 +115,6 @@ impl GatedVault { } } -#[contracttype] -enum DataKey { - Admin, - StellarIdContract, - KycSchemaId, - Balance(Address), -} - #[cfg(test)] mod tests { use super::*; @@ -175,35 +184,117 @@ The test code above shows a complete example. ## Backend / SDK Call -From a TypeScript backend using the Stellar SDK: +From a TypeScript backend using the Stellar SDK, you can verify credentials via `simulateTransaction` — no wallet or user signature is required since `has_valid_credential` is read-only. -```typescript -import { Contract, SorobanRpc, TransactionBuilder, Networks } from "@stellar/stellar-sdk"; +### Prerequisites -const server = new SorobanRpc.Server("https://soroban-testnet.stellar.org"); -const STELLAR_ID_CONTRACT = "YOUR_CONTRACT_ID"; +```bash +npm install @stellar/stellar-sdk +``` +### Complete Example + +```typescript +import { + Address, + Contract, + Keypair, + nativeToScVal, + Networks, + scvalToNative, + SorobanRpc, + TransactionBuilder, +} from "@stellar/stellar-sdk"; + +const RPC_URL = "https://soroban-testnet.stellar.org"; +const STELLAR_ID_CONTRACT_ID = "CA3D..."; // Deployed contract ID + +const server = new SorobanRpc.Server(RPC_URL); +const networkPassphrase = Networks.TESTNET; + +/** + * Check whether a subject address holds a valid credential for a given schema. + * + * This is a pure simulate call — no fees, no signatures, no on-chain footprint. + */ async function hasValidCredential( subjectAddress: string, - schemaId: number + schemaId: number, ): Promise { - const contract = new Contract(STELLAR_ID_CONTRACT); - - const result = await server.simulateTransaction( - new TransactionBuilder(/* ... */) - .addOperation( - contract.call( - "has_valid_credential", - // encode subject and schema_id as XDR args - ) - ) - .build() - ); - - return result.result?.retval?.value() === true; + const contract = new Contract(STELLAR_ID_CONTRACT_ID); + + const subjectScVal = Address.fromString(subjectAddress).toScVal(); + const schemaIdScVal = nativeToScVal(schemaId, { type: "u32" }); + + // A random keypair is fine for simulation — the source account is never + // charged because the transaction is never submitted. + const source = Keypair.random(); + + const tx = new TransactionBuilder(source, { + fee: "100", + networkPassphrase, + }) + .addOperation( + contract.call("has_valid_credential", subjectScVal, schemaIdScVal), + ) + .setTimeout(30) + .build(); + + const result = await server.simulateTransaction(tx); + + if (result.error) { + throw new Error(`Simulation failed: ${result.error}`); + } + + const retval = result.result?.retval; + if (!retval) { + throw new Error("No return value — check contract ID and network"); + } + + return scvalToNative(retval) as boolean; +} +``` + +### Usage + +```typescript +const isVerified = await hasValidCredential( + "GBPLP3Y3TPF6T3Q5KNHX6PRFGKELN33NX2K5C4YKP32Y36B6H5XJ7GKL", + 1, // KYC Verified schema +); + +if (isVerified) { + console.log("User is KYC-verified, granting access"); +} else { + console.log("User does not have a valid KYC credential"); } ``` +### Express Middleware Example + +```typescript +import express from "express"; + +const app = express(); + +app.use(async (req, res, next) => { + const userAddress = req.headers["x-stellar-address"] as string; + if (!userAddress) { + return res.status(401).json({ error: "Missing x-stellar-address header" }); + } + + try { + const isVerified = await hasValidCredential(userAddress, 1); + if (!isVerified) { + return res.status(403).json({ error: "KYC verification required" }); + } + next(); + } catch (err) { + return res.status(500).json({ error: "Verification check failed" }); + } +}); +``` + ## Common Schema IDs Contact the issuer or check on-chain to confirm schema IDs for your network deployment.