@solana/kora 0.0.0 → 0.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/README.md +60 -0
- package/dist/src/client.d.ts +187 -0
- package/dist/src/client.js +287 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/types/index.d.ts +383 -0
- package/dist/src/types/index.js +1 -0
- package/dist/src/utils/transaction.d.ts +8 -0
- package/dist/src/utils/transaction.js +32 -0
- package/dist/test/auth-setup.d.ts +1 -0
- package/dist/test/auth-setup.js +51 -0
- package/dist/test/integration.test.d.ts +1 -0
- package/dist/test/integration.test.js +307 -0
- package/dist/test/setup.d.ts +26 -0
- package/dist/test/setup.js +229 -0
- package/dist/test/unit.test.d.ts +1 -0
- package/dist/test/unit.test.js +552 -0
- package/package.json +59 -7
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import setupTestSuite from './setup.js';
|
|
2
|
+
import { runAuthenticationTests } from './auth-setup.js';
|
|
3
|
+
import { generateKeyPairSigner, getBase64EncodedWireTransaction, getBase64Encoder, getTransactionDecoder, signTransaction, } from '@solana/kit';
|
|
4
|
+
import { ASSOCIATED_TOKEN_PROGRAM_ADDRESS, findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
|
|
5
|
+
function transactionFromBase64(base64) {
|
|
6
|
+
const encoder = getBase64Encoder();
|
|
7
|
+
const decoder = getTransactionDecoder();
|
|
8
|
+
const messageBytes = encoder.encode(base64);
|
|
9
|
+
return decoder.decode(messageBytes);
|
|
10
|
+
}
|
|
11
|
+
const AUTH_ENABLED = process.env.ENABLE_AUTH === 'true';
|
|
12
|
+
const KORA_SIGNER_TYPE = process.env.KORA_SIGNER_TYPE || 'memory';
|
|
13
|
+
describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without auth'} | signer type: ${KORA_SIGNER_TYPE})`, () => {
|
|
14
|
+
let client;
|
|
15
|
+
let testWallet;
|
|
16
|
+
let testWalletAddress;
|
|
17
|
+
let destinationAddress;
|
|
18
|
+
let usdcMint;
|
|
19
|
+
let koraAddress;
|
|
20
|
+
let koraRpcUrl;
|
|
21
|
+
beforeAll(async () => {
|
|
22
|
+
const testSuite = await setupTestSuite();
|
|
23
|
+
client = testSuite.koraClient;
|
|
24
|
+
testWallet = testSuite.testWallet;
|
|
25
|
+
testWalletAddress = testWallet.address;
|
|
26
|
+
destinationAddress = testSuite.destinationAddress;
|
|
27
|
+
usdcMint = testSuite.usdcMint;
|
|
28
|
+
koraAddress = testSuite.koraAddress;
|
|
29
|
+
koraRpcUrl = testSuite.koraRpcUrl;
|
|
30
|
+
}, 90000); // allow adequate time for airdrops and token initialization
|
|
31
|
+
// Run authentication tests only when auth is enabled
|
|
32
|
+
if (AUTH_ENABLED) {
|
|
33
|
+
runAuthenticationTests();
|
|
34
|
+
}
|
|
35
|
+
describe('Configuration and Setup', () => {
|
|
36
|
+
it('should get config', async () => {
|
|
37
|
+
const config = await client.getConfig();
|
|
38
|
+
expect(config).toBeDefined();
|
|
39
|
+
expect(config.fee_payers).toBeDefined();
|
|
40
|
+
expect(Array.isArray(config.fee_payers)).toBe(true);
|
|
41
|
+
expect(config.fee_payers.length).toBeGreaterThan(0);
|
|
42
|
+
expect(config.validation_config).toBeDefined();
|
|
43
|
+
expect(config.validation_config.allowed_programs).toBeDefined();
|
|
44
|
+
expect(config.validation_config.allowed_tokens).toBeDefined();
|
|
45
|
+
expect(config.validation_config.max_allowed_lamports).toBeDefined();
|
|
46
|
+
expect(config.validation_config.max_signatures).toBeDefined();
|
|
47
|
+
expect(config.validation_config.price_source).toBeDefined();
|
|
48
|
+
expect(config.validation_config.price).toBeDefined();
|
|
49
|
+
expect(config.validation_config.price.type).toBeDefined();
|
|
50
|
+
expect(config.validation_config.fee_payer_policy).toBeDefined();
|
|
51
|
+
// System policy
|
|
52
|
+
expect(config.validation_config.fee_payer_policy.system).toBeDefined();
|
|
53
|
+
expect(config.validation_config.fee_payer_policy.system.allow_transfer).toBeDefined();
|
|
54
|
+
expect(config.validation_config.fee_payer_policy.system.allow_assign).toBeDefined();
|
|
55
|
+
expect(config.validation_config.fee_payer_policy.system.allow_create_account).toBeDefined();
|
|
56
|
+
expect(config.validation_config.fee_payer_policy.system.allow_allocate).toBeDefined();
|
|
57
|
+
// System nonce policy
|
|
58
|
+
expect(config.validation_config.fee_payer_policy.system.nonce).toBeDefined();
|
|
59
|
+
expect(config.validation_config.fee_payer_policy.system.nonce.allow_initialize).toBeDefined();
|
|
60
|
+
expect(config.validation_config.fee_payer_policy.system.nonce.allow_advance).toBeDefined();
|
|
61
|
+
expect(config.validation_config.fee_payer_policy.system.nonce.allow_authorize).toBeDefined();
|
|
62
|
+
expect(config.validation_config.fee_payer_policy.system.nonce.allow_withdraw).toBeDefined();
|
|
63
|
+
// SPL token policy
|
|
64
|
+
expect(config.validation_config.fee_payer_policy.spl_token).toBeDefined();
|
|
65
|
+
expect(config.validation_config.fee_payer_policy.spl_token.allow_transfer).toBeDefined();
|
|
66
|
+
expect(config.validation_config.fee_payer_policy.spl_token.allow_burn).toBeDefined();
|
|
67
|
+
expect(config.validation_config.fee_payer_policy.spl_token.allow_close_account).toBeDefined();
|
|
68
|
+
expect(config.validation_config.fee_payer_policy.spl_token.allow_approve).toBeDefined();
|
|
69
|
+
expect(config.validation_config.fee_payer_policy.spl_token.allow_revoke).toBeDefined();
|
|
70
|
+
expect(config.validation_config.fee_payer_policy.spl_token.allow_set_authority).toBeDefined();
|
|
71
|
+
expect(config.validation_config.fee_payer_policy.spl_token.allow_mint_to).toBeDefined();
|
|
72
|
+
expect(config.validation_config.fee_payer_policy.spl_token.allow_freeze_account).toBeDefined();
|
|
73
|
+
expect(config.validation_config.fee_payer_policy.spl_token.allow_thaw_account).toBeDefined();
|
|
74
|
+
// Token2022 policy
|
|
75
|
+
expect(config.validation_config.fee_payer_policy.token_2022).toBeDefined();
|
|
76
|
+
expect(config.validation_config.fee_payer_policy.token_2022.allow_transfer).toBeDefined();
|
|
77
|
+
expect(config.validation_config.fee_payer_policy.token_2022.allow_burn).toBeDefined();
|
|
78
|
+
expect(config.validation_config.fee_payer_policy.token_2022.allow_close_account).toBeDefined();
|
|
79
|
+
expect(config.validation_config.fee_payer_policy.token_2022.allow_approve).toBeDefined();
|
|
80
|
+
expect(config.validation_config.fee_payer_policy.token_2022.allow_revoke).toBeDefined();
|
|
81
|
+
expect(config.validation_config.fee_payer_policy.token_2022.allow_set_authority).toBeDefined();
|
|
82
|
+
expect(config.validation_config.fee_payer_policy.token_2022.allow_mint_to).toBeDefined();
|
|
83
|
+
expect(config.validation_config.fee_payer_policy.token_2022.allow_freeze_account).toBeDefined();
|
|
84
|
+
expect(config.validation_config.fee_payer_policy.token_2022.allow_thaw_account).toBeDefined();
|
|
85
|
+
expect(config.enabled_methods).toBeDefined();
|
|
86
|
+
expect(config.enabled_methods.liveness).toBeDefined();
|
|
87
|
+
expect(config.enabled_methods.estimate_transaction_fee).toBeDefined();
|
|
88
|
+
expect(config.enabled_methods.get_supported_tokens).toBeDefined();
|
|
89
|
+
expect(config.enabled_methods.sign_transaction).toBeDefined();
|
|
90
|
+
expect(config.enabled_methods.sign_and_send_transaction).toBeDefined();
|
|
91
|
+
expect(config.enabled_methods.transfer_transaction).toBeDefined();
|
|
92
|
+
expect(config.enabled_methods.get_blockhash).toBeDefined();
|
|
93
|
+
expect(config.enabled_methods.get_config).toBeDefined();
|
|
94
|
+
});
|
|
95
|
+
it('should get payer signer', async () => {
|
|
96
|
+
const { signer_address, payment_address } = await client.getPayerSigner();
|
|
97
|
+
expect(signer_address).toBeDefined();
|
|
98
|
+
expect(payment_address).toBeDefined();
|
|
99
|
+
});
|
|
100
|
+
it('should get supported tokens', async () => {
|
|
101
|
+
const { tokens } = await client.getSupportedTokens();
|
|
102
|
+
expect(Array.isArray(tokens)).toBe(true);
|
|
103
|
+
expect(tokens.length).toBeGreaterThan(0);
|
|
104
|
+
expect(tokens).toContain(usdcMint); // USDC should be supported
|
|
105
|
+
});
|
|
106
|
+
it('should get blockhash', async () => {
|
|
107
|
+
const { blockhash } = await client.getBlockhash();
|
|
108
|
+
expect(blockhash).toBeDefined();
|
|
109
|
+
expect(typeof blockhash).toBe('string');
|
|
110
|
+
expect(blockhash.length).toBeGreaterThanOrEqual(43);
|
|
111
|
+
expect(blockhash.length).toBeLessThanOrEqual(44); // Base58 encoded hash length
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe('Transaction Operations', () => {
|
|
115
|
+
it('should create transfer transaction', async () => {
|
|
116
|
+
const request = {
|
|
117
|
+
amount: 1000000, // 1 USDC
|
|
118
|
+
token: usdcMint,
|
|
119
|
+
source: testWalletAddress,
|
|
120
|
+
destination: destinationAddress,
|
|
121
|
+
};
|
|
122
|
+
const response = await client.transferTransaction(request);
|
|
123
|
+
expect(response).toBeDefined();
|
|
124
|
+
expect(response.transaction).toBeDefined();
|
|
125
|
+
expect(response.blockhash).toBeDefined();
|
|
126
|
+
expect(response.message).toBeDefined();
|
|
127
|
+
expect(response.instructions).toBeDefined();
|
|
128
|
+
// since setup created ATA for destination, we should not expect ata instruction, only transfer instruction
|
|
129
|
+
expect(response.instructions?.length).toBe(1);
|
|
130
|
+
expect(response.instructions?.[0].programAddress).toBe(TOKEN_PROGRAM_ADDRESS);
|
|
131
|
+
});
|
|
132
|
+
it('should create transfer transaction to address with no ATA', async () => {
|
|
133
|
+
const randomDestination = await generateKeyPairSigner();
|
|
134
|
+
const request = {
|
|
135
|
+
amount: 1000000, // 1 USDC
|
|
136
|
+
token: usdcMint,
|
|
137
|
+
source: testWalletAddress,
|
|
138
|
+
destination: randomDestination.address,
|
|
139
|
+
};
|
|
140
|
+
const response = await client.transferTransaction(request);
|
|
141
|
+
expect(response).toBeDefined();
|
|
142
|
+
expect(response.transaction).toBeDefined();
|
|
143
|
+
expect(response.blockhash).toBeDefined();
|
|
144
|
+
expect(response.message).toBeDefined();
|
|
145
|
+
expect(response.instructions).toBeDefined();
|
|
146
|
+
// since setup created ATA for destination, we should not expect ata instruction, only transfer instruction
|
|
147
|
+
expect(response.instructions?.length).toBe(2);
|
|
148
|
+
expect(response.instructions?.[0].programAddress).toBe(ASSOCIATED_TOKEN_PROGRAM_ADDRESS);
|
|
149
|
+
expect(response.instructions?.[1].programAddress).toBe(TOKEN_PROGRAM_ADDRESS);
|
|
150
|
+
});
|
|
151
|
+
it('should estimate transaction fee', async () => {
|
|
152
|
+
// First create a transaction
|
|
153
|
+
const transferRequest = {
|
|
154
|
+
amount: 1000000,
|
|
155
|
+
token: usdcMint,
|
|
156
|
+
source: testWalletAddress,
|
|
157
|
+
destination: testWalletAddress,
|
|
158
|
+
};
|
|
159
|
+
const { transaction } = await client.transferTransaction(transferRequest);
|
|
160
|
+
const fee = await client.estimateTransactionFee({ transaction, fee_token: usdcMint });
|
|
161
|
+
expect(fee).toBeDefined();
|
|
162
|
+
expect(typeof fee.fee_in_lamports).toBe('number');
|
|
163
|
+
expect(fee.fee_in_lamports).toBeGreaterThan(0);
|
|
164
|
+
expect(typeof fee.fee_in_token).toBe('number');
|
|
165
|
+
expect(fee.fee_in_token).toBeGreaterThan(0);
|
|
166
|
+
});
|
|
167
|
+
it('should sign transaction', async () => {
|
|
168
|
+
const config = await client.getConfig();
|
|
169
|
+
const paymentAddress = config.fee_payers[0];
|
|
170
|
+
const transferRequest = {
|
|
171
|
+
amount: 1000000,
|
|
172
|
+
token: usdcMint,
|
|
173
|
+
source: testWalletAddress,
|
|
174
|
+
destination: paymentAddress,
|
|
175
|
+
};
|
|
176
|
+
const { transaction } = await client.transferTransaction(transferRequest);
|
|
177
|
+
const signResult = await client.signTransaction({
|
|
178
|
+
transaction,
|
|
179
|
+
});
|
|
180
|
+
expect(signResult).toBeDefined();
|
|
181
|
+
expect(signResult.signed_transaction).toBeDefined();
|
|
182
|
+
});
|
|
183
|
+
it('should sign and send transaction', async () => {
|
|
184
|
+
const config = await client.getConfig();
|
|
185
|
+
const paymentAddress = config.fee_payers[0];
|
|
186
|
+
const transferRequest = {
|
|
187
|
+
amount: 1000000,
|
|
188
|
+
token: usdcMint,
|
|
189
|
+
source: testWalletAddress,
|
|
190
|
+
destination: paymentAddress,
|
|
191
|
+
};
|
|
192
|
+
const { transaction: transactionString } = await client.transferTransaction(transferRequest);
|
|
193
|
+
const transaction = transactionFromBase64(transactionString);
|
|
194
|
+
// Sign transaction with test wallet before sending
|
|
195
|
+
const signedTransaction = await signTransaction([testWallet.keyPair], transaction);
|
|
196
|
+
const base64SignedTransaction = getBase64EncodedWireTransaction(signedTransaction);
|
|
197
|
+
const signResult = await client.signAndSendTransaction({
|
|
198
|
+
transaction: base64SignedTransaction,
|
|
199
|
+
});
|
|
200
|
+
expect(signResult).toBeDefined();
|
|
201
|
+
expect(signResult.signed_transaction).toBeDefined();
|
|
202
|
+
});
|
|
203
|
+
it('should get payment instruction', async () => {
|
|
204
|
+
const transferRequest = {
|
|
205
|
+
amount: 1000000,
|
|
206
|
+
token: usdcMint,
|
|
207
|
+
source: testWalletAddress,
|
|
208
|
+
destination: destinationAddress,
|
|
209
|
+
};
|
|
210
|
+
const [expectedSenderAta] = await findAssociatedTokenPda({
|
|
211
|
+
owner: testWalletAddress,
|
|
212
|
+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
|
|
213
|
+
mint: usdcMint,
|
|
214
|
+
});
|
|
215
|
+
const [koraAta] = await findAssociatedTokenPda({
|
|
216
|
+
owner: koraAddress,
|
|
217
|
+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
|
|
218
|
+
mint: usdcMint,
|
|
219
|
+
});
|
|
220
|
+
const { transaction } = await client.transferTransaction(transferRequest);
|
|
221
|
+
const { payment_instruction, payment_amount, payment_token, payment_address, signer_address, original_transaction, } = await client.getPaymentInstruction({
|
|
222
|
+
transaction,
|
|
223
|
+
fee_token: usdcMint,
|
|
224
|
+
source_wallet: testWalletAddress,
|
|
225
|
+
});
|
|
226
|
+
expect(payment_instruction).toBeDefined();
|
|
227
|
+
expect(payment_instruction.programAddress).toBe(TOKEN_PROGRAM_ADDRESS);
|
|
228
|
+
expect(payment_instruction.accounts?.[0].address).toBe(expectedSenderAta);
|
|
229
|
+
expect(payment_instruction.accounts?.[1].address).toBe(koraAta);
|
|
230
|
+
expect(payment_instruction.accounts?.[2].address).toBe(testWalletAddress);
|
|
231
|
+
// todo math to verify payment amount
|
|
232
|
+
// expect(payment_amount).toBe(1000000);
|
|
233
|
+
expect(payment_token).toBe(usdcMint);
|
|
234
|
+
expect(payment_address).toBe(koraAddress);
|
|
235
|
+
expect(signer_address).toBe(koraAddress);
|
|
236
|
+
expect(original_transaction).toBe(transaction);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
describe('Error Handling', () => {
|
|
240
|
+
it('should handle invalid token address', async () => {
|
|
241
|
+
const request = {
|
|
242
|
+
amount: 1000000,
|
|
243
|
+
token: 'InvalidTokenAddress',
|
|
244
|
+
source: testWalletAddress,
|
|
245
|
+
destination: destinationAddress,
|
|
246
|
+
};
|
|
247
|
+
await expect(client.transferTransaction(request)).rejects.toThrow();
|
|
248
|
+
});
|
|
249
|
+
it('should handle invalid amount', async () => {
|
|
250
|
+
const request = {
|
|
251
|
+
amount: -1, // Invalid amount
|
|
252
|
+
token: usdcMint,
|
|
253
|
+
source: testWalletAddress,
|
|
254
|
+
destination: destinationAddress,
|
|
255
|
+
};
|
|
256
|
+
await expect(client.transferTransaction(request)).rejects.toThrow();
|
|
257
|
+
});
|
|
258
|
+
it('should handle invalid transaction for signing', async () => {
|
|
259
|
+
await expect(client.signTransaction({
|
|
260
|
+
transaction: 'invalid_transaction',
|
|
261
|
+
})).rejects.toThrow();
|
|
262
|
+
});
|
|
263
|
+
it('should handle invalid transaction for fee estimation', async () => {
|
|
264
|
+
await expect(client.estimateTransactionFee({ transaction: 'invalid_transaction', fee_token: usdcMint })).rejects.toThrow();
|
|
265
|
+
});
|
|
266
|
+
it('should handle non-allowed token for fee payment', async () => {
|
|
267
|
+
const transferRequest = {
|
|
268
|
+
amount: 1000000,
|
|
269
|
+
token: usdcMint,
|
|
270
|
+
source: testWalletAddress,
|
|
271
|
+
destination: destinationAddress,
|
|
272
|
+
};
|
|
273
|
+
// TODO: API has an error. this endpoint should verify the provided fee token is supported
|
|
274
|
+
const { transaction } = await client.transferTransaction(transferRequest);
|
|
275
|
+
const fee = await client.estimateTransactionFee({ transaction, fee_token: usdcMint });
|
|
276
|
+
expect(fee).toBeDefined();
|
|
277
|
+
expect(typeof fee.fee_in_lamports).toBe('number');
|
|
278
|
+
expect(fee.fee_in_lamports).toBeGreaterThan(0);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
describe('End-to-End Flows', () => {
|
|
282
|
+
it('should handle transfer and sign flow', async () => {
|
|
283
|
+
const config = await client.getConfig();
|
|
284
|
+
const paymentAddress = config.fee_payers[0];
|
|
285
|
+
const request = {
|
|
286
|
+
amount: 1000000,
|
|
287
|
+
token: usdcMint,
|
|
288
|
+
source: testWalletAddress,
|
|
289
|
+
destination: paymentAddress,
|
|
290
|
+
};
|
|
291
|
+
// Create and sign the transaction
|
|
292
|
+
const { transaction } = await client.transferTransaction(request);
|
|
293
|
+
const signResult = await client.signTransaction({ transaction });
|
|
294
|
+
expect(signResult.signed_transaction).toBeDefined();
|
|
295
|
+
});
|
|
296
|
+
it('should reject transaction with non-allowed token', async () => {
|
|
297
|
+
const invalidTokenMint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; // Mainnet USDC mint
|
|
298
|
+
const request = {
|
|
299
|
+
amount: 1000000,
|
|
300
|
+
token: invalidTokenMint,
|
|
301
|
+
source: testWalletAddress,
|
|
302
|
+
destination: destinationAddress,
|
|
303
|
+
};
|
|
304
|
+
await expect(client.transferTransaction(request)).rejects.toThrow();
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Commitment, KeyPairSigner, Address } from '@solana/kit';
|
|
2
|
+
import { KoraClient } from '../src/index.js';
|
|
3
|
+
interface TestSuite {
|
|
4
|
+
koraClient: KoraClient;
|
|
5
|
+
koraRpcUrl: string;
|
|
6
|
+
testWallet: KeyPairSigner<string>;
|
|
7
|
+
usdcMint: Address<string>;
|
|
8
|
+
destinationAddress: Address<string>;
|
|
9
|
+
koraAddress: Address<string>;
|
|
10
|
+
}
|
|
11
|
+
export declare function loadEnvironmentVariables(): {
|
|
12
|
+
koraRpcUrl: string;
|
|
13
|
+
koraAddress: Address<string>;
|
|
14
|
+
koraSignerType: string;
|
|
15
|
+
commitment: Commitment;
|
|
16
|
+
tokenDecimals: number;
|
|
17
|
+
tokenDropAmount: number;
|
|
18
|
+
solDropAmount: bigint;
|
|
19
|
+
solanaRpcUrl: string;
|
|
20
|
+
solanaWsUrl: string;
|
|
21
|
+
testWalletSecret: string;
|
|
22
|
+
testUsdcMintSecret: string;
|
|
23
|
+
destinationAddress: Address<string>;
|
|
24
|
+
};
|
|
25
|
+
declare function setupTestSuite(): Promise<TestSuite>;
|
|
26
|
+
export default setupTestSuite;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { getCreateAccountInstruction } from '@solana-program/system';
|
|
2
|
+
import { findAssociatedTokenPda, getCreateAssociatedTokenIdempotentInstructionAsync, getInitializeMintInstruction, getMintSize, getMintToInstruction, TOKEN_PROGRAM_ADDRESS, } from '@solana-program/token';
|
|
3
|
+
import { airdropFactory, createSolanaRpc, createSolanaRpcSubscriptions, lamports, sendAndConfirmTransactionFactory, pipe, createTransactionMessage, setTransactionMessageLifetimeUsingBlockhash, setTransactionMessageFeePayerSigner, appendTransactionMessageInstructions, signTransactionMessageWithSigners, getSignatureFromTransaction, assertIsAddress, createKeyPairSignerFromBytes, getBase58Encoder, } from '@solana/kit';
|
|
4
|
+
import { updateOrAppendSetComputeUnitLimitInstruction, updateOrAppendSetComputeUnitPriceInstruction, MAX_COMPUTE_UNIT_LIMIT, } from '@solana-program/compute-budget';
|
|
5
|
+
import { config } from 'dotenv';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { KoraClient } from '../src/index.js';
|
|
8
|
+
config({ path: path.resolve(process.cwd(), '.env') });
|
|
9
|
+
const DEFAULTS = {
|
|
10
|
+
DECIMALS: 6,
|
|
11
|
+
TOKEN_DROP_AMOUNT: 100_000,
|
|
12
|
+
KORA_RPC_URL: 'http://localhost:8080/',
|
|
13
|
+
SOLANA_RPC_URL: 'http://127.0.0.1:8899',
|
|
14
|
+
SOLANA_WS_URL: 'ws://127.0.0.1:8900',
|
|
15
|
+
COMMITMENT: 'processed',
|
|
16
|
+
SOL_DROP_AMOUNT: 1_000_000_000,
|
|
17
|
+
// DO NOT USE THESE KEYPAIRS IN PRODUCTION, TESTING KEYPAIRS ONLY
|
|
18
|
+
KORA_ADDRESS: '7AqpcUvgJ7Kh1VmJZ44rWp2XDow33vswo9VK9VqpPU2d', // Make sure this matches the kora-rpc signer address on launch (root .env)
|
|
19
|
+
SENDER_SECRET: 'tzgfgSWTE3KUA6qfRoFYLaSfJm59uUeZRDy4ybMrLn1JV2drA1mftiaEcVFvq1Lok6h6EX2C4Y9kSKLvQWyMpS5', // HhA5j2rRiPbMrpF2ZD36r69FyZf3zWmEHRNSZbbNdVjf
|
|
20
|
+
TEST_USDC_MINT_SECRET: '59kKmXphL5UJANqpFFjtH17emEq3oRNmYsx6a3P3vSGJRmhMgVdzH77bkNEi9bArRViT45e8L2TsuPxKNFoc3Qfg', // Make sure this matches the USDC mint in kora.toml (9BgeTKqmFsPVnfYscfM6NvsgmZxei7XfdciShQ6D3bxJ)
|
|
21
|
+
DESTINATION_ADDRESS: 'AVmDft8deQEo78bRKcGN5ZMf3hyjeLBK4Rd4xGB46yQM',
|
|
22
|
+
KORA_SIGNER_TYPE: 'memory', // Default signer type
|
|
23
|
+
};
|
|
24
|
+
const createKeyPairSignerFromB58Secret = async (b58Secret) => {
|
|
25
|
+
const base58Encoder = getBase58Encoder();
|
|
26
|
+
const b58SecretEncoded = base58Encoder.encode(b58Secret);
|
|
27
|
+
return await createKeyPairSignerFromBytes(b58SecretEncoded);
|
|
28
|
+
};
|
|
29
|
+
// TODO Add KORA_PRIVATE_KEY_2= support for multi-signer configs
|
|
30
|
+
export function loadEnvironmentVariables() {
|
|
31
|
+
const koraSignerType = process.env.KORA_SIGNER_TYPE || DEFAULTS.KORA_SIGNER_TYPE;
|
|
32
|
+
let koraAddress = process.env.KORA_ADDRESS;
|
|
33
|
+
if (!koraAddress) {
|
|
34
|
+
switch (koraSignerType) {
|
|
35
|
+
case 'turnkey':
|
|
36
|
+
koraAddress = process.env.TURNKEY_PUBLIC_KEY;
|
|
37
|
+
if (!koraAddress) {
|
|
38
|
+
throw new Error('TURNKEY_PUBLIC_KEY must be set when using Turnkey signer');
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
case 'privy':
|
|
42
|
+
koraAddress = process.env.PRIVY_PUBLIC_KEY;
|
|
43
|
+
if (!koraAddress) {
|
|
44
|
+
throw new Error('PRIVY_PUBLIC_KEY must be set when using Privy signer');
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
case 'memory':
|
|
48
|
+
default:
|
|
49
|
+
koraAddress = DEFAULTS.KORA_ADDRESS;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const koraRpcUrl = process.env.KORA_RPC_URL || DEFAULTS.KORA_RPC_URL;
|
|
54
|
+
const solanaRpcUrl = process.env.SOLANA_RPC_URL || DEFAULTS.SOLANA_RPC_URL;
|
|
55
|
+
const solanaWsUrl = process.env.SOLANA_WS_URL || DEFAULTS.SOLANA_WS_URL;
|
|
56
|
+
const commitment = (process.env.COMMITMENT || DEFAULTS.COMMITMENT);
|
|
57
|
+
const tokenDecimals = Number(process.env.TOKEN_DECIMALS || DEFAULTS.DECIMALS);
|
|
58
|
+
const tokenDropAmount = Number(process.env.TOKEN_DROP_AMOUNT || DEFAULTS.TOKEN_DROP_AMOUNT);
|
|
59
|
+
const solDropAmount = BigInt(process.env.SOL_DROP_AMOUNT || DEFAULTS.SOL_DROP_AMOUNT);
|
|
60
|
+
const testWalletSecret = process.env.SENDER_SECRET || DEFAULTS.SENDER_SECRET;
|
|
61
|
+
const testUsdcMintSecret = process.env.TEST_USDC_MINT_SECRET || DEFAULTS.TEST_USDC_MINT_SECRET;
|
|
62
|
+
const destinationAddress = process.env.DESTINATION_ADDRESS || DEFAULTS.DESTINATION_ADDRESS;
|
|
63
|
+
assertIsAddress(destinationAddress);
|
|
64
|
+
assertIsAddress(koraAddress);
|
|
65
|
+
return {
|
|
66
|
+
koraRpcUrl,
|
|
67
|
+
koraAddress,
|
|
68
|
+
koraSignerType,
|
|
69
|
+
commitment,
|
|
70
|
+
tokenDecimals,
|
|
71
|
+
tokenDropAmount,
|
|
72
|
+
solDropAmount,
|
|
73
|
+
solanaRpcUrl,
|
|
74
|
+
solanaWsUrl,
|
|
75
|
+
testWalletSecret,
|
|
76
|
+
testUsdcMintSecret,
|
|
77
|
+
destinationAddress,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function createKeyPairSigners() {
|
|
81
|
+
const { testWalletSecret, testUsdcMintSecret, destinationAddress } = loadEnvironmentVariables();
|
|
82
|
+
const testWallet = await createKeyPairSignerFromB58Secret(testWalletSecret);
|
|
83
|
+
const usdcMint = await createKeyPairSignerFromB58Secret(testUsdcMintSecret);
|
|
84
|
+
return {
|
|
85
|
+
testWallet,
|
|
86
|
+
usdcMint,
|
|
87
|
+
destinationAddress,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const createDefaultTransaction = async (client, feePayer, computeLimit = MAX_COMPUTE_UNIT_LIMIT, feeMicroLamports = 1n) => {
|
|
91
|
+
const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();
|
|
92
|
+
return pipe(createTransactionMessage({ version: 0 }), tx => setTransactionMessageFeePayerSigner(feePayer, tx), tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), tx => updateOrAppendSetComputeUnitPriceInstruction(feeMicroLamports, tx), tx => updateOrAppendSetComputeUnitLimitInstruction(computeLimit, tx));
|
|
93
|
+
};
|
|
94
|
+
const signAndSendTransaction = async (client, transactionMessage, commitment) => {
|
|
95
|
+
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
|
|
96
|
+
const signature = getSignatureFromTransaction(signedTransaction);
|
|
97
|
+
await sendAndConfirmTransactionFactory(client)(signedTransaction, { commitment, skipPreflight: true });
|
|
98
|
+
return signature;
|
|
99
|
+
};
|
|
100
|
+
function safeStringify(obj) {
|
|
101
|
+
return JSON.stringify(obj, (key, value) => {
|
|
102
|
+
if (typeof value === 'bigint') {
|
|
103
|
+
return value.toString();
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
106
|
+
}, 2);
|
|
107
|
+
}
|
|
108
|
+
async function sendAndConfirmInstructions(client, payer, instructions, description, commitment = loadEnvironmentVariables().commitment) {
|
|
109
|
+
try {
|
|
110
|
+
const signature = await pipe(await createDefaultTransaction(client, payer, 200_000), tx => appendTransactionMessageInstructions(instructions, tx), tx => signAndSendTransaction(client, tx, commitment));
|
|
111
|
+
return signature;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
console.error(safeStringify(error));
|
|
115
|
+
throw new Error(`Failed to ${description.toLowerCase()}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function initializeToken({ client, mintAuthority, payer, owner, mint, dropAmount, decimals, otherAtaWallets, }) {
|
|
119
|
+
// Get Owner ATA
|
|
120
|
+
const [ata] = await findAssociatedTokenPda({
|
|
121
|
+
mint: mint.address,
|
|
122
|
+
owner: owner.address,
|
|
123
|
+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
|
|
124
|
+
});
|
|
125
|
+
// Get Mint size & rent
|
|
126
|
+
const mintSpace = BigInt(getMintSize());
|
|
127
|
+
const mintRent = await client.rpc.getMinimumBalanceForRentExemption(mintSpace).send();
|
|
128
|
+
// Create instructions for new token mint
|
|
129
|
+
const baseInstructions = [
|
|
130
|
+
// Create the Mint Account
|
|
131
|
+
getCreateAccountInstruction({
|
|
132
|
+
payer,
|
|
133
|
+
newAccount: mint,
|
|
134
|
+
lamports: mintRent,
|
|
135
|
+
space: mintSpace,
|
|
136
|
+
programAddress: TOKEN_PROGRAM_ADDRESS,
|
|
137
|
+
}),
|
|
138
|
+
// Initialize the Mint
|
|
139
|
+
getInitializeMintInstruction({
|
|
140
|
+
mint: mint.address,
|
|
141
|
+
decimals,
|
|
142
|
+
mintAuthority: mintAuthority.address,
|
|
143
|
+
}),
|
|
144
|
+
// Create Associated Token Account
|
|
145
|
+
await getCreateAssociatedTokenIdempotentInstructionAsync({
|
|
146
|
+
mint: mint.address,
|
|
147
|
+
payer,
|
|
148
|
+
owner: owner.address,
|
|
149
|
+
}),
|
|
150
|
+
// Mint To the Destination Associated Token Account
|
|
151
|
+
getMintToInstruction({
|
|
152
|
+
mint: mint.address,
|
|
153
|
+
token: ata,
|
|
154
|
+
amount: BigInt(dropAmount * 10 ** decimals),
|
|
155
|
+
mintAuthority,
|
|
156
|
+
}),
|
|
157
|
+
];
|
|
158
|
+
// Generate Create ATA instructions for other token accounts we wish to add
|
|
159
|
+
const otherAtaInstructions = otherAtaWallets
|
|
160
|
+
? await Promise.all(otherAtaWallets.map(async (wallet) => await getCreateAssociatedTokenIdempotentInstructionAsync({
|
|
161
|
+
mint: mint.address,
|
|
162
|
+
payer,
|
|
163
|
+
owner: wallet,
|
|
164
|
+
})))
|
|
165
|
+
: [];
|
|
166
|
+
const alreadyExists = await mintExists(client, mint.address);
|
|
167
|
+
let instructions = alreadyExists ? [...otherAtaInstructions] : [...baseInstructions, ...otherAtaInstructions];
|
|
168
|
+
await sendAndConfirmInstructions(client, payer, instructions, 'Initialize token and ATAs', 'finalized');
|
|
169
|
+
}
|
|
170
|
+
async function setupTestSuite() {
|
|
171
|
+
const { koraAddress, koraRpcUrl, commitment, tokenDecimals, tokenDropAmount, solDropAmount, solanaRpcUrl, solanaWsUrl, } = await loadEnvironmentVariables();
|
|
172
|
+
// Load auth config from environment if not provided
|
|
173
|
+
const authConfig = process.env.ENABLE_AUTH === 'true'
|
|
174
|
+
? {
|
|
175
|
+
apiKey: process.env.KORA_API_KEY || 'test-api-key-123',
|
|
176
|
+
hmacSecret: process.env.KORA_HMAC_SECRET || 'test-hmac-secret-456',
|
|
177
|
+
}
|
|
178
|
+
: undefined;
|
|
179
|
+
// Create Solana client
|
|
180
|
+
const rpc = createSolanaRpc(solanaRpcUrl);
|
|
181
|
+
const rpcSubscriptions = createSolanaRpcSubscriptions(solanaWsUrl);
|
|
182
|
+
const airdrop = airdropFactory({ rpc, rpcSubscriptions });
|
|
183
|
+
const client = { rpc, rpcSubscriptions };
|
|
184
|
+
// Get or create keypairs
|
|
185
|
+
const { testWallet, usdcMint, destinationAddress } = await createKeyPairSigners();
|
|
186
|
+
const mintAuthority = testWallet; // test wallet can be used as mint authority for the test
|
|
187
|
+
// Airdrop SOL to test sender and kora wallets
|
|
188
|
+
await Promise.all([
|
|
189
|
+
airdrop({
|
|
190
|
+
commitment: 'finalized',
|
|
191
|
+
lamports: lamports(solDropAmount),
|
|
192
|
+
recipientAddress: koraAddress,
|
|
193
|
+
}),
|
|
194
|
+
airdrop({
|
|
195
|
+
commitment: 'finalized',
|
|
196
|
+
lamports: lamports(solDropAmount),
|
|
197
|
+
recipientAddress: testWallet.address,
|
|
198
|
+
}),
|
|
199
|
+
]);
|
|
200
|
+
// Initialize token and ATAs
|
|
201
|
+
await initializeToken({
|
|
202
|
+
client,
|
|
203
|
+
mintAuthority,
|
|
204
|
+
payer: mintAuthority,
|
|
205
|
+
owner: testWallet,
|
|
206
|
+
mint: usdcMint,
|
|
207
|
+
dropAmount: tokenDropAmount,
|
|
208
|
+
decimals: tokenDecimals,
|
|
209
|
+
otherAtaWallets: [testWallet.address, koraAddress, destinationAddress],
|
|
210
|
+
});
|
|
211
|
+
return {
|
|
212
|
+
koraClient: new KoraClient({ rpcUrl: koraRpcUrl, ...authConfig }),
|
|
213
|
+
koraRpcUrl,
|
|
214
|
+
testWallet,
|
|
215
|
+
usdcMint: usdcMint.address,
|
|
216
|
+
destinationAddress,
|
|
217
|
+
koraAddress,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const mintExists = async (client, mint) => {
|
|
221
|
+
try {
|
|
222
|
+
const mintAccount = await client.rpc.getAccountInfo(mint).send();
|
|
223
|
+
return mintAccount.value !== null;
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
export default setupTestSuite;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|