From 7af129b0527930276978a1e6f95f454dd597c4cd Mon Sep 17 00:00:00 2001 From: CaniceFavour Date: Wed, 29 Jul 2026 12:38:29 +0100 Subject: [PATCH] improve Soroban stream ID extraction in createStream --- apps/web/src/lib/api.ts | 104 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 3 deletions(-) diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts index 86616108..f9114558 100644 --- a/apps/web/src/lib/api.ts +++ b/apps/web/src/lib/api.ts @@ -1,10 +1,12 @@ import type { AssembledTransaction } from '@stellar/stellar-sdk/contract'; +import { scValToNative } from '@stellar/stellar-sdk'; import { PAYMENT_STREAM_CONTRACT_ID, DISTRIBUTOR_CONTRACT_ID, SOROBAN_RPC_URL, NETWORK_PASSPHRASE } from '@/lib/constants'; import { env } from '@/lib/env'; import { throwIfAborted } from '@/utils/retry'; import { StellarService, type Stream as ServiceStream, type AccountInfo } from '@/services'; import { PaymentStreamClient } from '../../../../packages/sdk/src/PaymentStreamClient'; import { DistributorClient } from '../../../../packages/sdk/src/DistributorClient'; +import { SorobanEventParser, type ContractEventRaw } from '../../../../packages/sdk/src/utils/SorobanEventParser'; import { Stream, StreamStatus } from '../types'; type WalletSigner = (xdr: string) => Promise; @@ -54,6 +56,102 @@ function ensureSafeNumber(value: bigint): number { return Number(value); } +function extractBigInt(value: unknown): bigint | undefined { + if (typeof value === 'bigint') return value; + if (typeof value === 'number' && Number.isSafeInteger(value)) return BigInt(value); + if (typeof value === 'string' && /^[0-9]+$/.test(value)) return BigInt(value); + if (typeof value !== 'object' || value === null) return undefined; + + try { + const native = scValToNative(value as never); + if (native === value) return undefined; + return extractBigInt(native); + } catch { + return undefined; + } +} + +function normalizeEventTopicItem(item: unknown): string | undefined { + if (typeof item === 'string') return item; + if (typeof item !== 'object' || item === null) return undefined; + try { + const native = scValToNative(item as never); + return typeof native === 'string' ? native : undefined; + } catch { + return undefined; + } +} + +function normalizeContractEventRaw(event: unknown): ContractEventRaw | undefined { + if (typeof event !== 'object' || event === null) return undefined; + + const item = event as Record; + const contract_id = + typeof item.contract_id === 'string' + ? item.contract_id + : typeof item.contractId === 'string' + ? item.contractId + : undefined; + + const topicRaw = item.topic ?? item.topics; + if (!contract_id || !Array.isArray(topicRaw)) return undefined; + + const topic = topicRaw.map(normalizeEventTopicItem); + if (topic.some((x) => typeof x !== 'string')) return undefined; + + const valueRaw = item.value; + let value: unknown = valueRaw; + if (valueRaw && typeof valueRaw === 'object') { + try { + value = scValToNative(valueRaw as never); + } catch { + value = valueRaw; + } + } + + return { contract_id, topic: topic as string[], value }; +} + +function extractStreamIdFromTxEvents(tx: unknown): bigint | undefined { + if (typeof tx !== 'object' || tx === null) return undefined; + + const item = tx as Record; + const events = + item.events ?? + (typeof item.simulation === 'object' && item.simulation !== null + ? (item.simulation as Record).events + : undefined) ?? + (typeof item.simulationResult === 'object' && item.simulationResult !== null + ? (item.simulationResult as Record).events + : undefined); + + if (!Array.isArray(events)) return undefined; + + const normalized: ContractEventRaw[] = []; + for (const event of events) { + const raw = normalizeContractEventRaw(event); + if (raw) normalized.push(raw); + } + + if (normalized.length === 0) return undefined; + + const parser = new SorobanEventParser({ contractId: PAYMENT_STREAM_CONTRACT_ID }); + const { parsed } = parser.parseAll(normalized); + + for (const evt of parsed) { + const payload = (evt as unknown as { payload?: unknown }).payload; + if (typeof payload !== 'object' || payload === null) continue; + const candidate = + (payload as Record).stream_id ?? + (payload as Record).streamId ?? + (payload as Record).id; + const streamId = extractBigInt(candidate); + if (streamId !== undefined) return streamId; + } + + return undefined; +} + async function signAndSendTx( tx: AssembledTransaction, signTransaction?: WalletSigner @@ -123,9 +221,9 @@ export async function createStream(params: { await signAndSendTx(tx as AssembledTransaction, params.signTransaction); - const streamId = tx.result; - if (typeof streamId !== 'bigint') { - throw new Error('Contract did not return a stream id'); + const streamId = extractBigInt(tx.result) ?? extractStreamIdFromTxEvents(tx); + if (streamId === undefined) { + throw new Error('Contract did not return a valid stream id'); } return ensureSafeNumber(streamId); }