본문으로 건너뛰기

WalletKit 릴레이 전송

릴레이 전송

RelayTransport

암호화된 메시지를 릴레이 토픽에서 구독하고 발행하는 전송 인터페이스입니다.

public interface RelayTransport {
public suspend fun connect(relayToken: String? = null)
public suspend fun subscribe(
topic: String,
since: String? = null,
): String
public suspend fun publish(
topic: String,
message: ByteArray,
tag: Int,
ttl: Long = 300,
): String
public val incoming: Flow<RelayEnvelope>
public suspend fun close()
}
멤버파라미터반환·완료 시점
connect지갑 측은 null, 인증된 dApp 측은 릴레이 토큰WebSocket이 열리면 완료
subscribe토픽과 선택적 메일박스 커서("0-0"은 전체 재생)릴레이 subscriptionId
publish토픽, 암호문, 태그, 초 단위 TTL릴레이 messageId(메시지의 SHA-256)
incoming없음여러 구독자가 공유하는 Flow
close없음소켓과 리소스 정리가 끝나면 완료

오류: 릴레이 JSON-RPC 오류 응답은 RelayRpcException으로 전달됩니다. 연결·읽기·종료에서는 Ktor·플랫폼 오류와 Kotlin 비동기 작업 취소도 발생할 수 있습니다. close()는 구현상 반복 호출에 안전합니다.

import io.lambda256.scopeconnect.walletkit.relay.KtorRelayTransport
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.first

suspend fun receiveOne(
relayUrl: String,
topic: String,
): ByteArray = coroutineScope {
val transport = KtorRelayTransport(relayUrl)
try {
transport.connect()
transport.subscribe(topic)
transport.incoming.first { it.topic == topic }.message
} finally {
transport.close()
}
}

이 예제는 incoming Flow를 소비하므로 Kotlin 전용입니다. Swift에서는 Flow를 볼 수 없으므로, 수신 루프를 공유 Kotlin 모듈에 두고 결과만 Swift로 넘깁니다.

토픽 생성 권한은 릴레이 토큰으로 인증한 dApp에만 있습니다. 지갑은 인증 없이 연결해 QR로 공유받은 토픽을 구독할 수 있고, 아직 생성되지 않은 토픽을 구독하면 코드 2403이 발생하므로 제한된 횟수와 지연을 두고 재시도합니다.

KtorRelayTransport

자동 재연결과 메일박스 커서 기반 재구독을 제공하는 기본 Ktor 구현입니다. WebSocket 엔진은 JVM·Android에서 OkHttp, iOS에서 Darwin이며 전 플랫폼에서 20초 간격으로 연결 유지 ping을 보냅니다.

public class KtorRelayTransport(
url: String,
client: HttpClient = newRelayHttpClient(),
userHash: String? = null,
) : RelayTransport
파라미터필수설명
urlwss: 릴레이 엔드포인트
client아니요WebSockets 플러그인이 구성된 Ktor 클라이언트
userHash아니요암호화되지 않은 릴레이 메타데이터에 포함되는 설치 단위 식별값. 생략하면 필드 자체가 빠지고 대체 값도 생성되지 않습니다(콘솔은 사용자 식별값 없이 기록)

connect()의 연결 제한 시간은 15초이고, JSON-RPC 요청 제한 시간은 20초입니다. 예기치 않은 연결 종료 뒤에는 500ms부터 최대 10초까지 지수 대기로 재연결합니다. 이 제한 시간과 재연결 대기 값은 공개 생성자에서 재정의할 수 없습니다. close()는 생성자에 주입한 client까지 함께 닫으므로, 다른 곳과 공유하는 HttpClient를 넘기지 않습니다.

RelayEnvelope

public class RelayEnvelope(
public val topic: String,
public val message: ByteArray,
public val tag: Int,
)
필드설명
topic릴레이 라우팅 토픽
message릴레이가 해독하지 못하는 암호문
tag메시지 종류를 구분하는 프로토콜 태그

RelayRpcException

public class RelayRpcException(
public val code: Int,
public val rpcMessage: String,
) : Exception {
public val isForbiddenTopic: Boolean
}

isForbiddenTopic은 코드가 2403일 때만 true입니다. -32602는 잘못된 파라미터이며 같은 입력으로 재시도하지 않습니다. 연결 종료와 요청 제한 시간은 현재 구현에서 -1로 나타날 수 있습니다.

try {
transport.subscribe(topic)
} catch (error: RelayRpcException) {
if (!error.isForbiddenTopic) throw error
}