@sign-global/tokentable-core 1.0.0 → 1.1.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@sign-global/tokentable-core",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -11,6 +11,11 @@
11
11
  "types": "./dist/index.d.ts"
12
12
  }
13
13
  },
14
+ "devDependencies": {
15
+ "vitest": "^1.0.0",
16
+ "@vitest/ui": "^1.0.0",
17
+ "jsdom": "^23.0.0"
18
+ },
14
19
  "dependencies": {
15
20
  "@tonconnect/sdk": "^3.0.6",
16
21
  "viem": "^2.0.0",
@@ -39,6 +44,8 @@
39
44
  "build": "tsup",
40
45
  "build:watch": "tsup --watch",
41
46
  "clean": "rm -rf dist",
42
- "test": "jest"
47
+ "test": "vitest",
48
+ "test:run": "vitest run",
49
+ "test:watch": "vitest --watch"
43
50
  }
44
51
  }
@@ -49,8 +49,8 @@ export const ChainConfig = {
49
49
  rpcUrl: 'https://zetachain-mainnet.g.alchemy.com/v2/1tmk0FrhwTJHXh0XIyae7_SXw1lDCjgs'
50
50
  },
51
51
  [arbitrum.id]: {
52
- contractAddress: '0x759ac3CA33FeeAfB8C41aEbC8687A7Bd849Fc87a',
53
- rpcUrl: arbitrum.rpcUrls.default.http[0]
52
+ contractAddress: '0xeD09c23677D02f4f486E55f0f081cfc114EEa53b',
53
+ rpcUrl: 'https://arb-mainnet.g.alchemy.com/v2/udrqNPSB6i5n5L6QSM31Ng72h_hFOrVT'
54
54
  },
55
55
  [berachainTestnet.id]: {
56
56
  contractAddress: '',
@@ -160,22 +160,27 @@ export class AirdropService {
160
160
  }
161
161
 
162
162
  async getClaimFee(address: string, amount: bigint): Promise<bigint | undefined> {
163
- const feeCollector = await this.feeCollectors(this.options.contractAddress as string);
164
- if (!feeCollector || feeCollector === ZERO_ADDRESS) {
165
- return undefined;
166
- }
167
- const feeClient = this.getFeeClient(feeCollector as string);
168
- const fee = await feeClient.getFee(address, amount);
169
- const version = await this.getVersion();
170
- if (version === AirdropVersionEnum.V3) {
171
- const threshold = await this.getFeelessThreshold();
172
- if (isGreaterThan(Number(amount), Number(threshold))) {
173
- return fee;
174
- } else {
163
+ try {
164
+ const feeCollector = await this.feeCollectors(this.options.contractAddress as string);
165
+ if (!feeCollector || feeCollector === ZERO_ADDRESS) {
175
166
  return undefined;
176
167
  }
168
+ const feeClient = this.getFeeClient(feeCollector as string);
169
+ const fee = await feeClient.getFee(address, amount);
170
+ const version = await this.getVersion();
171
+ if (version === AirdropVersionEnum.V3) {
172
+ const threshold = await this.getFeelessThreshold();
173
+ if (isGreaterThan(Number(amount), Number(threshold))) {
174
+ return fee;
175
+ } else {
176
+ return undefined;
177
+ }
178
+ }
179
+ return fee;
180
+ } catch (error) {
181
+ console.error('Failed to get claim fee:', error);
182
+ throw new Error(`Failed to calculate claim fee: ${error instanceof Error ? error.message : 'Unknown error'}`);
177
183
  }
178
- return fee;
179
184
  }
180
185
 
181
186
  async getKycThreshold() {
@@ -15,7 +15,9 @@ const createApiClient = (baseURL?: string) => {
15
15
  export const getAirdropProject = async (projectId: string, baseURL?: string): Promise<IProject> => {
16
16
  const client = createApiClient(baseURL);
17
17
  const res = await client.get<any>(`/airdrop-open/projects/${projectId}`);
18
- if (!res) return res; // res is null
18
+ if (!res) {
19
+ throw new Error(`Project not found: ${projectId}`);
20
+ }
19
21
  const projectConfig = res?.themeConf;
20
22
  const blockCountries = safeParseJSON(projectConfig?.blockCountries);
21
23
  return {
@@ -0,0 +1,24 @@
1
+ import { vi } from 'vitest';
2
+
3
+ // Mock console.log to avoid noise in tests
4
+ global.console = {
5
+ ...console,
6
+ log: vi.fn(),
7
+ warn: vi.fn(),
8
+ error: vi.fn()
9
+ };
10
+
11
+ // Setup global test environment
12
+ Object.defineProperty(window, 'matchMedia', {
13
+ writable: true,
14
+ value: vi.fn().mockImplementation(query => ({
15
+ matches: false,
16
+ media: query,
17
+ onchange: null,
18
+ addListener: vi.fn(),
19
+ removeListener: vi.fn(),
20
+ addEventListener: vi.fn(),
21
+ removeEventListener: vi.fn(),
22
+ dispatchEvent: vi.fn(),
23
+ })),
24
+ });
@@ -105,6 +105,7 @@ export type IAirdropClaim = {
105
105
  data: string;
106
106
  leaf: string;
107
107
  claimed?: boolean;
108
+ fees: string;
108
109
 
109
110
  claimId?: string;
110
111
 
@@ -118,9 +119,14 @@ export type IAirdropClaim = {
118
119
  project: IProject;
119
120
  };
120
121
 
122
+ export interface IExtraData {
123
+ isLegacy: boolean;
124
+ attestationId: bigint;
125
+ }
126
+
121
127
  export interface IAirdropClaimData extends IAirdropClaim {
122
- extraData: any;
123
- value: bigint;
128
+ extraData?: IExtraData;
129
+ value?: bigint | string;
124
130
  }
125
131
 
126
132
  export type IAirdropSignatureClaim = {
@@ -0,0 +1,326 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { Connection, PublicKey, Transaction, Commitment } from '@solana/web3.js';
3
+ import {
4
+ getAccount,
5
+ getAssociatedTokenAddress,
6
+ createAssociatedTokenAccountInstruction,
7
+ TokenAccountNotFoundError,
8
+ TokenInvalidAccountOwnerError,
9
+ TokenInvalidMintError,
10
+ TokenInvalidOwnerError,
11
+ TOKEN_PROGRAM_ID,
12
+ ASSOCIATED_TOKEN_PROGRAM_ID
13
+ } from '@solana/spl-token';
14
+ import { getOrCreateAssociatedTokenAccount } from '../web3';
15
+
16
+ // Mock @solana/spl-token
17
+ vi.mock('@solana/spl-token', () => ({
18
+ getAccount: vi.fn(),
19
+ getAssociatedTokenAddress: vi.fn(),
20
+ createAssociatedTokenAccountInstruction: vi.fn(),
21
+ TokenAccountNotFoundError: class extends Error {},
22
+ TokenInvalidAccountOwnerError: class extends Error {},
23
+ TokenInvalidMintError: class extends Error {},
24
+ TokenInvalidOwnerError: class extends Error {},
25
+ TOKEN_PROGRAM_ID: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'),
26
+ ASSOCIATED_TOKEN_PROGRAM_ID: new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL')
27
+ }));
28
+
29
+ // Mock @solana/web3.js
30
+ vi.mock('@solana/web3.js', async () => {
31
+ const actual = await vi.importActual('@solana/web3.js');
32
+ return {
33
+ ...actual,
34
+ Connection: vi.fn(),
35
+ PublicKey: actual.PublicKey,
36
+ Transaction: vi.fn()
37
+ };
38
+ });
39
+
40
+ describe('getOrCreateAssociatedTokenAccount', () => {
41
+ let mockConnection: any;
42
+ let mockSignTransaction: any;
43
+ let mockPayer: PublicKey;
44
+ let mockMint: PublicKey;
45
+ let mockOwner: PublicKey;
46
+ let mockAssociatedToken: PublicKey;
47
+ let mockTransaction: any;
48
+
49
+ beforeEach(() => {
50
+ vi.clearAllMocks();
51
+
52
+ // Setup mock objects with valid base58 public keys
53
+ mockPayer = new PublicKey('11111111111111111111111111111112');
54
+ mockMint = new PublicKey('So11111111111111111111111111111111111111112');
55
+ mockOwner = new PublicKey('9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM');
56
+ mockAssociatedToken = new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL');
57
+
58
+ mockConnection = {
59
+ getLatestBlockhash: vi.fn().mockResolvedValue({
60
+ blockhash: 'test-blockhash',
61
+ lastValidBlockHeight: 12345
62
+ }),
63
+ sendRawTransaction: vi.fn().mockResolvedValue('test-signature'),
64
+ confirmTransaction: vi.fn().mockResolvedValue({ value: { err: null } })
65
+ };
66
+
67
+ mockTransaction = {
68
+ add: vi.fn().mockReturnThis(),
69
+ serialize: vi.fn().mockReturnValue(Buffer.from('serialized-transaction')),
70
+ feePayer: undefined,
71
+ recentBlockhash: undefined
72
+ };
73
+
74
+ mockSignTransaction = vi.fn().mockResolvedValue(mockTransaction);
75
+
76
+ // Setup mocks
77
+ vi.mocked(getAssociatedTokenAddress).mockResolvedValue(mockAssociatedToken);
78
+ vi.mocked(Transaction).mockReturnValue(mockTransaction);
79
+ vi.mocked(createAssociatedTokenAccountInstruction).mockReturnValue({} as any);
80
+ });
81
+
82
+ it('should return existing account when account exists', async () => {
83
+ const mockAccount = {
84
+ address: mockAssociatedToken,
85
+ mint: mockMint,
86
+ owner: mockOwner,
87
+ amount: BigInt(1000),
88
+ delegate: null,
89
+ delegatedAmount: BigInt(0),
90
+ isInitialized: true,
91
+ isFrozen: false,
92
+ isNative: false,
93
+ rentExemptReserve: null,
94
+ closeAuthority: null,
95
+ tlvData: Buffer.alloc(0),
96
+ };
97
+
98
+ vi.mocked(getAccount).mockResolvedValue(mockAccount);
99
+
100
+ const result = await getOrCreateAssociatedTokenAccount({
101
+ connection: mockConnection,
102
+ payer: mockPayer,
103
+ mint: mockMint,
104
+ owner: mockOwner,
105
+ signTransaction: mockSignTransaction
106
+ });
107
+
108
+ expect(result).toBe(mockAccount);
109
+ expect(getAssociatedTokenAddress).toHaveBeenCalledWith(
110
+ mockMint,
111
+ mockOwner,
112
+ false,
113
+ TOKEN_PROGRAM_ID,
114
+ ASSOCIATED_TOKEN_PROGRAM_ID
115
+ );
116
+ expect(getAccount).toHaveBeenCalledWith(
117
+ mockConnection,
118
+ mockAssociatedToken,
119
+ undefined,
120
+ TOKEN_PROGRAM_ID
121
+ );
122
+ });
123
+
124
+ it('should create new account when TokenAccountNotFoundError occurs', async () => {
125
+ const mockAccount = {
126
+ address: mockAssociatedToken,
127
+ mint: mockMint,
128
+ owner: mockOwner,
129
+ amount: BigInt(1000),
130
+ delegate: null,
131
+ delegatedAmount: BigInt(0),
132
+ isInitialized: true,
133
+ isFrozen: false,
134
+ isNative: false,
135
+ rentExemptReserve: null,
136
+ closeAuthority: null,
137
+ tlvData: Buffer.alloc(0),
138
+ };
139
+
140
+ // First call throws error, second call returns account
141
+ vi.mocked(getAccount)
142
+ .mockRejectedValueOnce(new TokenAccountNotFoundError())
143
+ .mockResolvedValueOnce(mockAccount);
144
+
145
+ const result = await getOrCreateAssociatedTokenAccount({
146
+ connection: mockConnection,
147
+ payer: mockPayer,
148
+ mint: mockMint,
149
+ owner: mockOwner,
150
+ signTransaction: mockSignTransaction
151
+ });
152
+
153
+ expect(result).toBe(mockAccount);
154
+ expect(createAssociatedTokenAccountInstruction).toHaveBeenCalledWith(
155
+ mockPayer,
156
+ mockAssociatedToken,
157
+ mockOwner,
158
+ mockMint,
159
+ TOKEN_PROGRAM_ID,
160
+ ASSOCIATED_TOKEN_PROGRAM_ID
161
+ );
162
+ expect(mockSignTransaction).toHaveBeenCalledWith(mockTransaction);
163
+ expect(mockConnection.sendRawTransaction).toHaveBeenCalledWith(
164
+ Buffer.from('serialized-transaction')
165
+ );
166
+ expect(mockConnection.confirmTransaction).toHaveBeenCalled();
167
+ });
168
+
169
+ it('should create new account when TokenInvalidAccountOwnerError occurs', async () => {
170
+ const mockAccount = {
171
+ address: mockAssociatedToken,
172
+ mint: mockMint,
173
+ owner: mockOwner,
174
+ amount: BigInt(1000),
175
+ delegate: null,
176
+ delegatedAmount: BigInt(0),
177
+ isInitialized: true,
178
+ isFrozen: false,
179
+ isNative: false,
180
+ rentExemptReserve: null,
181
+ closeAuthority: null,
182
+ tlvData: Buffer.alloc(0),
183
+ };
184
+
185
+ // First call throws error, second call returns account
186
+ vi.mocked(getAccount)
187
+ .mockRejectedValueOnce(new TokenInvalidAccountOwnerError())
188
+ .mockResolvedValueOnce(mockAccount);
189
+
190
+ const result = await getOrCreateAssociatedTokenAccount({
191
+ connection: mockConnection,
192
+ payer: mockPayer,
193
+ mint: mockMint,
194
+ owner: mockOwner,
195
+ signTransaction: mockSignTransaction
196
+ });
197
+
198
+ expect(result).toBe(mockAccount);
199
+ expect(createAssociatedTokenAccountInstruction).toHaveBeenCalled();
200
+ expect(mockSignTransaction).toHaveBeenCalled();
201
+ });
202
+
203
+ it('should throw TokenInvalidMintError when mint does not match', async () => {
204
+ const wrongMint = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
205
+ const wrongAccount = {
206
+ address: mockAssociatedToken,
207
+ mint: wrongMint,
208
+ owner: mockOwner,
209
+ amount: BigInt(1000),
210
+ delegate: null,
211
+ delegatedAmount: BigInt(0),
212
+ isInitialized: true,
213
+ isFrozen: false,
214
+ isNative: false,
215
+ rentExemptReserve: null,
216
+ closeAuthority: null,
217
+ tlvData: Buffer.alloc(0),
218
+ };
219
+
220
+ vi.mocked(getAccount).mockResolvedValue(wrongAccount);
221
+
222
+ await expect(
223
+ getOrCreateAssociatedTokenAccount({
224
+ connection: mockConnection,
225
+ payer: mockPayer,
226
+ mint: mockMint,
227
+ owner: mockOwner,
228
+ signTransaction: mockSignTransaction
229
+ })
230
+ ).rejects.toThrow(TokenInvalidMintError);
231
+ });
232
+
233
+ it('should throw TokenInvalidOwnerError when owner does not match', async () => {
234
+ const wrongOwner = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
235
+ const wrongAccount = {
236
+ address: mockAssociatedToken,
237
+ mint: mockMint,
238
+ owner: wrongOwner,
239
+ amount: BigInt(1000),
240
+ delegate: null,
241
+ delegatedAmount: BigInt(0),
242
+ isInitialized: true,
243
+ isFrozen: false,
244
+ isNative: false,
245
+ rentExemptReserve: null,
246
+ closeAuthority: null,
247
+ tlvData: Buffer.alloc(0),
248
+ };
249
+
250
+ vi.mocked(getAccount).mockResolvedValue(wrongAccount);
251
+
252
+ await expect(
253
+ getOrCreateAssociatedTokenAccount({
254
+ connection: mockConnection,
255
+ payer: mockPayer,
256
+ mint: mockMint,
257
+ owner: mockOwner,
258
+ signTransaction: mockSignTransaction
259
+ })
260
+ ).rejects.toThrow(TokenInvalidOwnerError);
261
+ });
262
+
263
+ it('should handle custom parameters', async () => {
264
+ const customCommitment: Commitment = 'confirmed';
265
+ const customProgramId = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
266
+ const customAssociatedTokenProgramId = new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL');
267
+
268
+ const mockAccount = {
269
+ address: mockAssociatedToken,
270
+ mint: mockMint,
271
+ owner: mockOwner,
272
+ amount: BigInt(1000),
273
+ delegate: null,
274
+ delegatedAmount: BigInt(0),
275
+ isInitialized: true,
276
+ isFrozen: false,
277
+ isNative: false,
278
+ rentExemptReserve: null,
279
+ closeAuthority: null,
280
+ tlvData: Buffer.alloc(0),
281
+ };
282
+
283
+ vi.mocked(getAccount).mockResolvedValue(mockAccount);
284
+
285
+ await getOrCreateAssociatedTokenAccount({
286
+ connection: mockConnection,
287
+ payer: mockPayer,
288
+ mint: mockMint,
289
+ owner: mockOwner,
290
+ signTransaction: mockSignTransaction,
291
+ allowOwnerOffCurve: true,
292
+ commitment: customCommitment,
293
+ programId: customProgramId,
294
+ associatedTokenProgramId: customAssociatedTokenProgramId
295
+ });
296
+
297
+ expect(getAssociatedTokenAddress).toHaveBeenCalledWith(
298
+ mockMint,
299
+ mockOwner,
300
+ true,
301
+ customProgramId,
302
+ customAssociatedTokenProgramId
303
+ );
304
+ expect(getAccount).toHaveBeenCalledWith(
305
+ mockConnection,
306
+ mockAssociatedToken,
307
+ customCommitment,
308
+ customProgramId
309
+ );
310
+ });
311
+
312
+ it('should rethrow unexpected errors', async () => {
313
+ const unexpectedError = new Error('Unexpected error');
314
+ vi.mocked(getAccount).mockRejectedValue(unexpectedError);
315
+
316
+ await expect(
317
+ getOrCreateAssociatedTokenAccount({
318
+ connection: mockConnection,
319
+ payer: mockPayer,
320
+ mint: mockMint,
321
+ owner: mockOwner,
322
+ signTransaction: mockSignTransaction
323
+ })
324
+ ).rejects.toThrow('Unexpected error');
325
+ });
326
+ });
@@ -16,7 +16,7 @@ type ApiResponse = {
16
16
  };
17
17
 
18
18
  export class ApiClient {
19
- constructor(private options?: ApiClientOptions) {}
19
+ constructor(private options?: ApiClientOptions) { }
20
20
 
21
21
  extend(options: ApiClientOptions): ApiClient {
22
22
  return new ApiClient({
@@ -59,7 +59,7 @@ export class ApiClient {
59
59
 
60
60
  const res = await fetch(finalUrl, init);
61
61
  const [status, resData]: [number, ApiResponse] = await Promise.all([res.status, res.json() as any]);
62
- if ((status < 200 && status >= 300) || resData?.success !== true) {
62
+ if (status < 200 || status >= 300 || resData?.success !== true) {
63
63
  return Promise.reject(resData);
64
64
  }
65
65
  return resData.data;
@@ -6,11 +6,16 @@ export const getCustomNaNoId = (): string => {
6
6
  return nanoid();
7
7
  };
8
8
 
9
- export const safeParseJSON = (str: string): any => {
9
+ export const safeParseJSON = <T = any>(str: string | null | undefined, defaultValue: T = {} as T): T => {
10
+ if (!str || typeof str !== 'string') {
11
+ return defaultValue;
12
+ }
10
13
  try {
11
- return JSON.parse(str);
14
+ const parsed = JSON.parse(str);
15
+ return parsed !== null && parsed !== undefined ? parsed : defaultValue;
12
16
  } catch (error) {
13
- return {};
17
+ console.warn('Failed to parse JSON:', error);
18
+ return defaultValue;
14
19
  }
15
20
  };
16
21
 
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'jsdom',
6
+ globals: true,
7
+ setupFiles: ['./src/test/setup.ts']
8
+ },
9
+ resolve: {
10
+ alias: {
11
+ '@': './src'
12
+ }
13
+ }
14
+ });