Albatross SDK
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.log
|
||||||
@@ -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
|
||||||
Generated
+1473
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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;
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user