import { DEFAULT_HOSTS, Events, HttpRequester, type IPortalProvider, generateTraceId, sdkLogger, } from '@portal-hq/utils' import type { YieldXyzEnterRequest, YieldXyzEnterYieldResponse, YieldXyzExitRequest, YieldXyzExitResponse, YieldXyzGetBalancesRequest, YieldXyzGetBalancesResponse, YieldXyzGetHistoricalActionsRequest, YieldXyzGetHistoricalActionsResponse, YieldXyzGetTransactionResponse, YieldXyzGetYieldDefaultsRequest, YieldXyzGetYieldDefaultsResponse, YieldXyzGetYieldValidatorsResponse, YieldXyzGetYieldsRequest, YieldXyzGetYieldsResponse, YieldXyzManageYieldRequest, YieldXyzManageYieldResponse, YieldXyzTrackTransactionRequest, YieldXyzTrackTransactionResponse, } from '../../types' export interface IPortalYieldXyzApi { getYields( request?: YieldXyzGetYieldsRequest, ): Promise enterYield(request: YieldXyzEnterRequest): Promise exitYield(request: YieldXyzExitRequest): Promise manageYield( request: YieldXyzManageYieldRequest, ): Promise getYieldBalances( request: YieldXyzGetBalancesRequest, ): Promise getHistoricalYieldActions( request: YieldXyzGetHistoricalActionsRequest, ): Promise getYieldTransaction( transactionId: string, ): Promise submitTransactionHash( request: YieldXyzTrackTransactionRequest, ): Promise getYieldDefaults( request?: YieldXyzGetYieldDefaultsRequest, ): Promise getYieldValidators( yieldId: string, ): Promise } const YieldXyzEndpoint = { BASE_PATH: '/api/v3/clients/me/integrations/yield-xyz', yields(): string { return `${this.BASE_PATH}/yields` }, actionsEnter(): string { return `${this.BASE_PATH}/actions/enter` }, actionsExit(): string { return `${this.BASE_PATH}/actions/exit` }, actionsManage(): string { return `${this.BASE_PATH}/actions/manage` }, actions(): string { return `${this.BASE_PATH}/actions` }, balances(): string { return `${this.BASE_PATH}/yields/balances` }, transaction(transactionId: string): string { return `${this.BASE_PATH}/transactions/${transactionId}` }, submitHash(transactionId: string): string { return `${this.BASE_PATH}/transactions/${transactionId}/submit-hash` }, defaults(): string { return `${this.BASE_PATH}/yields/defaults` }, validators(yieldId: string): string { return `${this.BASE_PATH}/yields/${encodeURIComponent(yieldId)}/validators` }, } as const export interface PortalYieldXyzApiOptions { apiKey: string apiHost?: string provider?: IPortalProvider requests?: HttpRequester analytics?: { track: ( event: string, properties?: Record, ) => Promise<{ success: boolean }> } } export class PortalYieldXyzApi implements IPortalYieldXyzApi { private readonly apiKey: string private readonly apiUrl: string private readonly requests: HttpRequester private readonly provider?: IPortalProvider private readonly analytics?: PortalYieldXyzApiOptions['analytics'] constructor(options: PortalYieldXyzApiOptions) { this.apiKey = options.apiKey this.apiUrl = options.apiHost?.startsWith('localhost') ? `http://${options.apiHost}` : `https://${options.apiHost || DEFAULT_HOSTS.API}` this.provider = options.provider this.analytics = options.analytics this.requests = options.requests || new HttpRequester({ baseUrl: this.apiUrl, }) } public async getYields( request?: YieldXyzGetYieldsRequest, ): Promise { const path = YieldXyzEndpoint.yields() const queryParams = this.buildQueryParams({ offset: request?.offset, limit: request?.limit, network: request?.network, yieldId: request?.yieldId, type: request?.type, hasCooldownPeriod: request?.hasCooldownPeriod, hasWarmupPeriod: request?.hasWarmupPeriod, token: request?.token, inputToken: request?.inputToken, provider: request?.provider, search: request?.search, sort: request?.sort, }) const fullPath = queryParams ? `${path}?${queryParams}` : path const response = await this.get(fullPath) await this.trackEvent(Events.YieldXyzDiscover, { path: fullPath, network: request?.network, limit: request?.limit, offset: request?.offset, }) return response } public async enterYield( request: YieldXyzEnterRequest, ): Promise { const path = YieldXyzEndpoint.actionsEnter() const address = await this.resolveAddress(request.address) const body = { address, yieldId: request.yieldId, arguments: { amount: request.amount, ...request.arguments, }, } const response = await this.post(path, body) await this.trackEvent(Events.YieldXyzEnter, { path, yieldId: request.yieldId, amount: request.amount, }) return response } public async exitYield( request: YieldXyzExitRequest, ): Promise { const path = YieldXyzEndpoint.actionsExit() const address = await this.resolveAddress(request.address) const body = { address, yieldId: request.yieldId, arguments: { amount: request.amount, ...request.arguments, }, } const response = await this.post(path, body) await this.trackEvent(Events.YieldXyzExit, { path, yieldId: request.yieldId, amount: request.amount, }) return response } public async manageYield( request: YieldXyzManageYieldRequest, ): Promise { const path = YieldXyzEndpoint.actionsManage() const address = await this.resolveAddress(request.address) let passthrough: string if (request.passthrough && typeof request.passthrough === 'string') { passthrough = request.passthrough.trim() } else { const passthroughData = { addresses: { address }, args: request.arguments || {}, } passthrough = btoa(JSON.stringify(passthroughData)) } const body = { action: request.action, yieldId: request.yieldId, address, passthrough, } const response = await this.post(path, body) await this.trackEvent(Events.YieldXyzManage, { path, yieldId: request.yieldId, action: request.action, }) return response } public async getYieldBalances( request: YieldXyzGetBalancesRequest, ): Promise { const path = YieldXyzEndpoint.balances() const response = await this.post( path, request as unknown as Record, ) await this.trackEvent(Events.YieldXyzGetBalances, { path, queries: request.queries, }) return response } public async getHistoricalYieldActions( request: YieldXyzGetHistoricalActionsRequest, ): Promise { const path = YieldXyzEndpoint.actions() const queryParams = this.buildQueryParams({ address: request.address, offset: request.offset, limit: request.limit, status: request.status, intent: request.intent, type: request.type, yieldId: request.yieldId, }) const fullPath = queryParams ? `${path}?${queryParams}` : path const response = await this.get(fullPath) await this.trackEvent(Events.YieldXyzGetHistoricalActions, { path: fullPath, address: request.address, yieldId: request.yieldId, limit: request.limit, offset: request.offset, }) return response } public async getYieldTransaction( transactionId: string, ): Promise { const path = YieldXyzEndpoint.transaction(transactionId) const response = await this.get(path) await this.trackEvent(Events.YieldXyzGetTransaction, { path, transactionId, }) return response } public async submitTransactionHash( request: YieldXyzTrackTransactionRequest, ): Promise { const path = YieldXyzEndpoint.submitHash(request.transactionId) const response = await this.put( path, request as unknown as Record, ) await this.trackEvent(Events.YieldXyzTrack, { path, transactionId: request.transactionId, }) return response } public async getYieldDefaults( request?: YieldXyzGetYieldDefaultsRequest, ): Promise { const params = new URLSearchParams() if (request?.includeOpportunities !== undefined) { params.set('includeOpportunities', String(request.includeOpportunities)) } const qs = params.toString() const path = qs ? `${YieldXyzEndpoint.defaults()}?${qs}` : YieldXyzEndpoint.defaults() const response = await this.get(path) return response } public async getYieldValidators( yieldId: string, ): Promise { const path = YieldXyzEndpoint.validators(yieldId) return this.get(path) } private async resolveAddress( requestAddress: string | undefined, ): Promise { if (requestAddress) { return requestAddress } if (this.provider) { try { const addresses = await this.provider.addresses if (addresses?.eip155) { return addresses.eip155 } const legacyAddress = await this.provider.address if (legacyAddress) { return legacyAddress } } catch (error) { sdkLogger.warn( '[PortalYieldXyzApi] Failed to resolve address from provider, falling back to error', error, ) } } throw new Error('No address found') } private buildQueryParams(params: Record): string { const queryParts: string[] = [] for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null) { const encodedValue = encodeURIComponent(String(value)) queryParts.push(`${key}=${encodedValue}`) } } return queryParts.join('&') } private async get(path: string): Promise { return this.requests.get(path, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', }, traceId: generateTraceId(), }) } private async post>( path: string, body: B, ): Promise { return this.requests.post(path, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', 'Content-Type': 'application/json', }, body: body as unknown as Record, traceId: generateTraceId(), }) } private async put>( path: string, body: B, ): Promise { return this.requests.put(path, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', 'Content-Type': 'application/json', }, body: body as unknown as Record, traceId: generateTraceId(), }) } private async trackEvent( event: string, properties: Record, ): Promise { try { await this.analytics?.track(event, properties) } catch (error) { sdkLogger.warn( `[PortalYieldXyzApi] Failed to track event "${event}":`, error, ) } } } export default PortalYieldXyzApi