@nexus-cross/crossx-sdk-react 0.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +472 -0
- package/dist/CROSSxProvider.d.ts +20 -0
- package/dist/CROSSxProvider.d.ts.map +1 -0
- package/dist/hooks/useAuth.d.ts +14 -0
- package/dist/hooks/useAuth.d.ts.map +1 -0
- package/dist/hooks/useCROSSx.d.ts +5 -0
- package/dist/hooks/useCROSSx.d.ts.map +1 -0
- package/dist/hooks/useWallet.d.ts +13 -0
- package/dist/hooks/useWallet.d.ts.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +132 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
# @nexus-cross/crossx-sdk-react
|
|
2
|
+
|
|
3
|
+
CROSSx Embedded Wallet을 React 앱에 통합하기 위한 Provider + Hooks 패키지.
|
|
4
|
+
|
|
5
|
+
> **wagmi를 사용하는 경우** `@nexus-cross/crossx-sdk-wagmi`를 대신 사용하세요.
|
|
6
|
+
> 이 패키지는 wagmi 없이 순수 React만으로 CROSSx 지갑을 사용할 때 적합합니다.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 패키지 구조
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
@nexus-cross/crossx-sdk-core ← 핵심 SDK (인증, 서명, 트랜잭션)
|
|
14
|
+
@nexus-cross/crossx-sdk-react ← React Hooks & Provider (이 패키지)
|
|
15
|
+
@nexus-cross/crossx-sdk-wagmi ← wagmi Connector (wagmi 사용 시)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 설치
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# pnpm (monorepo 내부)
|
|
24
|
+
pnpm add @nexus-cross/crossx-sdk-react
|
|
25
|
+
|
|
26
|
+
# npm
|
|
27
|
+
npm install @nexus-cross/crossx-sdk-react
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`peerDependencies`로 `react ^18.0.0`이 필요합니다.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 환경 변수
|
|
35
|
+
|
|
36
|
+
`.env` 파일에 아래 값을 설정합니다. SDK 내부에서 자동으로 읽습니다.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# OAuth 서비스 URL (로그인용) — 필수
|
|
40
|
+
VITE_OAUTH_SERVICE_URL=https://dev-cross-wallet-oauth.crosstoken.io
|
|
41
|
+
|
|
42
|
+
# JWT 검증 API URL — 필수
|
|
43
|
+
VITE_AUTH_API_URL=https://dev-cross-auth.crosstoken.io
|
|
44
|
+
|
|
45
|
+
# Embedded Wallet Gateway URL (지갑 기능용) — 필수
|
|
46
|
+
VITE_WALLET_GATEWAY_URL=https://dev-embedded-wallet-gateway.crosstoken.io/api/v1
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> URL은 `SDKConfig`에 노출하지 않습니다. 환경 변수로만 전환 가능합니다. (외부 개발사에는 위 설정을 알리지 않을 예정)
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
### 1. Provider 설정
|
|
56
|
+
|
|
57
|
+
앱 최상위에 `CROSSxProvider`를 배치합니다. `config`에는 `SDKConfig` 객체를 전달합니다.
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
// main.tsx
|
|
61
|
+
import React from 'react';
|
|
62
|
+
import ReactDOM from 'react-dom/client';
|
|
63
|
+
import { CROSSxProvider } from '@nexus-cross/crossx-sdk-react';
|
|
64
|
+
import App from './App';
|
|
65
|
+
|
|
66
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
67
|
+
<React.StrictMode>
|
|
68
|
+
<CROSSxProvider config={{}}>
|
|
69
|
+
<App />
|
|
70
|
+
</CROSSxProvider>
|
|
71
|
+
</React.StrictMode>
|
|
72
|
+
);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Provider가 마운트되면 내부적으로 `createCROSSxSDK(config)` → `sdk.init()`을 자동 호출합니다.
|
|
76
|
+
기존 세션이 있으면 자동으로 복원됩니다.
|
|
77
|
+
|
|
78
|
+
### 2. 로그인 / 로그아웃 — `useAuth`
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { useAuth } from '@nexus-cross/crossx-sdk-react';
|
|
82
|
+
|
|
83
|
+
function LoginButton() {
|
|
84
|
+
const { signIn, signOut, isAuthenticated, isLoading, error } = useAuth();
|
|
85
|
+
|
|
86
|
+
if (isLoading) return <p>처리 중...</p>;
|
|
87
|
+
|
|
88
|
+
return isAuthenticated ? (
|
|
89
|
+
<button onClick={signOut}>로그아웃</button>
|
|
90
|
+
) : (
|
|
91
|
+
<div>
|
|
92
|
+
<button onClick={signIn}>로그인</button>
|
|
93
|
+
{error && <p style={{ color: 'red' }}>{error}</p>}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 3. 서명 / 전송 — `useWallet`
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
import { useWallet } from '@nexus-cross/crossx-sdk-react';
|
|
103
|
+
import { ChainId } from '@nexus-cross/crossx-sdk-react';
|
|
104
|
+
|
|
105
|
+
function WalletActions() {
|
|
106
|
+
const { address, signMessage, sendTransaction, isLoading, error } = useWallet();
|
|
107
|
+
|
|
108
|
+
const handleSign = async () => {
|
|
109
|
+
// chainId는 CAIP-2 형식 문자열 (예: 'eip155:612055')
|
|
110
|
+
const result = await signMessage(ChainId.CROSS_MAINNET, 'Hello CROSSx!');
|
|
111
|
+
console.log('signature:', result.signature);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const handleSend = async () => {
|
|
115
|
+
const result = await sendTransaction(ChainId.CROSS_MAINNET, {
|
|
116
|
+
from: address!,
|
|
117
|
+
to: '0x920A31f0E48739C3FbB790D992b0690f7F5C42ea',
|
|
118
|
+
value: '0x2386f26fc10000', // 0.01 CROSS (hex wei)
|
|
119
|
+
gasLimit: '0x5208',
|
|
120
|
+
maxFeePerGas: '0x77359400',
|
|
121
|
+
maxPriorityFeePerGas: '0x3b9aca00',
|
|
122
|
+
});
|
|
123
|
+
console.log('txHash:', result.txHash);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div>
|
|
128
|
+
<p>주소: {address ?? '—'}</p>
|
|
129
|
+
<button onClick={handleSign} disabled={isLoading}>메시지 서명</button>
|
|
130
|
+
<button onClick={handleSend} disabled={isLoading}>전송</button>
|
|
131
|
+
{error && <p style={{ color: 'red' }}>{error}</p>}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 4. SDK 직접 접근 — `useCROSSx`
|
|
138
|
+
|
|
139
|
+
hooks로 커버되지 않는 기능(잔액 조회, nonce 조회, RPC 요청 등)은
|
|
140
|
+
`useCROSSx`로 SDK 인스턴스에 직접 접근합니다.
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
import { useCROSSx } from '@nexus-cross/crossx-sdk-react';
|
|
144
|
+
|
|
145
|
+
function AdvancedPanel() {
|
|
146
|
+
const { sdk, isInitialized, isAuthenticated, walletAddress } = useCROSSx();
|
|
147
|
+
|
|
148
|
+
const handleGetBalance = async () => {
|
|
149
|
+
if (!sdk) return;
|
|
150
|
+
const { formatted } = await sdk.getBalance('eip155:612055');
|
|
151
|
+
console.log('잔액:', formatted, 'CROSS');
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handleSignTypedData = async () => {
|
|
155
|
+
if (!sdk) return;
|
|
156
|
+
const result = await sdk.signTypedData('eip155:612055', {
|
|
157
|
+
types: {
|
|
158
|
+
EIP712Domain: [
|
|
159
|
+
{ name: 'name', type: 'string' },
|
|
160
|
+
{ name: 'version', type: 'string' },
|
|
161
|
+
{ name: 'chainId', type: 'uint256' },
|
|
162
|
+
{ name: 'verifyingContract', type: 'address' },
|
|
163
|
+
],
|
|
164
|
+
Permit: [
|
|
165
|
+
{ name: 'owner', type: 'address' },
|
|
166
|
+
{ name: 'spender', type: 'address' },
|
|
167
|
+
{ name: 'value', type: 'uint256' },
|
|
168
|
+
{ name: 'nonce', type: 'uint256' },
|
|
169
|
+
{ name: 'deadline', type: 'uint256' },
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
primaryType: 'Permit',
|
|
173
|
+
domain: {
|
|
174
|
+
name: 'MyToken',
|
|
175
|
+
version: '1',
|
|
176
|
+
chainId: 612055,
|
|
177
|
+
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
|
|
178
|
+
},
|
|
179
|
+
message: {
|
|
180
|
+
owner: walletAddress,
|
|
181
|
+
spender: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
|
|
182
|
+
value: '1000000000000000000',
|
|
183
|
+
nonce: '0',
|
|
184
|
+
deadline: '1234567890',
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
console.log('typed data signature:', result.signature);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const handleRpcCall = async () => {
|
|
191
|
+
if (!sdk) return;
|
|
192
|
+
// eth_call로 ERC-20 balanceOf 직접 호출
|
|
193
|
+
const data = '0x70a08231000000000000000000000000' +
|
|
194
|
+
walletAddress!.slice(2).toLowerCase().padStart(64, '0');
|
|
195
|
+
const raw = await sdk.rpcRequest(
|
|
196
|
+
'eth_call',
|
|
197
|
+
[{ to: '0x9f85c7b5d7637e18f946cc8af9c131318c6833d9', data }, 'latest'],
|
|
198
|
+
'eip155:612044'
|
|
199
|
+
);
|
|
200
|
+
console.log('balanceOf result:', raw);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
if (!isInitialized) return <p>초기화 중...</p>;
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<div>
|
|
207
|
+
<p>인증: {isAuthenticated ? '✅' : '❌'}</p>
|
|
208
|
+
<p>주소: {walletAddress ?? '—'}</p>
|
|
209
|
+
<button onClick={handleGetBalance}>잔액 조회</button>
|
|
210
|
+
<button onClick={handleSignTypedData}>EIP-712 서명</button>
|
|
211
|
+
<button onClick={handleRpcCall}>eth_call (tUSDT)</button>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## API Reference
|
|
220
|
+
|
|
221
|
+
### `<CROSSxProvider>`
|
|
222
|
+
|
|
223
|
+
| Prop | 타입 | 설명 |
|
|
224
|
+
|------|------|------|
|
|
225
|
+
| `config` | `SDKConfig` | SDK 설정 객체 |
|
|
226
|
+
| `children` | `ReactNode` | 자식 컴포넌트 |
|
|
227
|
+
|
|
228
|
+
`SDKConfig` 주요 필드:
|
|
229
|
+
|
|
230
|
+
| 필드 | 타입 | 기본값 | 설명 |
|
|
231
|
+
|------|------|--------|------|
|
|
232
|
+
| `theme` | `'light' \| 'dark'` | `'light'` | 확인 모달 테마 |
|
|
233
|
+
| `themeTokens` | `ConfirmationTokenOverride` | — | 모달 색상 커스터마이징 |
|
|
234
|
+
| `oauthDisplayMode` | `'popup' \| 'modal'` | `'popup'` | OAuth 로그인 표시 방식 |
|
|
235
|
+
| `useMockWallet` | `boolean` | `false` | Mock 지갑 사용 (개발용) |
|
|
236
|
+
| `debug` | `boolean` | `true` | 디버그 로그 출력 |
|
|
237
|
+
|
|
238
|
+
`ConfirmationTokenOverride` 구조:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
themeTokens: {
|
|
242
|
+
light?: ConfirmationThemeTokens; // 라이트 모드 오버라이드
|
|
243
|
+
dark?: ConfirmationThemeTokens; // 다크 모드 오버라이드
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`ConfirmationThemeTokens` 오버라이드 가능 필드:
|
|
248
|
+
|
|
249
|
+
| 필드 | Semantic 토큰 | 기본값 (light / dark) |
|
|
250
|
+
|------|------|------|
|
|
251
|
+
| `primary` | Primary | `#019D92` / `#019D92` |
|
|
252
|
+
| `secondary` | Secondary | `#E70077` / `#E70077` |
|
|
253
|
+
| `onPrimary` | OnPrimary | `#FFFFFF` / `#FFFFFF` |
|
|
254
|
+
| `borderDefault` | Border/Default | `rgba(18,18,18,0.05)` / `rgba(255,255,255,0.05)` |
|
|
255
|
+
| `borderSubtle` | Border/Subtle | `rgba(18,18,18,0.1)` / `rgba(255,255,255,0.1)` |
|
|
256
|
+
| `textPrimary` | TextIcon/Primary | `#121212` / `#FFFFFF` |
|
|
257
|
+
| `textSecondary` | TextIcon/Secondary | `rgba(18,18,18,0.7)` / `rgba(255,255,255,0.7)` |
|
|
258
|
+
| `textTertiary` | TextIcon/Tertiary | `rgba(18,18,18,0.5)` / `rgba(255,255,255,0.5)` |
|
|
259
|
+
| `surfaceDefault` | Surface/default | `rgba(18,18,18,0.05)` / `rgba(255,255,255,0.05)` |
|
|
260
|
+
| `bg` | Surface/BG | `#FFFFFF` / `#121212` |
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
### `useAuth()`
|
|
265
|
+
|
|
266
|
+
인증(로그인/로그아웃/탈퇴) 관련 상태와 액션을 제공합니다.
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
const {
|
|
270
|
+
isAuthenticated, // boolean — 현재 인증 상태
|
|
271
|
+
isLoading, // boolean — 비동기 작업 진행 중
|
|
272
|
+
error, // string | null — 마지막 에러 메시지
|
|
273
|
+
signIn, // () => Promise<AuthResult> — OAuth 로그인
|
|
274
|
+
signOut, // () => Promise<void> — 로그아웃
|
|
275
|
+
withdraw, // () => Promise<void> — 계정 탈퇴 (데이터 삭제)
|
|
276
|
+
} = useAuth();
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`signIn()` 호출 시 OAuth 팝업/모달이 열립니다.
|
|
280
|
+
성공하면 내부적으로 `window.location.reload()`가 호출되어 Provider 상태가 갱신됩니다.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
### `useWallet()`
|
|
285
|
+
|
|
286
|
+
지갑 주소, 메시지 서명, 트랜잭션 전송 기능을 제공합니다.
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
const {
|
|
290
|
+
address, // string | null — 지갑 주소 (0x...)
|
|
291
|
+
isLoading, // boolean — 비동기 작업 진행 중
|
|
292
|
+
error, // string | null — 마지막 에러 메시지
|
|
293
|
+
signMessage, // (chainId, message) => Promise<SignMessageResult>
|
|
294
|
+
sendTransaction, // (chainId, tx) => Promise<TransactionResult>
|
|
295
|
+
} = useWallet();
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**`signMessage(chainId, message)`**
|
|
299
|
+
|
|
300
|
+
| 파라미터 | 타입 | 예시 |
|
|
301
|
+
|----------|------|------|
|
|
302
|
+
| `chainId` | `string` | `'eip155:612055'` 또는 `ChainId.CROSS_MAINNET` |
|
|
303
|
+
| `message` | `string` | `'Hello CROSSx!'` |
|
|
304
|
+
|
|
305
|
+
반환: `{ chainId, signature, message, address }`
|
|
306
|
+
|
|
307
|
+
**`sendTransaction(chainId, tx)`**
|
|
308
|
+
|
|
309
|
+
| 파라미터 | 타입 | 설명 |
|
|
310
|
+
|----------|------|------|
|
|
311
|
+
| `chainId` | `string` | CAIP-2 체인 ID |
|
|
312
|
+
| `tx` | `EvmTransactionRequest` | 트랜잭션 객체 |
|
|
313
|
+
|
|
314
|
+
`EvmTransactionRequest` 주요 필드:
|
|
315
|
+
|
|
316
|
+
| 필드 | 타입 | 필수 | 설명 |
|
|
317
|
+
|------|------|:----:|------|
|
|
318
|
+
| `from` | `string` | O | 보내는 주소 |
|
|
319
|
+
| `to` | `string` | O | 받는 주소 |
|
|
320
|
+
| `value` | `string` | | hex wei (예: `'0x2386f26fc10000'`) |
|
|
321
|
+
| `data` | `string` | | calldata hex |
|
|
322
|
+
| `gasLimit` | `string` | | gas limit hex |
|
|
323
|
+
| `maxFeePerGas` | `string` | | EIP-1559 max fee |
|
|
324
|
+
| `maxPriorityFeePerGas` | `string` | | EIP-1559 priority fee |
|
|
325
|
+
| `nonce` | `number` | | 트랜잭션 순서 |
|
|
326
|
+
|
|
327
|
+
반환: `{ chainId, txHash, status }`
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
### `useCROSSx()`
|
|
332
|
+
|
|
333
|
+
SDK 인스턴스와 전역 상태에 직접 접근합니다.
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
const {
|
|
337
|
+
sdk, // CROSSxSDK | null — SDK 인스턴스
|
|
338
|
+
isInitialized, // boolean — init() 완료 여부
|
|
339
|
+
isAuthenticated, // boolean — 인증 상태
|
|
340
|
+
walletAddress, // string | null — 지갑 주소
|
|
341
|
+
} = useCROSSx();
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
`sdk` 인스턴스를 통해 호출할 수 있는 주요 메서드:
|
|
345
|
+
|
|
346
|
+
| 메서드 | 설명 |
|
|
347
|
+
|--------|------|
|
|
348
|
+
| `sdk.signMessage(chainId, message)` | EIP-191 메시지 서명 |
|
|
349
|
+
| `sdk.signTypedData(chainId, typedData)` | EIP-712 Typed Data 서명 |
|
|
350
|
+
| `sdk.signTransaction(chainId, tx)` | 트랜잭션 서명 (전송 없음) |
|
|
351
|
+
| `sdk.sendTransaction(chainId, tx)` | 트랜잭션 서명 + 전송 |
|
|
352
|
+
| `sdk.sendTransactionAndWait(chainId, tx)` | 전송 + Receipt 폴링 |
|
|
353
|
+
| `sdk.getBalance(chainId)` | 네이티브 잔액 조회 |
|
|
354
|
+
| `sdk.getNonce(chainId)` | 현재 nonce 조회 |
|
|
355
|
+
| `sdk.rpcRequest(method, params, chainId)` | 범용 JSON-RPC 호출 |
|
|
356
|
+
| `sdk.getProvider(chainId)` | EIP-1193 Provider 반환 |
|
|
357
|
+
| `sdk.createWallet()` | 수동 지갑 생성 |
|
|
358
|
+
| `sdk.withdraw()` | 계정 탈퇴 |
|
|
359
|
+
| `sdk.setTheme('dark')` | 확인 모달 테마 런타임 전환 (`themeTokens` 오버라이드 유지) |
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## ChainId 상수
|
|
364
|
+
|
|
365
|
+
체인 ID 오타 방지를 위한 상수를 제공합니다.
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
import { ChainId } from '@nexus-cross/crossx-sdk-react';
|
|
369
|
+
|
|
370
|
+
ChainId.CROSS_MAINNET // 'eip155:612055'
|
|
371
|
+
ChainId.CROSS_TESTNET // 'eip155:612044'
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## wagmi와의 차이
|
|
377
|
+
|
|
378
|
+
| | `@nexus-cross/crossx-sdk-react` | `@nexus-cross/crossx-sdk-wagmi` |
|
|
379
|
+
|---|---|---|
|
|
380
|
+
| **의존성** | React만 필요 | wagmi + viem + @tanstack/react-query |
|
|
381
|
+
| **접근 방식** | CROSSx 전용 hooks | wagmi 표준 hooks (`useAccount`, `useConnect` 등) |
|
|
382
|
+
| **EIP-1193** | 필요 시 `sdk.getProvider()` | 자동 제공 (wagmi connector) |
|
|
383
|
+
| **적합 대상** | CROSSx 지갑만 사용하는 앱 | 멀티 지갑(MetaMask 등) 지원 앱 |
|
|
384
|
+
|
|
385
|
+
wagmi를 사용하는 경우 `@nexus-cross/crossx-sdk-wagmi`의 `createCROSSxConnector`를 사용하세요.
|
|
386
|
+
wagmi 예제는 `examples/wagmi-app`을 참고하세요.
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## 확인 모달 (Confirmation)
|
|
391
|
+
|
|
392
|
+
서명/전송 시 자동으로 확인 모달이 표시됩니다.
|
|
393
|
+
|
|
394
|
+
- **Message Sign** — "Signature Request" 모달 (Cancel / Confirm)
|
|
395
|
+
- **EIP-712 Typed Data** — 구조화된 key-value 표시
|
|
396
|
+
- **Transaction Sign/Send** — 수신자, 금액, 수수료 표시
|
|
397
|
+
|
|
398
|
+
모바일(480px 이하)에서는 자동으로 하단 바텀시트로 전환됩니다.
|
|
399
|
+
|
|
400
|
+
### 테마 설정
|
|
401
|
+
|
|
402
|
+
**초기화 시 고정:**
|
|
403
|
+
```tsx
|
|
404
|
+
<CROSSxProvider config={{ theme: 'dark' }}>
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**런타임 전환:**
|
|
408
|
+
```ts
|
|
409
|
+
const { sdk } = useCROSSx();
|
|
410
|
+
sdk?.setTheme('dark'); // 다음 모달부터 적용. themeTokens 오버라이드는 유지됨
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### 색상 커스터마이징
|
|
414
|
+
|
|
415
|
+
`themeTokens.light` / `themeTokens.dark` 로 각 모드를 독립적으로 오버라이드합니다.
|
|
416
|
+
지정하지 않은 항목은 해당 모드의 기본값을 유지합니다.
|
|
417
|
+
|
|
418
|
+
```tsx
|
|
419
|
+
<CROSSxProvider
|
|
420
|
+
config={{
|
|
421
|
+
theme: 'light',
|
|
422
|
+
themeTokens: {
|
|
423
|
+
light: {
|
|
424
|
+
primary: '#FF6B35', // 버튼·강조색 (기본: #019D92)
|
|
425
|
+
bg: '#F5F0EB', // 카드 배경색 (기본: #FFFFFF)
|
|
426
|
+
},
|
|
427
|
+
dark: {
|
|
428
|
+
primary: '#FF6B35',
|
|
429
|
+
bg: '#1A0A00', // 카드 배경색 (기본: #121212)
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
}}
|
|
433
|
+
>
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
특정 모드만 지정하는 것도 가능합니다.
|
|
437
|
+
|
|
438
|
+
```tsx
|
|
439
|
+
// 라이트 모드만 커스터마이징
|
|
440
|
+
themeTokens: { light: { primary: '#FF6B35' } }
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## 마이그레이션 (자동)
|
|
446
|
+
|
|
447
|
+
이전 CROSSx 네이티브 앱 사용자가 로그인하면,
|
|
448
|
+
기존 백업이 감지될 경우 PIN 입력 팝업이 자동으로 표시되어
|
|
449
|
+
Embedded Wallet로 마이그레이션됩니다.
|
|
450
|
+
|
|
451
|
+
별도 코드 작성이 필요 없습니다.
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## 트러블슈팅
|
|
456
|
+
|
|
457
|
+
### `useCROSSx must be used within CROSSxProvider`
|
|
458
|
+
앱 최상위에 `<CROSSxProvider>`가 빠져있습니다.
|
|
459
|
+
|
|
460
|
+
### OAuth 팝업이 열리지 않음
|
|
461
|
+
브라우저의 팝업 차단을 확인하세요.
|
|
462
|
+
또는 `config.oauthDisplayMode = 'modal'`로 변경하세요.
|
|
463
|
+
|
|
464
|
+
### 로그인 후 상태가 업데이트되지 않음
|
|
465
|
+
현재 `useAuth().signIn()`은 성공 시 `window.location.reload()`를 호출합니다.
|
|
466
|
+
SPA 라우터와 충돌하는 경우 `useCROSSx()`로 직접 상태를 관리하세요.
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## 라이센스
|
|
471
|
+
|
|
472
|
+
MIT
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { CROSSxSDK, SDKConfig } from '@nexus-cross/crossx-sdk-core';
|
|
3
|
+
|
|
4
|
+
export interface CROSSxContextValue {
|
|
5
|
+
sdk: CROSSxSDK | null;
|
|
6
|
+
isInitialized: boolean;
|
|
7
|
+
isAuthenticated: boolean;
|
|
8
|
+
walletAddress: string | null;
|
|
9
|
+
}
|
|
10
|
+
export declare const CROSSxContext: React.Context<CROSSxContextValue>;
|
|
11
|
+
export interface CROSSxProviderProps {
|
|
12
|
+
config: SDKConfig;
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* CROSSx Provider 컴포넌트
|
|
17
|
+
* 앱 최상위에 배치하여 SDK 인스턴스를 제공
|
|
18
|
+
*/
|
|
19
|
+
export declare function CROSSxProvider({ config, children }: CROSSxProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
//# sourceMappingURL=CROSSxProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CROSSxProvider.d.ts","sourceRoot":"","sources":["../src/CROSSxProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsD,MAAM,OAAO,CAAC;AAE3E,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAEzE,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,SAAS,GAAG,IAAI,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,eAAe,EAAE,OAAO,CAAC;IACzB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,eAAO,MAAM,aAAa,mCAKxB,CAAC;AAEH,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,mBAAmB,2CAuCvE"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AuthResult } from '@nexus-cross/crossx-sdk-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 인증 관련 기능을 제공하는 훅
|
|
5
|
+
*/
|
|
6
|
+
export declare function useAuth(): {
|
|
7
|
+
isAuthenticated: boolean;
|
|
8
|
+
isLoading: boolean;
|
|
9
|
+
error: string | null;
|
|
10
|
+
signIn: () => Promise<AuthResult>;
|
|
11
|
+
signOut: () => Promise<void>;
|
|
12
|
+
withdraw: () => Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=useAuth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAuth.d.ts","sourceRoot":"","sources":["../../src/hooks/useAuth.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAE/D;;GAEG;AACH,wBAAgB,OAAO;;;;;;;EAmFtB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCROSSx.d.ts","sourceRoot":"","sources":["../../src/hooks/useCROSSx.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,wBAAgB,SAAS,mDAQxB"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SignMessageResp, SendTxResp, EvmTransactionRequest, ChainIdValue } from '@nexus-cross/crossx-sdk-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 지갑 관련 기능을 제공하는 훅
|
|
5
|
+
*/
|
|
6
|
+
export declare function useWallet(): {
|
|
7
|
+
address: string | null;
|
|
8
|
+
isLoading: boolean;
|
|
9
|
+
error: string | null;
|
|
10
|
+
signMessage: (chainId: ChainIdValue | string, message: string) => Promise<SignMessageResp>;
|
|
11
|
+
sendTransaction: (chainId: ChainIdValue | string, tx: EvmTransactionRequest) => Promise<SendTxResp>;
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=useWallet.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useWallet.d.ts","sourceRoot":"","sources":["../../src/hooks/useWallet.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,eAAe,EACf,UAAU,EACV,qBAAqB,EACrB,YAAY,EACb,MAAM,8BAA8B,CAAC;AAEtC;;GAEG;AACH,wBAAgB,SAAS;;;;2BAML,YAAY,GAAG,MAAM,WAAW,MAAM,KAAG,OAAO,CAAC,eAAe,CAAC;+BAoBjE,YAAY,GAAG,MAAM,MAAM,qBAAqB,KAAG,OAAO,CAAC,UAAU,CAAC;EA0BzF"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const g=require("react/jsx-runtime"),s=require("react"),w=require("@nexus-cross/crossx-sdk-core"),h=s.createContext({sdk:null,isInitialized:!1,isAuthenticated:!1,walletAddress:null});function y({config:t,children:u}){const[i]=s.useState(()=>w.createCROSSxSDK(t)),[n,d]=s.useState(!1),[r,S]=s.useState(!1),[l,c]=s.useState(null);s.useEffect(()=>{(async()=>{try{const o=await i.initialize();d(!0),o!=null&&o.success&&(S(!0),c(o.address??null))}catch(o){console.error("CROSSx SDK 초기화 실패:",o)}})()},[i]);const e=s.useMemo(()=>({sdk:i,isInitialized:n,isAuthenticated:r,walletAddress:l}),[i,n,r,l]);return g.jsx(h.Provider,{value:e,children:u})}function f(){const t=s.useContext(h);if(!t)throw new Error("useCROSSx must be used within CROSSxProvider");return t}function C(){const{sdk:t,isAuthenticated:u}=f(),[i,n]=s.useState(!1),[d,r]=s.useState(null),S=s.useCallback(async()=>{if(!t)throw new Error("SDK가 초기화되지 않았습니다");n(!0),r(null);try{const e=await t.signIn();return e.success?(window.location.reload(),e):(r(e.error||"로그인 실패"),e)}catch(e){const a=e instanceof Error?e.message:"로그인 실패";throw r(a),e}finally{n(!1)}},[t]),l=s.useCallback(async()=>{if(!t)throw new Error("SDK가 초기화되지 않았습니다");n(!0),r(null);try{await t.signOut(),window.location.reload()}catch(e){const a=e instanceof Error?e.message:"로그아웃 실패";throw r(a),e}finally{n(!1)}},[t]),c=s.useCallback(async()=>{if(!t)throw new Error("SDK가 초기화되지 않았습니다");n(!0),r(null);try{await t.withdraw(),window.location.reload()}catch(e){const a=e instanceof Error?e.message:"탈퇴 실패";throw r(a),e}finally{n(!1)}},[t]);return{isAuthenticated:u,isLoading:i,error:d,signIn:S,signOut:l,withdraw:c}}function x(){const{sdk:t,walletAddress:u}=f(),[i,n]=s.useState(!1),[d,r]=s.useState(null),S=s.useCallback(async(c,e)=>{if(!t)throw new Error("SDK가 초기화되지 않았습니다");n(!0),r(null);try{return await t.signMessage(c,e)}catch(a){const o=a instanceof Error?a.message:"서명 실패";throw r(o),a}finally{n(!1)}},[t]),l=s.useCallback(async(c,e)=>{if(!t)throw new Error("SDK가 초기화되지 않았습니다");n(!0),r(null);try{return await t.sendTransaction(c,e)}catch(a){const o=a instanceof Error?a.message:"트랜잭션 전송 실패";throw r(o),a}finally{n(!1)}},[t]);return{address:u,isLoading:i,error:d,signMessage:S,sendTransaction:l}}Object.defineProperty(exports,"ChainId",{enumerable:!0,get:()=>w.ChainId});exports.CROSSxProvider=y;exports.useAuth=C;exports.useCROSSx=f;exports.useWallet=x;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { CROSSxProvider } from './CROSSxProvider';
|
|
2
|
+
export { useCROSSx } from './hooks/useCROSSx';
|
|
3
|
+
export { useAuth } from './hooks/useAuth';
|
|
4
|
+
export { useWallet } from './hooks/useWallet';
|
|
5
|
+
export type { CROSSxProviderProps } from './CROSSxProvider';
|
|
6
|
+
export type { SDKConfig, AuthResult, SignMessageResp, SendTxResp, SignInOptions, EvmTransactionRequest, ChainIdValue, } from '@nexus-cross/crossx-sdk-core';
|
|
7
|
+
export { ChainId } from '@nexus-cross/crossx-sdk-core';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,YAAY,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAG5D,YAAY,EACV,SAAS,EACT,UAAU,EACV,eAAe,EACf,UAAU,EACV,aAAa,EACb,qBAAqB,EACrB,YAAY,GACb,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { jsx as g } from "react/jsx-runtime";
|
|
2
|
+
import { createContext as m, useState as i, useEffect as y, useMemo as x, useContext as E, useCallback as w } from "react";
|
|
3
|
+
import { createCROSSxSDK as C } from "@nexus-cross/crossx-sdk-core";
|
|
4
|
+
import { ChainId as v } from "@nexus-cross/crossx-sdk-core";
|
|
5
|
+
const h = m({
|
|
6
|
+
sdk: null,
|
|
7
|
+
isInitialized: !1,
|
|
8
|
+
isAuthenticated: !1,
|
|
9
|
+
walletAddress: null
|
|
10
|
+
});
|
|
11
|
+
function K({ config: e, children: u }) {
|
|
12
|
+
const [a] = i(() => C(e)), [s, d] = i(!1), [r, f] = i(!1), [l, c] = i(null);
|
|
13
|
+
y(() => {
|
|
14
|
+
(async () => {
|
|
15
|
+
try {
|
|
16
|
+
const o = await a.initialize();
|
|
17
|
+
d(!0), o != null && o.success && (f(!0), c(o.address ?? null));
|
|
18
|
+
} catch (o) {
|
|
19
|
+
console.error("CROSSx SDK 초기화 실패:", o);
|
|
20
|
+
}
|
|
21
|
+
})();
|
|
22
|
+
}, [a]);
|
|
23
|
+
const t = x(
|
|
24
|
+
() => ({
|
|
25
|
+
sdk: a,
|
|
26
|
+
isInitialized: s,
|
|
27
|
+
isAuthenticated: r,
|
|
28
|
+
walletAddress: l
|
|
29
|
+
}),
|
|
30
|
+
[a, s, r, l]
|
|
31
|
+
);
|
|
32
|
+
return /* @__PURE__ */ g(h.Provider, { value: t, children: u });
|
|
33
|
+
}
|
|
34
|
+
function S() {
|
|
35
|
+
const e = E(h);
|
|
36
|
+
if (!e)
|
|
37
|
+
throw new Error("useCROSSx must be used within CROSSxProvider");
|
|
38
|
+
return e;
|
|
39
|
+
}
|
|
40
|
+
function O() {
|
|
41
|
+
const { sdk: e, isAuthenticated: u } = S(), [a, s] = i(!1), [d, r] = i(null), f = w(async () => {
|
|
42
|
+
if (!e)
|
|
43
|
+
throw new Error("SDK가 초기화되지 않았습니다");
|
|
44
|
+
s(!0), r(null);
|
|
45
|
+
try {
|
|
46
|
+
const t = await e.signIn();
|
|
47
|
+
return t.success ? (window.location.reload(), t) : (r(t.error || "로그인 실패"), t);
|
|
48
|
+
} catch (t) {
|
|
49
|
+
const n = t instanceof Error ? t.message : "로그인 실패";
|
|
50
|
+
throw r(n), t;
|
|
51
|
+
} finally {
|
|
52
|
+
s(!1);
|
|
53
|
+
}
|
|
54
|
+
}, [e]), l = w(async () => {
|
|
55
|
+
if (!e)
|
|
56
|
+
throw new Error("SDK가 초기화되지 않았습니다");
|
|
57
|
+
s(!0), r(null);
|
|
58
|
+
try {
|
|
59
|
+
await e.signOut(), window.location.reload();
|
|
60
|
+
} catch (t) {
|
|
61
|
+
const n = t instanceof Error ? t.message : "로그아웃 실패";
|
|
62
|
+
throw r(n), t;
|
|
63
|
+
} finally {
|
|
64
|
+
s(!1);
|
|
65
|
+
}
|
|
66
|
+
}, [e]), c = w(async () => {
|
|
67
|
+
if (!e)
|
|
68
|
+
throw new Error("SDK가 초기화되지 않았습니다");
|
|
69
|
+
s(!0), r(null);
|
|
70
|
+
try {
|
|
71
|
+
await e.withdraw(), window.location.reload();
|
|
72
|
+
} catch (t) {
|
|
73
|
+
const n = t instanceof Error ? t.message : "탈퇴 실패";
|
|
74
|
+
throw r(n), t;
|
|
75
|
+
} finally {
|
|
76
|
+
s(!1);
|
|
77
|
+
}
|
|
78
|
+
}, [e]);
|
|
79
|
+
return {
|
|
80
|
+
isAuthenticated: u,
|
|
81
|
+
isLoading: a,
|
|
82
|
+
error: d,
|
|
83
|
+
signIn: f,
|
|
84
|
+
signOut: l,
|
|
85
|
+
withdraw: c
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function R() {
|
|
89
|
+
const { sdk: e, walletAddress: u } = S(), [a, s] = i(!1), [d, r] = i(null), f = w(
|
|
90
|
+
async (c, t) => {
|
|
91
|
+
if (!e) throw new Error("SDK가 초기화되지 않았습니다");
|
|
92
|
+
s(!0), r(null);
|
|
93
|
+
try {
|
|
94
|
+
return await e.signMessage(c, t);
|
|
95
|
+
} catch (n) {
|
|
96
|
+
const o = n instanceof Error ? n.message : "서명 실패";
|
|
97
|
+
throw r(o), n;
|
|
98
|
+
} finally {
|
|
99
|
+
s(!1);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
[e]
|
|
103
|
+
), l = w(
|
|
104
|
+
async (c, t) => {
|
|
105
|
+
if (!e) throw new Error("SDK가 초기화되지 않았습니다");
|
|
106
|
+
s(!0), r(null);
|
|
107
|
+
try {
|
|
108
|
+
return await e.sendTransaction(c, t);
|
|
109
|
+
} catch (n) {
|
|
110
|
+
const o = n instanceof Error ? n.message : "트랜잭션 전송 실패";
|
|
111
|
+
throw r(o), n;
|
|
112
|
+
} finally {
|
|
113
|
+
s(!1);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
[e]
|
|
117
|
+
);
|
|
118
|
+
return {
|
|
119
|
+
address: u,
|
|
120
|
+
isLoading: a,
|
|
121
|
+
error: d,
|
|
122
|
+
signMessage: f,
|
|
123
|
+
sendTransaction: l
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export {
|
|
127
|
+
K as CROSSxProvider,
|
|
128
|
+
v as ChainId,
|
|
129
|
+
O as useAuth,
|
|
130
|
+
S as useCROSSx,
|
|
131
|
+
R as useWallet
|
|
132
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nexus-cross/crossx-sdk-react",
|
|
3
|
+
"version": "0.0.0-beta.2",
|
|
4
|
+
"description": "CROSSx React SDK - React Hooks and Components for Embedded Wallet",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"require": "./dist/index.cjs",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"crossx",
|
|
21
|
+
"react",
|
|
22
|
+
"hooks",
|
|
23
|
+
"wallet",
|
|
24
|
+
"embedded-wallet"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"registry": "https://registry.npmjs.org",
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"react": "^18.0.0"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@nexus-cross/crossx-sdk-core": "0.0.0-beta.2"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/react": "^18.2.48",
|
|
40
|
+
"react": "^18.2.0",
|
|
41
|
+
"typescript": "^5.3.3",
|
|
42
|
+
"vite": "^5.0.11",
|
|
43
|
+
"vite-plugin-dts": "^3.7.1"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "vite build",
|
|
47
|
+
"dev": "vite build --watch",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"test": "echo \"Tests not implemented yet\""
|
|
50
|
+
}
|
|
51
|
+
}
|