nim-chat-poc/library/api/client_api.nim

183 lines
5.4 KiB
Nim
Raw Normal View History

## 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
2026-01-09 11:29:14 +02:00
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
2026-01-12 18:16:01 +02:00
): 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]
2026-02-20 13:12:58 +02:00
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())
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
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)
2026-01-12 18:16:01 +02:00
registerReqFFI(CreateClientRequest, ctx: ptr FFIContext[ChatClient]):
proc(
configJson: cstring, chatCallbacks: ChatCallbacks
): Future[Result[string, string]] {.async.} =
2026-01-09 11:49:04 +02:00
ctx.myLib[] = (await createChatClient(configJson, chatCallbacks)).valueOr:
error "CreateClientRequest failed", error = error
return err($error)
return ok("")
#################################################
2026-01-12 18:16:01 +02:00
# ChatClient Lifecycle Operations
#################################################
proc chat_start(
2026-01-12 18:16:01 +02:00
ctx: ptr FFIContext[ChatClient],
callback: FFICallBack,
userData: pointer
) {.ffi.} =
try:
2026-01-09 11:49:04 +02:00
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(
2026-01-12 18:16:01 +02:00
ctx: ptr FFIContext[ChatClient],
callback: FFICallBack,
userData: pointer
) {.ffi.} =
try:
2026-01-09 11:49:04 +02:00
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)
#################################################
2026-01-12 18:16:01 +02:00
# ChatClient Info Operations
#################################################
proc chat_get_id(
2026-01-12 18:16:01 +02:00
ctx: ptr FFIContext[ChatClient],
callback: FFICallBack,
userData: pointer
) {.ffi.} =
## Get the client's identifier
2026-01-09 11:49:04 +02:00
let clientId = ctx.myLib[].getId()
return ok(clientId)
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
#################################################
# 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(
2026-01-12 18:16:01 +02:00
ctx: ptr FFIContext[ChatClient],
callback: FFICallBack,
userData: pointer
) {.ffi.} =
## List all conversations as JSON array
2026-01-09 11:49:04 +02:00
let convos = ctx.myLib[].listConversations()
var convoList = newJArray()
for convo in convos:
convoList.add(%*{"id": convo.id()})
return ok($convoList)
proc chat_get_conversation(
2026-01-12 18:16:01 +02:00
ctx: ptr FFIContext[ChatClient],
callback: FFICallBack,
userData: pointer,
convoId: cstring
) {.ffi.} =
## Get a specific conversation by ID
2026-01-09 11:49:04 +02:00
let convo = ctx.myLib[].getConversation($convoId)
return ok($(%*{"id": convo.id()}))