@pioneer-platform/pioneer-sdk 0.0.81 → 4.13.30

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.
@@ -0,0 +1,191 @@
1
+ /**
2
+ * KKAPI Batch Client for Pioneer SDK
3
+ *
4
+ * Provides batch operations for kkapi:// protocol to optimize performance:
5
+ * 1. Single batch call to get ALL cached pubkeys
6
+ * 2. Audit which ones are missing
7
+ * 3. Individual fallback only for missing pubkeys
8
+ */
9
+
10
+ export interface KkapiPubkey {
11
+ pubkey: string;
12
+ address: string;
13
+ path: string;
14
+ pathMaster: string;
15
+ scriptType: string;
16
+ networks: string[];
17
+ type: string;
18
+ note?: string;
19
+ available_scripts_types?: string[];
20
+ context?: string;
21
+ }
22
+
23
+ export interface KkapiBatchPubkeysResponse {
24
+ pubkeys: KkapiPubkey[];
25
+ cached_count: number;
26
+ total_requested: number;
27
+ device_id?: string;
28
+ }
29
+
30
+ export interface KkapiHealthStatus {
31
+ available: boolean;
32
+ device_connected: boolean;
33
+ cached_pubkeys: number;
34
+ vault_version?: string;
35
+ }
36
+
37
+ /**
38
+ * Check if kkapi:// vault is available and ready
39
+ */
40
+ export async function checkKkapiHealth(baseUrl: string = 'kkapi://'): Promise<KkapiHealthStatus> {
41
+ try {
42
+ const healthResponse = await fetch(`${baseUrl}/api/health`);
43
+ if (!healthResponse.ok) {
44
+ return { available: false, device_connected: false, cached_pubkeys: 0 };
45
+ }
46
+
47
+ const healthData = await healthResponse.json();
48
+ if (healthData.cached_pubkeys !== undefined) {
49
+ return {
50
+ available: true,
51
+ device_connected: healthData.device_connected || false,
52
+ cached_pubkeys: healthData.cached_pubkeys || 0,
53
+ vault_version: healthData.version
54
+ };
55
+ }
56
+
57
+ const cacheResponse = await fetch(`${baseUrl}/api/cache/status`);
58
+ if (!cacheResponse.ok) {
59
+ return { available: true, device_connected: true, cached_pubkeys: 0 };
60
+ }
61
+
62
+ const cacheData = await cacheResponse.json();
63
+ return {
64
+ available: true,
65
+ device_connected: true,
66
+ cached_pubkeys: cacheData.cached_pubkeys || 0,
67
+ vault_version: cacheData.vault_version
68
+ };
69
+ } catch (error: any) {
70
+ return { available: false, device_connected: false, cached_pubkeys: 0 };
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Get multiple pubkeys in a single batch call from kkapi:// vault
76
+ * Returns cached pubkeys and indicates which paths are missing
77
+ */
78
+ export async function batchGetPubkeys(paths: any[], context: string, baseUrl: string = 'kkapi://'): Promise<KkapiBatchPubkeysResponse> {
79
+ try {
80
+ const batchRequest = {
81
+ paths: paths.map(path => ({
82
+ address_n: path.addressNList,
83
+ script_type: path.script_type,
84
+ networks: path.networks,
85
+ type: path.type,
86
+ note: path.note
87
+ })),
88
+ context: context
89
+ };
90
+ const response = await fetch(`${baseUrl}/api/pubkeys/batch`, {
91
+ method: 'POST',
92
+ headers: {
93
+ 'Content-Type': 'application/json'
94
+ },
95
+ body: JSON.stringify(batchRequest)
96
+ });
97
+
98
+ if (!response.ok) {
99
+ throw new Error(`Batch request failed: ${response.status}`);
100
+ }
101
+
102
+ const batchResponse: KkapiBatchPubkeysResponse = await response.json();
103
+
104
+ return batchResponse;
105
+
106
+ } catch (error: any) {
107
+ return {
108
+ pubkeys: [],
109
+ cached_count: 0,
110
+ total_requested: paths.length
111
+ };
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Optimized getPubkeys that uses batch fetching with individual fallback
117
+ *
118
+ * Strategy:
119
+ * 1. Try batch fetch from kkapi:// vault first (fast)
120
+ * 2. Audit which pubkeys are missing
121
+ * 3. Individual keepkey-sdk calls only for missing ones
122
+ */
123
+ export async function optimizedGetPubkeys(
124
+ blockchains: string[],
125
+ paths: any[],
126
+ keepKeySdk: any,
127
+ context: string,
128
+ getPubkeyFunction: (networkId: string, path: any, sdk: any, context: string) => Promise<any>
129
+ ): Promise<any[]> {
130
+ let baseUrl: string;
131
+ if (typeof window !== 'undefined' && '__TAURI__' in window) {
132
+ baseUrl = 'kkapi://';
133
+ } else if (typeof window !== 'undefined' && window.location.hostname === 'localhost') {
134
+ baseUrl = 'http://localhost:1646';
135
+ } else {
136
+ baseUrl = 'http://localhost:1646';
137
+ }
138
+
139
+ const vaultHealth = await checkKkapiHealth(baseUrl);
140
+
141
+ let pubkeys: any[] = [];
142
+ let remainingPaths: any[] = [];
143
+ let remainingBlockchains: string[] = [];
144
+
145
+ if (vaultHealth.available && vaultHealth.cached_pubkeys > 0) {
146
+ const batchResponse = await batchGetPubkeys(paths, context, baseUrl);
147
+ pubkeys = batchResponse.pubkeys;
148
+ const cachedPaths = new Set(batchResponse.pubkeys.map(p => p.path));
149
+
150
+ for (let i = 0; i < blockchains.length; i++) {
151
+ const blockchain = blockchains[i];
152
+ const pathsForChain = paths.filter(path => path.networks && Array.isArray(path.networks) && path.networks.includes(blockchain));
153
+
154
+ for (const path of pathsForChain) {
155
+ const { addressNListToBIP32 } = await import('@pioneer-platform/pioneer-coins');
156
+ const pathBip32 = addressNListToBIP32(path.addressNList);
157
+
158
+ if (!cachedPaths.has(pathBip32)) {
159
+ remainingPaths.push(path);
160
+ if (!remainingBlockchains.includes(blockchain)) {
161
+ remainingBlockchains.push(blockchain);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ } else {
167
+ remainingPaths = paths;
168
+ remainingBlockchains = blockchains;
169
+ }
170
+
171
+ if (remainingPaths.length > 0) {
172
+
173
+ for (let i = 0; i < remainingBlockchains.length; i++) {
174
+ const blockchain = remainingBlockchains[i];
175
+ const pathsForChain = remainingPaths.filter(path => path.networks && Array.isArray(path.networks) && path.networks.includes(blockchain));
176
+
177
+ for (const path of pathsForChain) {
178
+ try {
179
+ const pubkey = await getPubkeyFunction(blockchain, path, keepKeySdk, context);
180
+ if (pubkey) {
181
+ pubkeys.push(pubkey);
182
+ }
183
+ } catch (error: any) {
184
+ throw error;
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ return pubkeys;
191
+ }
@@ -0,0 +1,287 @@
1
+ // Offline-first client for KeepKey Vault integration
2
+
3
+ const TAG = ' | offline-client | ';
4
+
5
+ export interface OfflineClientConfig {
6
+ vaultUrl: string;
7
+ timeout: number;
8
+ fallbackToRemote: boolean;
9
+ }
10
+
11
+ export interface BootstrapRequest {
12
+ device_id?: string;
13
+ paths: string[];
14
+ include: {
15
+ pubkeys: boolean;
16
+ addresses: boolean;
17
+ balances: boolean;
18
+ transactions: boolean;
19
+ };
20
+ cache_strategy: 'prefer_cache' | 'force_refresh' | 'cache_only';
21
+ }
22
+
23
+ export interface BootstrapResponse {
24
+ device_id: string;
25
+ response_time_ms: number;
26
+ cache_status: {
27
+ total_requested: number;
28
+ cache_hits: number;
29
+ cache_misses: number;
30
+ missing_paths: string[];
31
+ cache_freshness: string;
32
+ };
33
+ data: {
34
+ pubkeys: Record<string, any>;
35
+ addresses: Record<string, any>;
36
+ balances: Record<string, any>;
37
+ };
38
+ background_tasks: {
39
+ missing_data_fetch: string;
40
+ balance_refresh: string;
41
+ transaction_sync: string;
42
+ };
43
+ }
44
+
45
+ export interface FastHealthResponse {
46
+ status: string;
47
+ device_connected: boolean;
48
+ device_id?: string;
49
+ cache_status: string;
50
+ response_time_ms: number;
51
+ }
52
+
53
+ export class OfflineClient {
54
+ private config: OfflineClientConfig;
55
+ private isVaultAvailable: boolean = false;
56
+ private lastHealthCheck: number = 0;
57
+ private healthCheckCacheMs: number = 5000; // 5 second cache
58
+
59
+ constructor(config: OfflineClientConfig) {
60
+ this.config = config;
61
+ }
62
+
63
+ /**
64
+ * Fast health check with caching
65
+ */
66
+ async checkVaultHealth(forceRefresh = false): Promise<boolean> {
67
+ const now = Date.now();
68
+
69
+ if (!forceRefresh && (now - this.lastHealthCheck) < this.healthCheckCacheMs) {
70
+ return this.isVaultAvailable;
71
+ }
72
+
73
+ const controller = new AbortController();
74
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
75
+
76
+ try {
77
+ const response = await fetch(`${this.config.vaultUrl}/api/v1/health/fast`, {
78
+ method: 'GET',
79
+ signal: controller.signal,
80
+ headers: {
81
+ 'Content-Type': 'application/json',
82
+ },
83
+ });
84
+
85
+ clearTimeout(timeoutId);
86
+
87
+ if (response.ok) {
88
+ const health: FastHealthResponse = await response.json();
89
+ this.isVaultAvailable = health.status === 'healthy';
90
+ this.lastHealthCheck = now;
91
+
92
+ return this.isVaultAvailable;
93
+ } else {
94
+ this.isVaultAvailable = false;
95
+ return false;
96
+ }
97
+ } catch (error) {
98
+ clearTimeout(timeoutId);
99
+ this.isVaultAvailable = false;
100
+ this.lastHealthCheck = now;
101
+ return false;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Get complete wallet bootstrap data
107
+ */
108
+ async getWalletBootstrap(paths: string[], includeOptions = {
109
+ pubkeys: true,
110
+ addresses: true,
111
+ balances: true,
112
+ transactions: false
113
+ }): Promise<BootstrapResponse | null> {
114
+ const isAvailable = await this.checkVaultHealth();
115
+ if (!isAvailable) {
116
+ return null;
117
+ }
118
+
119
+ const request: BootstrapRequest = {
120
+ paths,
121
+ include: includeOptions,
122
+ cache_strategy: 'prefer_cache'
123
+ };
124
+
125
+ const controller = new AbortController();
126
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout * 2);
127
+
128
+ try {
129
+ const startTime = performance.now();
130
+
131
+ const response = await fetch(`${this.config.vaultUrl}/api/v1/wallet/bootstrap`, {
132
+ method: 'POST',
133
+ headers: {
134
+ 'Content-Type': 'application/json',
135
+ },
136
+ body: JSON.stringify(request),
137
+ signal: controller.signal,
138
+ });
139
+
140
+ clearTimeout(timeoutId);
141
+
142
+ if (!response.ok) {
143
+ throw new Error(`Bootstrap request failed: ${response.status} ${response.statusText}`);
144
+ }
145
+
146
+ const bootstrap: BootstrapResponse = await response.json();
147
+
148
+ return bootstrap;
149
+ } catch (error) {
150
+ clearTimeout(timeoutId);
151
+ return null;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Convert vault bootstrap response to pioneer-sdk format
157
+ */
158
+ convertBootstrapToPubkeys(bootstrap: BootstrapResponse): any[] {
159
+ const pubkeys: any[] = [];
160
+
161
+ for (const [path, pubkeyData] of Object.entries(bootstrap.data.pubkeys)) {
162
+ const addressData = bootstrap.data.addresses[path];
163
+ const balanceData = bootstrap.data.balances[path];
164
+
165
+ const pubkey = {
166
+ pubkey: pubkeyData.pubkey,
167
+ address: addressData?.address || '',
168
+ path,
169
+ pathMaster: path, // For now, same as path
170
+ networks: this.getNetworksForPath(path),
171
+ coin: pubkeyData.coin,
172
+ script_type: addressData?.script_type || 'p2pkh',
173
+ cached: pubkeyData.cached,
174
+ source: 'vault_cache'
175
+ };
176
+
177
+ pubkeys.push(pubkey);
178
+ }
179
+
180
+ return pubkeys;
181
+ }
182
+
183
+ /**
184
+ * Get network identifiers for a BIP32 path
185
+ */
186
+ private getNetworksForPath(path: string): string[] {
187
+ const parts = path.split('/');
188
+ if (parts.length < 3) return ['bitcoin'];
189
+
190
+ const coinType = parts[2].replace("'", "");
191
+ const coinTypeMap: Record<string, string[]> = {
192
+ '0': ['bip122:000000000019d6689c085ae165831e93'], // Bitcoin
193
+ '1': ['bip122:000000000019d6689c085ae165831e93'], // Bitcoin testnet
194
+ '2': ['bip122:000000000000000082ccf8f1557c5d40'], // Litecoin
195
+ '3': ['bip122:12a765e31ffd4059bada1e25190f6e98'], // Dogecoin
196
+ '5': ['bip122:000000000000000000651ef99cb9fcbe'], // Dash
197
+ '60': ['eip155:1'], // Ethereum
198
+ '118': ['cosmos:cosmoshub-4'], // Cosmos
199
+ '144': ['bip122:000000000000000000651ef99cb9fcbe'], // Ripple
200
+ '145': ['bip122:000000000000000082ccf8f1557c5d40'], // Bitcoin Cash
201
+ };
202
+
203
+ return coinTypeMap[coinType] || ['bitcoin'];
204
+ }
205
+
206
+ /**
207
+ * Initialize offline mode
208
+ */
209
+ async initOffline(paths: string[]): Promise<{ pubkeys: any[], balances: any[], cached: boolean }> {
210
+ const startTime = performance.now();
211
+ const bootstrap = await this.getWalletBootstrap(paths);
212
+
213
+ if (bootstrap) {
214
+ const pubkeys = this.convertBootstrapToPubkeys(bootstrap);
215
+ const balances = this.extractBalancesFromBootstrap(bootstrap);
216
+
217
+ return {
218
+ pubkeys,
219
+ balances,
220
+ cached: true
221
+ };
222
+ }
223
+
224
+ return {
225
+ pubkeys: [],
226
+ balances: [],
227
+ cached: false
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Extract balance information from bootstrap response
233
+ */
234
+ private extractBalancesFromBootstrap(bootstrap: BootstrapResponse): any[] {
235
+ const balances: any[] = [];
236
+
237
+ for (const [path, balanceData] of Object.entries(bootstrap.data.balances)) {
238
+ const addressData = bootstrap.data.addresses[path];
239
+ const pubkeyData = bootstrap.data.pubkeys[path];
240
+
241
+ if (balanceData && addressData) {
242
+ const balance = {
243
+ address: addressData.address,
244
+ balance: balanceData.confirmed,
245
+ unconfirmed: balanceData.unconfirmed,
246
+ currency: balanceData.currency,
247
+ valueUsd: balanceData.usd_value,
248
+ networks: this.getNetworksForPath(path),
249
+ path,
250
+ cached: balanceData.cached,
251
+ last_updated: balanceData.last_updated
252
+ };
253
+
254
+ balances.push(balance);
255
+ }
256
+ }
257
+
258
+ return balances;
259
+ }
260
+
261
+ /**
262
+ * Get current availability status
263
+ */
264
+ isAvailable(): boolean {
265
+ return this.isVaultAvailable;
266
+ }
267
+
268
+ /**
269
+ * Background sync
270
+ */
271
+ async backgroundSync(paths: string[]): Promise<void> {
272
+ try {
273
+ const bootstrap = await this.getWalletBootstrap(paths, {
274
+ pubkeys: true,
275
+ addresses: true,
276
+ balances: true,
277
+ transactions: true
278
+ });
279
+
280
+ if (bootstrap) {
281
+ // TODO: Emit events for updated data
282
+ }
283
+ } catch (error) {
284
+ // Silent error handling
285
+ }
286
+ }
287
+ }
@@ -0,0 +1,36 @@
1
+ // supportedCaips.ts
2
+
3
+ export const UTXO_SUPPORT = [
4
+ 'bip122:000000000019d6689c085ae165831e93/slip44:0', // BTC
5
+ 'bip122:000000000000000000651ef99cb9fcbe/slip44:145', // BCH
6
+ 'bip122:000007d91d1254d60e2dd1ae58038307/slip44:5', // DASH
7
+ 'bip122:00000000001a91e3dace36e2be3bf030/slip44:3', // DOGE
8
+ 'bip122:12a765e31ffd4059bada1e25190f6e98/slip44:2', // LTC
9
+ ];
10
+
11
+ export const TENDERMINT_SUPPORT = [
12
+ 'cosmos:mayachain-mainnet-v1/slip44:931', // CACAO (native)
13
+ 'cosmos:mayachain-mainnet-v1/denom:maya', // MAYA token
14
+ 'cosmos:osmosis-1/slip44:118',
15
+ 'cosmos:cosmoshub-4/slip44:118',
16
+ 'cosmos:kaiyo-1/slip44:118',
17
+ 'cosmos:thorchain-mainnet-v1/slip44:931',
18
+ ];
19
+
20
+ // Mapping of CAIP identifiers to KeepKey coin names for UTXO chains
21
+ export const CAIP_TO_COIN_MAP: { [key: string]: string } = {
22
+ 'bip122:000000000019d6689c085ae165831e93/slip44:0': 'Bitcoin',
23
+ 'bip122:000000000000000000651ef99cb9fcbe/slip44:145': 'BitcoinCash',
24
+ 'bip122:000007d91d1254d60e2dd1ae58038307/slip44:5': 'Dash',
25
+ 'bip122:00000000001a91e3dace36e2be3bf030/slip44:3': 'Dogecoin',
26
+ 'bip122:12a765e31ffd4059bada1e25190f6e98/slip44:2': 'Litecoin',
27
+ };
28
+
29
+ export const OTHER_SUPPORT = ['ripple:4109c6f2045fc7eff4cde8f9905d19c2/slip44:144'];
30
+
31
+ export const SUPPORTED_CAIPS = {
32
+ UTXO: UTXO_SUPPORT,
33
+ TENDERMINT: TENDERMINT_SUPPORT,
34
+ EIP155: ['eip155:*'],
35
+ OTHER: OTHER_SUPPORT,
36
+ };