@sign-global/tokentable-core 1.1.0 → 1.2.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 +6 -0
- package/dist/index.d.mts +305 -220
- package/dist/index.d.ts +305 -220
- package/dist/index.js +457 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +454 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -2
- package/src/constants/chain-config.ts +18 -1
- package/src/constants/network.ts +53 -0
- package/src/contracts/index.ts +3 -0
- package/src/contracts/solana/AirdropSignatureClient.ts +2 -13
- package/src/contracts/solana/SignatureAirdropService.ts +2 -14
- package/src/contracts/solana/SolanaContractClient.ts +2 -2
- package/src/contracts/sui/BaseDistributorClient.ts +162 -0
- package/src/contracts/sui/DistributorWithFeeClient.ts +102 -0
- package/src/contracts/sui/SignatureAirdropService.ts +59 -0
- package/src/contracts/sui/SuiContractClient.ts +181 -0
- package/src/contracts/sui/__tests__/SignatureAirdropService.test.ts +71 -0
- package/src/services/airdrop/index.ts +1 -0
- package/src/types/index.ts +26 -4
- package/vitest.config.ts +3 -3
- package/src/test/setup.ts +0 -24
- package/src/utils/__tests__/web3.test.ts +0 -326
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { SuiClient, getFullnodeUrl } from '@mysten/sui/client';
|
|
2
|
+
import { bcs } from '@mysten/sui/bcs';
|
|
3
|
+
import { Transaction } from '@mysten/sui/transactions';
|
|
4
|
+
import { signTransaction, Wallet } from '@mysten/wallet-standard';
|
|
5
|
+
|
|
6
|
+
export interface SuiContractInfo {
|
|
7
|
+
packageId: string;
|
|
8
|
+
chainId?: 'testnet' | 'mainnet';
|
|
9
|
+
walletClient: Wallet;
|
|
10
|
+
coinType: string;
|
|
11
|
+
distributorId?: string;
|
|
12
|
+
chainRpc?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export abstract class SuiContractClientBase {
|
|
16
|
+
protected packageId: string;
|
|
17
|
+
protected chainId: 'testnet' | 'mainnet';
|
|
18
|
+
protected moduleName: string;
|
|
19
|
+
protected distributorId?: string;
|
|
20
|
+
protected wallet: Wallet;
|
|
21
|
+
protected client: SuiClient;
|
|
22
|
+
|
|
23
|
+
constructor(contractInfo: SuiContractInfo & { moduleName: string }) {
|
|
24
|
+
this.packageId = contractInfo.packageId;
|
|
25
|
+
this.chainId = contractInfo.chainId ? contractInfo.chainId : 'mainnet';
|
|
26
|
+
this.moduleName = contractInfo.moduleName;
|
|
27
|
+
this.distributorId = contractInfo.distributorId;
|
|
28
|
+
this.wallet = contractInfo.walletClient;
|
|
29
|
+
this.client = new SuiClient({ network: this.chainId, url: getFullnodeUrl(this.chainId) });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async signAndExecute(tx: Transaction) {
|
|
33
|
+
if (!this.wallet) {
|
|
34
|
+
throw new Error('Wallet not connected');
|
|
35
|
+
}
|
|
36
|
+
if (!this.wallet.features?.['sui:signTransaction'] && !this.wallet.features?.['sui:signTransactionBlock']) {
|
|
37
|
+
throw new Error("Wallet doesn't support transaction signing");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const accounts = await this.wallet.accounts;
|
|
41
|
+
const client = this.client;
|
|
42
|
+
if (!accounts || accounts.length === 0) {
|
|
43
|
+
throw new Error('No wallet account connected');
|
|
44
|
+
}
|
|
45
|
+
const signerAccount = accounts[0];
|
|
46
|
+
|
|
47
|
+
// 设置交易发送者
|
|
48
|
+
if ('setSenderIfNotSet' in tx) {
|
|
49
|
+
tx.setSenderIfNotSet(signerAccount.address);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log(
|
|
53
|
+
tx,
|
|
54
|
+
await tx.toJSON({
|
|
55
|
+
supportedIntents: [],
|
|
56
|
+
client
|
|
57
|
+
})
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// 准备链标识,匹配 hook 里的格式
|
|
61
|
+
const chain = `sui:${this.chainId}` as `${string}:${string}`;
|
|
62
|
+
|
|
63
|
+
// 调用 wallet-standard 的 signTransaction 来签名
|
|
64
|
+
const { signature, bytes } = await signTransaction(this.wallet, {
|
|
65
|
+
transaction: {
|
|
66
|
+
async toJSON() {
|
|
67
|
+
return typeof tx === 'string'
|
|
68
|
+
? tx
|
|
69
|
+
: await tx.toJSON({
|
|
70
|
+
supportedIntents: [],
|
|
71
|
+
client
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
account: signerAccount,
|
|
76
|
+
chain: chain
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// 发送交易
|
|
80
|
+
const result = await this.client.executeTransactionBlock({
|
|
81
|
+
transactionBlock: bytes,
|
|
82
|
+
signature,
|
|
83
|
+
options: {
|
|
84
|
+
showEffects: true,
|
|
85
|
+
showEvents: true
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// 等待交易确认
|
|
90
|
+
await this.waitForTx(result.digest);
|
|
91
|
+
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
protected async waitForTx(digest: string) {
|
|
96
|
+
return this.client.waitForTransaction({
|
|
97
|
+
digest,
|
|
98
|
+
options: { showEffects: true, showEvents: true }
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 读取合约返回值(只读调用)
|
|
104
|
+
* @param func Move function 名
|
|
105
|
+
* @param typeArgs 类型参数
|
|
106
|
+
* @param args Move function 参数
|
|
107
|
+
*/
|
|
108
|
+
async readMoveFunction<T = any>(func: string, typeArgs: string[] = [], args: any[] = []): Promise<T> {
|
|
109
|
+
const DEFAULT_SENDER = '0x0000000000000000000000000000000000000000000000000000000000000001';
|
|
110
|
+
const sender = this.wallet?.accounts?.[0]?.address || DEFAULT_SENDER;
|
|
111
|
+
if (!sender) throw new Error('No wallet account selected');
|
|
112
|
+
|
|
113
|
+
// 创建 Transaction
|
|
114
|
+
const tx = new Transaction();
|
|
115
|
+
tx.moveCall({
|
|
116
|
+
target: `${this.packageId}::${this.moduleName}::${func}`,
|
|
117
|
+
typeArguments: typeArgs,
|
|
118
|
+
arguments: args.map((arg) => {
|
|
119
|
+
// 自动把纯数据转换成 Move 所需格式
|
|
120
|
+
if (Array.isArray(arg)) return tx.pure(bcs.vector(bcs.vector(bcs.u8())).serialize(arg)); // vector<u8>
|
|
121
|
+
if (typeof arg === 'string' && arg.startsWith('0x')) return tx.object(arg); // object ID
|
|
122
|
+
return tx.pure(arg); // 纯量 u64、address 等
|
|
123
|
+
})
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// devInspectTransactionBlock 不消耗 Gas
|
|
127
|
+
const res = await this.client.devInspectTransactionBlock({
|
|
128
|
+
sender,
|
|
129
|
+
transactionBlock: tx // ✅ 这里传 Transaction,不再报 TS 类型错误
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
console.log(res, 'res');
|
|
133
|
+
|
|
134
|
+
if (!res || !res.results?.[0]?.returnValues) {
|
|
135
|
+
throw new Error(`Failed to read ${func}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return res.results[0].returnValues[0] as T;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 查询 Distributor MoveObject 的 fields
|
|
143
|
+
*/
|
|
144
|
+
async getObjectFields(objectId: string) {
|
|
145
|
+
const res = await this.client.getObject({
|
|
146
|
+
id: objectId,
|
|
147
|
+
options: { showContent: true }
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (res.data?.content?.dataType !== 'moveObject') {
|
|
151
|
+
throw new Error(`Object ${objectId} is not a moveObject`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return res.data.content.fields;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async getSharedObjectRef(tx: Transaction, objectId: string, mutable: boolean) {
|
|
158
|
+
const obj = await this.client.getObject({
|
|
159
|
+
id: objectId,
|
|
160
|
+
options: { showOwner: true }
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
console.log(obj, 'obj');
|
|
164
|
+
|
|
165
|
+
if (!obj.data) {
|
|
166
|
+
throw new Error(`Object ${objectId} not found`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!(obj.data.owner as any)?.Shared) {
|
|
170
|
+
throw new Error(`Object ${objectId} is not a shared object`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const initialSharedVersion = (obj.data.owner as any).Shared.initial_shared_version;
|
|
174
|
+
|
|
175
|
+
return tx.sharedObjectRef({
|
|
176
|
+
objectId,
|
|
177
|
+
initialSharedVersion: Number(initialSharedVersion),
|
|
178
|
+
mutable
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { SignatureAirdropService } from '../SignatureAirdropService';
|
|
3
|
+
|
|
4
|
+
describe('SignatureAirdropService Tests', () => {
|
|
5
|
+
let service: SignatureAirdropService;
|
|
6
|
+
|
|
7
|
+
const mockOptions = {
|
|
8
|
+
chainId: 'testnet' as const,
|
|
9
|
+
coinType: '0x294e0f1f6b59549d024451b5cd2399c39a1060ee34732933e8d733d9f7693836::test_coin::TEST_COIN',
|
|
10
|
+
distributorId: '0x19b0eb3facd913b78d94787175547e33e7f641ad64ed9671bcd10e6ac846a3c0',
|
|
11
|
+
chainRpc: 'https://fullnode.testnet.sui.io',
|
|
12
|
+
packageId: '0xb15f286ba26aa10fc237c2c905fe630c840947234fe0b307b93deae271967380'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.clearAllMocks();
|
|
17
|
+
|
|
18
|
+
// Create real service instance
|
|
19
|
+
service = new SignatureAirdropService({
|
|
20
|
+
...mockOptions,
|
|
21
|
+
walletClient: null as any
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.restoreAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('batchFetchClaimed Real Test', () => {
|
|
30
|
+
it('should call real batchFetchClaimed method with actual service', async () => {
|
|
31
|
+
const mockClaimIds = [
|
|
32
|
+
'0xc189c7c93c1adf3c135076b39c5c860cf18168141da13076f65a95412a51ff35',
|
|
33
|
+
'0x06e7e937178c19b5ba82999fe118af45abc9442d1f0e556ff1cfa454e6e05081'
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// This will call the real method and may fail due to network/contract issues
|
|
37
|
+
const result = await service.batchFetchClaimed(mockClaimIds);
|
|
38
|
+
console.log('BatchFetchClaimed result:', result);
|
|
39
|
+
|
|
40
|
+
// If we get here, the method executed successfully
|
|
41
|
+
expect(Array.isArray(result)).toBe(true);
|
|
42
|
+
expect(result.length).toBe(mockClaimIds.length);
|
|
43
|
+
// 期望result是两个bool值
|
|
44
|
+
expect(result.every((item) => typeof item === 'boolean')).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('getContractInfo', () => {
|
|
49
|
+
it('should return actual contract info when called with real service', async () => {
|
|
50
|
+
// This test calls the real method and may fail due to network/contract issues
|
|
51
|
+
try {
|
|
52
|
+
const result = await service.getContractInfo();
|
|
53
|
+
console.log('Contract info result:', result);
|
|
54
|
+
|
|
55
|
+
// If we get here, the method executed successfully
|
|
56
|
+
expect(result).toBeDefined();
|
|
57
|
+
expect(typeof result).toBe('object');
|
|
58
|
+
|
|
59
|
+
// Check that the result has the expected structure
|
|
60
|
+
if (result) {
|
|
61
|
+
expect(result).toHaveProperty('feeCollector');
|
|
62
|
+
expect(result).toHaveProperty('token');
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.log('Real contract info test failed (expected in test environment):', error);
|
|
66
|
+
// In test environment, this might fail due to network issues, which is acceptable
|
|
67
|
+
expect(error).toBeDefined();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
package/src/types/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { TonAirdropService, EvmAirdropService } from '../contracts';
|
|
|
3
3
|
import { ChainConfig } from '../constants';
|
|
4
4
|
import { Chain, WalletClient as ViemWalletClient } from 'viem';
|
|
5
5
|
import { WalletContextState as SolanaWalletState } from '@solana/wallet-adapter-react';
|
|
6
|
+
import { Wallet as SuiWalletClient } from '@mysten/wallet-standard';
|
|
6
7
|
|
|
7
8
|
export type ChainId = keyof typeof ChainConfig;
|
|
8
9
|
|
|
@@ -18,7 +19,8 @@ export interface EvmContractClientOption {
|
|
|
18
19
|
export enum ChainType {
|
|
19
20
|
Evm = 'evm',
|
|
20
21
|
Ton = 'ton',
|
|
21
|
-
Solana = 'solana'
|
|
22
|
+
Solana = 'solana',
|
|
23
|
+
Sui = 'sui'
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export type ThemeConfig = {
|
|
@@ -137,13 +139,33 @@ export type IAirdropSignatureClaim = {
|
|
|
137
139
|
recipient: string;
|
|
138
140
|
recipientType: string;
|
|
139
141
|
claimId: string;
|
|
140
|
-
data?:
|
|
142
|
+
data?: {
|
|
143
|
+
claimableTimestamp: string;
|
|
144
|
+
claimableAmount: string;
|
|
145
|
+
};
|
|
141
146
|
signature?: string;
|
|
142
147
|
claimed?: boolean;
|
|
143
148
|
note: string;
|
|
144
149
|
fees: string;
|
|
145
150
|
};
|
|
146
151
|
|
|
152
|
+
export interface ISignatureClaimData {
|
|
153
|
+
recipient: string;
|
|
154
|
+
data:
|
|
155
|
+
| string
|
|
156
|
+
| {
|
|
157
|
+
claimableTimestamp: string;
|
|
158
|
+
claimableAmount: string;
|
|
159
|
+
};
|
|
160
|
+
claimId: string;
|
|
161
|
+
signature: string;
|
|
162
|
+
extraData?: string;
|
|
163
|
+
unlockingAt: number;
|
|
164
|
+
value: string;
|
|
165
|
+
fees: string;
|
|
166
|
+
hash: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
147
169
|
export interface IAirdropSignatureIdentity {
|
|
148
170
|
type: AirdropSigIdentityTypeEnum;
|
|
149
171
|
value: string;
|
|
@@ -180,9 +202,9 @@ export enum EvmAirdropVersionEnum {
|
|
|
180
202
|
V4 = '0.4.0' // zeta kyc、kyc threshold
|
|
181
203
|
}
|
|
182
204
|
|
|
183
|
-
export type IWalletClient = TonWalletClient | ViemWalletClient | SolanaWalletState;
|
|
205
|
+
export type IWalletClient = TonWalletClient | ViemWalletClient | SolanaWalletState | SuiWalletClient;
|
|
184
206
|
|
|
185
|
-
export type { TonWalletClient, ViemWalletClient, SolanaWalletState };
|
|
207
|
+
export type { TonWalletClient, ViemWalletClient, SolanaWalletState, SuiWalletClient };
|
|
186
208
|
export type IAirdropService = TonAirdropService | EvmAirdropService;
|
|
187
209
|
|
|
188
210
|
type BaseAirdropOptions = {
|
package/vitest.config.ts
CHANGED
|
@@ -2,13 +2,13 @@ import { defineConfig } from 'vitest/config';
|
|
|
2
2
|
|
|
3
3
|
export default defineConfig({
|
|
4
4
|
test: {
|
|
5
|
+
silent: false,
|
|
5
6
|
environment: 'jsdom',
|
|
6
|
-
globals: true
|
|
7
|
-
setupFiles: ['./src/test/setup.ts']
|
|
7
|
+
globals: true
|
|
8
8
|
},
|
|
9
9
|
resolve: {
|
|
10
10
|
alias: {
|
|
11
11
|
'@': './src'
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
-
});
|
|
14
|
+
});
|
package/src/test/setup.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,326 +0,0 @@
|
|
|
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
|
-
});
|