Albatross SDK

This commit is contained in:
2026-07-23 13:41:20 +00:00
commit 1d8788dec7
6 changed files with 2048 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
node_modules/
dist/
*.log
+116
View File
@@ -0,0 +1,116 @@
# @usealbatross/sdk
Official SDK for [Albatross](https://albatrossprotocol.com), the privacy protocol on Solana.
Move SOL privately with the bridge, or send SOL to anyone with a single unlinkable
Ghost Link. No wallet connection, no signup, noncustodial.
```bash
npm install @usealbatross/sdk
```
## Quick start
```ts
import { Albatross } from '@usealbatross/sdk'
const albatross = new Albatross()
// 1. Create a private bridge
const bridge = await albatross.createBridge({
amount: 1, // SOL the recipient receives (0.1, 0.5, 1 or 5)
destination: 'YourCleanWallet…',
})
console.log(`Send ${bridge.depositSol} SOL to ${bridge.depositAddress}`)
// 2. Wait until the payout lands
const result = await albatross.waitForBridge(bridge.id, {
onUpdate: (s) => console.log(s.status),
})
console.log('Done:', result.payoutSignature)
```
Even shorter, with the one shot helper:
```ts
const { bridge, wait } = await albatross.bridge({ amount: 1, destination })
console.log(`Send ${bridge.depositSol} SOL to ${bridge.depositAddress}`)
const done = await wait()
```
## Ghost Links
Send SOL to anyone with just a link. No address, no wallet connect, no onchain trail
between sender and receiver. The claim secret lives only in the URL fragment and is
never sent to or stored by the server.
```ts
import { Albatross } from '@usealbatross/sdk'
const albatross = new Albatross()
// 1. Create a Ghost Link (secret + claim URL are generated locally)
const { link, claimUrl } = await albatross.createGhostLink({
amount: 0.5, // SOL the receiver claims (0.1, 0.5, 1 or 5)
refundAddress: 'YourWallet…', // where funds return if never claimed
note: 'gm 👻', // optional, shown to the receiver
})
console.log(`Fund it: send ${link.depositSol} SOL to ${link.depositAddress}`)
console.log(`Then share: ${claimUrl}`) // keep this safe — it can never be recovered
// 2. The receiver claims to any wallet
await albatross.claimFromUrl(claimUrl, 'ReceiverWallet…')
```
⚠️ The `secret` and `claimUrl` are never stored server side and cannot be recovered.
Persist or share them the moment you create the link.
## How the bridge works
You send SOL to the fresh `depositAddress`. Albatross detects it and pays the same
amount to your `destination` from an unrelated wallet. The two transfers are never
linked onchain. The fee is 0.5%, added on top of the amount. Ghost Links work the
same way, but the payout goes to whoever claims the link.
## API
### `new Albatross(options?)`
| Option | Type | Default |
|---|---|---|
| `baseUrl` | `string` | `https://backend.albatrossprotocol.com/api` |
| `appUrl` | `string` | `https://albatrossprotocol.com` |
| `fetch` | `typeof fetch` | global `fetch` |
### Bridge methods
- `createBridge({ amount, destination })``Bridge` — create a bridge, returns the deposit address.
- `getBridge(id)``BridgeState` — fetch current status.
- `waitForBridge(id, opts?)``BridgeState` — poll until terminal (`completed`, `refunded`, `failed`, `expired`).
- `bridge({ amount, destination })``{ bridge, wait() }` — create and get a ready `wait()` helper.
- `getAmounts()``{ amounts, feeBps }` — selectable amounts and fee.
- `getStats()``{ bridges, volumeSol, fundingWallets, feeBps }` — network stats.
### Ghost Link methods
- `createGhostLink({ amount, refundAddress, note? })``{ link, secret, claimUrl }` — create a link locally hashing the secret.
- `getGhostLink(id)``GhostLinkState` — fetch current status.
- `claimGhostLink({ id, secret, destination })` → claim to a wallet.
- `claimFromUrl(claimUrl, destination)` → parse a claim URL and claim it.
- `waitForGhostLink(id, opts?)` → poll until terminal (`claimed`, `refunded`, `failed`, `expired`).
- `getGhostAmounts()``{ amounts, feeBps, claimExpiryDays }`.
- `buildClaimUrl(id, secret)` → build a shareable claim URL.
- `generateClaimSecret()` (named export) → `{ secret, secretHash }` low level helper.
### Statuses
Bridge: `awaiting_deposit``paying_out``completed`, or `refunded` / `failed` / `expired`.
Ghost Link: `awaiting_deposit``funded``claiming``claimed`, or `refunded` / `failed` / `expired`.
## License
MIT
+1473
View File
File diff suppressed because it is too large Load Diff
+43
View File
@@ -0,0 +1,43 @@
{
"name": "@usealbatross/sdk",
"version": "0.2.0",
"description": "Official SDK for Albatross, the privacy protocol on Solana. Private bridging and unlinkable Ghost Links in a few lines of code.",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"files": ["dist", "README.md"],
"scripts": {
"build": "tsup src/index.ts --format esm,cjs --dts --clean --minify"
},
"keywords": [
"albatross",
"solana",
"privacy",
"bridge",
"sdk",
"crypto",
"web3"
],
"author": "Albatross",
"license": "MIT",
"homepage": "https://albatrossprotocol.com",
"repository": {
"type": "git",
"url": "https://github.com/usealbatross/sdk"
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"tsup": "^8.3.0",
"typescript": "^5.6.0"
}
}
+399
View File
@@ -0,0 +1,399 @@
/**
* @usealbatross/sdk
* Official SDK for Albatross — the privacy protocol on Solana.
*
* Move SOL privately with the bridge, or send SOL to anyone with a single
* unlinkable Ghost Link. No wallet connection, noncustodial.
*/
export const DEFAULT_BASE_URL = 'https://backend.albatrossprotocol.com/api';
export const DEFAULT_APP_URL = 'https://albatrossprotocol.com';
export type BridgeStatus =
| 'awaiting_deposit'
| 'deposit_detected'
| 'paying_out'
| 'completed'
| 'refunding'
| 'refunded'
| 'failed'
| 'expired';
/** Terminal states: the bridge will not change after reaching one of these. */
export const TERMINAL_STATUSES: BridgeStatus[] = ['completed', 'refunded', 'failed', 'expired'];
export interface AlbatrossOptions {
/** API base URL. Defaults to the public Albatross API. */
baseUrl?: string;
/** Web app URL used to build shareable Ghost Link URLs. Defaults to albatrossprotocol.com. */
appUrl?: string;
/** Custom fetch implementation (for environments without global fetch). */
fetch?: typeof fetch;
}
export interface Amounts {
/** Selectable bridge amounts, in SOL. */
amounts: number[];
/** Bridge fee in basis points (50 = 0.5%). */
feeBps: number;
}
export interface Stats {
bridges: number;
volumeSol: number;
fundingWallets: number;
feeBps: number;
}
export interface CreateBridgeParams {
/** Amount to bridge (what the recipient receives). Must be one of the allowed amounts. */
amount: number;
/** Destination Solana address that receives the clean payout. */
destination: string;
}
export interface Bridge {
id: string;
/** Amount the recipient receives, in SOL. */
amountSol: number;
/** Amount the user must send (amount + fee), in SOL. */
depositSol: number;
/** Bridge fee, in SOL. */
feeSol: number;
/** Freshly generated address to send the deposit to. */
depositAddress: string;
destinationAddress: string;
status: BridgeStatus;
expiresAt: string;
}
export interface BridgeState extends Bridge {
depositedSol: number;
/** Payout transaction signature, once completed. */
payoutSignature: string | null;
/** Refund transaction signature, if refunded. */
refundSignature: string | null;
senderAddress: string | null;
error: string | null;
createdAt: string;
}
export interface WaitOptions {
/** Poll interval in ms. Default 5000. */
intervalMs?: number;
/** Give up after this many ms. Default 3600000 (1 hour). */
timeoutMs?: number;
/** Called on every poll with the latest state. */
onUpdate?: (state: BridgeState) => void;
/** AbortSignal to cancel waiting. */
signal?: AbortSignal;
}
// ===================== Ghost Links =====================
export type GhostStatus =
| 'awaiting_deposit'
| 'funded'
| 'claiming'
| 'claimed'
| 'refunding'
| 'refunded'
| 'failed'
| 'expired';
/** Terminal states: a Ghost Link will not change after reaching one of these. */
export const GHOST_TERMINAL_STATUSES: GhostStatus[] = ['claimed', 'refunded', 'failed', 'expired'];
export interface GhostAmounts {
/** Selectable Ghost Link amounts, in SOL. */
amounts: number[];
/** Fee in basis points (50 = 0.5%). */
feeBps: number;
/** Days a funded link stays claimable before auto refund. */
claimExpiryDays: number;
}
export interface CreateGhostLinkParams {
/** Amount the receiver claims. Must be one of the allowed amounts. */
amount: number;
/** Address the funds return to if the link is never claimed. */
refundAddress: string;
/** Optional note shown to the receiver (max 140 chars). */
note?: string;
}
export interface GhostLink {
id: string;
/** Amount the receiver claims, in SOL. */
amountSol: number;
/** Amount the sender must deposit (amount + fee), in SOL. */
depositSol: number;
feeSol: number;
/** Freshly generated address to fund the link. */
depositAddress: string;
note: string;
status: GhostStatus;
claimable: boolean;
expiresAt: string;
claimExpiresAt: string | null;
}
export interface GhostLinkState extends GhostLink {
depositedSol: number;
claimAddress: string | null;
payoutSignature: string | null;
refundSignature: string | null;
error: string | null;
createdAt: string;
}
/** Result of creating a Ghost Link. Keep `secret`/`claimUrl` safe — they are never recoverable. */
export interface CreatedGhostLink {
link: GhostLink;
/** The claim secret. Lives only here and in the claim URL — never sent to or stored by the server. */
secret: string;
/** The full shareable URL, including the secret in the fragment. */
claimUrl: string;
}
export interface GhostWaitOptions {
intervalMs?: number;
timeoutMs?: number;
onUpdate?: (state: GhostLinkState) => void;
signal?: AbortSignal;
}
const toHex = (bytes: Uint8Array) =>
Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
/**
* Generate a random claim secret and its SHA-256 hash. Only the hash is sent to
* the server; the secret stays client side and belongs in the URL fragment.
* Uses the Web Crypto API (available in browsers and Node 20+).
*/
export async function generateClaimSecret(): Promise<{ secret: string; secretHash: string }> {
const c = globalThis.crypto;
if (!c?.subtle) {
throw new AlbatrossError('Web Crypto API not available (needs a browser or Node 20+)');
}
const bytes = new Uint8Array(32);
c.getRandomValues(bytes);
const secret = toHex(bytes);
const digest = await c.subtle.digest('SHA-256', new TextEncoder().encode(secret));
return { secret, secretHash: toHex(new Uint8Array(digest)) };
}
export class AlbatrossError extends Error {
status?: number;
constructor(message: string, status?: number) {
super(message);
this.name = 'AlbatrossError';
this.status = status;
}
}
const sleep = (ms: number, signal?: AbortSignal) =>
new Promise<void>((resolve, reject) => {
const t = setTimeout(resolve, ms);
signal?.addEventListener('abort', () => {
clearTimeout(t);
reject(new AlbatrossError('Aborted'));
}, { once: true });
});
/**
* Albatross API client.
*
* @example
* ```ts
* import { Albatross } from '@usealbatross/sdk'
*
* const albatross = new Albatross()
*
* const bridge = await albatross.createBridge({ amount: 1, destination: 'YourWallet…' })
* console.log('Send', bridge.depositSol, 'SOL to', bridge.depositAddress)
*
* const result = await albatross.waitForBridge(bridge.id, {
* onUpdate: (s) => console.log(s.status),
* })
* console.log('Done:', result.payoutSignature)
* ```
*/
export class Albatross {
private baseUrl: string;
private appUrl: string;
private _fetch: typeof fetch;
constructor(options: AlbatrossOptions = {}) {
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, '');
this.appUrl = (options.appUrl ?? DEFAULT_APP_URL).replace(/\/$/, '');
const f = options.fetch ?? globalThis.fetch;
if (!f) {
throw new AlbatrossError(
'No fetch implementation found. Pass one via options.fetch (Node < 18).'
);
}
this._fetch = f.bind(globalThis);
}
private async req<T>(path: string, init?: RequestInit): Promise<T> {
const res = await this._fetch(this.baseUrl + path, {
...init,
headers: { 'Content-Type': 'application/json', ...(init?.headers ?? {}) },
});
let data: any = null;
try {
data = await res.json();
} catch {
/* ignore */
}
if (!res.ok) {
throw new AlbatrossError(data?.error ?? `Request failed (${res.status})`, res.status);
}
return data as T;
}
/** Public network statistics. */
getStats(): Promise<Stats> {
return this.req<Stats>('/stats');
}
/** The selectable bridge amounts and current fee. */
getAmounts(): Promise<Amounts> {
return this.req<Amounts>('/bridge/amounts');
}
/** Create a new private bridge and get a fresh deposit address. */
createBridge(params: CreateBridgeParams): Promise<Bridge> {
if (!params?.amount) throw new AlbatrossError('amount is required');
if (!params?.destination) throw new AlbatrossError('destination is required');
return this.req<Bridge>('/bridge', {
method: 'POST',
body: JSON.stringify({
amountSol: params.amount,
destinationAddress: params.destination,
}),
});
}
/** Fetch the current state of a bridge. */
getBridge(id: string): Promise<BridgeState> {
if (!id) throw new AlbatrossError('id is required');
return this.req<BridgeState>(`/bridge/${id}`);
}
/**
* Poll a bridge until it reaches a terminal state (completed, refunded,
* failed or expired), then resolve with the final state.
*/
async waitForBridge(id: string, opts: WaitOptions = {}): Promise<BridgeState> {
const interval = opts.intervalMs ?? 5000;
const timeout = opts.timeoutMs ?? 3_600_000;
const start = Date.now();
// Date.now is fine at runtime for a client SDK.
for (;;) {
const state = await this.getBridge(id);
opts.onUpdate?.(state);
if (TERMINAL_STATUSES.includes(state.status)) return state;
if (Date.now() - start > timeout) {
throw new AlbatrossError('Timed out waiting for bridge to complete');
}
await sleep(interval, opts.signal);
}
}
/**
* One shot helper: create a bridge and return it with a `wait()` function.
* @example
* const { bridge, wait } = await albatross.bridge({ amount: 1, destination })
* console.log('send', bridge.depositSol, 'to', bridge.depositAddress)
* const done = await wait()
*/
async bridge(params: CreateBridgeParams): Promise<{ bridge: Bridge; wait: (o?: WaitOptions) => Promise<BridgeState> }> {
const bridge = await this.createBridge(params);
return { bridge, wait: (o?: WaitOptions) => this.waitForBridge(bridge.id, o) };
}
// ===================== Ghost Links =====================
/** The selectable Ghost Link amounts, fee, and claim expiry. */
getGhostAmounts(): Promise<GhostAmounts> {
return this.req<GhostAmounts>('/ghost/amounts');
}
/** Build the shareable claim URL for a link id and secret. */
buildClaimUrl(id: string, secret: string): string {
return `${this.appUrl}/g/${id}#${secret}`;
}
/**
* Create a Ghost Link. Generates a claim secret locally, sends only its hash,
* and returns the link plus the secret and the full shareable claim URL.
*
* IMPORTANT: the secret and claimUrl are never stored by the server and cannot
* be recovered. Persist or share them immediately.
*/
async createGhostLink(params: CreateGhostLinkParams): Promise<CreatedGhostLink> {
if (!params?.amount) throw new AlbatrossError('amount is required');
if (!params?.refundAddress) throw new AlbatrossError('refundAddress is required');
const { secret, secretHash } = await generateClaimSecret();
const link = await this.req<GhostLink>('/ghost', {
method: 'POST',
body: JSON.stringify({
amountSol: params.amount,
refundAddress: params.refundAddress,
secretHash,
note: params.note,
}),
});
return { link, secret, claimUrl: this.buildClaimUrl(link.id, secret) };
}
/** Fetch the current state of a Ghost Link. */
getGhostLink(id: string): Promise<GhostLinkState> {
if (!id) throw new AlbatrossError('id is required');
return this.req<GhostLinkState>(`/ghost/${id}`);
}
/**
* Claim a Ghost Link to any wallet. The secret comes from the claim URL's
* fragment. The payout is sent from an unrelated wallet by the protocol.
*/
claimGhostLink(params: { id: string; secret: string; destination: string }): Promise<{ ok: boolean; status: GhostStatus; id: string }> {
if (!params?.id) throw new AlbatrossError('id is required');
if (!params?.secret) throw new AlbatrossError('secret is required');
if (!params?.destination) throw new AlbatrossError('destination is required');
return this.req(`/ghost/${params.id}/claim`, {
method: 'POST',
body: JSON.stringify({ secret: params.secret, destinationAddress: params.destination }),
});
}
/** Poll a Ghost Link until it reaches a terminal state, then resolve. */
async waitForGhostLink(id: string, opts: GhostWaitOptions = {}): Promise<GhostLinkState> {
const interval = opts.intervalMs ?? 5000;
const timeout = opts.timeoutMs ?? 3_600_000;
const start = Date.now();
for (;;) {
const state = await this.getGhostLink(id);
opts.onUpdate?.(state);
if (GHOST_TERMINAL_STATUSES.includes(state.status)) return state;
if (Date.now() - start > timeout) {
throw new AlbatrossError('Timed out waiting for Ghost Link');
}
await sleep(interval, opts.signal);
}
}
/**
* One shot helper: parse a claim URL and claim it to a destination wallet.
* Accepts a full URL (…/g/<id>#<secret>) and extracts id + secret.
*/
async claimFromUrl(claimUrl: string, destination: string): Promise<{ ok: boolean; status: GhostStatus; id: string }> {
const m = claimUrl.match(/\/g\/([^/#?]+)#(.+)$/);
if (!m) throw new AlbatrossError('Invalid Ghost Link URL');
return this.claimGhostLink({ id: m[1], secret: m[2], destination });
}
}
export default Albatross;
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2020", "DOM"],
"declaration": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src"]
}