Skip to content

Latest commit

 

History

History
411 lines (315 loc) · 11.1 KB

File metadata and controls

411 lines (315 loc) · 11.1 KB

TrustLink Video Tutorial — Companion Guide

This guide accompanies the TrustLink video tutorial.
It covers the same material in written form so you can follow along at your own pace, copy-paste commands, and refer back without scrubbing through video.


What You'll Learn

  • What TrustLink is and why it exists
  • How to deploy TrustLink to the Stellar testnet
  • How to issue and verify attestations via the CLI
  • How to integrate TrustLink into a Rust smart contract
  • How to query TrustLink from a JavaScript / TypeScript frontend

Prerequisites

Tool Install
Rust + Cargo https://rustup.rs
wasm32-unknown-unknown target rustup target add wasm32-unknown-unknown
Soroban CLI cargo install --locked soroban-cli
Stellar CLI (alternative) cargo install --locked stellar-cli --features opt
Node.js ≥ 18 (for JS section) https://nodejs.org

Fund a testnet account via Friendbot:

curl "https://friendbot.stellar.org?addr=YOUR_PUBLIC_KEY"

1. What is TrustLink?

TrustLink is a Soroban smart contract that provides a shared attestation layer on the Stellar blockchain. It solves a common problem: every dApp that needs identity verification (KYC, AML, accredited investor status) ends up building its own system from scratch.

With TrustLink:

  • An admin controls which addresses are trusted issuers
  • Issuers create attestations that link a claim type (e.g. KYC_PASSED) to a subject wallet address
  • Any contract or frontend can call has_valid_claim to check whether a wallet holds a valid attestation
  • Attestations support optional expiration and can be revoked at any time

This means one deployed TrustLink instance can serve as the trust backbone for an entire ecosystem of contracts.


2. Clone and Build

git clone https://github.com/unixfundz/TrustLink.git
cd TrustLink

# Confirm tests pass
make test

# Build optimized wasm
make optimize

The optimized wasm lands at target/wasm32-unknown-unknown/release/trustlink.wasm — that's what you deploy.


3. Deploy to Testnet

# Deploy the wasm
soroban contract deploy \
  --wasm target/wasm32-unknown-unknown/release/trustlink.wasm \
  --network testnet \
  --source YOUR_SECRET_KEY

Save the contract ID printed to stdout — you'll use it in every subsequent command.

# Initialize with your admin address
soroban contract invoke \
  --id <CONTRACT_ID> \
  --network testnet \
  --source YOUR_SECRET_KEY \
  -- initialize \
  --admin YOUR_PUBLIC_KEY

4. Register an Issuer and Create Attestations

Only the admin can register issuers. Only registered issuers can create attestations.

# Admin registers a trusted issuer
soroban contract invoke \
  --id <CONTRACT_ID> \
  --network testnet \
  --source YOUR_SECRET_KEY \
  -- register_issuer \
  --issuer ISSUER_PUBLIC_KEY
# Issuer creates a KYC attestation (no expiration)
soroban contract invoke \
  --id <CONTRACT_ID> \
  --network testnet \
  --source ISSUER_SECRET_KEY \
  -- create_attestation \
  --issuer ISSUER_PUBLIC_KEY \
  --subject SUBJECT_PUBLIC_KEY \
  --claim_type KYC_PASSED \
  --expiration null
# Verify the claim (read-only, no signing needed)
soroban contract invoke \
  --id <CONTRACT_ID> \
  --network testnet \
  -- has_valid_claim \
  --subject SUBJECT_PUBLIC_KEY \
  --claim_type KYC_PASSED

Expected output: true

Available Claim Types

Claim Type Meaning
KYC_PASSED Subject passed KYC identity verification
ACCREDITED_INVESTOR Subject qualifies as an accredited investor
MERCHANT_VERIFIED Subject is a verified merchant
AML_CLEARED Subject passed AML screening
SANCTIONS_CHECKED Subject checked against sanctions lists

You can also register custom claim types:

soroban contract invoke \
  --id <CONTRACT_ID> \
  --network testnet \
  --source YOUR_SECRET_KEY \
  -- register_claim_type \
  --claim_type MY_CUSTOM_CLAIM \
  --description "Description of what this claim means"

5. Cross-Contract Integration (Rust)

Add TrustLink as a dependency in your contract's Cargo.toml:

[dependencies]
soroban-sdk = "21.0.0"
trustlink = { git = "https://github.com/unixfundz/TrustLink.git", tag = "v0.1.0" }

Import the generated client and gate your function:

mod trustlink {
    soroban_sdk::contractimport!(
        file = "../trustlink/target/wasm32-unknown-unknown/release/trustlink.wasm"
    );
}

#[contractimpl]
impl LendingContract {
    pub fn borrow(
        env: Env,
        borrower: Address,
        trustlink_id: Address,
        amount: i128,
    ) -> Result<(), Error> {
        borrower.require_auth();

        let trustlink = trustlink::Client::new(&env, &trustlink_id);
        let claim = String::from_str(&env, "KYC_PASSED");

        if !trustlink.has_valid_claim(&borrower, &claim) {
            return Err(Error::KYCRequired);
        }

        // lending logic
        Ok(())
    }
}

#[contracterror]
#[derive(Copy, Clone)]
#[repr(u32)]
pub enum Error {
    KYCRequired = 1,
}

The contractimport! macro generates a fully typed client at compile time — no manual ABI wrangling.

To test your contract in isolation, mock TrustLink using a test contract that implements the same interface:

cargo test

Checking Multiple Claims

// Require KYC AND AML clearance
let mut required = soroban_sdk::Vec::new(&env);
required.push_back(String::from_str(&env, "KYC_PASSED"));
required.push_back(String::from_str(&env, "AML_CLEARED"));

if !trustlink.has_all_claims(&borrower, &required) {
    return Err(Error::InsufficientCredentials);
}

6. JavaScript / TypeScript Integration

npm install @stellar/stellar-sdk

Read-only claim check (no signing)

Simulating the transaction is enough for read-only calls — no wallet signing required. The result comes back as a native JS boolean.

import {
  Contract, Networks, TransactionBuilder,
  SorobanRpc, nativeToScVal, scValToNative,
} from "@stellar/stellar-sdk";

const server = new SorobanRpc.Server("https://soroban-testnet.stellar.org");
const CONTRACT_ID = "<YOUR_CONTRACT_ID>";

async function hasValidClaim(subject: string, claimType: string): Promise<boolean> {
  const contract = new Contract(CONTRACT_ID);
  const op = contract.call(
    "has_valid_claim",
    nativeToScVal(subject, { type: "address" }),
    nativeToScVal(claimType, { type: "string" })
  );

  const account = await server.getAccount(subject);
  const tx = new TransactionBuilder(account, {
    fee: "100",
    networkPassphrase: Networks.TESTNET,
  }).addOperation(op).setTimeout(30).build();

  const sim = await server.simulateTransaction(tx);
  if (SorobanRpc.Api.isSimulationError(sim)) throw new Error(sim.error);

  return scValToNative(sim.result?.retval);
}

// Example
const isKYCd = await hasValidClaim("GABC...XYZ", "KYC_PASSED");
console.log("KYC valid:", isKYCd);

Error handling

const ERRORS: Record<number, string> = {
  1: "Already initialized",
  2: "Not initialized",
  3: "Unauthorized",
  4: "Not found",
  5: "Duplicate attestation",
  6: "Already revoked",
  7: "Expired",
};

function parseTrustLinkError(err: unknown): string {
  const match = String(err).match(/Error\(Contract, #(\d+)\)/);
  if (match) return ERRORS[parseInt(match[1])] ?? `Unknown error #${match[1]}`;
  return String(err);
}

7. Multi-Sig Attestations

High-value claims such as ACCREDITED_INVESTOR can require multiple registered issuers to co-sign before the attestation becomes active. This prevents a single compromised issuer from unilaterally issuing sensitive credentials.

How it works

  1. A registered issuer calls propose_attestation — they automatically count as the first signer.
  2. Other required issuers call cosign_attestation with the returned proposal_id.
  3. Once the number of signatures reaches the threshold, the attestation is finalized and stored as a normal active attestation.
  4. Proposals expire after 7 days if the threshold is not reached.

Step 1 — Propose a 2-of-3 attestation

soroban contract invoke \
  --id <CONTRACT_ID> \
  --network testnet \
  --source ISSUER_A_SECRET \
  -- propose_attestation \
  --proposer ISSUER_A_PUBLIC_KEY \
  --subject SUBJECT_PUBLIC_KEY \
  --claim_type ACCREDITED_INVESTOR \
  --required_signers '[{"address":"ISSUER_A_PUBLIC_KEY"},{"address":"ISSUER_B_PUBLIC_KEY"},{"address":"ISSUER_C_PUBLIC_KEY"}]' \
  --threshold 2

Save the proposal_id returned in the output.

Step 2 — Co-sign the proposal

# issuer_b co-signs — threshold reached, attestation is now active
soroban contract invoke \
  --id <CONTRACT_ID> \
  --network testnet \
  --source ISSUER_B_SECRET \
  -- cosign_attestation \
  --issuer ISSUER_B_PUBLIC_KEY \
  --proposal_id <PROPOSAL_ID>

Step 3 — Verify the attestation is active

soroban contract invoke \
  --id <CONTRACT_ID> \
  --network testnet \
  -- has_valid_claim \
  --subject SUBJECT_PUBLIC_KEY \
  --claim_type ACCREDITED_INVESTOR

Expected output: true

Inspecting a proposal

soroban contract invoke \
  --id <CONTRACT_ID> \
  --network testnet \
  -- get_multisig_proposal \
  --proposal_id <PROPOSAL_ID>

The returned struct includes:

Field Description
signers Addresses that have signed so far
threshold Required number of signatures
finalized true once the attestation is active
expires_at Unix timestamp after which co-signing is rejected

Rust snippet

// Build the required-signers list (all must be registered issuers)
let mut required_signers = soroban_sdk::Vec::new(&env);
required_signers.push_back(issuer_a.clone());
required_signers.push_back(issuer_b.clone());
required_signers.push_back(issuer_c.clone());

// Propose a 2-of-3 multi-sig attestation — issuer_a auto-signs
let proposal_id = contract.propose_attestation(
    &issuer_a,
    &user_address,
    &String::from_str(&env, "ACCREDITED_INVESTOR"),
    &required_signers,
    &2,
);

// issuer_b co-signs — threshold reached, attestation activated
contract.cosign_attestation(&issuer_b, &proposal_id);

assert!(contract.has_valid_claim(
    &user_address,
    &String::from_str(&env, "ACCREDITED_INVESTOR")
));

Error cases to handle

Error Meaning
InvalidThreshold Threshold is 0 or exceeds the number of required signers
Unauthorized Proposer or a required signer is not a registered issuer
NotRequiredSigner Cosigner is not in the proposal's required-signers list
AlreadySigned The issuer has already co-signed this proposal
ProposalFinalized The proposal has already been activated
ProposalExpired The 7-day window has passed without reaching threshold

8. Further Reading