WalletKit 릴레이 전송
릴레이 전송
RelayTransport
암호화된 메시지를 릴레이 토픽에서 구독하고 발행하는 전송 인터페이스입니다.
- Kotlin (Android · JVM)
- Swift (iOS)
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()
}
protocol RelayTransport {
func connect(relayToken: String?) async throws
func subscribe(topic: String, since: String?) async throws -> String
func publish(
topic: String,
message: KotlinByteArray,
tag: Int32,
ttl: Int64
) async throws -> String
func close() async throws
// incoming 은 Kotlin Flow — Swift 에 노출되지 않습니다.
}
| 멤버 | 파라미터 | 반환·완료 시점 |
|---|---|---|
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을 보냅니다.
- Kotlin (Android · JVM)
- Swift (iOS)
public class KtorRelayTransport(
url: String,
client: HttpClient = newRelayHttpClient(),
userHash: String? = null,
) : RelayTransport
// client 파라미터는 Ktor HttpClient 이므로 Swift 에서 만들 수 없습니다.
// 전송 구성은 공유 Kotlin 모듈에 둡니다.
class KtorRelayTransport: RelayTransport {
init(url: String, client: Ktor_client_coreHttpClient, userHash: String?)
}
| 파라미터 | 필수 | 설명 |
|---|---|---|
url | 예 | wss: 릴레이 엔드포인트 |
client | 아니요 | WebSockets 플러그인이 구성된 Ktor 클라이언트 |
userHash | 아니요 | 암호화되지 않은 릴레이 메타데이터에 포함되는 설치 단위 식별값. 생략하면 필드 자체가 빠지고 대체 값도 생성되지 않습니다(콘솔은 사용자 식별값 없이 기록) |
connect()의 연결 제한 시간은 15초이고, JSON-RPC 요청 제한 시간은 20초입니다. 예기치 않은 연결 종료 뒤에는 500ms부터 최대 10초까지 지수 대기로 재연결합니다. 이 제한 시간과 재연결 대기 값은 공개 생성자에서 재정의할 수 없습니다. close()는 생성자에 주입한 client까지 함께 닫으므로, 다른 곳과 공유하는 HttpClient를 넘기지 않습니다.
RelayEnvelope
- Kotlin (Android · JVM)
- Swift (iOS)
public class RelayEnvelope(
public val topic: String,
public val message: ByteArray,
public val tag: Int,
)
class RelayEnvelope {
init(topic: String, message: KotlinByteArray, tag: Int32)
var topic: String { get }
var message: KotlinByteArray { get }
var tag: Int32 { get }
}
| 필드 | 설명 |
|---|---|
topic | 릴레이 라우팅 토픽 |
message | 릴레이가 해독하지 못하는 암호문 |
tag | 메시지 종류를 구분하는 프로토콜 태그 |
RelayRpcException
- Kotlin (Android · JVM)
- Swift (iOS)
public class RelayRpcException(
public val code: Int,
public val rpcMessage: String,
) : Exception {
public val isForbiddenTopic: Boolean
}
// 던져진 예외는 NSError 로 감싸여 오므로
// userInfo["KotlinException"] 에서 이 타입으로 복원합니다.
class RelayRpcException {
init(code: Int32, rpcMessage: String)
var code: Int32 { get }
var rpcMessage: String { get }
var isForbiddenTopic: Bool { get }
}
isForbiddenTopic은 코드가 2403일 때만 true입니다. -32602는 잘못된 파라미터이며 같은 입력으로 재시도하지 않습니다. 연결 종료와 요청 제한 시간은 현재 구현에서 -1로 나타날 수 있습니다.
- Kotlin (Android · JVM)
- Swift (iOS)
try {
transport.subscribe(topic)
} catch (error: RelayRpcException) {
if (!error.isForbiddenTopic) throw error
}
do {
_ = try await transport.subscribe(topic: topic, since: nil)
} catch {
// Kotlin 예외는 NSError로 감싸여 전달됩니다 — userInfo의 KotlinException 키로 원본을 복원해 분기합니다
guard let rpcError = (error as NSError).userInfo["KotlinException"] as? RelayRpcException,
rpcError.isForbiddenTopic
else { throw error }
}