@pioneer-platform/pioneer-sdk 8.15.44 → 8.17.0

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/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "author": "highlander",
3
3
  "name": "@pioneer-platform/pioneer-sdk",
4
- "version": "8.15.44",
4
+ "version": "8.17.0",
5
5
  "dependencies": {
6
- "@keepkey/keepkey-sdk": "^0.2.62",
7
- "@pioneer-platform/pioneer-caip": "^9.10.21",
6
+ "keepkey-vault-sdk": "^1.0.2",
7
+ "@pioneer-platform/pioneer-caip": "^9.12.0",
8
8
  "@pioneer-platform/pioneer-client": "^9.10.24",
9
- "@pioneer-platform/pioneer-coins": "^9.11.21",
10
- "@pioneer-platform/pioneer-discovery": "^8.15.44",
11
- "@pioneer-platform/pioneer-events": "^8.12.13",
9
+ "@pioneer-platform/pioneer-coins": "^9.13.0",
10
+ "@pioneer-platform/pioneer-discovery": "^8.17.0",
11
+ "@pioneer-platform/pioneer-events": "^8.13.0",
12
12
  "coinselect": "^3.1.13",
13
13
  "eventemitter3": "^5.0.1",
14
14
  "neotraverse": "^0.6.8",
@@ -6,7 +6,9 @@ import type EventEmitter from 'events';
6
6
  import { CAIP_TO_COIN_MAP, SUPPORTED_CAIPS } from './supportedCaips';
7
7
  import { createUnsignedEvmTx } from './txbuilder/createUnsignedEvmTx';
8
8
  import { createUnsignedRippleTx } from './txbuilder/createUnsignedRippleTx';
9
+ import { createUnsignedSolanaTx } from './txbuilder/createUnsignedSolanaTx';
9
10
  import { createUnsignedTendermintTx } from './txbuilder/createUnsignedTendermintTx';
11
+ import { createUnsignedTronTx } from './txbuilder/createUnsignedTronTx';
10
12
  import { createUnsignedUxtoTx } from './txbuilder/createUnsignedUxtoTx';
11
13
 
12
14
  const TAG = ' | Transaction | ';
@@ -138,16 +140,43 @@ export class TransactionManager {
138
140
  break;
139
141
  }
140
142
  case 'OTHER': {
141
- unsignedTx = await createUnsignedRippleTx(
142
- caip,
143
- to,
144
- amount,
145
- memo,
146
- this.pubkeys,
147
- this.pioneer,
148
- this.pubkeyContext,
149
- isMax,
150
- );
143
+ // Distinguish between different chains in the OTHER category
144
+ if (caip.startsWith('solana:')) {
145
+ unsignedTx = await createUnsignedSolanaTx(
146
+ caip,
147
+ to,
148
+ amount,
149
+ memo,
150
+ this.pubkeys,
151
+ this.pioneer,
152
+ this.pubkeyContext,
153
+ isMax,
154
+ );
155
+ } else if (caip === 'ripple:4109c6f2045fc7eff4cde8f9905d19c2/slip44:144') {
156
+ unsignedTx = await createUnsignedRippleTx(
157
+ caip,
158
+ to,
159
+ amount,
160
+ memo,
161
+ this.pubkeys,
162
+ this.pioneer,
163
+ this.pubkeyContext,
164
+ isMax,
165
+ );
166
+ } else if (caip.startsWith('tron:')) {
167
+ unsignedTx = await createUnsignedTronTx(
168
+ caip,
169
+ to,
170
+ amount,
171
+ memo,
172
+ this.pubkeys,
173
+ this.pioneer,
174
+ this.pubkeyContext,
175
+ isMax,
176
+ );
177
+ } else {
178
+ throw new Error(`Unsupported OTHER CAIP for transaction building: ${caip}`);
179
+ }
151
180
  break;
152
181
  }
153
182
  default: {
@@ -331,10 +360,79 @@ export class TransactionManager {
331
360
  break;
332
361
  }
333
362
  case 'OTHER': {
334
- if (caip === 'ripple:4109c6f2045fc7eff4cde8f9905d19c2/slip44:144') {
363
+ if (caip.startsWith('solana:')) {
364
+ // Solana signing via keepkey-server REST API
365
+ const solanaSignRequest = {
366
+ addressNList: unsignedTx.addressNList,
367
+ serialized: unsignedTx.serialized,
368
+ fromAddress: unsignedTx.fromAddress,
369
+ toAddress: unsignedTx.toAddress,
370
+ lamports: unsignedTx.lamports,
371
+ blockhash: unsignedTx.blockhash
372
+ };
373
+
374
+ try {
375
+ const response = await fetch('http://localhost:1646/solana/sign-transaction', {
376
+ method: 'POST',
377
+ headers: {
378
+ 'Content-Type': 'application/json',
379
+ },
380
+ body: JSON.stringify(solanaSignRequest)
381
+ });
382
+
383
+ if (!response.ok) {
384
+ const errorData = await response.json();
385
+ throw new Error(`Solana signing failed: ${errorData.error || response.statusText}`);
386
+ }
387
+
388
+ const responseSign = await response.json();
389
+ if (responseSign?.serialized) {
390
+ signedTx = responseSign.serialized;
391
+ } else {
392
+ throw new Error('Solana signing failed - no serialized transaction in response');
393
+ }
394
+ } catch (e: any) {
395
+ console.error(tag, 'Solana signing error:', e);
396
+ throw new Error(`Solana signing failed: ${e.message}`);
397
+ }
398
+ } else if (caip === 'ripple:4109c6f2045fc7eff4cde8f9905d19c2/slip44:144') {
335
399
  let responseSign = await this.keepKeySdk.xrp.xrpSignTransaction(unsignedTx);
336
400
  if (typeof responseSign === 'string') responseSign = JSON.parse(responseSign);
337
401
  signedTx = responseSign.value.signatures[0].serializedTx;
402
+ } else if (caip.startsWith('tron:')) {
403
+ // TRON signing via keepkey-server REST API
404
+ const tronSignRequest = {
405
+ addressNList: unsignedTx.addressNList,
406
+ from: unsignedTx.from,
407
+ to: unsignedTx.to,
408
+ amount: unsignedTx.amountInSun,
409
+ memo: unsignedTx.memo || '',
410
+ };
411
+
412
+ try {
413
+ const response = await fetch('http://localhost:1646/tron/sign-transaction', {
414
+ method: 'POST',
415
+ headers: {
416
+ 'Content-Type': 'application/json',
417
+ },
418
+ body: JSON.stringify(tronSignRequest)
419
+ });
420
+
421
+ if (!response.ok) {
422
+ const errorData = await response.json();
423
+ throw new Error(`TRON signing failed: ${errorData.error || response.statusText}`);
424
+ }
425
+
426
+ const responseSign = await response.json();
427
+ if (responseSign?.serialized) {
428
+ signedTx = responseSign.serialized;
429
+ } else {
430
+ throw new Error('TRON signing failed - no serialized transaction in response');
431
+ }
432
+ } catch (e: any) {
433
+ console.error(tag, 'TRON signing error:', e);
434
+ throw new Error(`TRON signing failed: ${e.message}`);
435
+ }
338
436
  } else {
339
437
  throw new Error(`Unsupported OTHER CAIP: ${caip}`);
340
438
  }
package/src/getPubkey.ts CHANGED
@@ -38,7 +38,9 @@ export const getPubkey = async (networkId: string, path: any, sdk: any, context:
38
38
  'eip155:137': 'EVM',
39
39
  'eip155:*': 'EVM',
40
40
  'ripple:4109c6f2045fc7eff4cde8f9905d19c2': 'XRP',
41
- 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': 'SOLANA', // Solana (not yet supported by KeepKey)
41
+ 'solana:5eykt4usfv8p8njdtrepy1vzqkqzkvdp': 'SOLANA', // Solana
42
+ 'tron:0x2b6653dc': 'TRON', // TRON (Shasta testnet)
43
+ 'ton:-239': 'TON', // TON (testnet)
42
44
  'zcash:main': 'UTXO',
43
45
  };
44
46
 
@@ -73,8 +75,32 @@ export const getPubkey = async (networkId: string, path: any, sdk: any, context:
73
75
  ({ address } = await sdk.address.xrpGetAddress(addressInfo));
74
76
  break;
75
77
  case 'SOLANA':
76
- console.warn(`⚠️ Solana address derivation not yet supported by KeepKey SDK. Skipping ${networkId}`);
77
- return null; // Skip Solana for now until KeepKey SDK supports it
78
+ // Check if SDK has solanaGetAddress method
79
+ if (sdk.address.solanaGetAddress) {
80
+ ({ address } = await sdk.address.solanaGetAddress(addressInfo));
81
+ } else {
82
+ console.warn(`⚠️ Solana address derivation not yet supported by KeepKey SDK. Skipping ${networkId}`);
83
+ return null;
84
+ }
85
+ break;
86
+ case 'TRON':
87
+ // Check if SDK has tronGetAddress method
88
+ if (sdk.address.tronGetAddress) {
89
+ ({ address } = await sdk.address.tronGetAddress(addressInfo));
90
+ } else {
91
+ console.warn(`⚠️ TRON address derivation not yet supported by KeepKey SDK. Skipping ${networkId}`);
92
+ return null;
93
+ }
94
+ break;
95
+ case 'TON':
96
+ // Check if SDK has tonGetAddress method
97
+ if (sdk.address.tonGetAddress) {
98
+ ({ address } = await sdk.address.tonGetAddress(addressInfo));
99
+ } else {
100
+ console.warn(`⚠️ TON address derivation not yet supported by KeepKey SDK. Skipping ${networkId}`);
101
+ return null;
102
+ }
103
+ break;
78
104
  default:
79
105
  throw new Error(`Unsupported network type for networkId: ${networkId}`);
80
106
  }
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { KeepKeySdk } from '@keepkey/keepkey-sdk';
1
+ import { KeepKeySdk } from 'keepkey-vault-sdk';
2
2
  import { caipToNetworkId, networkIdToCaip } from '@pioneer-platform/pioneer-caip';
3
3
  import { Pioneer } from '@pioneer-platform/pioneer-client';
4
4
  import { addressNListToBIP32, getPaths } from '@pioneer-platform/pioneer-coins';
@@ -10,7 +10,7 @@ import { getCharts } from './charts/index.js';
10
10
  //internal
11
11
  import { logger } from './utils/logger.js';
12
12
  import { getPubkey } from './getPubkey.js';
13
- import { optimizedGetPubkeys } from './kkapi-batch-client.js';
13
+ import { optimizedGetPubkeys, checkKkapiHealth } from './kkapi-batch-client.js';
14
14
  import { OfflineClient } from './offline-client.js';
15
15
  import { TransactionManager } from './TransactionManager.js';
16
16
  import { createUnsignedTendermintTx } from './txbuilder/createUnsignedTendermintTx.js';
@@ -301,6 +301,11 @@ export class SDK {
301
301
  const key = getPubkeyKey(pubkey);
302
302
  if (this.pubkeySet.has(key)) return false;
303
303
 
304
+ // PHASE 4: Normalize network IDs to lowercase for consistency
305
+ if (pubkey.networks && Array.isArray(pubkey.networks)) {
306
+ pubkey.networks = pubkey.networks.map((n: string) => n.toLowerCase());
307
+ }
308
+
304
309
  this.pubkeys.push(pubkey);
305
310
  this.pubkeySet.add(key);
306
311
  return true;
@@ -375,6 +380,12 @@ export class SDK {
375
380
 
376
381
  // Update SDK state if we have balances
377
382
  if (allBalances.length > 0) {
383
+ // PHASE 5: Normalize all CAIPs to lowercase for consistency
384
+ allBalances.forEach((balance: any) => {
385
+ if (balance.caip) balance.caip = balance.caip.toLowerCase();
386
+ if (balance.networkId) balance.networkId = balance.networkId.toLowerCase();
387
+ });
388
+
378
389
  this.balances = allBalances;
379
390
  this.events.emit('SET_BALANCES', this.balances);
380
391
  }
@@ -1840,6 +1851,12 @@ export class SDK {
1840
1851
 
1841
1852
  console.log(`⏱️ [PERF] Enrichment completed in ${(performance.now() - enrichStart).toFixed(0)}ms`);
1842
1853
 
1854
+ // PHASE 5: Normalize all CAIPs to lowercase for consistency
1855
+ balances.forEach((balance: any) => {
1856
+ if (balance.caip) balance.caip = balance.caip.toLowerCase();
1857
+ if (balance.networkId) balance.networkId = balance.networkId.toLowerCase();
1858
+ });
1859
+
1843
1860
  // CRITICAL: Deduplicate balances BEFORE setting
1844
1861
  // Merge new balances with existing balances, deduplicating by identifier (caip:pubkey)
1845
1862
  console.log(tag, `⚙️ Merging balances: ${balances.length} new + ${this.balances.length} existing`);
@@ -2061,6 +2078,10 @@ export class SDK {
2061
2078
  }
2062
2079
 
2063
2080
  if (!asset.caip) throw Error('Invalid Asset! missing caip!');
2081
+
2082
+ // PHASE 6: Normalize CAIP to lowercase for consistency
2083
+ asset.caip = asset.caip.toLowerCase();
2084
+
2064
2085
  if (!asset.networkId) asset.networkId = caipToNetworkId(asset.caip);
2065
2086
 
2066
2087
  // Validate pubkeys for network (throws descriptive errors)
@@ -2318,6 +2339,10 @@ export class SDK {
2318
2339
  }
2319
2340
 
2320
2341
  if (!asset.caip) throw Error('Invalid Asset! missing caip!');
2342
+
2343
+ // PHASE 6: Normalize CAIP to lowercase for consistency
2344
+ asset.caip = asset.caip.toLowerCase();
2345
+
2321
2346
  if (!asset.networkId) asset.networkId = caipToNetworkId(asset.caip);
2322
2347
 
2323
2348
  console.log(tag, 'networkId:', asset.networkId);
@@ -32,42 +32,93 @@ export interface KkapiHealthStatus {
32
32
  device_connected: boolean;
33
33
  cached_pubkeys: number;
34
34
  vault_version?: string;
35
+ apiVersion?: number;
36
+ supportedChains?: string[];
35
37
  }
36
38
 
37
39
  /**
38
40
  * Check if kkapi:// vault is available and ready
39
41
  */
40
42
  export async function checkKkapiHealth(baseUrl: string = 'kkapi://'): Promise<KkapiHealthStatus> {
43
+ // Default V1 supported chains (legacy chains without Solana, TRON, TON)
44
+ const V1_SUPPORTED_CHAINS = [
45
+ 'bip122:000000000019d6689c085ae165831e93', // BTC
46
+ 'bip122:000000000000000000651ef99cb9fcbe', // BCH
47
+ 'bip122:000007d91d1254d60e2dd1ae58038307', // DASH
48
+ 'bip122:00000000001a91e3dace36e2be3bf030', // DOGE
49
+ 'bip122:12a765e31ffd4059bada1e25190f6e98', // LTC
50
+ 'bip122:4da631f2ac1bed857bd968c67c913978', // DGB
51
+ 'bip122:00040fe8ec8471911baa1db1266ea15d', // ZEC
52
+ 'eip155:1', // ETH
53
+ 'eip155:137', // MATIC
54
+ 'eip155:8453', // BASE
55
+ 'eip155:56', // BSC
56
+ 'cosmos:cosmoshub-4', // ATOM
57
+ 'cosmos:osmosis-1', // OSMO
58
+ 'cosmos:mayachain-mainnet-v1', // MAYA
59
+ 'cosmos:thorchain-mainnet-v1', // RUNE
60
+ 'ripple:4109c6f2045fc7eff4cde8f9905d19c2', // XRP
61
+ ];
62
+
41
63
  try {
42
64
  const healthResponse = await fetch(`${baseUrl}/api/health`);
65
+
66
+ // If health endpoint doesn't exist (404), assume v1 server (Desktop app)
67
+ if (healthResponse.status === 404) {
68
+ console.log('📡 [VERSION] No /api/health endpoint - assuming v1 server (Desktop app)');
69
+ return {
70
+ available: true,
71
+ device_connected: true,
72
+ cached_pubkeys: 0,
73
+ apiVersion: 1,
74
+ supportedChains: V1_SUPPORTED_CHAINS
75
+ };
76
+ }
77
+
43
78
  if (!healthResponse.ok) {
44
- return { available: false, device_connected: false, cached_pubkeys: 0 };
79
+ return {
80
+ available: false,
81
+ device_connected: false,
82
+ cached_pubkeys: 0,
83
+ apiVersion: 1,
84
+ supportedChains: V1_SUPPORTED_CHAINS
85
+ };
45
86
  }
46
87
 
47
88
  const healthData = await healthResponse.json();
48
- if (healthData.cached_pubkeys !== undefined) {
89
+
90
+ // V2 server returns apiVersion and supportedChains
91
+ if (healthData.apiVersion === 2 && healthData.supportedChains) {
49
92
  return {
50
93
  available: true,
51
94
  device_connected: healthData.device_connected || false,
52
95
  cached_pubkeys: healthData.cached_pubkeys || 0,
53
- vault_version: healthData.version
96
+ vault_version: healthData.version,
97
+ apiVersion: healthData.apiVersion,
98
+ supportedChains: healthData.supportedChains
54
99
  };
55
100
  }
56
101
 
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();
102
+ // V1 server or old format - use default v1 chains
63
103
  return {
64
104
  available: true,
65
- device_connected: true,
66
- cached_pubkeys: cacheData.cached_pubkeys || 0,
67
- vault_version: cacheData.vault_version
105
+ device_connected: healthData.device_connected || false,
106
+ cached_pubkeys: healthData.cached_pubkeys || 0,
107
+ vault_version: healthData.version,
108
+ apiVersion: 1,
109
+ supportedChains: V1_SUPPORTED_CHAINS
68
110
  };
111
+
69
112
  } catch (error: any) {
70
- return { available: false, device_connected: false, cached_pubkeys: 0 };
113
+ // Network error or server unreachable - assume v1 for backward compatibility
114
+ console.log('📡 [VERSION] Health check failed - assuming v1 server');
115
+ return {
116
+ available: false,
117
+ device_connected: false,
118
+ cached_pubkeys: 0,
119
+ apiVersion: 1,
120
+ supportedChains: V1_SUPPORTED_CHAINS
121
+ };
71
122
  }
72
123
  }
73
124
 
@@ -138,6 +189,28 @@ export async function optimizedGetPubkeys(
138
189
 
139
190
  const vaultHealth = await checkKkapiHealth(baseUrl);
140
191
 
192
+ // Filter blockchains based on server's supportedChains
193
+ let supportedBlockchains = blockchains;
194
+ if (vaultHealth.supportedChains && vaultHealth.supportedChains.length > 0) {
195
+ console.log(`📡 [VERSION] Server API v${vaultHealth.apiVersion} supports ${vaultHealth.supportedChains.length} chains`);
196
+
197
+ // Filter blockchains to only include ones the server supports
198
+ const unsupportedBlockchains: string[] = [];
199
+ supportedBlockchains = blockchains.filter(blockchain => {
200
+ // Check if this blockchain is in the server's supported list
201
+ const isSupported = vaultHealth.supportedChains!.includes(blockchain);
202
+ if (!isSupported) {
203
+ unsupportedBlockchains.push(blockchain);
204
+ }
205
+ return isSupported;
206
+ });
207
+
208
+ if (unsupportedBlockchains.length > 0) {
209
+ console.log(`⏭️ [VERSION] Skipping ${unsupportedBlockchains.length} unsupported chains:`, unsupportedBlockchains.join(', '));
210
+ }
211
+ console.log(`✅ [VERSION] Using ${supportedBlockchains.length} supported chains`);
212
+ }
213
+
141
214
  let pubkeys: any[] = [];
142
215
  let remainingPaths: any[] = [];
143
216
  let remainingBlockchains: string[] = [];
@@ -146,15 +219,15 @@ export async function optimizedGetPubkeys(
146
219
  const batchResponse = await batchGetPubkeys(paths, context, baseUrl);
147
220
  pubkeys = batchResponse.pubkeys;
148
221
  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];
222
+
223
+ for (let i = 0; i < supportedBlockchains.length; i++) {
224
+ const blockchain = supportedBlockchains[i];
152
225
  const pathsForChain = paths.filter(path => path.networks && Array.isArray(path.networks) && path.networks.includes(blockchain));
153
-
226
+
154
227
  for (const path of pathsForChain) {
155
228
  const { addressNListToBIP32 } = await import('@pioneer-platform/pioneer-coins');
156
229
  const pathBip32 = addressNListToBIP32(path.addressNList);
157
-
230
+
158
231
  if (!cachedPaths.has(pathBip32)) {
159
232
  remainingPaths.push(path);
160
233
  if (!remainingBlockchains.includes(blockchain)) {
@@ -164,8 +237,12 @@ export async function optimizedGetPubkeys(
164
237
  }
165
238
  }
166
239
  } else {
167
- remainingPaths = paths;
168
- remainingBlockchains = blockchains;
240
+ // Filter paths to only include supported blockchains
241
+ remainingPaths = paths.filter(path =>
242
+ path.networks && Array.isArray(path.networks) &&
243
+ path.networks.some((network: string) => supportedBlockchains.includes(network))
244
+ );
245
+ remainingBlockchains = supportedBlockchains;
169
246
  }
170
247
 
171
248
  if (remainingPaths.length > 0) {
@@ -30,7 +30,12 @@ export const CAIP_TO_COIN_MAP: { [key: string]: string } = {
30
30
  'bip122:00040fe8ec8471911baa1db1266ea15d/slip44:133': 'Zcash',
31
31
  };
32
32
 
33
- export const OTHER_SUPPORT = ['ripple:4109c6f2045fc7eff4cde8f9905d19c2/slip44:144'];
33
+ export const OTHER_SUPPORT = [
34
+ 'ripple:4109c6f2045fc7eff4cde8f9905d19c2/slip44:144', // XRP
35
+ 'solana:5eykt4usfv8p8njdtrepy1vzqkqzkvdp/solana:so11111111111111111111111111111111111111112', // SOL
36
+ 'tron:0x2b6653dc/slip44:195', // TRX
37
+ 'ton:-239/slip44:607', // TON
38
+ ];
34
39
 
35
40
  export const SUPPORTED_CAIPS = {
36
41
  UTXO: UTXO_SUPPORT,
@@ -0,0 +1,132 @@
1
+ /*
2
+ Create Unsigned Solana Transaction
3
+ */
4
+ // @ts-ignore
5
+ import { caipToNetworkId } from '@pioneer-platform/pioneer-caip';
6
+ // @ts-ignore
7
+ import {
8
+ Connection,
9
+ PublicKey,
10
+ Transaction,
11
+ SystemProgram,
12
+ LAMPORTS_PER_SOL,
13
+ } from '@solana/web3.js';
14
+
15
+ const TAG = ' | createUnsignedSolanaTx | ';
16
+
17
+ // Default Solana RPC (could be made configurable)
18
+ const DEFAULT_RPC_URL = 'https://api.mainnet-beta.solana.com';
19
+
20
+ export async function createUnsignedSolanaTx(
21
+ caip: string,
22
+ to: string,
23
+ amount: any,
24
+ memo: string,
25
+ pubkeys: any,
26
+ pioneer: any,
27
+ pubkeyContext: any,
28
+ isMax: boolean,
29
+ ): Promise<any> {
30
+ let tag = TAG + ' | createUnsignedSolanaTx | ';
31
+
32
+ try {
33
+ if (!pioneer) throw new Error('Failed to init! pioneer');
34
+
35
+ // Determine networkId from caip
36
+ const networkId = caipToNetworkId(caip);
37
+
38
+ // Use the passed pubkeyContext directly - it's already been set by Pioneer SDK
39
+ if (!pubkeyContext) {
40
+ throw new Error(`No pubkey context provided for networkId: ${networkId}`);
41
+ }
42
+
43
+ if (!pubkeyContext.networks?.includes(networkId)) {
44
+ throw new Error(
45
+ `Pubkey context is for wrong network. Expected ${networkId}, got ${pubkeyContext.networks}`,
46
+ );
47
+ }
48
+
49
+ console.log(tag, `✅ Using pubkeyContext for network ${networkId}:`, {
50
+ address: pubkeyContext.address,
51
+ });
52
+
53
+ const fromAddress = pubkeyContext.address || pubkeyContext.pubkey;
54
+
55
+ // Get account balance to calculate max amount if needed
56
+ let accountInfo = await pioneer.GetAccountInfo({
57
+ address: fromAddress,
58
+ network: 'solana',
59
+ });
60
+ accountInfo = accountInfo.data;
61
+
62
+ // Calculate amount
63
+ let amountInSol: number;
64
+ if (isMax) {
65
+ // Reserve fee (approximately 0.000005 SOL = 5000 lamports)
66
+ const feeInSol = 0.000005;
67
+ amountInSol = parseFloat(accountInfo.balance) - feeInSol;
68
+ if (amountInSol <= 0) {
69
+ throw new Error('Insufficient balance to cover fee');
70
+ }
71
+ } else {
72
+ amountInSol = parseFloat(amount);
73
+ }
74
+
75
+ // Build transaction using @solana/web3.js
76
+ const connection = new Connection(DEFAULT_RPC_URL, 'confirmed');
77
+
78
+ // Convert to PublicKey objects
79
+ const fromPubkey = new PublicKey(fromAddress);
80
+ const toPubkey = new PublicKey(to);
81
+
82
+ // Convert SOL to lamports
83
+ const lamports = Math.floor(amountInSol * LAMPORTS_PER_SOL);
84
+
85
+ // Get recent blockhash
86
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');
87
+
88
+ // Create transfer instruction
89
+ const transferInstruction = SystemProgram.transfer({
90
+ fromPubkey,
91
+ toPubkey,
92
+ lamports,
93
+ });
94
+
95
+ // Create transaction
96
+ const transaction = new Transaction({
97
+ feePayer: fromPubkey,
98
+ blockhash,
99
+ lastValidBlockHeight,
100
+ }).add(transferInstruction);
101
+
102
+ // Add memo instruction if provided
103
+ if (memo && memo.trim() !== '') {
104
+ // TODO: Add memo instruction
105
+ console.log(tag, 'Memo support not yet implemented');
106
+ }
107
+
108
+ // Serialize transaction message for signing
109
+ const serialized = transaction.serializeMessage();
110
+
111
+ const unsignedTx = {
112
+ transaction,
113
+ serialized: serialized.toString('base64'),
114
+ blockhash,
115
+ lastValidBlockHeight,
116
+ addressNList: pubkeyContext.addressNList || pubkeyContext.addressNListMaster,
117
+ fromAddress,
118
+ toAddress: to,
119
+ amount: amountInSol,
120
+ lamports,
121
+ caip,
122
+ networkId,
123
+ };
124
+
125
+ console.log(tag, '✅ Solana transaction built successfully');
126
+
127
+ return unsignedTx;
128
+ } catch (error) {
129
+ console.error(tag, 'Error:', error);
130
+ throw error;
131
+ }
132
+ }