mirror of
https://github.com/logos-messaging/nim-chat-poc.git
synced 2026-07-03 15:09:43 +00:00
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.
183 lines
5.4 KiB
Nim
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()}))
|
|
|