@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.
- package/CHANGELOG.md +24 -0
- package/README.md +72 -0
- package/dist/index.d.mts +314 -224
- package/dist/index.d.ts +314 -224
- package/dist/index.js +489 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +486 -22
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -3
- package/src/constants/chain-config.ts +20 -3
- package/src/constants/network.ts +53 -0
- package/src/contracts/evm/AirdropService.ts +18 -13
- 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 +4 -1
- package/src/types/index.ts +34 -6
- package/src/utils/api-client.ts +2 -2
- package/src/utils/utils.ts +8 -3
- package/vitest.config.ts +14 -0
|
@@ -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)
|
|
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) => ({
|
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 = {
|
|
@@ -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
|
|
123
|
-
value
|
|
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?:
|
|
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 = {
|
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
|
|