본문으로 건너뛰기

승인 단계

WalletKit은 세션 프로토콜을 처리하지만 사용자 동의는 대신하지 않습니다. connect()는 두 지점에서 지갑 앱의 승인을 받으며, 이 승인 UI는 setApprovalHandler()로 연결합니다.

세션 전체 흐름은 세션 연결, 승인 후 실제 서명은 서명과 개인키 처리에서 다룹니다.

승인 핸들러 등록

지갑 앱은 연결·서명 승인 UI를 하나의 suspend 콜백으로 등록합니다. 콜백은 요청 종류에 따라 분기해 사용자 확인 결과를 반환합니다.

wallet.setApprovalHandler { request ->
when (request) {
is ApprovalRequest.Connection ->
if (userAllows(request.info)) ApprovalDecision.APPROVE
else ApprovalDecision.reject("user declined")
is ApprovalRequest.Sign ->
if (userConfirms(request.info)) ApprovalDecision.APPROVE
else ApprovalDecision.reject("user declined")
}
}

콜백이 suspend이므로 실제 확인 화면을 띄우고 사용자 입력을 기다리는 동안 자연스럽게 일시 중단할 수 있습니다. Swift에서는 invokeasync이므로 withCheckedContinuation 등으로 사용자 입력을 기다렸다가 재개하면 됩니다.

운영에서는 승인 핸들러가 필수입니다

핸들러를 등록하지 않으면 모든 연결과 서명 요청이 자동 승인됩니다. 이 기본값은 UI 없는 테스트와 데모를 위한 것입니다. 운영 지갑은 반드시 사용자 확인 UI를 거치는 핸들러를 등록하세요.

연결 승인과 서명 승인

승인 단계시점거절 시 동작
ApprovalRequest.ConnectiondApp 연결 제안 수신 후, 세션 연결 응답 전세션을 연결하지 않고 approved = false로 반환
ApprovalRequest.Sign서명 직전dApp에 EIP-1193 4001 오류로 응답하고 approved = false로 반환

연결 승인과 서명 승인은 독립된 결정입니다. 연결 승인은 이후 서명 승인을 포함하지 않으며, 각 요청은 별도 화면에서 다시 확인합니다.

ApprovalRequestApprovalDecision

public sealed interface ApprovalRequest {
public data class Connection(val info: ConnectionRequestInfo) : ApprovalRequest
public data class Sign(val info: SignRequestInfo) : ApprovalRequest
}

public data class ApprovalDecision(val approved: Boolean, val reason: String? = null) {
public companion object {
public val APPROVE: ApprovalDecision
public fun reject(reason: String): ApprovalDecision
}
}

승인은 ApprovalDecision.APPROVE, 거절은 ApprovalDecision.reject(reason)으로 반환합니다. reason은 호출한 앱 코드에서 쓰기 위한 문자열입니다 — dApp에 전달되거나 라이브러리가 기록하지는 않으며, 서명 거절 시 dApp에는 고정 메시지와 4001 코드만 전송됩니다.

sealed 계층은 Swift에 평탄화된 클래스 이름으로 노출됩니다 — ApprovalRequest.ConnectionApprovalRequestConnection, ApprovalRequest.SignApprovalRequestSign으로 캐스팅해 분기합니다. companion 멤버는 ApprovalDecision.companion.APPROVE·ApprovalDecision.companion.reject(reason:)으로 접근합니다.

연결 승인

연결 제안을 받으면 dApp이 밝힌 정보로 연결 요청을 구성해 표시합니다.

public data class ConnectionRequestInfo(
val dappName: String,
val chains: List<String>,
val pairingTopic: String,
)
필드표시 내용
dappNameproposer.metadata에 담긴 dApp 이름
chains요청된 CAIP-2 체인 목록 (예: eip155:11155111)
pairingTopic페어링 토픽

dappName은 상대가 자기 자신을 밝힌 값이므로 신뢰할 수 있는 식별자가 아닙니다. 사용자가 판단할 수 있도록 이 값을 표시하고, 요청된 체인이 지갑이 지원하는 네트워크인지 확인합니다. 이후 세션 연결 단계에서는 사용자가 승인한 계정만 전송합니다.

서명 승인

서명 요청은 EIP-3009 eth_signTypedData_v4 요청을 디코딩한 표시용 정보로 전달됩니다.

public data class SignRequestInfo(
val account: String,
val chainId: String,
val method: String,
val tokenName: String,
val verifyingContract: String,
val from: String,
val to: String,
val value: String,
val validAfter: String,
val validBefore: String,
val nonce: String,
)
필드표시·검증 포인트
account서명 계정
chainIdeip155:{n} 형식의 CAIP-2 체인
methodeth_signTypedData_v4
tokenNameEIP-712 domain.name
verifyingContract서명 검증에 사용할 컨트랙트 주소 — 신뢰하는 주소인지 확인
from, to전송 당사자
value토큰의 최소 단위로 표현된 uint256 정수
validAfter, validBefore유효 기간
noncebytes32 랜덤 nonce

value는 토큰의 최소 단위로 표현된 정수입니다. 사용자에게 표시할 때는 토큰의 소수 자릿수를 반영해 사람이 읽을 수 있는 금액으로 변환합니다. 승인 전에 도메인, 체인 ID, 서명 검증에 사용할 컨트랙트와 수취인·금액을 모두 사용자에게 보여주고 확인받습니다.

서명 전에 요청 내용을 직접 검증하세요

SignRequestInfo의 값은 dApp이 보낸 요청에서 디코딩된 것입니다. WalletKit은 내용의 정당성(수취인·금액·컨트랙트가 사용자가 의도한 것인지)을 판단하지 않습니다. 도메인·체인·컨트랙트·금액 검증과 피싱 방지는 지갑 앱의 승인 화면 책임입니다.

거절 처리

거절 결과는 승인 단계마다 다르게 전달됩니다.

  • 연결 거절 — 세션을 수립하지 않고 WalletSessionResultapproved = false, 빈 sessionTopic으로 반환합니다. 이때 dApp에는 별도의 거절 응답을 발행하지 않으므로, dApp은 자체 대기 시간이 지나야 거절을 인지합니다.
  • 서명 거절 — dApp에 EIP-1193 4001(사용자 거절) 오류를 응답한 뒤 approved = false로 반환합니다. dApp은 이 코드를 사용자 거절로 해석합니다.

두 SDK의 오류 코드 대응 관계는 WalletKit 오류 처리에서 다룹니다.

다음 단계