본문으로 건너뛰기

빠른 시작

이 가이드를 완료하면 WalletKit으로 dApp이 발급한 scope: 페어링 URI를 받아 릴레이 세션을 수립하고, 연결·서명 승인을 거쳐 EIP-3009 서명 요청 한 건에 응답할 수 있습니다. 실기기나 플랫폼 설정 없이 세션 흐름을 가장 빨리 재현할 수 있도록 JVM(JDK 21) 콘솔 지갑으로 진행합니다. JVM은 지원 플랫폼 중 하나이며, Android·iOS 앱으로 옮길 때 바뀌는 부분은 4단계에서 정리합니다.

파트너 프리뷰입니다

Android·JVM 패키지는 아직 Maven Central에 게시하지 않았습니다. 파트너 프리뷰 기간에는 SCOPE Connect 담당자에게 0.0.1 Maven 아티팩트를 전달받아 로컬 Maven 저장소(~/.m2)에 설치합니다. 자세한 전달 방식은 설치에서 다룹니다.

이 빠른 시작은 Kotlin(JVM)만 다룹니다. iOS(Swift) 호출 방법은 세션 연결승인 단계의 Swift 탭에서 다룹니다.

사전 준비

  • JDK 21 (secp256k1-kmp JNI 아티팩트가 JVM 21을 요구합니다)
  • Gradle (또는 Gradle을 내장한 IDE)
  • 전달받은 WalletKit Maven 아티팩트가 로컬 Maven(~/.m2)에 설치된 상태
  • 테스트 상대가 될 dApp — scope: 페어링 URI를 QR로 표시하고, 연결 후 EIP-3009(eth_signTypedData_v4) 서명 요청을 보내는 dApp이 필요합니다. 테스트용 dApp은 SCOPE Connect 담당자에게 요청합니다.

릴레이 엔드포인트는 런타임 설정이 아니라 WalletKit 빌드 시점에 고정되며, 스캔한 페어링 URI의 relay-url보다 우선합니다. 전달받은 아티팩트와 상대 dApp이 같은 릴레이 환경을 사용해야 페어링이 성립합니다. 환경이 다르면 상대를 찾지 못하고 결과의 진단용 stepsrelay-url 불일치가 기록됩니다. 자세한 동작은 설치에서 다룹니다.

1. JVM 프로젝트 만들기

빈 디렉토리에 아래 두 Gradle 파일을 만듭니다.

settings.gradle.kts
rootProject.name = "walletkit-quickstart"

dependencyResolutionManagement {
repositories {
mavenLocal() // 전달받은 파트너 프리뷰 아티팩트
mavenCentral()
google()
}
}
build.gradle.kts
plugins {
kotlin("jvm") version "2.3.20"
application
}

kotlin {
jvmToolchain(21)
}

dependencies {
implementation("io.lambda256.scopeconnect:walletkit:0.0.1")
// connect()가 suspend 함수라 Kotlin 비동기 작업을 실행하는 runBlocking과 coroutines 라이브러리가 필요합니다.
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
}

application {
mainClass.set("MainKt")
}

kotlinx-coroutines-core는 WalletKit이 컴파일 클래스패스에 노출하지 않으므로 직접 선언합니다.

2. JVM 명령줄 지갑 구현

src/main/kotlin/Main.kt를 아래 코드로 만듭니다. 연결·서명 승인을 콘솔 y/N 프롬프트로 처리하고, 라이브러리에 포함된 테스트용 InMemorySigner로 서명합니다.

src/main/kotlin/Main.kt
import io.lambda256.scopeconnect.walletkit.ApprovalDecision
import io.lambda256.scopeconnect.walletkit.ApprovalRequest
import io.lambda256.scopeconnect.walletkit.WalletKit
import io.lambda256.scopeconnect.walletkit.WalletKitConfig
import io.lambda256.scopeconnect.walletkit.WalletMetadata
import io.lambda256.scopeconnect.walletkit.signer.InMemorySigner
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.security.SecureRandom

fun main(args: Array<String>): Unit = runBlocking {
// dApp 화면의 QR 원문(scope: URI)을 프로그램 인자로 받습니다. 실제 앱에서는 QR 스캔·딥링크로 수신합니다.
val pairingUri = args.firstOrNull()
?: error("사용법: gradle run --args='<scope: 페어링 URI>'")

val wallet = WalletKit(
WalletKitConfig(metadata = WalletMetadata(name = "Quickstart Wallet")),
)

// 연결과 서명은 서로 독립된 승인 단계입니다. 콘솔 y/N으로 각각 확인합니다.
wallet.setApprovalHandler { request ->
when (request) {
is ApprovalRequest.Connection -> {
println("── 연결 요청 ─────────────────────")
println(" dApp : ${request.info.dappName}")
println(" chains : ${request.info.chains}")
if (confirm("이 dApp에 연결할까요?")) ApprovalDecision.APPROVE
else ApprovalDecision.reject("user declined")
}
is ApprovalRequest.Sign -> {
val info = request.info
println("── 서명 요청 (${info.method}) ──────")
println(" chain : ${info.chainId}")
println(" token : ${info.tokenName} (${info.verifyingContract})")
println(" from → to: ${info.from}${info.to}")
println(" value : ${info.value} (토큰 최소 단위 정수)")
println(" validity : ${info.validAfter}${info.validBefore}")
if (confirm("이 요청에 서명할까요?")) ApprovalDecision.APPROVE
else ApprovalDecision.reject("user declined")
}
}
}

// 빠른 시작용 Signer — 라이브러리의 InMemorySigner에 임의의 32바이트 키를 넣습니다.
// 테스트·데모 전용입니다. 실제 지갑은 플랫폼 키 저장소를 백엔드로 하는 Signer를 구현합니다.
val privateKey = ByteArray(32).also { SecureRandom().nextBytes(it) }
val signer = InMemorySigner(privateKey)
println("서명 계정: ${signer.address}")

// connect() 한 번이 세션 연결부터 서명 응답 한 건까지 처리하고 반환합니다.
val result = wallet.connect(pairingUri, signer)

println()
println("approved : ${result.approved}")
println("account : ${result.account}")
println("sessionTopic : ${result.sessionTopic}")
println("signatureHex : ${result.signatureHex}")
}

/** 블로킹 stdin 읽기는 Dispatchers.IO에서 수행해 릴레이 메시지 처리를 막지 않습니다. */
private suspend fun confirm(prompt: String): Boolean = withContext(Dispatchers.IO) {
print("$prompt [y/N]: ")
System.out.flush()
readlnOrNull()?.trim()?.lowercase() in setOf("y", "yes")
}

승인 핸들러는 suspend 콜백이라 사용자 입력을 기다리는 동안 다른 작업을 막지 않고 일시 중단할 수 있습니다. 다만 명령줄 입력처럼 스레드를 막는 작업은 Dispatchers.IO에서 실행해 릴레이 메시지 수신을 막지 않게 합니다. 연결·서명 요청별 표시·검증 항목은 승인 단계에서 다룹니다.

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

setApprovalHandler()를 생략하면 모든 연결·서명 요청이 자동 승인됩니다. 이 기본값은 UI 없는 테스트·데모용입니다. 운영 지갑은 반드시 사용자 확인 UI를 거치는 핸들러를 등록하세요.

InMemorySigner는 32바이트 secp256k1 개인키를 메모리에 보관하는 참조 구현으로 테스트·데모 전용입니다. 임의 키로 만든 계정은 온체인 자산이 없는 새 계정이지만, EIP-3009 서명은 오프체인 구조화 데이터 서명이므로 서명 응답을 확인하는 데는 문제가 없습니다. 실제 지갑의 개인키 처리 책임과 Signer 구현 방법은 서명과 개인키 처리에서 다룹니다.

3. 실행과 성공 확인

상대 dApp에서 릴레이 페어링을 시작해 QR을 표시하고, QR 원문(scope: 페어링 URI)을 복사해 프로그램 인자로 전달합니다.

터미널
gradle run --args='scope:{topic}@1?relay-url={wss…}&symKey={hex}&relay-protocol=scr&version=1'
페어링 URI는 비밀값입니다

페어링 URI에는 채널 대칭키인 symKey가 포함됩니다. 이 값이 지갑과 dApp만 메시지를 열 수 있게 하는 근거이므로, URI 원문을 로그·셸 히스토리 공유·이슈 트래커에 남기지 마세요.

  1. 콘솔에 연결 요청(dApp 이름, 요청 체인)이 표시되면 y로 승인합니다. 세션이 수립되고 서명 계정이 세션에 등록됩니다.
  2. dApp이 서명 요청을 보내면 콘솔에 서명 요청(토큰, 수취인, 금액)이 표시됩니다. y로 승인합니다.
  3. 다음과 같은 결과가 출력되는지 확인합니다.
기대 결과 예시
approved : true
account : 0x8ba1… (세션에 등록된 서명 계정)
sessionTopic : 64자리 hex (세션 토픽)
signatureHex : 130자리 hex (65바이트 r||s||v, 0x 접두사 없음)

approvedtrue이고 signatureHex가 130자리 hex이면 서명 응답까지 완료된 것입니다. dApp 쪽에는 0x가 붙은 서명이 전달됩니다. 연결 또는 서명을 거절하면 예외가 아니라 approved = false(서명 거절 시 dApp에는 EIP-1193 4001 오류)로 반환됩니다.

흐름이 멈춘 것처럼 보일 때는 제한 시간과 WalletSessionResult.steps를 확인합니다. 세션 연결의 각 단계는 기본 20초, 연결 후 서명 요청 대기는 기본 5분이며, 초과 시 TimeoutCancellationException이 발생합니다. steps에는 디버깅을 위한 단계별 처리 기록이 남습니다. 자세한 대기·취소 동작은 세션 연결에서 다룹니다.

현재 connect() 호출 한 번은 서명 요청 한 건을 처리하고 종료됩니다. 추가 요청을 처리하려면 dApp이 새 페어링 URI를 발급하고 지갑이 connect()를 다시 호출합니다.

4. 실제 지갑 앱으로 확장

이 콘솔 지갑을 Android·iOS 지갑 앱으로 옮길 때 바뀌는 부분은 다음과 같습니다.

빠른 시작 (JVM 콘솔)실제 지갑 앱문서
페어링 URI를 프로그램 인자로 수신QR 스캔·scope: 딥링크로 수신 — URL 스킴과 권한을 플랫폼별로 등록설치 — 페어링 URI 수신 설정
콘솔 y/N 승인승인 화면 UI에서 도메인·컨트랙트·수취인·금액을 표시하고 검증승인 단계
InMemorySigner + 임의 키지갑의 보안 구조에 맞춘 Signer 구현서명과 개인키 처리
JVM 단일 모듈Android는 minSdk 28, iOS는 공개 Swift Package 연결과 공유 Kotlin 모듈 구성설치

다음 단계