@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/CHANGELOG.md +18 -0
- package/README.md +72 -0
- package/dist/index.d.mts +10 -5
- package/dist/index.d.ts +10 -5
- package/dist/index.js +32 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +32 -20
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -2
- package/src/constants/chain-config.ts +2 -2
- package/src/contracts/evm/AirdropService.ts +18 -13
- package/src/services/airdrop/index.ts +3 -1
- package/src/test/setup.ts +24 -0
- package/src/types/index.ts +8 -2
- package/src/utils/__tests__/web3.test.ts +326 -0
- package/src/utils/api-client.ts +2 -2
- package/src/utils/utils.ts +8 -3
- package/vitest.config.ts +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sign-global/tokentable-core",
|
|
3
|
-
"version": "1.
|
|
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": "
|
|
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: '
|
|
53
|
-
rpcUrl:
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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)
|
|
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
|
+
});
|
package/src/types/index.ts
CHANGED
|
@@ -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
|
|
123
|
-
value
|
|
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
|
+
});
|
package/src/utils/api-client.ts
CHANGED
|
@@ -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 (
|
|
62
|
+
if (status < 200 || status >= 300 || resData?.success !== true) {
|
|
63
63
|
return Promise.reject(resData);
|
|
64
64
|
}
|
|
65
65
|
return resData.data;
|
package/src/utils/utils.ts
CHANGED
|
@@ -6,11 +6,16 @@ export const getCustomNaNoId = (): string => {
|
|
|
6
6
|
return nanoid();
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
export const safeParseJSON = (str: string):
|
|
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
|
-
|
|
14
|
+
const parsed = JSON.parse(str);
|
|
15
|
+
return parsed !== null && parsed !== undefined ? parsed : defaultValue;
|
|
12
16
|
} catch (error) {
|
|
13
|
-
|
|
17
|
+
console.warn('Failed to parse JSON:', error);
|
|
18
|
+
return defaultValue;
|
|
14
19
|
}
|
|
15
20
|
};
|
|
16
21
|
|
package/vitest.config.ts
ADDED