Skip to Content
API ReferenceCore
View .md

Alien Solana SSO Core API Reference: AlienSolanaSsoClient

@alien-id/sso-solana is the framework-free client for linking Solana wallets to verified Alien IDs through on-chain attestations. For the step-by-step flow, see the Core Integration guide. For React apps, prefer the provider and hooks from @alien-id/sso-solana-react.

AlienSolanaSsoClient

Creates the client. Every method that talks to the SSO server or builds the attestation transaction lives on the instance.

new AlienSolanaSsoClient(config: AlienSolanaSsoClientConfig)
OptionTypeDefaultDescription
ssoBaseUrlstringBase URL of the SSO service. HTTPS is enforced for non-loopback hosts
providerAddressstringThe platform provider address (see below)
pollingIntervalnumber5000Polling interval in milliseconds
credentialSignerProgramIdstring9cstDz8WWRAFaq1vVpTjfHz6tjgh6SJaqYFeZWi1pFHGCredential Signer program ID
sessionRegistryProgramIdstringDeHa6pyZ2CFSbQQiNMm7FgoCXqmkX6tXG77C4Qycpta6Session Registry program ID
sasProgramIdstring22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdGSAS program ID
allowInsecureSsoBaseUrlbooleanfalseDev opt-in: accept http:// for a non-loopback ssoBaseUrl. localhost / 127.0.0.1 / [::1] are always allowed

Single-platform-provider model. Solana SSO attestations bind a wallet to the platform’s Alien ID, not to your dApp. The oracle is pinned to a single platform provider address, and the /solana/attestation endpoint returns 404 unless providerAddress matches that platform provider — third-party provider addresses always get null from getAttestation(). If you are not the platform provider, read the attestation directly on-chain via the attestation PDA instead.

const client = new AlienSolanaSsoClient({ ssoBaseUrl: 'https://sso.alien-api.com', providerAddress: 'platform-provider-address', });

Methods

Starts the attestation creation flow by generating a deep link and a polling code. Use it when the wallet doesn’t have an attestation yet. Throws on network failure, or when the response doesn’t match the expected schema.

async generateDeeplink(solanaAddress: string): Promise<SolanaLinkResponse>

solanaAddress is the user’s Solana wallet public key as a base58 string. The response:

{ deep_link: string; // deep link for a QR code or mobile redirect polling_code: string; // code for polling the linking status expired_at: number; // Unix timestamp when the polling code expires }
const userPublicKey = wallet.publicKey.toBase58(); const { deep_link, polling_code } = await client.generateDeeplink(userPublicKey); displayQRCode(deep_link);

pollAuth()

Polls the linking status for a polling code from generateDeeplink(). Throws on network failure, or when the response doesn’t match the expected schema.

async pollAuth(pollingCode: string): Promise<SolanaPollResponse>

The response:

{ status: 'pending' | 'authorized' | 'rejected' | 'expired'; oracle_signature?: string; // oracle signature (hex-encoded string) oracle_public_key?: string; // oracle public key (hex-encoded string) solana_address?: string; // wallet address the attestation is for timestamp?: number; // Unix timestamp signed by the oracle session_address?: string; // session identifier }
StatusMeaning
pendingThe user hasn’t completed attestation creation yet
authorizedThe user approved; oracle signature data is available
rejectedThe user denied attestation creation
expiredThe polling code expired

No pre-built transaction. The server never returns a pre-built transaction. On authorized, the SDK assembles the attestation transaction entirely client-side — pass the poll response fields to buildCreateAttestationTransaction().

const response = await client.pollAuth(polling_code); if (response.status === 'authorized') { console.log('Session address:', response.session_address); console.log('Oracle signature:', response.oracle_signature); console.log('Timestamp:', response.timestamp); }

getAttestation()

Checks whether a Solana wallet has a linking attestation backed by an active Alien session, and returns its session address, or null if none is found. Also returns null (a 404 under the hood) when the client’s providerAddress doesn’t match the platform provider the attestation belongs to — see the single-platform-provider note. Throws on network failure — and note that when an attestation exists but the backing Alien session has been revoked, the server responds 403 Session is not active, which the SDK surfaces as a throw (GetAttestation failed: <statusText>), not as null. The error message interpolates the response’s statusText, whose exact reason phrase depends on the fetch runtime (e.g. Forbidden for a 403).

This is not proof of authentication. A non-null result only means a linking record exists for that public wallet address — anyone who knows the address gets the same result. It does not prove the current user controls the wallet. To authenticate a user, require a fresh wallet signature over a server-issued challenge and verify it server-side; this SDK does not provide that primitive.

async getAttestation(solanaAddress: string): Promise<string | null>
const sessionAddress = await client.getAttestation(wallet.publicKey.toBase58()); if (sessionAddress) { // Wallet already has a linking attestation — skip the creation flow console.log('Wallet is already linked. Session:', sessionAddress); } else { // No attestation — start the flow with generateDeeplink() console.log('No attestation found, need to create one'); }

buildCreateAttestationTransaction()

Builds the Solana transaction that creates the on-chain attestation. Internally it fetches the Credential Signer program state account to read the credential and schema addresses, derives the required PDAs, and combines an Ed25519 signature-verification instruction with the attestation instruction.

Returns an unsigned Transaction with neither recentBlockhash nor feePayer set — fetch a recent blockhash and set both before asking the wallet to sign, as shown below. Throws when the RPC request fails, when the program state account is missing, owned by the wrong program, or too short, and when the oracle public key from the poll response doesn’t match the one recorded on-chain — a deliberate security gate that prevents a compromised backend from substituting its own oracle key.

async buildCreateAttestationTransaction(params: BuildAttestationParams): Promise<Transaction>
interface BuildAttestationParams { connection: Connection; // Solana RPC connection payerPublicKey: PublicKey; // user's wallet public key sessionAddress: string; // session address from the poll response oracleSignature: Uint8Array; // oracle signature, hex-decoded from the poll response oraclePublicKey: PublicKey; // oracle public key, hex-decoded from the poll response timestamp: number; // timestamp from the poll response (oracle-signed) expiry: number; // client-supplied; pass 0 (see below) }

expiry is a no-op. It is not returned by the server, not covered by the oracle signature, and not validated on-chain — it is only stored verbatim in the attestation record. Pass 0.

Timestamp freshness. The on-chain program rejects the transaction with TimestampTooOld if it is submitted more than 300 seconds (5 minutes) after the oracle-signed timestamp. The check is two-sided (|now − timestamp| ≤ 300), so a future-skewed timestamp also reverts. Build and submit the transaction promptly after a successful poll.

import { Connection, PublicKey } from '@solana/web3.js'; import { Buffer } from 'buffer'; const connection = new Connection('https://api.mainnet-beta.solana.com'); const transaction = await client.buildCreateAttestationTransaction({ connection, payerPublicKey: wallet.publicKey, sessionAddress: pollResponse.session_address!, oracleSignature: Uint8Array.from(Buffer.from(pollResponse.oracle_signature!, 'hex')), oraclePublicKey: new PublicKey(Buffer.from(pollResponse.oracle_public_key!, 'hex')), timestamp: pollResponse.timestamp!, expiry: 0, // currently unused — not signed by the oracle or validated on-chain }); // Set the blockhash and fee payer, then sign and send const { blockhash } = await connection.getLatestBlockhash(); transaction.recentBlockhash = blockhash; transaction.feePayer = wallet.publicKey; const signedTransaction = await wallet.signTransaction(transaction); const signature = await connection.sendRawTransaction( signedTransaction.serialize() ); await connection.confirmTransaction(signature);

PDA Derivation

The attestation system stores its state in Program Derived Addresses. The SDK derives all of them internally when building the attestation transaction — the derivation helpers are not exported from the published @alien-id/sso-solana package. To read on-chain state directly — for example when your provider address isn’t the platform provider and getAttestation() is not available to you — derive the PDAs yourself with PublicKey.findProgramAddressSync. Every derivation returns a [PublicKey, number] tuple: the PDA address and its bump seed.

The credential and schema addresses an attestation is derived from live in the Credential Signer program state account. Its layout is an 8-byte Anchor discriminator followed by six 32-byte public keys — oracle, credential, schema, event authority, authority, session registry:

import { Connection, PublicKey } from '@solana/web3.js'; import { Buffer } from 'buffer'; // Program IDs as PublicKey instances — readonly fields on the client // (defaults listed in the Solana Programs table below) const { credentialSignerProgramId, sasProgramId } = client; // 1. Read the credential and schema addresses from program state const [programStatePda] = PublicKey.findProgramAddressSync( [Buffer.from('program_state')], credentialSignerProgramId ); const account = await connection.getAccountInfo(programStatePda); const credential = new PublicKey(account!.data.slice(8 + 32, 8 + 64)); const schema = new PublicKey(account!.data.slice(8 + 64, 8 + 96)); // 2. Derive the wallet's attestation PDA const [attestationPda] = PublicKey.findProgramAddressSync( [ Buffer.from('attestation'), credential.toBuffer(), // credential PDA from program state schema.toBuffer(), // schema PDA from program state userPublicKey.toBuffer(), // the wallet (nonce seed) ], sasProgramId ); // 3. A non-null account at attestationPda means the wallet is linked const attestation = await connection.getAccountInfo(attestationPda);

The seed lists below mirror the SDK’s internal helpers (pda.ts) and the on-chain programs; each heading names the corresponding internal helper.

deriveProgramStatePda()

Seeds ["program_state"] on the Credential Signer program. Holds the oracle key and the credential/schema addresses (layout above).

deriveCredentialSignerPda()

Seeds ["credential_signer"] on the Credential Signer program.

deriveSessionRegistryPda()

Seeds ["session_registry"] on the Session Registry program.

deriveSessionEntryPda()

Seeds ["session", sessionAddress] (the session address as UTF-8 bytes) on the Session Registry program. One entry per linked session.

deriveSolanaEntryPda()

Seeds ["solana", walletPublicKey] on the Session Registry program. Maps a Solana wallet to its session.

deriveAttestationPda()

Seeds ["attestation", credential, schema, walletPublicKey] on the SAS program — the wallet’s public key is the nonce seed. This is the entry point for reading a wallet’s attestation directly on-chain; see the worked example above.

deriveCredentialPda()

Seeds ["credential", authority, name] (the credential name as UTF-8 bytes) on the SAS program. You normally don’t need this — read the credential address from program state instead.

deriveSchemaPda()

Seeds ["schema", credentialPda, name, version] on the SAS program — the first seed input is the credential PDA (not an authority key), name is UTF-8 bytes, and version is a single u8 byte. You normally don’t need this — read the schema address from program state instead.

Types

Canonical definitions for the package’s public types. The React package re-uses these — import them from @alien-id/sso-solana.

AlienSolanaSsoClientConfig

interface AlienSolanaSsoClientConfig { ssoBaseUrl: string; providerAddress: string; pollingInterval?: number; credentialSignerProgramId?: string; sasProgramId?: string; sessionRegistryProgramId?: string; allowInsecureSsoBaseUrl?: boolean; }

SolanaLinkResponse

interface SolanaLinkResponse { deep_link: string; polling_code: string; expired_at: number; }

SolanaPollResponse

interface SolanaPollResponse { status: 'pending' | 'authorized' | 'rejected' | 'expired'; oracle_signature?: string; // hex-encoded oracle_public_key?: string; // hex-encoded solana_address?: string; timestamp?: number; session_address?: string; }

BuildAttestationParams

interface BuildAttestationParams { connection: Connection; payerPublicKey: PublicKey; sessionAddress: string; oracleSignature: Uint8Array; oraclePublicKey: PublicKey; timestamp: number; expiry: number; // pass 0 — currently unused }

Solana Programs

The on-chain programs behind Solana SSO. Their IDs are the defaults in the client config and can be overridden there.

ProgramProgram IDRole
Credential Signer9cstDz8WWRAFaq1vVpTjfHz6tjgh6SJaqYFeZWi1pFHGManages credential signing and schema definitions
Session RegistryDeHa6pyZ2CFSbQQiNMm7FgoCXqmkX6tXG77C4Qycpta6Stores session entries and maps Solana addresses to sessions
SAS (Solana Attestation Service)22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdGCreates and manages on-chain attestations linking credentials, schemas, and user addresses

Error Handling

All async methods can throw — network failures, schema-validation failures, and missing on-chain accounts surface as exceptions. Wrap calls in try/catch:

try { const { deep_link, polling_code } = await client.generateDeeplink(solanaAddress); } catch (error) { console.error('Failed to generate deep link:', error); } try { const transaction = await client.buildCreateAttestationTransaction({ connection, payerPublicKey: wallet.publicKey, sessionAddress: pollResponse.session_address!, oracleSignature: Uint8Array.from(Buffer.from(pollResponse.oracle_signature!, 'hex')), oraclePublicKey: new PublicKey(Buffer.from(pollResponse.oracle_public_key!, 'hex')), timestamp: pollResponse.timestamp!, expiry: 0, }); } catch (error) { console.error('Transaction building failed:', error); }

Two on-chain failure modes are worth handling explicitly: the ±300-second timestamp freshness window (TimestampTooOld), and account already in use when a wallet already has an attestation — see Attestation Lifecycle and Re-Linking.

Complete Example

import { AlienSolanaSsoClient } from '@alien-id/sso-solana'; import { Connection, PublicKey } from '@solana/web3.js'; import { Buffer } from 'buffer'; const client = new AlienSolanaSsoClient({ ssoBaseUrl: 'https://sso.alien-api.com', providerAddress: 'platform-provider-address', }); const connection = new Connection('https://api.mainnet-beta.solana.com'); async function linkWallet(wallet: any) { const solanaAddress = wallet.publicKey.toBase58(); // Check whether the wallet already has a linking attestation // (existence of a link — not proof the current user controls the wallet) const existingSession = await client.getAttestation(solanaAddress); if (existingSession) { console.log('Wallet already linked:', existingSession); return; } // Generate deep link const { deep_link, polling_code } = await client.generateDeeplink(solanaAddress); displayQRCode(deep_link); // Poll for authorization const pollInterval = setInterval(async () => { const response = await client.pollAuth(polling_code); if (response.status === 'authorized') { clearInterval(pollInterval); // Build attestation transaction (submit within 5 minutes of the // oracle-signed timestamp, or it reverts with TimestampTooOld) const transaction = await client.buildCreateAttestationTransaction({ connection, payerPublicKey: wallet.publicKey, sessionAddress: response.session_address!, oracleSignature: Uint8Array.from(Buffer.from(response.oracle_signature!, 'hex')), oraclePublicKey: new PublicKey(Buffer.from(response.oracle_public_key!, 'hex')), timestamp: response.timestamp!, expiry: 0, }); // Set the blockhash and fee payer, then sign and send const { blockhash } = await connection.getLatestBlockhash(); transaction.recentBlockhash = blockhash; transaction.feePayer = wallet.publicKey; const signedTransaction = await wallet.signTransaction(transaction); const signature = await connection.sendRawTransaction( signedTransaction.serialize() ); await connection.confirmTransaction(signature); console.log('Attestation created! Signature:', signature); // Confirm the link is now visible const sessionAddress = await client.getAttestation(solanaAddress); console.log('Linked session:', sessionAddress); hideQRCode(); } else if (response.status === 'rejected' || response.status === 'expired') { clearInterval(pollInterval); console.error('Attestation flow failed:', response.status); hideQRCode(); } }, 5000); }

Next Steps

Last updated on