2025-12-22 14:14:37 +02:00
|
|
|
## 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
|
2025-12-22 14:14:37 +02:00
|
|
|
|
|
|
|
|
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.} =
|
2025-12-22 14:14:37 +02:00
|
|
|
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]
|
|
|
|
|
|
2025-12-22 14:14:37 +02:00
|
|
|
if config.hasKey("staticPeers"):
|
|
|
|
|
wakuCfg.staticPeers = @[]
|
|
|
|
|
for peer in config["staticPeers"]:
|
|
|
|
|
wakuCfg.staticPeers.add(peer.getStr())
|
feat(mix): mixnet sender-anonymity for Logos Chat (static RLN, no LEZ)
Global, restart-based Required/None anonymity mode: route chat messages through the
libp2p mixnet for sender anonymity, on the logos-delivery (efafdfdc2) nwaku stack.
Static RLN spam protection; no on-chain LEZ gifter / dynamic membership.
Delivery (src/chat/delivery/waku_client.nim):
- WakuConfig.mixEnabled/mixNodes/minMixPoolSize; parseMixNodes, waitForMixPool,
getMixPoolSize, mixReady.
- Required mode: sendBytes -> lightpushPublish(mixify=true) over the mix pool,
fail-fast below minMixPoolSize (no relay fallback). None mode: relay publish.
Errors propagate up to chat_send_message.
- Receive via WakuFilter (subscribe to static peers; no relay mounted), refreshed
by a 60s keep-alive.
- Static RLN: pre-populated rln_tree.db + per-peer keystore; nodekey config to adopt
a provisioned identity. No per-send root-convergence wait (static membership).
API / build:
- chat_get_mix_status FFI -> {mixEnabled,mixReady,mixPoolSize,minPoolSize}.
- Reproducible nix build: librln consumed as a cdylib (avoids the two-Rust-staticlib
symbol collision); -d:libp2p_mix_experimental_exit_is_dest.
- vendor/nwaku -> efafdfdc2; vendor/nim-protobuf-serialization -> 38d24eb (0.4.0).
2026-06-25 21:45:41 +05:30
|
|
|
|
|
|
|
|
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("minMixPoolSize"):
|
|
|
|
|
wakuCfg.minMixPoolSize = config["minMixPoolSize"].getInt(4)
|
|
|
|
|
# Adopt a fixed identity (e.g. a provisioned mix-sim chat credential) so the
|
|
|
|
|
# peer-ID-derived RLN keystore (rln_keystore_<peerId>.json) resolves.
|
|
|
|
|
if config.hasKey("nodekey"):
|
|
|
|
|
let hex = config["nodekey"].getStr()
|
|
|
|
|
if hex.len > 0:
|
|
|
|
|
wakuCfg.nodekey = parseNodeKey(hex).valueOr:
|
|
|
|
|
return err("invalid nodekey: " & error)
|
|
|
|
|
|
2025-12-22 14:14:37 +02:00
|
|
|
# Create Waku client
|
|
|
|
|
let wakuClient = initWakuClient(wakuCfg)
|
|
|
|
|
|
|
|
|
|
# Create Chat client
|
2026-02-22 17:51:59 -08:00
|
|
|
let client = ?newClient(wakuClient, installation_name = name)
|
2025-12-22 14:14:37 +02:00
|
|
|
|
|
|
|
|
# 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]):
|
2025-12-22 14:14:37 +02:00
|
|
|
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:
|
2025-12-22 14:14:37 +02:00
|
|
|
error "CreateClientRequest failed", error = error
|
|
|
|
|
return err($error)
|
|
|
|
|
return ok("")
|
|
|
|
|
|
|
|
|
|
#################################################
|
2026-01-12 18:16:01 +02:00
|
|
|
# ChatClient Lifecycle Operations
|
2025-12-22 14:14:37 +02:00
|
|
|
#################################################
|
|
|
|
|
|
|
|
|
|
proc chat_start(
|
2026-01-12 18:16:01 +02:00
|
|
|
ctx: ptr FFIContext[ChatClient],
|
2025-12-22 14:14:37 +02:00
|
|
|
callback: FFICallBack,
|
|
|
|
|
userData: pointer
|
|
|
|
|
) {.ffi.} =
|
|
|
|
|
try:
|
2026-01-09 11:49:04 +02:00
|
|
|
await ctx.myLib[].start()
|
2025-12-22 14:14:37 +02:00
|
|
|
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],
|
2025-12-22 14:14:37 +02:00
|
|
|
callback: FFICallBack,
|
|
|
|
|
userData: pointer
|
|
|
|
|
) {.ffi.} =
|
|
|
|
|
try:
|
2026-01-09 11:49:04 +02:00
|
|
|
await ctx.myLib[].stop()
|
2025-12-22 14:14:37 +02:00
|
|
|
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
|
2025-12-22 14:14:37 +02:00
|
|
|
#################################################
|
|
|
|
|
|
|
|
|
|
proc chat_get_id(
|
2026-01-12 18:16:01 +02:00
|
|
|
ctx: ptr FFIContext[ChatClient],
|
2025-12-22 14:14:37 +02:00
|
|
|
callback: FFICallBack,
|
|
|
|
|
userData: pointer
|
|
|
|
|
) {.ffi.} =
|
|
|
|
|
## Get the client's identifier
|
2026-01-09 11:49:04 +02:00
|
|
|
let clientId = ctx.myLib[].getId()
|
2025-12-22 14:14:37 +02:00
|
|
|
return ok(clientId)
|
|
|
|
|
|
feat(mix): mixnet sender-anonymity for Logos Chat (static RLN, no LEZ)
Global, restart-based Required/None anonymity mode: route chat messages through the
libp2p mixnet for sender anonymity, on the logos-delivery (efafdfdc2) nwaku stack.
Static RLN spam protection; no on-chain LEZ gifter / dynamic membership.
Delivery (src/chat/delivery/waku_client.nim):
- WakuConfig.mixEnabled/mixNodes/minMixPoolSize; parseMixNodes, waitForMixPool,
getMixPoolSize, mixReady.
- Required mode: sendBytes -> lightpushPublish(mixify=true) over the mix pool,
fail-fast below minMixPoolSize (no relay fallback). None mode: relay publish.
Errors propagate up to chat_send_message.
- Receive via WakuFilter (subscribe to static peers; no relay mounted), refreshed
by a 60s keep-alive.
- Static RLN: pre-populated rln_tree.db + per-peer keystore; nodekey config to adopt
a provisioned identity. No per-send root-convergence wait (static membership).
API / build:
- chat_get_mix_status FFI -> {mixEnabled,mixReady,mixPoolSize,minPoolSize}.
- Reproducible nix build: librln consumed as a cdylib (avoids the two-Rust-staticlib
symbol collision); -d:libp2p_mix_experimental_exit_is_dest.
- vendor/nwaku -> efafdfdc2; vendor/nim-protobuf-serialization -> 38d24eb (0.4.0).
2026-06-25 21:45:41 +05:30
|
|
|
#################################################
|
|
|
|
|
# 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)
|
|
|
|
|
|
2025-12-22 14:14:37 +02:00
|
|
|
#################################################
|
|
|
|
|
# Conversation List Operations
|
|
|
|
|
#################################################
|
|
|
|
|
|
|
|
|
|
proc chat_list_conversations(
|
2026-01-12 18:16:01 +02:00
|
|
|
ctx: ptr FFIContext[ChatClient],
|
2025-12-22 14:14:37 +02:00
|
|
|
callback: FFICallBack,
|
|
|
|
|
userData: pointer
|
|
|
|
|
) {.ffi.} =
|
|
|
|
|
## List all conversations as JSON array
|
2026-01-09 11:49:04 +02:00
|
|
|
let convos = ctx.myLib[].listConversations()
|
2025-12-22 14:14:37 +02:00
|
|
|
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],
|
2025-12-22 14:14:37 +02:00
|
|
|
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)
|
2025-12-22 14:14:37 +02:00
|
|
|
return ok($(%*{"id": convo.id()}))
|
|
|
|
|
|