asyncapi: 3.0.0

info:
  title: Tripport — Watchlist Push WS (custom)
  version: 0.2.0-api-vector-s3
  description: |
    Tripport-specific watchlist push channel. After the REST control plane
    (`POST /v1/sol/watchlist`) registers a pubkey + callback_type=`ws`,
    the matching server-side push events are delivered via this channel.

    Lifecycle:
      1. Client opens `/ws/sol-watch` with bearer auth.
      2. Client sends `WatchSubscribe` with the set of entry ids to listen
         on (or `["*"]` for all tenant-owned entries).
      3. Server replies `WatchSubscribeAck` with the set of effective ids
         (filters away entries owned by other tenants).
      4. Server pushes `WatchEvent` messages until the connection closes
         or `WatchEntryRevoked` fires for every active id.

    Push trigger: internal accountSubscribe stream against the registered
    pubkey + a payload classifier that decides "did the watched state
    actually change" (avoids noise from no-op rent rebates etc.).

    Error envelope: WSErrorFrame from `_shared.yaml`; canonical close
    codes 4001/4003/4029/4030 per PLAN_TECH §11.

x-tier-vector-version: 1
x-ws-close-codes:
  $ref: '_shared.yaml#/x-ws-close-codes'

defaultContentType: application/json

servers:
  production:
    $ref: '_shared.yaml#/servers/production'
  staging:
    $ref: '_shared.yaml#/servers/staging'

channels:

  /ws/sol-watch:
    address: /ws/sol-watch
    description: |
      Custom Tripport watchlist push. One socket per client connection;
      filtered server-side to the tenant identified by the bearer token.
    x-tier-category: sol_watchlist
    x-tier-max-pubkeys:
      business: 10000
    x-tier-max-pubkeys-source: proxy-agg/config/tier-matrix.yaml#/tiers/*/features/sol_watchlist/max_pubkeys
    bindings:
      ws:
        bindingVersion: 0.1.0
    messages:
      WatchSubscribe:      {$ref: '#/components/messages/WatchSubscribe'}
      WatchSubscribeAck:   {$ref: '#/components/messages/WatchSubscribeAck'}
      WatchEvent:          {$ref: '#/components/messages/WatchEvent'}
      WatchEntryRevoked:   {$ref: '#/components/messages/WatchEntryRevoked'}
      WatchHeartbeat:      {$ref: '#/components/messages/WatchHeartbeat'}
      WSError:             {$ref: '_shared.yaml#/components/messages/WSError'}

operations:

  sendWatchSubscribe:
    action: send
    channel: {$ref: '#/channels/~1ws~1sol-watch'}
    summary: Subscribe to a set of watchlist entries.
    description: |
      Client → server. After auth, send this once to begin receiving
      events. Sending again replaces the active id-set atomically.
    x-tier-required: business
    x-tier-category: sol_watchlist
    messages:
      - {$ref: '#/channels/~1ws~1sol-watch/messages/WatchSubscribe'}

  receiveWatchEvent:
    action: receive
    channel: {$ref: '#/channels/~1ws~1sol-watch'}
    summary: Server-pushed watchlist events.
    description: |
      Server → client. Heartbeats every 30s when idle; WatchEntryRevoked
      fires when REST control plane deletes an entry mid-stream.
    messages:
      - {$ref: '#/channels/~1ws~1sol-watch/messages/WatchSubscribeAck'}
      - {$ref: '#/channels/~1ws~1sol-watch/messages/WatchEvent'}
      - {$ref: '#/channels/~1ws~1sol-watch/messages/WatchEntryRevoked'}
      - {$ref: '#/channels/~1ws~1sol-watch/messages/WatchHeartbeat'}
      - {$ref: '#/channels/~1ws~1sol-watch/messages/WSError'}

components:

  schemas:

    WatchSubscribePayload:
      type: object
      required: [action, entries]
      properties:
        action:
          type: string
          const: watchlist.subscribe
        entries:
          type: array
          minItems: 1
          description: |
            List of watchlist entry ids to listen on. `["*"]` opts into
            all entries owned by the authenticated tenant.
          items:
            type: string
            description: UUID of a watchlist entry (returned by REST POST).

    WatchSubscribeAckPayload:
      type: object
      required: [action, accepted]
      properties:
        action:
          type: string
          const: watchlist.subscribed
        accepted:
          type: array
          description: Effective entry ids the server is streaming.
          items: {type: string, format: uuid}
        rejected:
          type: array
          description: Ids excluded (wrong tenant / deleted / unknown).
          items:
            type: object
            properties:
              id:     {type: string, format: uuid}
              reason: {type: string, enum: [unknown, not_owned, deleted]}

    WatchEventPayload:
      type: object
      required: [action, entry_id, pubkey, slot, change_kind]
      properties:
        action:
          type: string
          const: watchlist.event
        entry_id:    {type: string, format: uuid}
        pubkey:      {type: string, description: "watched base58 pubkey"}
        slot:        {type: integer, format: int64, minimum: 0}
        change_kind:
          type: string
          enum: [account_data, lamports, owner_change, executable_change, rent_epoch]
          description: Classifier hint for the kind of state change observed.
        before:
          type: object
          description: Account state immediately before the change (subset).
          properties:
            lamports:    {type: integer, format: int64}
            owner:       {type: string}
            data_size:   {type: integer}
        after:
          type: object
          description: Account state immediately after the change (subset).
          properties:
            lamports:    {type: integer, format: int64}
            owner:       {type: string}
            data_size:   {type: integer}
        signature:
          type: string
          nullable: true
          description: Signature of the tx that caused the change, if observable.
        timestamp:
          type: string
          format: date-time

    WatchEntryRevokedPayload:
      type: object
      required: [action, entry_id, reason]
      properties:
        action:
          type: string
          const: watchlist.revoked
        entry_id: {type: string, format: uuid}
        reason:
          type: string
          enum: [deleted, tier_downgrade, tenant_suspended]

    WatchHeartbeatPayload:
      type: object
      required: [action, ts]
      properties:
        action:
          type: string
          const: watchlist.heartbeat
        ts:
          type: string
          format: date-time
        active_entries:
          type: integer
          minimum: 0

  messages:

    WatchSubscribe:
      title: Watchlist subscribe
      summary: Client → server; declare entry-id set to stream.
      payload: {$ref: '#/components/schemas/WatchSubscribePayload'}

    WatchSubscribeAck:
      title: Watchlist subscribe ack
      summary: Server → client; effective accepted/rejected ids.
      payload: {$ref: '#/components/schemas/WatchSubscribeAckPayload'}

    WatchEvent:
      title: Watchlist event
      summary: Server-pushed account-state change for a watched entry.
      payload: {$ref: '#/components/schemas/WatchEventPayload'}

    WatchEntryRevoked:
      title: Watchlist entry revoked
      summary: Server-pushed revocation when REST deletes a streaming entry mid-stream.
      payload: {$ref: '#/components/schemas/WatchEntryRevokedPayload'}

    WatchHeartbeat:
      title: Watchlist heartbeat
      summary: Idle keepalive every 30s.
      payload: {$ref: '#/components/schemas/WatchHeartbeatPayload'}
