Skip to content

Commit 605cc55

Browse files
authored
Merge pull request #318 from dimka90/feat/wave-229-238-189-115
feat: implement wave issues 229, 238, 189, 115
2 parents 9009713 + aea43cc commit 605cc55

4 files changed

Lines changed: 263 additions & 0 deletions

File tree

soroban-client/lib/checkin.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
export interface CheckInParams {
2+
organizer: string;
3+
eventId: number;
4+
tokenId: number;
5+
}
6+
7+
export interface CheckInResult {
8+
success: boolean;
9+
timestamp: number;
10+
transactionHash: string;
11+
}
12+
13+
export async function checkInEvent(params: CheckInParams): Promise<CheckInResult> {
14+
const EVENT_MANAGER_CONTRACT =
15+
process.env.NEXT_PUBLIC_EVENT_MANAGER_CONTRACT || "<MISSING_CONTRACT_ID>";
16+
const HORIZON_URL =
17+
process.env.NEXT_PUBLIC_HORIZON_URL || "https://horizon-testnet.stellar.org";
18+
const NETWORK_PASSPHRASE =
19+
process.env.NEXT_PUBLIC_NETWORK_PASSPHRASE || "Test SDF Network ; September 2015";
20+
21+
if (!EVENT_MANAGER_CONTRACT || EVENT_MANAGER_CONTRACT === "<MISSING_CONTRACT_ID>") {
22+
throw new Error(
23+
"EVENT_MANAGER_CONTRACT is not configured. Set NEXT_PUBLIC_EVENT_MANAGER_CONTRACT in your env."
24+
);
25+
}
26+
27+
const { Server, TransactionBuilder, Operation, Networks } = await import("@stellar/stellar-sdk");
28+
const { nativeToScVal } = await import("@stellar/stellar-base");
29+
const { signTransaction } = await import("@stellar/freighter-api");
30+
31+
const server = new Server(HORIZON_URL);
32+
const sourceAccount = await server.loadAccount(params.organizer);
33+
const fee = await server.fetchBaseFee();
34+
35+
const args = [
36+
nativeToScVal(params.eventId, { type: "u32" }),
37+
nativeToScVal(params.tokenId, { type: "u32" }),
38+
];
39+
40+
const operation = Operation.invokeContractFunction({
41+
contract: EVENT_MANAGER_CONTRACT,
42+
function: "check_in",
43+
args,
44+
});
45+
46+
const tx = new TransactionBuilder(sourceAccount, {
47+
fee: fee.toString(),
48+
networkPassphrase: NETWORK_PASSPHRASE,
49+
})
50+
.addOperation(operation)
51+
.setTimeout(30)
52+
.build();
53+
54+
const txXdr = tx.toXDR();
55+
const { signedTxXdr } = await signTransaction(txXdr, {
56+
networkPassphrase: NETWORK_PASSPHRASE,
57+
address: params.organizer,
58+
});
59+
60+
const response = await server.submitTransaction(signedTxXdr);
61+
62+
return {
63+
success: true,
64+
timestamp: Math.floor(Date.now() / 1000),
65+
transactionHash: response.hash,
66+
};
67+
}
68+
69+
export async function verifyCheckIn(
70+
eventId: number,
71+
tokenId: number
72+
): Promise<boolean> {
73+
const EVENT_MANAGER_CONTRACT =
74+
process.env.NEXT_PUBLIC_EVENT_MANAGER_CONTRACT || "<MISSING_CONTRACT_ID>";
75+
const HORIZON_URL =
76+
process.env.NEXT_PUBLIC_HORIZON_URL || "https://horizon-testnet.stellar.org";
77+
78+
if (!EVENT_MANAGER_CONTRACT || EVENT_MANAGER_CONTRACT === "<MISSING_CONTRACT_ID>") {
79+
throw new Error("EVENT_MANAGER_CONTRACT is not configured");
80+
}
81+
82+
const server = new Server(HORIZON_URL);
83+
84+
try {
85+
const result = await server.getContractEvents({
86+
contract: EVENT_MANAGER_CONTRACT,
87+
topic: [["check_in", eventId, tokenId]],
88+
});
89+
90+
return result.events.length > 0;
91+
} catch {
92+
return false;
93+
}
94+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
export interface EventListenerOptions {
2+
eventContractId: string;
3+
startCursor?: string;
4+
onEvent: (event: ContractEvent) => void;
5+
onError?: (error: Error) => void;
6+
}
7+
8+
export interface ContractEvent {
9+
id: string;
10+
contractId: string;
11+
type: string;
12+
data: Record<string, unknown>;
13+
timestamp: number;
14+
cursor: string;
15+
}
16+
17+
export type ConnectionStatus = "connecting" | "connected" | "disconnecting" | "disconnected" | "error";
18+
19+
export class EventMonitor {
20+
private ws: WebSocket | null = null;
21+
private status: ConnectionStatus = "disconnected";
22+
private reconnectAttempts = 0;
23+
private maxReconnectAttempts = 5;
24+
private reconnectDelay = 1000;
25+
private options: EventListenerOptions | null = null;
26+
27+
async connect(options: EventListenerOptions): Promise<void> {
28+
this.options = options;
29+
this.status = "connecting";
30+
31+
const wsUrl = this.buildWsUrl(options.eventContractId, options.startCursor);
32+
33+
try {
34+
this.ws = new WebSocket(wsUrl);
35+
36+
this.ws.onopen = () => {
37+
this.status = "connected";
38+
this.reconnectAttempts = 0;
39+
};
40+
41+
this.ws.onmessage = (event) => {
42+
try {
43+
const data = JSON.parse(event.data);
44+
const contractEvent: ContractEvent = {
45+
id: data.id,
46+
contractId: data.contractId || options.eventContractId,
47+
type: data.type || data.topic || "unknown",
48+
data: data.data || {},
49+
timestamp: data.timestamp || Date.now(),
50+
cursor: data.cursor || "",
51+
};
52+
options.onEvent(contractEvent);
53+
} catch (e) {
54+
console.error("Failed to parse WebSocket message:", e);
55+
}
56+
};
57+
58+
this.ws.onerror = () => {
59+
this.status = "error";
60+
options.onError?.(new Error("WebSocket connection error"));
61+
};
62+
63+
this.ws.onclose = () => {
64+
this.status = "disconnected";
65+
this.attemptReconnect();
66+
};
67+
} catch (e) {
68+
this.status = "error";
69+
throw e;
70+
}
71+
}
72+
73+
private buildWsUrl(contractId: string, cursor?: string): string {
74+
const horizonUrl = process.env.NEXT_PUBLIC_HORIZON_URL || "https://horizon-testnet.stellar.org";
75+
const protocol = horizonUrl.includes("testnet") ? "wss" : "wss";
76+
return `${protocol}${
77+
horizonUrl.replace("https://", "").replace("http://", "")
78+
}/events?contractId=${contractId}${cursor ? `&cursor=${cursor}` : ""}`;
79+
}
80+
81+
private attemptReconnect(): void {
82+
if (this.options && this.reconnectAttempts < this.maxReconnectAttempts) {
83+
this.reconnectAttempts++;
84+
setTimeout(() => {
85+
this.options && this.connect(this.options);
86+
}, this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1));
87+
}
88+
}
89+
90+
disconnect(): void {
91+
if (this.ws) {
92+
this.status = "disconnecting";
93+
this.ws.close();
94+
this.ws = null;
95+
}
96+
this.status = "disconnected";
97+
}
98+
99+
getStatus(): ConnectionStatus {
100+
return this.status;
101+
}
102+
}
103+
104+
export function createEventMonitor(): EventMonitor {
105+
return new EventMonitor();
106+
}

soroban-client/lib/pagination.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
export interface PaginationParams {
2+
cursor?: string;
3+
limit?: number;
4+
}
5+
6+
export interface PaginatedResult<T> {
7+
data: T[];
8+
nextCursor: string | null;
9+
hasMore: boolean;
10+
total?: number;
11+
}
12+
13+
export function createPaginationParams(
14+
cursor?: string,
15+
limit: number = 10
16+
): PaginationParams {
17+
return {
18+
cursor,
19+
limit: Math.min(Math.max(1, limit), 100),
20+
};
21+
}
22+
23+
export function parsePaginationResponse<T>(
24+
data: T[],
25+
total?: number
26+
): PaginatedResult<T> {
27+
return {
28+
data,
29+
nextCursor: data.length > 0 ? encodeCursor(data[data.length - 1]) : null,
30+
hasMore: data.length > 0,
31+
total,
32+
};
33+
}
34+
35+
function encodeCursor(item: unknown): string {
36+
return Buffer.from(JSON.stringify(item)).toString("base64");
37+
}
38+
39+
export function decodeCursor<T>(cursor: string): T | null {
40+
try {
41+
return JSON.parse(Buffer.from(cursor, "base64").toString()) as T;
42+
} catch {
43+
return null;
44+
}
45+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export interface WalletAdapter {
2+
isConnected: boolean;
3+
isInstalled: boolean;
4+
address: string | null;
5+
connect(): Promise<void>;
6+
disconnect(): void;
7+
signTransaction(txXdr: string, options?: SignOptions): Promise<string>;
8+
}
9+
10+
export interface SignOptions {
11+
networkPassphrase: string;
12+
address: string;
13+
}
14+
15+
export interface WalletConfig {
16+
networkPassphrase: string;
17+
horizonUrl: string;
18+
}

0 commit comments

Comments
 (0)