Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions soroban-client/lib/checkin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
export interface CheckInParams {
organizer: string;
eventId: number;
tokenId: number;
}

export interface CheckInResult {
success: boolean;
timestamp: number;
transactionHash: string;
}

export async function checkInEvent(params: CheckInParams): Promise<CheckInResult> {
const EVENT_MANAGER_CONTRACT =
process.env.NEXT_PUBLIC_EVENT_MANAGER_CONTRACT || "<MISSING_CONTRACT_ID>";
const HORIZON_URL =
process.env.NEXT_PUBLIC_HORIZON_URL || "https://horizon-testnet.stellar.org";
const NETWORK_PASSPHRASE =
process.env.NEXT_PUBLIC_NETWORK_PASSPHRASE || "Test SDF Network ; September 2015";

if (!EVENT_MANAGER_CONTRACT || EVENT_MANAGER_CONTRACT === "<MISSING_CONTRACT_ID>") {
throw new Error(
"EVENT_MANAGER_CONTRACT is not configured. Set NEXT_PUBLIC_EVENT_MANAGER_CONTRACT in your env."
);
}

const { Server, TransactionBuilder, Operation, Networks } = await import("@stellar/stellar-sdk");
const { nativeToScVal } = await import("@stellar/stellar-base");
const { signTransaction } = await import("@stellar/freighter-api");

const server = new Server(HORIZON_URL);
const sourceAccount = await server.loadAccount(params.organizer);
const fee = await server.fetchBaseFee();

const args = [
nativeToScVal(params.eventId, { type: "u32" }),
nativeToScVal(params.tokenId, { type: "u32" }),
];

const operation = Operation.invokeContractFunction({
contract: EVENT_MANAGER_CONTRACT,
function: "check_in",
args,
});

const tx = new TransactionBuilder(sourceAccount, {
fee: fee.toString(),
networkPassphrase: NETWORK_PASSPHRASE,
})
.addOperation(operation)
.setTimeout(30)
.build();

const txXdr = tx.toXDR();
const { signedTxXdr } = await signTransaction(txXdr, {
networkPassphrase: NETWORK_PASSPHRASE,
address: params.organizer,
});

const response = await server.submitTransaction(signedTxXdr);

return {
success: true,
timestamp: Math.floor(Date.now() / 1000),
transactionHash: response.hash,
};
}

export async function verifyCheckIn(
eventId: number,
tokenId: number
): Promise<boolean> {
const EVENT_MANAGER_CONTRACT =
process.env.NEXT_PUBLIC_EVENT_MANAGER_CONTRACT || "<MISSING_CONTRACT_ID>";
const HORIZON_URL =
process.env.NEXT_PUBLIC_HORIZON_URL || "https://horizon-testnet.stellar.org";

if (!EVENT_MANAGER_CONTRACT || EVENT_MANAGER_CONTRACT === "<MISSING_CONTRACT_ID>") {
throw new Error("EVENT_MANAGER_CONTRACT is not configured");
}

const server = new Server(HORIZON_URL);

try {
const result = await server.getContractEvents({
contract: EVENT_MANAGER_CONTRACT,
topic: [["check_in", eventId, tokenId]],
});

return result.events.length > 0;
} catch {
return false;
}
}
106 changes: 106 additions & 0 deletions soroban-client/lib/events/websocket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
export interface EventListenerOptions {
eventContractId: string;
startCursor?: string;
onEvent: (event: ContractEvent) => void;
onError?: (error: Error) => void;
}

export interface ContractEvent {
id: string;
contractId: string;
type: string;
data: Record<string, unknown>;
timestamp: number;
cursor: string;
}

export type ConnectionStatus = "connecting" | "connected" | "disconnecting" | "disconnected" | "error";

export class EventMonitor {
private ws: WebSocket | null = null;
private status: ConnectionStatus = "disconnected";
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 1000;
private options: EventListenerOptions | null = null;

async connect(options: EventListenerOptions): Promise<void> {
this.options = options;
this.status = "connecting";

const wsUrl = this.buildWsUrl(options.eventContractId, options.startCursor);

try {
this.ws = new WebSocket(wsUrl);

this.ws.onopen = () => {
this.status = "connected";
this.reconnectAttempts = 0;
};

this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
const contractEvent: ContractEvent = {
id: data.id,
contractId: data.contractId || options.eventContractId,
type: data.type || data.topic || "unknown",
data: data.data || {},
timestamp: data.timestamp || Date.now(),
cursor: data.cursor || "",
};
options.onEvent(contractEvent);
} catch (e) {
console.error("Failed to parse WebSocket message:", e);
}
};

this.ws.onerror = () => {
this.status = "error";
options.onError?.(new Error("WebSocket connection error"));
};

this.ws.onclose = () => {
this.status = "disconnected";
this.attemptReconnect();
};
} catch (e) {
this.status = "error";
throw e;
}
}

private buildWsUrl(contractId: string, cursor?: string): string {
const horizonUrl = process.env.NEXT_PUBLIC_HORIZON_URL || "https://horizon-testnet.stellar.org";
const protocol = horizonUrl.includes("testnet") ? "wss" : "wss";
return `${protocol}${
horizonUrl.replace("https://", "").replace("http://", "")
}/events?contractId=${contractId}${cursor ? `&cursor=${cursor}` : ""}`;
}

private attemptReconnect(): void {
if (this.options && this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
setTimeout(() => {
this.options && this.connect(this.options);
}, this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1));
}
}

disconnect(): void {
if (this.ws) {
this.status = "disconnecting";
this.ws.close();
this.ws = null;
}
this.status = "disconnected";
}

getStatus(): ConnectionStatus {
return this.status;
}
}

export function createEventMonitor(): EventMonitor {
return new EventMonitor();
}
45 changes: 45 additions & 0 deletions soroban-client/lib/pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export interface PaginationParams {
cursor?: string;
limit?: number;
}

export interface PaginatedResult<T> {
data: T[];
nextCursor: string | null;
hasMore: boolean;
total?: number;
}

export function createPaginationParams(
cursor?: string,
limit: number = 10
): PaginationParams {
return {
cursor,
limit: Math.min(Math.max(1, limit), 100),
};
}

export function parsePaginationResponse<T>(
data: T[],
total?: number
): PaginatedResult<T> {
return {
data,
nextCursor: data.length > 0 ? encodeCursor(data[data.length - 1]) : null,
hasMore: data.length > 0,
total,
};
}

function encodeCursor(item: unknown): string {
return Buffer.from(JSON.stringify(item)).toString("base64");
}

export function decodeCursor<T>(cursor: string): T | null {
try {
return JSON.parse(Buffer.from(cursor, "base64").toString()) as T;
} catch {
return null;
}
}
18 changes: 18 additions & 0 deletions soroban-client/lib/wallet/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export interface WalletAdapter {
isConnected: boolean;
isInstalled: boolean;
address: string | null;
connect(): Promise<void>;
disconnect(): void;
signTransaction(txXdr: string, options?: SignOptions): Promise<string>;
}

export interface SignOptions {
networkPassphrase: string;
address: string;
}

export interface WalletConfig {
networkPassphrase: string;
horizonUrl: string;
}
Loading