@everstake/wallet-sdk-hysp-solana 1.4.0 → 1.5.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/dist/index.d.mts CHANGED
@@ -88,6 +88,10 @@ declare const ERROR_MESSAGES: {
88
88
  GET_BALANCE_ERROR: string;
89
89
  VAULT_LOAD_ERROR: string;
90
90
  VAULT_NOT_FOUND_ERROR: string;
91
+ TX_TOO_LARGE: string;
92
+ MEMO_REQUIRED_ERROR: string;
93
+ MEMO_TOO_LONG_ERROR: string;
94
+ MEMO_INVALID_CHARACTERS_ERROR: string;
91
95
  };
92
96
 
93
97
  /**
@@ -106,6 +110,7 @@ type VaultsMap = {
106
110
  [K in SupportedToken]: VaultInfo;
107
111
  };
108
112
  declare const VAULTS: VaultsMap;
113
+ declare const MAX_TRANSACTION_SIZE = 1232;
109
114
 
110
115
  /**
111
116
  * Copyright (c) 2025, Everstake.
@@ -170,6 +175,10 @@ declare class HyspSolana extends Blockchain {
170
175
  GET_BALANCE_ERROR: string;
171
176
  VAULT_LOAD_ERROR: string;
172
177
  VAULT_NOT_FOUND_ERROR: string;
178
+ TX_TOO_LARGE: string;
179
+ MEMO_REQUIRED_ERROR: string;
180
+ MEMO_TOO_LONG_ERROR: string;
181
+ MEMO_INVALID_CHARACTERS_ERROR: string;
173
182
  };
174
183
  protected ORIGINAL_ERROR_MESSAGES: {
175
184
  INITIALIZATION_ERROR: string;
@@ -179,6 +188,10 @@ declare class HyspSolana extends Blockchain {
179
188
  GET_BALANCE_ERROR: string;
180
189
  VAULT_LOAD_ERROR: string;
181
190
  VAULT_NOT_FOUND_ERROR: string;
191
+ TX_TOO_LARGE: string;
192
+ MEMO_REQUIRED_ERROR: string;
193
+ MEMO_TOO_LONG_ERROR: string;
194
+ MEMO_INVALID_CHARACTERS_ERROR: string;
182
195
  };
183
196
  private connection;
184
197
  private vault;
@@ -282,4 +295,4 @@ declare class HyspSolana extends Blockchain {
282
295
  private buildTx;
283
296
  }
284
297
 
285
- export { type ApiResponse, Blockchain, ERROR_MESSAGES, HyspSolana, type Params, type ShareToken, type SupportedToken, VAULTS, type VaultContracts, type VaultInfo, type VaultMeta, type VaultsMap, WalletSDKError };
298
+ export { type ApiResponse, Blockchain, ERROR_MESSAGES, HyspSolana, MAX_TRANSACTION_SIZE, type Params, type ShareToken, type SupportedToken, VAULTS, type VaultContracts, type VaultInfo, type VaultMeta, type VaultsMap, WalletSDKError };
package/dist/index.d.ts CHANGED
@@ -88,6 +88,10 @@ declare const ERROR_MESSAGES: {
88
88
  GET_BALANCE_ERROR: string;
89
89
  VAULT_LOAD_ERROR: string;
90
90
  VAULT_NOT_FOUND_ERROR: string;
91
+ TX_TOO_LARGE: string;
92
+ MEMO_REQUIRED_ERROR: string;
93
+ MEMO_TOO_LONG_ERROR: string;
94
+ MEMO_INVALID_CHARACTERS_ERROR: string;
91
95
  };
92
96
 
93
97
  /**
@@ -106,6 +110,7 @@ type VaultsMap = {
106
110
  [K in SupportedToken]: VaultInfo;
107
111
  };
108
112
  declare const VAULTS: VaultsMap;
113
+ declare const MAX_TRANSACTION_SIZE = 1232;
109
114
 
110
115
  /**
111
116
  * Copyright (c) 2025, Everstake.
@@ -170,6 +175,10 @@ declare class HyspSolana extends Blockchain {
170
175
  GET_BALANCE_ERROR: string;
171
176
  VAULT_LOAD_ERROR: string;
172
177
  VAULT_NOT_FOUND_ERROR: string;
178
+ TX_TOO_LARGE: string;
179
+ MEMO_REQUIRED_ERROR: string;
180
+ MEMO_TOO_LONG_ERROR: string;
181
+ MEMO_INVALID_CHARACTERS_ERROR: string;
173
182
  };
174
183
  protected ORIGINAL_ERROR_MESSAGES: {
175
184
  INITIALIZATION_ERROR: string;
@@ -179,6 +188,10 @@ declare class HyspSolana extends Blockchain {
179
188
  GET_BALANCE_ERROR: string;
180
189
  VAULT_LOAD_ERROR: string;
181
190
  VAULT_NOT_FOUND_ERROR: string;
191
+ TX_TOO_LARGE: string;
192
+ MEMO_REQUIRED_ERROR: string;
193
+ MEMO_TOO_LONG_ERROR: string;
194
+ MEMO_INVALID_CHARACTERS_ERROR: string;
182
195
  };
183
196
  private connection;
184
197
  private vault;
@@ -282,4 +295,4 @@ declare class HyspSolana extends Blockchain {
282
295
  private buildTx;
283
296
  }
284
297
 
285
- export { type ApiResponse, Blockchain, ERROR_MESSAGES, HyspSolana, type Params, type ShareToken, type SupportedToken, VAULTS, type VaultContracts, type VaultInfo, type VaultMeta, type VaultsMap, WalletSDKError };
298
+ export { type ApiResponse, Blockchain, ERROR_MESSAGES, HyspSolana, MAX_TRANSACTION_SIZE, type Params, type ShareToken, type SupportedToken, VAULTS, type VaultContracts, type VaultInfo, type VaultMeta, type VaultsMap, WalletSDKError };
package/dist/index.js CHANGED
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  Blockchain: () => Blockchain,
24
24
  ERROR_MESSAGES: () => ERROR_MESSAGES,
25
25
  HyspSolana: () => HyspSolana,
26
+ MAX_TRANSACTION_SIZE: () => MAX_TRANSACTION_SIZE,
26
27
  VAULTS: () => VAULTS,
27
28
  WalletSDKError: () => WalletSDKError
28
29
  });
@@ -126,7 +127,11 @@ var ERROR_MESSAGES = {
126
127
  GET_SHARES_ERROR: "An error occurred while fetching vault balance",
127
128
  GET_BALANCE_ERROR: "An error occurred while fetching user token balance",
128
129
  VAULT_LOAD_ERROR: "An error occurred while loading vault info",
129
- VAULT_NOT_FOUND_ERROR: "Vault not found for token: {0}"
130
+ VAULT_NOT_FOUND_ERROR: "Vault not found for token: {0}",
131
+ TX_TOO_LARGE: "Transaction exceeds the maximum size limit of 1232 bytes",
132
+ MEMO_REQUIRED_ERROR: "Memo is required. Please contact us to get your referrer ID.",
133
+ MEMO_TOO_LONG_ERROR: 'Invalid memo: "{0}". Must be max 64 characters',
134
+ MEMO_INVALID_CHARACTERS_ERROR: 'Invalid memo: "{0}". Must contain only [A-Za-z0-9:_-] characters'
130
135
  };
131
136
 
132
137
  // src/constants/index.ts
@@ -140,6 +145,7 @@ var VAULTS = {
140
145
  shareTokenDecimals: 6
141
146
  }
142
147
  };
148
+ var MAX_TRANSACTION_SIZE = 1232;
143
149
 
144
150
  // src/hysp.ts
145
151
  var HyspSolana = class extends Blockchain {
@@ -400,37 +406,29 @@ var HyspSolana = class extends Blockchain {
400
406
  * @returns Memo instruction
401
407
  */
402
408
  processMemo(memo) {
403
- let processedMemo;
404
409
  if (!memo || memo.trim() === "") {
405
- processedMemo = "SDK";
410
+ throw this.throwError("MEMO_REQUIRED_ERROR");
411
+ }
412
+ const trimmedMemo = memo.trim();
413
+ let processedMemo;
414
+ if (!trimmedMemo.startsWith("SDK")) {
415
+ processedMemo = `SDK:${trimmedMemo}`;
406
416
  } else {
407
- const trimmedMemo = memo.trim();
408
- if (trimmedMemo === "SDK") {
409
- processedMemo = trimmedMemo;
410
- } else if (!trimmedMemo.startsWith("SDK:")) {
411
- processedMemo = `SDK:${trimmedMemo}`;
412
- } else {
413
- processedMemo = trimmedMemo;
414
- }
417
+ processedMemo = trimmedMemo;
415
418
  }
416
419
  if (processedMemo.length > 64) {
417
- throw new Error(
418
- `Invalid memo: "${processedMemo}". Must be max 64 characters`
419
- );
420
+ throw this.throwError("MEMO_TOO_LONG_ERROR", processedMemo);
420
421
  }
421
422
  const validPattern = /^[A-Za-z0-9:_-]*$/;
422
423
  if (!validPattern.test(processedMemo)) {
423
- throw new Error(
424
- `Invalid memo: "${processedMemo}". Must contain only [A-Za-z0-9:_-] characters`
425
- );
424
+ throw this.throwError("MEMO_INVALID_CHARACTERS_ERROR", processedMemo);
426
425
  }
427
426
  return (0, import_memo.getAddMemoInstruction)({ memo: processedMemo });
428
427
  }
429
428
  async buildTx(sender, instructions, params, lookupTableAddresses) {
430
- let transactionMessage = (0, import_kit2.pipe)(
431
- (0, import_kit2.createTransactionMessage)({ version: 0 }),
432
- (tx) => (0, import_kit2.setTransactionMessageFeePayer)((0, import_kit2.address)(sender), tx)
433
- );
429
+ let transactionMessage = (0, import_kit2.createTransactionMessage)({
430
+ version: 0
431
+ });
434
432
  if (params?.computeUnitLimit !== void 0 && params?.computeUnitLimit > 0) {
435
433
  const unitLimitInstruction = (0, import_compute_budget.getSetComputeUnitLimitInstruction)({
436
434
  /** Transaction compute unit limit used for prioritization fees. */
@@ -482,11 +480,21 @@ var HyspSolana = class extends Blockchain {
482
480
  fetchedTables
483
481
  );
484
482
  }
483
+ const txWithFeePayer = (0, import_kit2.setTransactionMessageFeePayer)(
484
+ (0, import_kit2.address)(sender),
485
+ transactionMessage
486
+ );
485
487
  const finalLatestBlockhash = params?.finalLatestBlockhash || (await this.connection.getLatestBlockhash().send()).value;
486
488
  const txMessageWithBlockhashLifetime = (0, import_kit2.setTransactionMessageLifetimeUsingBlockhash)(
487
489
  finalLatestBlockhash,
488
- transactionMessage
490
+ txWithFeePayer
489
491
  );
492
+ const compiledTx = (0, import_kit2.compileTransaction)(txMessageWithBlockhashLifetime);
493
+ const serializedTx = (0, import_kit2.getTransactionEncoder)().encode(compiledTx);
494
+ const txSize = serializedTx.length;
495
+ if (txSize > MAX_TRANSACTION_SIZE) {
496
+ throw this.throwError("TX_TOO_LARGE");
497
+ }
490
498
  return txMessageWithBlockhashLifetime;
491
499
  }
492
500
  };
@@ -495,6 +503,7 @@ var HyspSolana = class extends Blockchain {
495
503
  Blockchain,
496
504
  ERROR_MESSAGES,
497
505
  HyspSolana,
506
+ MAX_TRANSACTION_SIZE,
498
507
  VAULTS,
499
508
  WalletSDKError
500
509
  });
package/dist/index.mjs CHANGED
@@ -3,14 +3,15 @@ import {
3
3
  createSolanaRpc,
4
4
  createNoopSigner,
5
5
  address as address2,
6
- pipe,
7
6
  createTransactionMessage,
8
7
  setTransactionMessageFeePayer,
9
8
  setTransactionMessageLifetimeUsingBlockhash,
10
9
  appendTransactionMessageInstruction,
11
10
  prependTransactionMessageInstruction,
12
11
  compressTransactionMessageUsingAddressLookupTables,
13
- fetchAddressesForLookupTables
12
+ fetchAddressesForLookupTables,
13
+ compileTransaction,
14
+ getTransactionEncoder
14
15
  } from "@solana/kit";
15
16
  import {
16
17
  getSetComputeUnitLimitInstruction,
@@ -111,7 +112,11 @@ var ERROR_MESSAGES = {
111
112
  GET_SHARES_ERROR: "An error occurred while fetching vault balance",
112
113
  GET_BALANCE_ERROR: "An error occurred while fetching user token balance",
113
114
  VAULT_LOAD_ERROR: "An error occurred while loading vault info",
114
- VAULT_NOT_FOUND_ERROR: "Vault not found for token: {0}"
115
+ VAULT_NOT_FOUND_ERROR: "Vault not found for token: {0}",
116
+ TX_TOO_LARGE: "Transaction exceeds the maximum size limit of 1232 bytes",
117
+ MEMO_REQUIRED_ERROR: "Memo is required. Please contact us to get your referrer ID.",
118
+ MEMO_TOO_LONG_ERROR: 'Invalid memo: "{0}". Must be max 64 characters',
119
+ MEMO_INVALID_CHARACTERS_ERROR: 'Invalid memo: "{0}". Must contain only [A-Za-z0-9:_-] characters'
115
120
  };
116
121
 
117
122
  // src/constants/index.ts
@@ -125,6 +130,7 @@ var VAULTS = {
125
130
  shareTokenDecimals: 6
126
131
  }
127
132
  };
133
+ var MAX_TRANSACTION_SIZE = 1232;
128
134
 
129
135
  // src/hysp.ts
130
136
  var HyspSolana = class extends Blockchain {
@@ -385,37 +391,29 @@ var HyspSolana = class extends Blockchain {
385
391
  * @returns Memo instruction
386
392
  */
387
393
  processMemo(memo) {
388
- let processedMemo;
389
394
  if (!memo || memo.trim() === "") {
390
- processedMemo = "SDK";
395
+ throw this.throwError("MEMO_REQUIRED_ERROR");
396
+ }
397
+ const trimmedMemo = memo.trim();
398
+ let processedMemo;
399
+ if (!trimmedMemo.startsWith("SDK")) {
400
+ processedMemo = `SDK:${trimmedMemo}`;
391
401
  } else {
392
- const trimmedMemo = memo.trim();
393
- if (trimmedMemo === "SDK") {
394
- processedMemo = trimmedMemo;
395
- } else if (!trimmedMemo.startsWith("SDK:")) {
396
- processedMemo = `SDK:${trimmedMemo}`;
397
- } else {
398
- processedMemo = trimmedMemo;
399
- }
402
+ processedMemo = trimmedMemo;
400
403
  }
401
404
  if (processedMemo.length > 64) {
402
- throw new Error(
403
- `Invalid memo: "${processedMemo}". Must be max 64 characters`
404
- );
405
+ throw this.throwError("MEMO_TOO_LONG_ERROR", processedMemo);
405
406
  }
406
407
  const validPattern = /^[A-Za-z0-9:_-]*$/;
407
408
  if (!validPattern.test(processedMemo)) {
408
- throw new Error(
409
- `Invalid memo: "${processedMemo}". Must contain only [A-Za-z0-9:_-] characters`
410
- );
409
+ throw this.throwError("MEMO_INVALID_CHARACTERS_ERROR", processedMemo);
411
410
  }
412
411
  return getAddMemoInstruction({ memo: processedMemo });
413
412
  }
414
413
  async buildTx(sender, instructions, params, lookupTableAddresses) {
415
- let transactionMessage = pipe(
416
- createTransactionMessage({ version: 0 }),
417
- (tx) => setTransactionMessageFeePayer(address2(sender), tx)
418
- );
414
+ let transactionMessage = createTransactionMessage({
415
+ version: 0
416
+ });
419
417
  if (params?.computeUnitLimit !== void 0 && params?.computeUnitLimit > 0) {
420
418
  const unitLimitInstruction = getSetComputeUnitLimitInstruction({
421
419
  /** Transaction compute unit limit used for prioritization fees. */
@@ -467,11 +465,21 @@ var HyspSolana = class extends Blockchain {
467
465
  fetchedTables
468
466
  );
469
467
  }
468
+ const txWithFeePayer = setTransactionMessageFeePayer(
469
+ address2(sender),
470
+ transactionMessage
471
+ );
470
472
  const finalLatestBlockhash = params?.finalLatestBlockhash || (await this.connection.getLatestBlockhash().send()).value;
471
473
  const txMessageWithBlockhashLifetime = setTransactionMessageLifetimeUsingBlockhash(
472
474
  finalLatestBlockhash,
473
- transactionMessage
475
+ txWithFeePayer
474
476
  );
477
+ const compiledTx = compileTransaction(txMessageWithBlockhashLifetime);
478
+ const serializedTx = getTransactionEncoder().encode(compiledTx);
479
+ const txSize = serializedTx.length;
480
+ if (txSize > MAX_TRANSACTION_SIZE) {
481
+ throw this.throwError("TX_TOO_LARGE");
482
+ }
475
483
  return txMessageWithBlockhashLifetime;
476
484
  }
477
485
  };
@@ -479,6 +487,7 @@ export {
479
487
  Blockchain,
480
488
  ERROR_MESSAGES,
481
489
  HyspSolana,
490
+ MAX_TRANSACTION_SIZE,
482
491
  VAULTS,
483
492
  WalletSDKError
484
493
  };
package/jest.config.ts CHANGED
@@ -9,10 +9,11 @@ const config: Config = {
9
9
  collectCoverageFrom: [
10
10
  'src/**/*.ts',
11
11
  '!src/**/*.d.ts',
12
- '!src/**/index.ts',
12
+ '!src/index.ts',
13
13
  ],
14
14
  coverageDirectory: 'coverage',
15
15
  coverageReporters: ['text', 'lcov', 'html'],
16
+ testTimeout: 60000,
16
17
  };
17
18
 
18
19
  export default config;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everstake/wallet-sdk-hysp-solana",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "HYSP Solana - Everstake Wallet SDK",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "sideEffects": false,
18
18
  "scripts": {
19
- "build": "tsup src/index.ts --format cjs,esm --dts",
19
+ "build": "pnpm run prebuild && tsup src/index.ts --format cjs,esm --dts",
20
20
  "type-check": "tsc",
21
21
  "lint": "eslint 'src/**/*.{ts,tsx}'",
22
22
  "prettier": "prettier --write 'src/**/*.{ts,tsx}'",
@@ -73,7 +73,17 @@
73
73
  "js-yaml@>=4.0.0 <4.1.1": ">=4.1.1",
74
74
  "glob@>=10.2.0 <10.5.0": ">=10.5.0",
75
75
  "js-yaml": ">=4.1.1",
76
- "glob": ">=10.5.0"
76
+ "glob": ">=10.5.0",
77
+ "lodash@>=4.0.0 <=4.17.22": ">=4.17.23",
78
+ "diff@>=4.0.0 <4.0.4": ">=4.0.4",
79
+ "@isaacs/brace-expansion@<=5.0.0": ">=5.0.1",
80
+ "axios@>=1.0.0 <=1.13.4": ">=1.13.5",
81
+ "bn.js@>=5.0.0 <5.2.3": ">=5.2.3",
82
+ "rollup@>=4.0.0 <4.59.0": ">=4.59.0",
83
+ "minimatch@<10.2.3": ">=10.2.3",
84
+ "flatted@<3.4.0": ">=3.4.0",
85
+ "flatted@<=3.4.1": ">=3.4.2",
86
+ "picomatch@<4.0.4": ">=4.0.4"
77
87
  }
78
88
  }
79
89
  }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Copyright (c) 2025, Everstake.
3
+ * Licensed under the BSD-3-Clause License. See LICENSE file for details.
4
+ */
5
+
6
+ import { HyspSolana } from '..';
7
+ import { SupportedToken } from '../constants';
8
+ import { WalletSDKError } from '../../../utils';
9
+
10
+ // Create a test class to access protected methods
11
+ class TestableHyspSolana extends HyspSolana {
12
+ public testProcessMemo(memo?: string) {
13
+ return this.processMemo(memo);
14
+ }
15
+ }
16
+
17
+ describe('HyspSolana processMemo method', () => {
18
+ const hyspSolana: TestableHyspSolana = new TestableHyspSolana(
19
+ 'USDC' as SupportedToken,
20
+ 'https://api.mainnet-beta.solana.com',
21
+ );
22
+
23
+ describe('Error cases', () => {
24
+ it('should throw MEMO_REQUIRED_ERROR when memo is undefined', () => {
25
+ expect(() => hyspSolana.testProcessMemo(undefined)).toThrow(
26
+ WalletSDKError,
27
+ );
28
+ expect(() => hyspSolana.testProcessMemo(undefined)).toThrow(
29
+ 'Memo is required. Please contact us to get your referrer ID.',
30
+ );
31
+ });
32
+
33
+ it('should throw MEMO_REQUIRED_ERROR when memo is empty string', () => {
34
+ expect(() => hyspSolana.testProcessMemo('')).toThrow(WalletSDKError);
35
+ expect(() => hyspSolana.testProcessMemo('')).toThrow(
36
+ 'Memo is required. Please contact us to get your referrer ID.',
37
+ );
38
+ });
39
+
40
+ it('should throw MEMO_REQUIRED_ERROR when memo is only whitespace', () => {
41
+ expect(() => hyspSolana.testProcessMemo(' ')).toThrow(WalletSDKError);
42
+ expect(() => hyspSolana.testProcessMemo(' ')).toThrow(
43
+ 'Memo is required. Please contact us to get your referrer ID.',
44
+ );
45
+ });
46
+
47
+ it('should throw MEMO_TOO_LONG_ERROR when processed memo exceeds 64 characters', () => {
48
+ const longMemo = 'a'.repeat(70);
49
+ expect(() => hyspSolana.testProcessMemo(longMemo)).toThrow(
50
+ WalletSDKError,
51
+ );
52
+ expect(() => hyspSolana.testProcessMemo(longMemo)).toThrow(
53
+ 'Must be max 64 characters',
54
+ );
55
+ });
56
+
57
+ it('should throw MEMO_INVALID_CHARACTERS_ERROR when memo contains invalid characters', () => {
58
+ expect(() => hyspSolana.testProcessMemo('test@domain')).toThrow(
59
+ WalletSDKError,
60
+ );
61
+ expect(() => hyspSolana.testProcessMemo('test@domain')).toThrow(
62
+ 'Must contain only [A-Za-z0-9:_-] characters',
63
+ );
64
+ });
65
+
66
+ it('should throw MEMO_INVALID_CHARACTERS_ERROR when memo contains spaces', () => {
67
+ expect(() => hyspSolana.testProcessMemo('test memo')).toThrow(
68
+ WalletSDKError,
69
+ );
70
+ expect(() => hyspSolana.testProcessMemo('test memo')).toThrow(
71
+ 'Must contain only [A-Za-z0-9:_-] characters',
72
+ );
73
+ });
74
+
75
+ it('should throw MEMO_INVALID_CHARACTERS_ERROR when memo contains special characters', () => {
76
+ expect(() => hyspSolana.testProcessMemo('test!memo#')).toThrow(
77
+ WalletSDKError,
78
+ );
79
+ expect(() => hyspSolana.testProcessMemo('test!memo#')).toThrow(
80
+ 'Must contain only [A-Za-z0-9:_-] characters',
81
+ );
82
+ });
83
+ });
84
+
85
+ describe('Success cases', () => {
86
+ it('should prepend SDK: to memo without SDK prefix', () => {
87
+ const result = hyspSolana.testProcessMemo('acme:pilotQ1:prod:v1');
88
+
89
+ expect(new TextDecoder().decode(result.data)).toBe(
90
+ 'SDK:acme:pilotQ1:prod:v1',
91
+ );
92
+ });
93
+
94
+ it('should keep memo unchanged when it starts with SDK (but not SDK:)', () => {
95
+ const result = hyspSolana.testProcessMemo('SDKtest');
96
+
97
+ expect(new TextDecoder().decode(result.data)).toBe('SDKtest');
98
+ });
99
+
100
+ it('should keep memo unchanged when it starts with SDK:', () => {
101
+ const result = hyspSolana.testProcessMemo('SDK:bankxyz::prod:v1');
102
+
103
+ expect(new TextDecoder().decode(result.data)).toBe(
104
+ 'SDK:bankxyz::prod:v1',
105
+ );
106
+ });
107
+
108
+ it('should handle simple referrer ID', () => {
109
+ const result = hyspSolana.testProcessMemo('referrer123');
110
+
111
+ expect(new TextDecoder().decode(result.data)).toBe('SDK:referrer123');
112
+ });
113
+
114
+ it('should handle referrer ID with underscores and hyphens', () => {
115
+ const result = hyspSolana.testProcessMemo('test_ref-id_123');
116
+
117
+ expect(new TextDecoder().decode(result.data)).toBe('SDK:test_ref-id_123');
118
+ });
119
+
120
+ it('should handle referrer ID with colons', () => {
121
+ const result = hyspSolana.testProcessMemo('org:team:user');
122
+
123
+ expect(new TextDecoder().decode(result.data)).toBe('SDK:org:team:user');
124
+ });
125
+
126
+ it('should handle maximum allowed length without SDK prefix', () => {
127
+ const maxMemo = 'a'.repeat(59); // 59 + 'SDK:' = 63 chars (under limit)
128
+ const result = hyspSolana.testProcessMemo(maxMemo);
129
+
130
+ expect(new TextDecoder().decode(result.data)).toBe(`SDK:${maxMemo}`);
131
+ });
132
+
133
+ it('should handle maximum allowed length with SDK prefix', () => {
134
+ const maxMemo = 'SDK:' + 'a'.repeat(60); // exactly 64 chars
135
+ const result = hyspSolana.testProcessMemo(maxMemo);
136
+
137
+ expect(new TextDecoder().decode(result.data)).toBe(maxMemo);
138
+ });
139
+
140
+ it('should trim whitespace from memo', () => {
141
+ const result = hyspSolana.testProcessMemo(' referrer123 ');
142
+
143
+ expect(new TextDecoder().decode(result.data)).toBe('SDK:referrer123');
144
+ });
145
+ });
146
+ });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Copyright (c) 2025, Everstake.
3
+ * Licensed under the BSD-3-Clause License. See LICENSE file for details.
4
+ */
5
+
6
+ import { address, Instruction } from '@solana/kit';
7
+ import { Decimal } from 'decimal.js';
8
+ import { HyspSolana } from '../hysp';
9
+
10
+ declare global {
11
+ interface BigInt {
12
+ toJSON(): number;
13
+ }
14
+ }
15
+
16
+ BigInt.prototype.toJSON = function () {
17
+ return Number(this);
18
+ };
19
+
20
+ const PLACEHOLDER_USER_ADDRESS = address(
21
+ 'Crc3qfV8QqdmGXNWSQnc8REsrDjViiSZMyjn6jsE51kj',
22
+ );
23
+ const PLACEHOLDER_TARGET_ADDRESS = address('11111111111111111111111111111113');
24
+ const PLACEHOLDER_SHARES_AMOUNT = new Decimal('0.1');
25
+
26
+ function createAdditionalInstruction(): Instruction {
27
+ return {
28
+ programAddress: address('11111111111111111111111111111112'),
29
+ accounts: [
30
+ { address: PLACEHOLDER_USER_ADDRESS, role: 0 },
31
+ { address: PLACEHOLDER_TARGET_ADDRESS, role: 0 },
32
+ ],
33
+ data: new Uint8Array(
34
+ Array.from({ length: 12 }, () => Math.floor(Math.random() * 256)),
35
+ ),
36
+ } as unknown as Instruction;
37
+ }
38
+
39
+ describe('HyspSolana Transaction Size Tests', () => {
40
+ let hyspSolana: HyspSolana;
41
+
42
+ beforeAll(() => {
43
+ hyspSolana = new HyspSolana('USDC');
44
+ });
45
+
46
+ describe('Transaction Size Limits', () => {
47
+ it('should fetch user shares successfully', async () => {
48
+ const userShares = await hyspSolana.getUserShares(
49
+ PLACEHOLDER_USER_ADDRESS,
50
+ );
51
+
52
+ console.log(`User shares: ${userShares.result.toString()}`);
53
+
54
+ expect(userShares.result).toBeDefined();
55
+ expect(userShares.result).toBeInstanceOf(Decimal);
56
+
57
+ if (userShares.result.lt(PLACEHOLDER_SHARES_AMOUNT)) {
58
+ console.warn(
59
+ 'Note: User has insufficient shares for withdrawal tests.',
60
+ );
61
+ }
62
+ }, 30000);
63
+
64
+ it('should create a normal withdraw transaction within size limits', async () => {
65
+ const result = await hyspSolana.withdraw(
66
+ PLACEHOLDER_USER_ADDRESS,
67
+ PLACEHOLDER_SHARES_AMOUNT,
68
+ );
69
+
70
+ expect(result.result).toBeDefined();
71
+ }, 30000);
72
+
73
+ it('should fail when transaction exceeds size limits with too many instructions', async () => {
74
+ const additionalInstructions = [];
75
+
76
+ for (let i = 0; i < 100; i++) {
77
+ additionalInstructions.push(createAdditionalInstruction());
78
+ }
79
+
80
+ await expect(
81
+ hyspSolana.withdraw(
82
+ PLACEHOLDER_USER_ADDRESS,
83
+ PLACEHOLDER_SHARES_AMOUNT,
84
+ {
85
+ afterInstructions: additionalInstructions,
86
+ },
87
+ ),
88
+ ).rejects.toThrow(
89
+ 'Transaction exceeds the maximum size limit of 1232 bytes',
90
+ );
91
+ }, 30000);
92
+
93
+ it('should handle edge case with some additional instructions', async () => {
94
+ const moderateInstructions = [];
95
+
96
+ for (let i = 0; i < 10; i++) {
97
+ moderateInstructions.push(createAdditionalInstruction());
98
+ }
99
+
100
+ try {
101
+ const result = await hyspSolana.withdraw(
102
+ PLACEHOLDER_USER_ADDRESS,
103
+ PLACEHOLDER_SHARES_AMOUNT,
104
+ {
105
+ afterInstructions: moderateInstructions,
106
+ },
107
+ );
108
+
109
+ expect(result.result).toBeDefined();
110
+ } catch (error: unknown) {
111
+ const errorMessage = (error as Error).message;
112
+ expect(errorMessage).toContain(
113
+ 'Transaction exceeds the maximum size limit',
114
+ );
115
+ }
116
+ }, 30000);
117
+ });
118
+ });
@@ -11,4 +11,10 @@ export const ERROR_MESSAGES = {
11
11
  GET_BALANCE_ERROR: 'An error occurred while fetching user token balance',
12
12
  VAULT_LOAD_ERROR: 'An error occurred while loading vault info',
13
13
  VAULT_NOT_FOUND_ERROR: 'Vault not found for token: {0}',
14
+ TX_TOO_LARGE: 'Transaction exceeds the maximum size limit of 1232 bytes',
15
+ MEMO_REQUIRED_ERROR:
16
+ 'Memo is required. Please contact us to get your referrer ID.',
17
+ MEMO_TOO_LONG_ERROR: 'Invalid memo: "{0}". Must be max 64 characters',
18
+ MEMO_INVALID_CHARACTERS_ERROR:
19
+ 'Invalid memo: "{0}". Must contain only [A-Za-z0-9:_-] characters',
14
20
  };
@@ -28,4 +28,6 @@ export const VAULTS: VaultsMap = {
28
28
  },
29
29
  };
30
30
 
31
+ export const MAX_TRANSACTION_SIZE = 1232;
32
+
31
33
  export * from './errors';
package/src/hysp.ts CHANGED
@@ -8,7 +8,6 @@ import {
8
8
  Address,
9
9
  createNoopSigner,
10
10
  address,
11
- pipe,
12
11
  createTransactionMessage,
13
12
  setTransactionMessageFeePayer,
14
13
  setTransactionMessageLifetimeUsingBlockhash,
@@ -21,6 +20,8 @@ import {
21
20
  Rpc,
22
21
  SolanaRpcApi,
23
22
  Instruction,
23
+ compileTransaction,
24
+ getTransactionEncoder,
24
25
  } from '@solana/kit';
25
26
 
26
27
  import {
@@ -35,6 +36,7 @@ import { KaminoVault, VaultHoldings, APY } from '@kamino-finance/klend-sdk';
35
36
  import { Decimal } from 'decimal.js';
36
37
  import { Blockchain } from '../../utils';
37
38
  import { ERROR_MESSAGES } from './constants/errors';
39
+ import { MAX_TRANSACTION_SIZE } from './constants';
38
40
  import { VAULTS, SupportedToken, VaultInfo } from './constants';
39
41
  import { ApiResponse, Params, VaultMeta } from './types';
40
42
 
@@ -349,33 +351,29 @@ export class HyspSolana extends Blockchain {
349
351
  * @returns Memo instruction
350
352
  */
351
353
  protected processMemo(memo?: string): Instruction {
352
- // Process memo text
353
- let processedMemo: string;
354
+ // Check if memo is empty
354
355
  if (!memo || memo.trim() === '') {
355
- processedMemo = 'SDK';
356
+ throw this.throwError('MEMO_REQUIRED_ERROR');
357
+ }
358
+
359
+ const trimmedMemo = memo.trim();
360
+ let processedMemo: string;
361
+
362
+ // If memo doesn't start with 'SDK', prepend 'SDK:'
363
+ if (!trimmedMemo.startsWith('SDK')) {
364
+ processedMemo = `SDK:${trimmedMemo}`;
356
365
  } else {
357
- const trimmedMemo = memo.trim();
358
- if (trimmedMemo === 'SDK') {
359
- processedMemo = trimmedMemo;
360
- } else if (!trimmedMemo.startsWith('SDK:')) {
361
- processedMemo = `SDK:${trimmedMemo}`;
362
- } else {
363
- processedMemo = trimmedMemo;
364
- }
366
+ processedMemo = trimmedMemo;
365
367
  }
366
368
 
367
369
  // Validate memo
368
370
  if (processedMemo.length > 64) {
369
- throw new Error(
370
- `Invalid memo: "${processedMemo}". Must be max 64 characters`,
371
- );
371
+ throw this.throwError('MEMO_TOO_LONG_ERROR', processedMemo);
372
372
  }
373
373
 
374
374
  const validPattern = /^[A-Za-z0-9:_-]*$/;
375
375
  if (!validPattern.test(processedMemo)) {
376
- throw new Error(
377
- `Invalid memo: "${processedMemo}". Must contain only [A-Za-z0-9:_-] characters`,
378
- );
376
+ throw this.throwError('MEMO_INVALID_CHARACTERS_ERROR', processedMemo);
379
377
  }
380
378
 
381
379
  return getAddMemoInstruction({ memo: processedMemo });
@@ -387,10 +385,9 @@ export class HyspSolana extends Blockchain {
387
385
  params?: Params,
388
386
  lookupTableAddresses?: Address[],
389
387
  ): Promise<TransactionMessageWithLifetime> {
390
- let transactionMessage: TransactionMessage = pipe(
391
- createTransactionMessage({ version: 0 }),
392
- (tx) => setTransactionMessageFeePayer(address(sender), tx),
393
- );
388
+ let transactionMessage: TransactionMessage = createTransactionMessage({
389
+ version: 0,
390
+ });
394
391
 
395
392
  if (
396
393
  params?.computeUnitLimit !== undefined &&
@@ -457,6 +454,11 @@ export class HyspSolana extends Blockchain {
457
454
  );
458
455
  }
459
456
 
457
+ const txWithFeePayer = setTransactionMessageFeePayer(
458
+ address(sender),
459
+ transactionMessage,
460
+ );
461
+
460
462
  const finalLatestBlockhash =
461
463
  params?.finalLatestBlockhash ||
462
464
  (await this.connection.getLatestBlockhash().send()).value;
@@ -464,9 +466,17 @@ export class HyspSolana extends Blockchain {
464
466
  const txMessageWithBlockhashLifetime =
465
467
  setTransactionMessageLifetimeUsingBlockhash(
466
468
  finalLatestBlockhash,
467
- transactionMessage,
469
+ txWithFeePayer,
468
470
  );
469
471
 
472
+ const compiledTx = compileTransaction(txMessageWithBlockhashLifetime);
473
+ const serializedTx = getTransactionEncoder().encode(compiledTx);
474
+ const txSize = serializedTx.length;
475
+
476
+ if (txSize > MAX_TRANSACTION_SIZE) {
477
+ throw this.throwError('TX_TOO_LARGE');
478
+ }
479
+
470
480
  return txMessageWithBlockhashLifetime;
471
481
  }
472
482
  }
@@ -1,41 +0,0 @@
1
- /**
2
- * Copyright (c) 2025, Everstake.
3
- * Licensed under the BSD-3-Clause License. See LICENSE file for details.
4
- */
5
-
6
- import { HyspSolana } from '..';
7
- import { SupportedToken } from '../constants';
8
-
9
- // Create a test class to access protected methods
10
- class TestableHyspSolana extends HyspSolana {
11
- public testProcessMemo(memo?: string) {
12
- return this.processMemo(memo);
13
- }
14
- }
15
-
16
- describe('HyspSolana processMemo method', () => {
17
- const hyspSolana: TestableHyspSolana = new TestableHyspSolana(
18
- 'USDC' as SupportedToken,
19
- 'https://api.mainnet-beta.solana.com',
20
- );
21
-
22
- it('should return SDK when memo is empty', () => {
23
- const result = hyspSolana.testProcessMemo('');
24
-
25
- expect(new TextDecoder().decode(result.data)).toBe('SDK');
26
- });
27
-
28
- it('should prepend SDK: to memo without SDK prefix', () => {
29
- const result = hyspSolana.testProcessMemo('acme:pilotQ1:prod:v1');
30
-
31
- expect(new TextDecoder().decode(result.data)).toBe(
32
- 'SDK:acme:pilotQ1:prod:v1',
33
- );
34
- });
35
-
36
- it('should keep memo unchanged when it has SDK prefix', () => {
37
- const result = hyspSolana.testProcessMemo('SDK:bankxyz::prod:v1');
38
-
39
- expect(new TextDecoder().decode(result.data)).toBe('SDK:bankxyz::prod:v1');
40
- });
41
- });