세션 연결
지갑이 dApp과 연결되는 전 과정은 WalletKit.connect() 한 번의 호출로 처리됩니다. 이 문서는 스캔한 페어링 URI가 세션과 서명 응답으로 이어지는 흐름과 각 단계에서 지갑 제공사가 책임지는 부분을 설명합니다.
각 요청을 사용자에게 표시하고 승인받는 방법은 승인 단계, 서명을 만드는 Signer 구현은 서명과 개인키 처리에서 다룹니다.
페어링 URI
dApp은 릴레이 채널을 만들고 scope: 페어링 URI를 QR 코드나 딥링크로 표시합니다. 지갑은 이 URI를 스캔·수신해 세션 연결을 시작합니다. 딥링크가 앱에 도달하려면 URL 스킴과 네트워크 권한을 먼저 등록해야 합니다. 플랫폼별 설정은 설치에서 다룹니다.
scope:{topic}@1?relay-url={wss…}&symKey={hex}&relay-protocol=scr
| 파라미터 | 의미 |
|---|---|
topic | 페어링 채널 토픽 |
relay-url | dApp의 릴레이 환경을 표시하고 불일치를 진단하는 값. 실제 연결 대상은 WalletKit 빌드의 RelayDefaults.DEFAULT_RELAY_URL |
symKey | 페어링 채널 대칭키. 릴레이에 전달되지 않고 dApp과 지갑 사이에서만 공유됩니다 |
relay-protocol | 항상 scr. WalletConnect의 irn과 호환되지 않습니다 |
symKey는 QR·딥링크로만 전달되고 릴레이에는 절대 실리지 않습니다. 이 값이 지갑과 dApp만 메시지를 열 수 있게 하는 근거이므로 로그에 남기지 않습니다. URI의 relay-url은 소켓 대상을 바꾸지 않습니다. WalletKit은 신뢰할 수 없는 QR이 연결 대상을 바꾸지 못하도록 빌드 시 고정된 릴레이에 연결하고, 값이 다르면 WalletSessionResult.steps에 기록합니다. URI 파싱은 라이브러리의 PairingUri.parse()가 담당하며, connect()에 URI 문자열을 그대로 넘기면 내부에서 파싱합니다.
connect() 호출
connect()는 스캔한 URI 문자열과 지갑이 구현한 Signer를 받아 세션 연결부터 서명 응답까지 진행하고, 흐름이 끝나거나 사용자가 거절하면 반환합니다. 한 번의 connect()는 서명 요청 한 건을 처리하고, 반환하기 전에 열었던 전송 리소스를 닫습니다. 추가 요청을 처리하려면 dApp이 새 페어링 URI를 발급하고 지갑이 connect()를 다시 호출합니다. Signer는 서명자 주소(address)와 32바이트 서명 해시를 처리하는 signRecoverable()을 제공하는 개인키 연동 인터페이스입니다. 구현 방법은 서명과 개인키 처리에서 다룹니다.
- Kotlin (Android · JVM)
- Swift (iOS)
import io.lambda256.scopeconnect.walletkit.WalletKit
import io.lambda256.scopeconnect.walletkit.WalletKitConfig
import io.lambda256.scopeconnect.walletkit.WalletMetadata
import io.lambda256.scopeconnect.walletkit.signer.Signer
val wallet = WalletKit(WalletKitConfig(metadata = WalletMetadata("My Wallet")))
wallet.setApprovalHandler { request -> /* 사용자 확인 UI — 승인 단계 문서 참고 */ }
// 지갑이 구현한 Signer — 개인키 보관과 서명 연산은 지갑 제공사의 책임 (서명과 개인키 처리 문서 참고)
val signer: Signer = keystoreSigner
val result = wallet.connect(scannedUri, signer)
if (result.approved) {
// result.signatureHex 를 사용자에게 확인시키거나 후속 처리에 전달
}
import WalletKit
// wallet: 공유 Kotlin 모듈에서 구성한 WalletKit 인스턴스 (설치 문서의 iOS 안내 참고)
// 승인 핸들러 등록은 승인 단계 문서 참고
wallet.setApprovalHandler(handler: approvalHandler)
// 지갑이 구현한 Signer — 개인키 보관과 서명 연산은 지갑 제공사의 책임 (서명과 개인키 처리 문서 참고)
let signer: Signer = keychainSigner
do {
let result = try await wallet.connect(pairingUri: scannedUri, signer: signer)
if result.approved {
// result.signatureHex 를 사용자에게 확인시키거나 후속 처리에 전달
}
} catch {
// 전파된 KMP 예외 — try/catch로 감싸지 않으면 앱이 종료됩니다
}
connect()는 suspend 함수이므로 Kotlin 비동기 작업인 코루틴 안에서 호출합니다. Swift에는 async throws 함수로 노출되어 try await로 호출합니다. 화면 이탈이나 사용자 취소 시 connect()를 실행한 비동기 작업(Swift에서는 Task)을 취소하면 대기 중인 네트워크 작업과 전송 리소스도 함께 정리됩니다.
세션 연결 과정
connect()는 내부적으로 다음 순서를 따릅니다. 태그 번호는 릴레이 메시지 종류를 구분하는 프로토콜 태그입니다.
- 페어링 URI를 파싱하고 인증 없이 WSS 릴레이에 연결합니다.
pairingTopic을 구독합니다. 토픽이 아직 생성되지 않아2403이 발생하면 제한된 횟수와 간격으로 재시도합니다.- 태그 1100
scope_sessionPropose를 복호화해 dApp 이름과 요청 체인을 읽고, 연결 승인을 받습니다. - X25519 ECDH로 세션 키를 합의하고 HKDF-SHA256으로 파생한 뒤 태그 1101로 응답합니다.
sessionTopic은 세션 키의 SHA-256입니다. sessionTopic을 구독하고 태그 1102scope_sessionSettle로 서명자 계정을 전송합니다.- 태그 1108
scope_sessionRequest(EIP-3009 구조화 데이터)를 수신하면 서명 승인을 받습니다. - 승인 시
Signer로 서명해 태그 1109 결과로, 거절 시 같은 태그 1109에 EIP-11934001오류로 응답합니다.
페어링 URI 수신부터 서명 응답까지의 교환을 시퀀스로 나타내면 다음과 같습니다.
| 단계 | 태그 | 방향 | 지갑 제공사의 책임 |
|---|---|---|---|
| propose 수신 | 1100 | dApp → 지갑 | 연결 요청 표시·승인 |
| propose 응답 | 1101 | 지갑 → dApp | 세션 키 합의·공개키 회신 |
| 세션 연결(settle) | 1102 | 지갑 → dApp | 승인한 계정만 전송 |
| 서명 요청 | 1108 | dApp → 지갑 | 요청 내용 표시·서명 승인 |
| 서명 응답 | 1109 | 지갑 → dApp | 서명 또는 4001 회신 |
연결 승인과 서명 승인은 서로 독립된 단계입니다. 연결 승인은 이후 서명 승인을 포함하지 않습니다.
릴레이 토픽 구독과 재시도
토픽 생성 권한은 릴레이 토큰으로 인증한 dApp에만 있습니다. 지갑은 인증 없이 연결해 QR로 공유받은 토픽을 구독합니다. dApp이 아직 토픽을 만들지 않은 순간에 구독하면 릴레이가 2403 forbidden topic으로 응답하므로, 라이브러리는 250ms 간격으로 최대 60회(지연 합계 약 15초 + 왕복 시간) 재시도합니다. 이 재시도 기간은 handshakeTimeoutMs와 별개입니다. 기간이 끝날 때까지 토픽이 없으면 IllegalStateException을 던집니다. 릴레이 오류 코드와 재연결 동작은 릴레이 전송에서 다룹니다.
WalletSessionResult
connect()는 흐름의 결과를 하나의 값으로 반환합니다.
- Kotlin (Android · JVM)
- Swift (iOS)
public data class WalletSessionResult(
val sessionTopic: String,
val account: String,
val signatureHex: String,
val approved: Boolean,
val steps: List<String>,
)
class WalletSessionResult {
var sessionTopic: String { get }
var account: String { get }
var signatureHex: String { get }
var approved: Bool { get }
var steps: [String] { get }
}
| 필드 | 설명 |
|---|---|
sessionTopic | 연결된 세션 토픽. 연결 거절 시 빈 문자열 |
account | 세션에 등록된 계정 주소 |
signatureHex | 65바이트 서명 hex(130자, 0x 접두사 없음). 거절 시 빈 문자열 |
approved | 사용자 승인 여부 |
steps | 단계별 처리 기록. 디버깅용 |
성공 여부는 approved로 판단합니다. 사용자가 연결 또는 서명을 거절하면 approved가 false이고 signatureHex가 비어 있습니다. steps는 실기기에서 흐름을 추적하기 위한 디버깅 문자열이며 사용자 화면 로직의 기준으로 삼지 않습니다. signatureHex에는 0x 접두사가 없습니다 — 릴레이로 나가는 1109 응답에만 0x가 붙으므로, 후속 처리에 0x 형식이 필요하면 직접 붙입니다.
시간 초과와 취소
- 세션 연결의 각 단계는
handshakeTimeoutMs(기본 20초) 안에 상대 메시지를 기다립니다. - 세션 연결 이후 서명 요청 대기는
signRequestTimeoutMs(기본 5분)를 사용합니다. dApp과 마찬가지로 사용자가 내용을 확인하고 승인할 수 있도록 5분간 기다립니다. - 앱이 백그라운드로 내려가 소켓이 끊겨도 전송이 자동 재연결하고 대기 중 도착한 서명 요청을 재생하므로, 대기 중 화면 전환에도 흐름을 이어갈 수 있습니다.
두 대기 모두 시간을 초과하면 TimeoutCancellationException을 던집니다. 이 예외는 CancellationException의 하위 타입이므로, 취소를 그대로 상위로 전달하는 코드는 타임아웃까지 사용자 취소로 오분류할 수 있습니다. 타임아웃 실패 화면에서는 TimeoutCancellationException 여부를 먼저 확인합니다.
connect()를 실행한 비동기 작업을 취소하면 진행 중인 대기를 중단하고 전송 리소스를 정리합니다. 취소 예외는 해당 작업의 호출자에게 전달됩니다.
다음 단계
- 승인 단계 — 연결·서명 요청의 표시와 승인
- WalletKit API —
connect()호출 형식과 단계별 태그 정의