본문으로 건너뛰기

릴레이 전송

WalletKit의 모든 세션 메시지는 릴레이 WSS를 통해 암호화된 메시지로 전송됩니다. connect()를 쓰면 전송은 내부에서 자동으로 관리되므로 대부분의 지갑은 전송 계층을 직접 다루지 않습니다. 이 문서는 전송 인터페이스와 기본 구현의 동작을 이해하고, 테스트용 전송을 구현하거나 오류를 처리할 때 참고합니다.

세션 흐름 전체는 세션 연결에서 다룹니다.

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토픽과 선택적 mailbox cursor("0-0"은 전체 재생)릴레이 subscriptionId
publish토픽, 암호문, 태그, 초 단위 TTL릴레이 messageId(메시지의 SHA-256)
incoming없음구독 토픽의 hot Flow
close없음소켓·리소스 정리가 끝나면 완료

지갑은 connect(null)로 인증 없이 연결합니다. 토픽 생성 권한은 릴레이 토큰으로 인증한 dApp에만 있으므로, 지갑은 QR로 공유받은 토픽을 구독합니다. close()는 반복 호출에 안전합니다.

RelayEnvelope

구독한 토픽으로 도착하는 메시지 한 건입니다.

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

RelayEnvelope.message는 채널 대칭키로 암호화된 값이며, 릴레이는 내용을 볼 수 없습니다. 복호화는 세션 키를 가진 지갑과 dApp만 수행합니다.

KtorRelayTransport

자동 재연결과 mailbox cursor 기반 재구독을 제공하는 기본 Ktor 구현입니다. WalletKitConfig.transportFactory의 기본값이 이 구현을 만듭니다.

public class KtorRelayTransport(
url: String,
client: HttpClient = newRelayHttpClient(),
userHash: String? = null,
) : RelayTransport
파라미터필수설명
urlwss: 릴레이 엔드포인트
client아니요WebSockets 플러그인이 구성된 Ktor 클라이언트
userHash아니요암호화되지 않은 릴레이 메타데이터에 포함되는 설치 단위 식별값. 생략하면 사용자 식별값과 방문 이벤트를 보내지 않습니다. 플랫폼 값은 이 옵션과 무관하게 발행 요청마다 포함됩니다

WebSocket 엔진은 JVM·Android에서 OkHttp, iOS에서 Darwin이며, 전 플랫폼에서 20초 간격으로 연결 유지 ping을 보냅니다. 사용자가 연결·서명 승인을 검토하는 동안에도 연결이 끊기지 않도록 하기 위한 동작입니다.

타임아웃과 재연결

설정
연결 제한 시간15초
JSON-RPC 요청 제한 시간20초
재연결 대기500ms부터 최대 10초까지 지수 백오프

예기치 않게 소켓이 끊기면 백그라운드 비동기 작업이 이를 감지해 지수 백오프로 다시 연결하고, 구독 중이던 각 토픽을 마지막 커서부터 다시 구독합니다. 이 덕분에 앱이 백그라운드로 내려가 있는 동안 도착한 서명 요청도 재연결 후 재생되어 대기 중이던 흐름이 그대로 이어집니다. 위 제한 시간과 재연결 대기 값은 공개 생성자에서 재정의할 수 없습니다. close()는 생성자에 주입한 client까지 함께 닫으므로 전송 전용 HttpClient를 주입합니다.

userHash와 플랫폼 값은 암호화되지 않습니다

플랫폼 값은 모든 릴레이 발행 요청에 암호화되지 않은 메타데이터로 포함됩니다. userHash를 지정하면 그 값도 발행 요청에 포함되고, 첫 토픽 구독 시 방문 이벤트(태그 1110)를 한 번 보냅니다. userHashnull이면 사용자 식별값과 방문 이벤트만 생략됩니다. 운영 적용 전 수집 목적과 동의, 보존 기간, 비활성화 방법을 보안 검토로 확정하세요.

오류 처리 — RelayRpcException

릴레이 JSON-RPC 오류 응답은 RelayRpcException으로 전달됩니다.

public class RelayRpcException(
public val code: Int,
public val rpcMessage: String,
) : Exception {
public val isForbiddenTopic: Boolean
}
코드의미처리
2403forbidden topic — dApp이 아직 토픽 미생성isForbiddenTopic == true. 제한된 횟수·지연으로 재시도
-32602invalid params같은 입력으로 재시도하지 않음
-1연결 종료·요청 제한 시간현재 구현에서 나타날 수 있는 값

connect()2403을 만나면 250ms 간격으로 최대 60회까지 토픽 구독을 재시도합니다. 직접 전송을 다룰 때도 isForbiddenTopic만 재시도하고 나머지는 호출자에게 전달합니다.

try {
transport.subscribe(topic)
} catch (error: RelayRpcException) {
if (!error.isForbiddenTopic) throw error
// forbidden topic: 지연 후 재시도
}

연결·읽기·종료 과정에서는 릴레이 오류 외에 Ktor·TLS·WebSocket·플랫폼 예외나 Kotlin 비동기 작업 취소도 전달될 수 있습니다. 자동 재연결 중에도 사용자 취소와 명시적 close()를 구분합니다.

테스트에서 전송 구현 바꾸기

WalletKitConfig.transportFactory에 테스트용 전송 구현을 주입하면 실제 릴레이 없이 세션 연결 과정을 테스트할 수 있습니다. 테스트용 구현은 incoming Flow를 직접 제공해야 하는데 Flow는 Swift에 노출되지 않으므로, 이 경로는 공유 Kotlin 모듈에서만 작성합니다.

val config = WalletKitConfig(
metadata = WalletMetadata("Test Wallet"),
transportFactory = { _, _ -> FakeRelayTransport() }, // RelayTransport 구현
)

예시의 FakeRelayTransport는 라이브러리가 제공하는 클래스가 아니라 테스트 코드에서 직접 작성하는 RelayTransport 구현입니다. RelayTransport.incoming은 Kotlin 비동기 스트림인 Flow 타입이라 Swift에 노출되지 않습니다. 테스트용 전송 주입과 인터페이스 구현은 Kotlin 공유 모듈에서 수행합니다.

전송 인터페이스의 정확한 함수·타입 정의와 릴레이 프로토콜 태그는 WalletKit API에서 확인합니다.

다음 단계