Whiterail SDK는 이메일 OTP 인증과 Turnkey 통합을 통해 사용자를 안전하게 인증하는 간단한 방법을 제공합니다. SDK는 완전한 Non-custodial 아키텍처를 따르며, 개인키는 절대 클라이언트를 벗어나지 않습니다.
빠른 시작
1. SDK 초기화
import { WhiterailSDK } from "@whiterail/sdk" ;
const sdk = new WhiterailSDK ();
API 연결 : SDK는 기본적으로 http://localhost:3009에 연결됩니다. 프로덕션
환경에서는 백엔드 서버 URL을 확인하세요.
2. 계정 생성
사용자를 위한 Turnkey sub-organization을 생성합니다. 이 메서드는 멱등성(idempotent)이므로 동일한 이메일로 여러 번 호출해도 기존 계정을 반환합니다.
const result = await sdk . createAccount ({
email: "[email protected] " ,
});
if ( ! result . success ) {
console . error ( "계정 생성 실패:" , result . error );
return ;
}
if ( result . accountExists ) {
console . log ( "✅ 계정이 이미 존재합니다" );
} else {
console . log ( "✅ 새 계정이 생성되었습니다" );
}
console . log ( "Sub-Organization ID:" , result . subOrganizationId );
console . log ( "Solana 지갑 주소:" , result . solanaAddress ); // ✅ 지갑 주소 확인
계정 생성 시 지갑도 함께 생성됨 : createAccount를 호출하면 Solana 지갑
주소도 함께 생성되어 반환됩니다.
3. 인증 시작 (OTP 전송)
사용자의 이메일로 OTP 코드를 전송합니다.
const authResult = await sdk . initAuth ({
email: "[email protected] " ,
});
if ( ! authResult . success ) {
console . error ( "OTP 전송 실패:" , authResult . error );
return ;
}
console . log ( "✅ 이메일로 OTP가 전송되었습니다" );
console . log ( "OTP ID:" , authResult . otpId );
4. 인증 완료 (OTP 검증 + 지갑 생성)
OTP 코드를 검증하고 인증을 완료합니다. 첫 번째 인증 시 지갑이 생성됩니다.
const completeResult = await sdk . completeAuth ({
email: "[email protected] " ,
otpCode: "123456" , // 이메일로 받은 6자리 코드
});
if ( ! completeResult . success ) {
console . error ( "인증 실패:" , completeResult . error );
return ;
}
if ( completeResult . walletCreated ) {
console . log ( "✅ 지갑이 생성되었습니다!" );
} else {
console . log ( "✅ 기존 지갑으로 인증되었습니다" );
}
console . log ( "Solana 주소:" , completeResult . solanaAddress );
console . log ( "Sub-Organization ID:" , completeResult . subOrganizationId );
// ✅ 세션이 자동으로 저장됩니다
// ✅ 이제 트랜잭션에 서명할 수 있습니다
if ( sdk . isAuthenticated ()) {
console . log ( "🎉 인증 완료! 결제를 전송할 수 있습니다" );
}
5. 결제 전송
// 통화 심볼 사용
const payment = await sdk . sendPayment ({
destination: "9aE476sH92Vz7DMPyq5WLPkrKWivxeuTKEFKd2sZZcde" ,
currency: "USDC" ,
amount: 100 ,
network: "devnet" ,
});
if ( ! payment . success ) {
console . error ( "결제 실패:" , payment . error );
return ;
}
console . log ( "🎉 결제가 전송되었습니다!" );
console . log ( "트랜잭션 서명:" , payment . signature );
console . log ( "탐색기 URL:" , payment . explorerUrl ); // ✅ 트랜잭션 확인 링크
또는 민트 주소 직접 사용:
const payment = await sdk . sendPayment ({
destination: "9aE476sH92Vz7DMPyq5WLPkrKWivxeuTKEFKd2sZZcde" ,
mintAddress: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU" ,
decimals: 6 ,
amount: 100 ,
network: "devnet" ,
});
트랜잭션 확인 : explorerUrl을 통해 Solscan에서 트랜잭션 상태를 확인할 수
있습니다.
주요 특징
이메일 OTP 인증 사용자의 이메일로 6자리 코드 전송
완전한 Non-custodial 개인키는 클라이언트 측에서만 생성 및 저장
Turnkey 통합 Turnkey 인프라와 원활한 통합
인증 흐름
1. createAccount() → Turnkey sub-organization 생성
2. initAuth() → 이메일로 OTP 전송
3. completeAuth() → OTP 검증 + 지갑 생성 (첫 인증 시)
세션 만료 : 인증 후 30분 동안 세션이 유효합니다. 페이지를 새로고침하거나
30분이 지나면 재인증이 필요합니다.
보안 설계 : 개인키는 절대 localStorage에 저장되지 않으며, 메모리에만
존재합니다. 이는 XSS 공격으로부터 보호하기 위한 설계입니다.
세션 관리
인증 상태 확인
// 사용자가 트랜잭션에 서명할 수 있는지 확인
if ( sdk . isAuthenticated ()) {
console . log ( "✅ 사용자가 로그인되어 있습니다" );
// 결제 기능 활성화
} else {
console . log ( "❌ 재인증이 필요합니다" );
// 로그인 화면 표시
}
현재 세션 가져오기
const session = sdk . getSession ();
if ( session ) {
console . log ( "이메일:" , session . email );
console . log ( "만료 시간:" , new Date ( session . expiresAt ));
// Note: ephemeralPrivateKey는 보안을 위해 접근 불가
}
로그아웃
const handleLogout = () => {
sdk . clearSession ();
console . log ( "✅ 로그아웃 완료" );
// 로그인 페이지로 리다이렉트
};
세션 제한사항 : - 페이지를 새로고침하면 메모리가 지워지고 재인증이
필요합니다 - ephemeralPrivateKey는 메모리에만 존재 (localStorage에 저장 안
됨) - 이메일과 기본 정보는 localStorage에 저장되어 다음 로그인 시 자동으로
채워집니다
완전한 예제 (에러 처리 포함)
import { WhiterailSDK } from "@whiterail/sdk" ;
const sdk = new WhiterailSDK ();
async function authenticate ( email : string , otpCode : string ) {
try {
// 1단계: 계정 생성 (멱등성)
const account = await sdk . createAccount ({ email });
if ( ! account . success ) {
throw new Error ( account . error );
}
console . log ( "지갑 주소:" , account . solanaAddress );
// 2단계: OTP 전송
const auth = await sdk . initAuth ({ email });
if ( ! auth . success ) {
throw new Error ( auth . error );
}
console . log ( "OTP가 이메일로 전송되었습니다" );
// 사용자가 OTP 입력할 때까지 대기...
// 3단계: 인증 완료
const complete = await sdk . completeAuth ({ email , otpCode });
if ( ! complete . success ) {
throw new Error ( complete . error );
}
console . log ( "✅ 인증 완료!" );
return complete . solanaAddress ;
} catch ( error ) {
console . error ( "인증 실패:" , error . message );
throw error ;
}
}
async function sendPayment () {
// 인증 확인
if ( ! sdk . isAuthenticated ()) {
throw new Error ( "재인증이 필요합니다" );
}
// 결제 전송
const payment = await sdk . sendPayment ({
destination: "9aE476sH92Vz7DMPyq5WLPkrKWivxeuTKEFKd2sZZcde" ,
currency: "USDC" ,
amount: 100 ,
network: "devnet" ,
});
if ( ! payment . success ) {
throw new Error ( payment . error );
}
console . log ( "트랜잭션:" , payment . explorerUrl );
return payment . signature ;
}
고급 사용법
3단계로 나누어서 결제하기
더 세밀한 제어가 필요한 경우, 결제를 3단계로 나누어 처리할 수 있습니다:
// 1단계: Payment intent 생성
const intent = await sdk . createPaymentIntent ({
destination: "9aE476sH92Vz7DMPyq5WLPkrKWivxeuTKEFKd2sZZcde" ,
currency: "USDC" ,
amount: 100 ,
network: "devnet" ,
});
if ( ! intent . success ) {
throw new Error ( intent . error );
}
console . log ( "Payment Intent ID:" , intent . paymentIntentId );
console . log ( "트랜잭션 상세:" , intent . details );
// 2단계: 트랜잭션 서명
const signed = await sdk . signTransaction ( intent . transaction );
if ( ! signed . success ) {
throw new Error ( signed . error );
}
// 3단계: 서명된 트랜잭션 제출
const result = await sdk . submitTransaction ({
paymentIntentId: intent . paymentIntentId ,
signedTransaction: signed . signedTransaction ,
network: "devnet" ,
});
if ( ! result . success ) {
throw new Error ( result . error );
}
console . log ( "트랜잭션 서명:" , result . signature );
console . log ( "확인 상태:" , result . confirmationStatus );
사용자 정의 수수료 지불자 (feePayer) feePayer 파라미터를 사용하려면 위의 3단계 흐름을 사용해야 하며, 2단계와 3단계 사이에 수수료 지불자의 서명을 추가해야 합니다. 현재 SDK는 수수료 지불자 서명 기능을 제공하지 않으므로, 클라이언트에서 별도로 처리해야 합니다.const intent = await sdk . createPaymentIntent ({
destination: "RECIPIENT_ADDRESS" ,
currency: "USDC" ,
amount: 100 ,
feePayer: "FEE_PAYER_ADDRESS" , // 사용자 정의 수수료 지불자
network: "devnet" ,
});
// SDK 서명
const signed = await sdk . signTransaction ( intent . transaction );
// ⚠️ 여기서 feePayer의 서명을 추가해야 함 (SDK 외부에서 처리)
// const feePayerSigned = await addFeePayerSignature(signed.signedTransaction);
// 제출
const result = await sdk . submitTransaction ({
paymentIntentId: intent . paymentIntentId ,
signedTransaction: signed . signedTransaction , // 또는 feePayerSigned
network: "devnet" ,
});
다음 단계