nim-chat-poc/library/api/client_api.nim
Arseniy Klempner 29c64b340d
feat: mix+LEZ+RLN chat over the testnet via 2-phase gifter
Chat-side integration of the LEZ-backed RLN mix protocol:
- src/chat/delivery/waku_client.nim: mount waku_mix with onchain
  RLN spam protection wired to logos_core_client fetchers; gate
  the first publish on (a) gifter status confirmation, (b)
  cushion of 2 poll intervals after confirmation, and (c) proof
  root stability in the local valid_roots window; wrap mix
  lightpush in withTimeout so vanished SURB replies surface as
  Err instead of pinning the send coroutine.
- src/chat/client.nim: surface sendBytes errors via asyncSpawn
  wrapped try/except instead of discarding the future (was
  hiding every mix-publish failure).
- chat-side gifter client invocation (RLN membership service
  wire format, EIP-191 ethereum-allowlist auth).
- Background membership status watcher that reconciles the
  optimistic leaf returned by the gifter against the chain's
  authoritative leaf via the status RPC.

Simulation harness (simulations/mix_lez_chat/):
- Spin up sequencer + run_setup + 4 mix nodes (one of which
  runs the gifter service) + chat sender + chat receiver.
- SIM_NETWORK={local,testnet}, SIM_SLIM for testnet (reuses
  shipped config_account + cached payment_account), Docker
  image + GHCR for cross-platform testing.
- Strict mix-pool readiness gate, kademlia + RLN root activity
  checks, gifter EIP-191 auth fixture, slim-mode submodule
  minimization.
- TREE_ID_HEX pinned to the canonical testnet deployment.

Submodule bumps:
- vendor/nwaku to 8e6ba04 (LEZ-backed RLN mix + 2-phase gifter).
- vendor/logos-lez-rln to 950f287 (SPEL RLN program + mix sim
  infrastructure + canonical testnet deploy).

Docs:
- RUN_SLIM_TESTNET.md: slim sim recipe.
- cleanup/MODE_A_GIFTER_SLOT_BUG.md: per-signer nonce collision
  postmortem driving the queue+worker fix.
2026-05-28 10:53:36 -06:00

183 lines
5.4 KiB
Nim

## Client API - FFI bindings for Client lifecycle and operations
## Uses the {.ffi.} pragma for async request handling
import std/json
import chronicles
import chronos
import ffi
import src/chat
import src/chat/delivery/waku_client
import library/utils
logScope:
topics = "chat ffi client"
#################################################
# Client Creation Request (for chat_new)
#################################################
type ChatCallbacks* = object
onNewMessage*: MessageCallback
onNewConversation*: NewConvoCallback
onDeliveryAck*: DeliveryAckCallback
proc createChatClient(
configJson: cstring, chatCallbacks: ChatCallbacks
): Future[Result[ChatClient, string]] {.async.} =
try:
let config = parseJson($configJson)
# Parse identity name
let name = config.getOrDefault("name").getStr("anonymous")
# Parse Waku configuration or use defaults
var wakuCfg = DefaultConfig()
if config.hasKey("port"):
wakuCfg.port = config["port"].getInt().uint16
if config.hasKey("clusterId"):
wakuCfg.clusterId = config["clusterId"].getInt().uint16
if config.hasKey("shardId"):
wakuCfg.shardId = @[config["shardId"].getInt().uint16]
if config.hasKey("clusterId") or config.hasKey("shardId"):
wakuCfg.pubsubTopic = "/waku/2/rs/" & $wakuCfg.clusterId & "/" & $wakuCfg.shardId[0]
if config.hasKey("staticPeers"):
wakuCfg.staticPeers = @[]
for peer in config["staticPeers"]:
wakuCfg.staticPeers.add(peer.getStr())
if config.hasKey("mixEnabled"):
wakuCfg.mixEnabled = config["mixEnabled"].getBool(false)
if config.hasKey("mixNodes"):
wakuCfg.mixNodes = @[]
for node in config["mixNodes"]:
wakuCfg.mixNodes.add(node.getStr())
if config.hasKey("destPeerAddr"):
wakuCfg.destPeerAddr = config["destPeerAddr"].getStr("")
if config.hasKey("minMixPoolSize"):
wakuCfg.minMixPoolSize = config["minMixPoolSize"].getInt(4)
if config.hasKey("gifterNodeAddr"):
wakuCfg.gifterNodeAddr = config["gifterNodeAddr"].getStr("")
if config.hasKey("gifterAuthKey"):
wakuCfg.gifterAuthKey = config["gifterAuthKey"].getStr("")
# Create Waku client
let wakuClient = initWakuClient(wakuCfg)
# Create Chat client
let client = ?newClient(wakuClient, installation_name = name)
# Register event handlers
client.onNewMessage(chatCallbacks.onNewMessage)
client.onNewConversation(chatCallbacks.onNewConversation)
client.onDeliveryAck(chatCallbacks.onDeliveryAck)
notice "Chat client created", name = name
return ok(client)
except CatchableError as e:
return err("failed to create client: " & e.msg)
registerReqFFI(CreateClientRequest, ctx: ptr FFIContext[ChatClient]):
proc(
configJson: cstring, chatCallbacks: ChatCallbacks
): Future[Result[string, string]] {.async.} =
ctx.myLib[] = (await createChatClient(configJson, chatCallbacks)).valueOr:
error "CreateClientRequest failed", error = error
return err($error)
return ok("")
#################################################
# ChatClient Lifecycle Operations
#################################################
proc chat_start(
ctx: ptr FFIContext[ChatClient],
callback: FFICallBack,
userData: pointer
) {.ffi.} =
try:
await ctx.myLib[].start()
return ok("")
except CatchableError as e:
error "chat_start failed", error = e.msg
return err("failed to start client: " & e.msg)
proc chat_stop(
ctx: ptr FFIContext[ChatClient],
callback: FFICallBack,
userData: pointer
) {.ffi.} =
try:
await ctx.myLib[].stop()
return ok("")
except CatchableError as e:
error "chat_stop failed", error = e.msg
return err("failed to stop client: " & e.msg)
#################################################
# ChatClient Info Operations
#################################################
proc chat_get_id(
ctx: ptr FFIContext[ChatClient],
callback: FFICallBack,
userData: pointer
) {.ffi.} =
## Get the client's identifier
let clientId = ctx.myLib[].getId()
return ok(clientId)
#################################################
# Mix Protocol Status
#################################################
proc chat_get_mix_status(
ctx: ptr FFIContext[ChatClient],
callback: FFICallBack,
userData: pointer
) {.ffi.} =
let client = ctx.myLib[]
let mixEnabled = client.ds.cfg.mixEnabled
var poolSize = 0
if mixEnabled:
poolSize = client.ds.getMixPoolSize()
let status = %*{
"mixEnabled": mixEnabled,
"mixReady": client.ds.mixReady,
"mixPoolSize": poolSize,
"minPoolSize": client.ds.cfg.minMixPoolSize
}
return ok($status)
#################################################
# Conversation List Operations
#################################################
proc chat_list_conversations(
ctx: ptr FFIContext[ChatClient],
callback: FFICallBack,
userData: pointer
) {.ffi.} =
## List all conversations as JSON array
let convos = ctx.myLib[].listConversations()
var convoList = newJArray()
for convo in convos:
convoList.add(%*{"id": convo.id()})
return ok($convoList)
proc chat_get_conversation(
ctx: ptr FFIContext[ChatClient],
callback: FFICallBack,
userData: pointer,
convoId: cstring
) {.ffi.} =
## Get a specific conversation by ID
let convo = ctx.myLib[].getConversation($convoId)
return ok($(%*{"id": convo.id()}))