@sign-global/tokentable-core 1.0.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.
@@ -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
+ });
@@ -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 {
@@ -91,6 +93,7 @@ interface WalletBindParams {
91
93
  timestamp: number;
92
94
  recipientType: string;
93
95
  address: string;
96
+ encodedMessage?: string;
94
97
  }
95
98
 
96
99
  const getSidHeader = (projectId: string) => ({
@@ -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 = {
@@ -105,6 +107,7 @@ export type IAirdropClaim = {
105
107
  data: string;
106
108
  leaf: string;
107
109
  claimed?: boolean;
110
+ fees: string;
108
111
 
109
112
  claimId?: string;
110
113
 
@@ -118,9 +121,14 @@ export type IAirdropClaim = {
118
121
  project: IProject;
119
122
  };
120
123
 
124
+ export interface IExtraData {
125
+ isLegacy: boolean;
126
+ attestationId: bigint;
127
+ }
128
+
121
129
  export interface IAirdropClaimData extends IAirdropClaim {
122
- extraData: any;
123
- value: bigint;
130
+ extraData?: IExtraData;
131
+ value?: bigint | string;
124
132
  }
125
133
 
126
134
  export type IAirdropSignatureClaim = {
@@ -131,13 +139,33 @@ export type IAirdropSignatureClaim = {
131
139
  recipient: string;
132
140
  recipientType: string;
133
141
  claimId: string;
134
- data?: string;
142
+ data?: {
143
+ claimableTimestamp: string;
144
+ claimableAmount: string;
145
+ };
135
146
  signature?: string;
136
147
  claimed?: boolean;
137
148
  note: string;
138
149
  fees: string;
139
150
  };
140
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
+
141
169
  export interface IAirdropSignatureIdentity {
142
170
  type: AirdropSigIdentityTypeEnum;
143
171
  value: string;
@@ -174,9 +202,9 @@ export enum EvmAirdropVersionEnum {
174
202
  V4 = '0.4.0' // zeta kyc、kyc threshold
175
203
  }
176
204
 
177
- export type IWalletClient = TonWalletClient | ViemWalletClient | SolanaWalletState;
205
+ export type IWalletClient = TonWalletClient | ViemWalletClient | SolanaWalletState | SuiWalletClient;
178
206
 
179
- export type { TonWalletClient, ViemWalletClient, SolanaWalletState };
207
+ export type { TonWalletClient, ViemWalletClient, SolanaWalletState, SuiWalletClient };
180
208
  export type IAirdropService = TonAirdropService | EvmAirdropService;
181
209
 
182
210
  type BaseAirdropOptions = {
@@ -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
+ silent: false,
6
+ environment: 'jsdom',
7
+ globals: true
8
+ },
9
+ resolve: {
10
+ alias: {
11
+ '@': './src'
12
+ }
13
+ }
14
+ });