@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sign-global/tokentable-core",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -35,7 +35,10 @@
35
35
  "@solana/wallet-adapter-react": "^0.15.0",
36
36
  "bignumber.js": "^9.1.2",
37
37
  "dayjs": "^1.11.13",
38
- "nanoid": "^5.0.9"
38
+ "nanoid": "^5.0.9",
39
+ "@mysten/dapp-kit": "^0.16.3",
40
+ "@mysten/sui": "^1.29.1",
41
+ "@mysten/wallet-standard": "^0.16.10"
39
42
  },
40
43
  "publishConfig": {
41
44
  "access": "public"
@@ -12,7 +12,16 @@ import {
12
12
  berachainTestnet,
13
13
  arbitrum
14
14
  } from 'viem/chains';
15
- import { mevmDevNet, signBrBTestnet, solanaDevNet, solanaMainNet, tonMainNet, tonTestNet } from './network';
15
+ import {
16
+ mevmDevNet,
17
+ signBrBTestnet,
18
+ solanaDevNet,
19
+ solanaMainNet,
20
+ suiMainNet,
21
+ suiTestNet,
22
+ tonMainNet,
23
+ tonTestNet
24
+ } from './network';
16
25
  import { CHAIN } from '@tonconnect/sdk';
17
26
 
18
27
  export const ChainConfig = {
@@ -96,6 +105,14 @@ export const ChainConfig = {
96
105
  [solanaMainNet.id]: {
97
106
  contractAddress: '',
98
107
  rpcUrl: solanaMainNet.rpcUrls.default.http[0]
108
+ },
109
+ [suiTestNet.id]: {
110
+ contractAddress: '0xb99be918b901c472b9839871b0341bf53a4d94a19312008a2e6c98908c92b778',
111
+ rpcUrl: suiTestNet.rpcUrls.default.http[0]
112
+ },
113
+ [suiMainNet.id]: {
114
+ contractAddress: '',
115
+ rpcUrl: suiMainNet.rpcUrls.default.http[0]
99
116
  }
100
117
  };
101
118
 
@@ -1,3 +1,4 @@
1
+ import { getFullnodeUrl } from '@mysten/sui/client';
1
2
  import { CHAIN } from '@tonconnect/sdk';
2
3
  import { defineChain } from 'viem';
3
4
 
@@ -150,3 +151,55 @@ export const signBrBTestnet = /*#__PURE__*/ defineChain({
150
151
  },
151
152
  testnet: true
152
153
  });
154
+
155
+ export const suiTestNet = {
156
+ id: 'testnet',
157
+ name: 'Sui Testnet',
158
+ network: 'Sui Testnet',
159
+ nativeCurrency: {
160
+ decimals: 9,
161
+ name: 'Sui',
162
+ symbol: 'SUI'
163
+ },
164
+ rpcUrls: {
165
+ public: {
166
+ http: [getFullnodeUrl('testnet')]
167
+ },
168
+ default: {
169
+ http: [getFullnodeUrl('testnet')]
170
+ }
171
+ },
172
+ blockExplorers: {
173
+ default: {
174
+ name: 'sui scan',
175
+ url: 'https://testnet.suivision.xyz/'
176
+ }
177
+ },
178
+ testnet: true
179
+ };
180
+
181
+ export const suiMainNet = {
182
+ id: 'mainnet',
183
+ name: 'Sui Mainnet',
184
+ network: 'Sui Mainnet',
185
+ nativeCurrency: {
186
+ decimals: 9,
187
+ name: 'Sui',
188
+ symbol: 'SUI'
189
+ },
190
+ rpcUrls: {
191
+ public: {
192
+ http: [getFullnodeUrl('mainnet')]
193
+ },
194
+ default: {
195
+ http: [getFullnodeUrl('mainnet')]
196
+ }
197
+ },
198
+ blockExplorers: {
199
+ default: {
200
+ name: 'sui scan',
201
+ url: 'https://suivision.xyz/'
202
+ }
203
+ },
204
+ testnet: false
205
+ };
@@ -8,3 +8,6 @@ export { SignatureAirdropService as SolanaSignatureAirdropService } from './sola
8
8
 
9
9
  // TON Contracts
10
10
  export { AirdropService as TonAirdropService } from './ton/AirdropService';
11
+
12
+ // SUI Contracts
13
+ export { SignatureAirdropService as SuiSignatureAirdropService } from './sui/SignatureAirdropService';
@@ -6,6 +6,7 @@ import { EddsaTokenWithFeesDistributorSolana } from './eddsa_token_with_fees_dis
6
6
  import { TOKEN_2022_PROGRAM_ID } from '@solana/spl-token';
7
7
  import { FeeCollectorClient } from './FeeCollectorClient';
8
8
  import { fetchTokenVersion, getOrCreateAssociatedTokenAccount, hexStrToU8Array } from '../../utils';
9
+ import { ISignatureClaimData } from '../../types';
9
10
 
10
11
  export class AirdropSignatureClient extends SolanaContractClientBase<EddsaTokenWithFeesDistributorSolana> {
11
12
  projectId: string;
@@ -156,19 +157,7 @@ export class AirdropSignatureClient extends SolanaContractClientBase<EddsaTokenW
156
157
  };
157
158
  }
158
159
 
159
- async claim(
160
- data: {
161
- recipient: string;
162
- data: string;
163
- claimId: string;
164
- signature: string;
165
- extraData?: string;
166
- unlockingAt: number;
167
- value: string;
168
- fees: string;
169
- hash: string;
170
- }[]
171
- ) {
160
+ async claim(data: ISignatureClaimData[]) {
172
161
  const config = await this.getContractInfo();
173
162
  const feeCollectorStoragePDA = await this.FeeCollector.getStoragePDA();
174
163
  const feePDA = await this.FeeCollector.getProjectFeePDA(this.programId);
@@ -1,3 +1,4 @@
1
+ import { ISignatureClaimData } from '../../types';
1
2
  import { AirdropSignatureClient } from './AirdropSignatureClient';
2
3
  import { SolanaContractInfo } from './SolanaContractClient';
3
4
 
@@ -33,20 +34,7 @@ export class SignatureAirdropService {
33
34
  return this.airdropClient.getPaused();
34
35
  }
35
36
 
36
- async claim(
37
- data: {
38
- recipient: string;
39
- claimId: string;
40
- data: string;
41
- signature: string;
42
- extraData?: string;
43
- unlockingAt: number;
44
- value: string;
45
- hash: string;
46
- fees: string;
47
- }[],
48
- recipient?: string
49
- ) {
37
+ async claim(data: ISignatureClaimData[], recipient?: string) {
50
38
  return this.airdropClient.claim(data);
51
39
  }
52
40
  }
@@ -32,13 +32,13 @@ export abstract class SolanaContractClientBase<T extends Idl = Idl> {
32
32
  this.wallet = walletClient;
33
33
  this.chainRpc = chainRpc;
34
34
 
35
- const provider = new AnchorProvider(this.connection, this.wallet, { commitment: 'confirmed' });
35
+ const provider = new AnchorProvider(this.connection as any, this.wallet, { commitment: 'confirmed' });
36
36
  setProvider(provider);
37
37
 
38
38
  this.program = new Program(idl, provider);
39
39
  }
40
40
 
41
- get programId() {
41
+ get programId(): PublicKey {
42
42
  return this.program.programId;
43
43
  }
44
44
 
@@ -0,0 +1,162 @@
1
+ import { SuiContractClientBase, SuiContractInfo } from './SuiContractClient';
2
+ import { Transaction } from '@mysten/sui/transactions';
3
+ import { bcs } from '@mysten/sui/bcs';
4
+
5
+ export class SuiBaseDistributorClient extends SuiContractClientBase {
6
+ constructor(info: SuiContractInfo) {
7
+ super({ ...info, moduleName: 'base_distributor' }); // 固定 base_distributor
8
+ }
9
+
10
+ async createDistributor(coinType: string, projectId: string, projectRegistryId: string) {
11
+ const tx = new Transaction();
12
+ // 使用 Uint8Array 编码 vector<u8>
13
+ const projectIdBytes = bcs.vector(bcs.u8()).serialize(Array.from(Buffer.from(projectId, 'utf8')));
14
+
15
+ tx.moveCall({
16
+ target: `${this.packageId}::${this.moduleName}::create_and_share_distributor`,
17
+ typeArguments: [coinType],
18
+ arguments: [tx.pure(projectIdBytes), tx.object(projectRegistryId)]
19
+ });
20
+
21
+ const result = await this.signAndExecute(tx);
22
+
23
+ console.log(result, 'res');
24
+
25
+ let distributorId: string | undefined;
26
+ if (result.effects?.created) {
27
+ const createdObj = result.effects.created.find(
28
+ (obj: any) => obj.owner === 'Shared' || obj.owner?.Shared // 某些版本字段是对象
29
+ );
30
+ if (createdObj) {
31
+ distributorId = createdObj.reference.objectId;
32
+ }
33
+ }
34
+
35
+ return {
36
+ txResult: result,
37
+ distributorId
38
+ };
39
+ }
40
+
41
+ async setBaseParams(
42
+ coinType: string,
43
+ startTime: string | number,
44
+ endTime: string | number,
45
+ authorizedSigner: string,
46
+ ownerCapId: string
47
+ ) {
48
+ const tx = new Transaction();
49
+ tx.moveCall({
50
+ target: `${this.packageId}::${this.moduleName}::set_base_params`,
51
+ typeArguments: [coinType],
52
+ arguments: [
53
+ tx.object(this.distributorId!),
54
+ tx.pure.u64(startTime),
55
+ tx.pure.u64(endTime),
56
+ tx.pure.address(authorizedSigner),
57
+ tx.object(ownerCapId)
58
+ ]
59
+ });
60
+ return this.signAndExecute(tx);
61
+ }
62
+
63
+ async togglePause(coinType: string, ownerCapId: string) {
64
+ const tx = new Transaction();
65
+ tx.moveCall({
66
+ target: `${this.packageId}::${this.moduleName}::toggle_pause`,
67
+ typeArguments: [coinType],
68
+ arguments: [tx.object(this.distributorId!), tx.object(ownerCapId)]
69
+ });
70
+ return this.signAndExecute(tx);
71
+ }
72
+
73
+ async getDistributorInfo() {
74
+ console.log(this.client, this.distributorId, 'distributorId');
75
+ const res = await this.client.getObject({
76
+ id: this.distributorId!,
77
+ options: { showContent: true }
78
+ });
79
+ const content: any = (res as any).data?.content as any;
80
+ const fields = content?.fields as any;
81
+ const match = content?.type.match(/<(.+)>$/);
82
+ const coinType = match ? match[1] : '';
83
+ return {
84
+ startTime: fields?.start_time,
85
+ endTime: fields?.end_time,
86
+ signer: fields?.authorized_signer,
87
+ ownerCapId: fields?.owner_cap_id,
88
+ version: fields?.version ? String.fromCharCode(...(fields.version as number[])) : undefined,
89
+ feeCollector: fields?.fee_collector,
90
+ token: coinType,
91
+ tokenBalance: fields?.token_balance,
92
+ paused: fields?.paused
93
+ };
94
+ }
95
+
96
+ async getVersion() {
97
+ const { version } = await this.getDistributorInfo();
98
+ return version;
99
+ }
100
+
101
+ async getPaused() {
102
+ const { paused } = await this.getDistributorInfo();
103
+ return paused;
104
+ }
105
+
106
+ async getClaimFee(amount: bigint) {
107
+ // const { token } = await this.getDistributorInfo();
108
+ // const fee = await this.readMoveFunction<{ Some?: string; None?: null }>(
109
+ // 'get_claim_fee',
110
+ // [token],
111
+ // [amount]
112
+ // );
113
+ // return fee;
114
+ return 0n;
115
+ }
116
+
117
+ async batchFetchClaimed(coinType: string, claimIds: string[]): Promise<boolean[]> {
118
+ const claimIdsBytes = claimIds.map((id) => Array.from(new TextEncoder().encode(id)));
119
+ const res = await this.readMoveFunction<[number[], string]>(
120
+ 'get_claim_status',
121
+ [coinType],
122
+ [this.distributorId!, claimIdsBytes]
123
+ );
124
+ console.log(res, 'claims');
125
+
126
+ const [data, type] = res;
127
+
128
+ // 使用 BCS 解码 vector<bool>
129
+ if (type === 'vector<bool>' && data) {
130
+ // data 通常是 Uint8Array 格式
131
+ const uint8Array = new Uint8Array(data);
132
+ return Array.from(bcs.vector(bcs.bool()).parse(uint8Array));
133
+ }
134
+ return [];
135
+ }
136
+
137
+ async getDistributorId(projectId: string, projectRegistryId: string) {
138
+ const distributorId = await this.readMoveFunction<{ Some?: string; None?: null }>(
139
+ 'get_distributor_by_project_id',
140
+ [],
141
+ [projectRegistryId, Array.from(Buffer.from(projectId, 'utf8'))]
142
+ );
143
+ console.log(distributorId, 'id');
144
+ return distributorId.Some;
145
+ }
146
+
147
+ async getOwnerCapId() {
148
+ const objects = await this.client.getOwnedObjects({
149
+ owner: this.wallet.accounts[0].address,
150
+ filter: {
151
+ StructType: `${this.packageId}::ownable::OwnerCap`
152
+ }
153
+ });
154
+
155
+ console.log(objects, 'objects');
156
+
157
+ // 获取 OwnerCap 对象ID
158
+ const ownerCapId = objects.data[0]?.data?.objectId;
159
+
160
+ return ownerCapId;
161
+ }
162
+ }
@@ -0,0 +1,102 @@
1
+ import { Transaction } from '@mysten/sui/transactions';
2
+ import { SuiContractClientBase, SuiContractInfo } from './SuiContractClient';
3
+ import { bcs } from '@mysten/sui/bcs';
4
+ import { ISignatureClaimData } from '../../types';
5
+
6
+ export class SuiDistributorWithFeeClient extends SuiContractClientBase {
7
+ constructor(info: SuiContractInfo) {
8
+ super({ ...info, moduleName: 'fungible_token_with_fees_distributor' });
9
+ }
10
+
11
+ private async prepareFeePayment(totalFeesStr: string, feeTokenType: string, tx: Transaction): Promise<any> {
12
+ const totalFees = BigInt(totalFeesStr);
13
+
14
+ // 获取用户的 coins
15
+ const coins = await this.client.getCoins({
16
+ owner: this.wallet.accounts[0].address,
17
+ coinType: feeTokenType
18
+ });
19
+
20
+ // 找到足够余额的单个 coin
21
+ const suitableCoin = coins.data.find((coin) => BigInt(coin.balance) >= totalFees);
22
+
23
+ if (suitableCoin) {
24
+ // 如果coin的余额正好等于所需费用,直接使用
25
+ if (BigInt(suitableCoin.balance) === totalFees) {
26
+ return tx.object(suitableCoin.coinObjectId);
27
+ }
28
+ // 如果coin的余额大于所需费用,需要分割coin
29
+ const [feeCoin] = tx.splitCoins(tx.object(suitableCoin.coinObjectId), [tx.pure(bcs.u64().serialize(totalFees))]);
30
+ return feeCoin;
31
+ }
32
+
33
+ // 如果没有单个足够的 coin,需要合并多个 coins
34
+ // 这里可以实现 coin 合并逻辑或抛出错误
35
+ throw new Error(`Insufficient balance. Required: ${totalFeesStr}, Available coins: ${coins.data.length}`);
36
+ }
37
+
38
+ async claimWithFee(coinType: string, feeCollector: string, data: ISignatureClaimData[]) {
39
+ console.log(data, feeCollector, this.distributorId, 'data');
40
+ const recipients = data.map((item) => item.recipient);
41
+ const claimIds = data.map((item) => item.claimId);
42
+ const signatures = data.map((item) => item.signature);
43
+
44
+ function encodeClaimDataWithFees(timestamp: bigint, amount: bigint, fees: bigint): number[] {
45
+ const tsBytes = bcs.u64().serialize(timestamp).toBytes(); // 8B, LE
46
+ const amtBytes = bcs.u64().serialize(amount).toBytes(); // 8B, LE
47
+ const feeBytes = bcs.u64().serialize(fees).toBytes(); // 8B, LE
48
+ return Array.from(new Uint8Array([...tsBytes, ...amtBytes, ...feeBytes])); // 24B
49
+ }
50
+ const datas = data.map((item) => {
51
+ const claimData = item.data as {
52
+ claimableTimestamp: string;
53
+ claimableAmount: string;
54
+ };
55
+ return encodeClaimDataWithFees(
56
+ BigInt(claimData.claimableTimestamp),
57
+ BigInt(claimData.claimableAmount),
58
+ BigInt(item.fees || '0')
59
+ );
60
+ });
61
+ const totalFees = data.map((item) => BigInt(item.fees || '0')).reduce((sum, fee) => sum + fee, BigInt(0));
62
+ const tx = new Transaction();
63
+ // 获取 distributor (mutable)
64
+ const distributor = await this.getSharedObjectRef(tx, this.distributorId!, true);
65
+
66
+ // 获取 feeCollector (mutable)
67
+ const feeConfig = await this.getSharedObjectRef(tx, feeCollector, true);
68
+
69
+ // 获取 clock (immutable, Sui 系统对象)
70
+ const clock = await this.getSharedObjectRef(tx, '0x6', false);
71
+
72
+ const hexToBytes = (hex: string) => Array.from(Buffer.from(hex.replace(/^0x/, ''), 'hex'));
73
+
74
+ const feeTokenType = '0x2::sui::SUI'; // 或其他费用代币类型
75
+
76
+ const [feeCoin] = tx.splitCoins(
77
+ tx.gas, // 用 gas 对象作为源
78
+ [tx.pure(bcs.u64().serialize(totalFees))]
79
+ );
80
+
81
+ // 调用 claim_with_fees 方法
82
+ tx.moveCall({
83
+ target: `${this.packageId}::${this.moduleName}::claim_with_fees`,
84
+ arguments: [
85
+ distributor,
86
+ feeConfig,
87
+ tx.pure(bcs.vector(bcs.Address).serialize(recipients as string[])), // recipients: vector<address>
88
+ tx.pure(
89
+ bcs.vector(bcs.vector(bcs.u8())).serialize(claimIds.map((id) => Array.from(new TextEncoder().encode(id))))
90
+ ), // claim_ids: vector<vector<u8>>
91
+ tx.pure(bcs.vector(bcs.vector(bcs.u8())).serialize(datas)), // claim_datas: vector<vector<u8>>
92
+ tx.pure(bcs.vector(bcs.vector(bcs.u8())).serialize(signatures.map((sig) => hexToBytes(sig)))), // signatures: vector<vector<u8>>
93
+ feeCoin,
94
+ clock
95
+ ],
96
+ typeArguments: [coinType, feeTokenType] // Token类型, Fee类型
97
+ });
98
+
99
+ const res = await this.signAndExecute(tx);
100
+ return res.digest;
101
+ }
102
+ }
@@ -0,0 +1,59 @@
1
+ import { getChainConfig } from '../../common';
2
+ import { ISignatureClaimData } from '../../types';
3
+ import { SuiBaseDistributorClient } from './BaseDistributorClient';
4
+ import { SuiDistributorWithFeeClient } from './DistributorWithFeeClient';
5
+ import { SuiContractInfo } from './SuiContractClient';
6
+
7
+ export class SignatureAirdropService {
8
+ options: SuiContractInfo;
9
+ constructor(options: Omit<SuiContractInfo, 'packageId'>) {
10
+ const chainConfig = getChainConfig(options.chainId as any);
11
+ this.options = {
12
+ ...options,
13
+ packageId: chainConfig.contractAddress
14
+ };
15
+ }
16
+
17
+ private get baseDistributorClient(): SuiBaseDistributorClient {
18
+ if (!this.options) {
19
+ throw new Error('options is empty');
20
+ }
21
+
22
+ return new SuiBaseDistributorClient(this.options);
23
+ }
24
+
25
+ private get feeDistributorClient(): SuiDistributorWithFeeClient {
26
+ if (!this.options) {
27
+ throw new Error('options is empty');
28
+ }
29
+
30
+ return new SuiDistributorWithFeeClient(this.options);
31
+ }
32
+
33
+ async getVersion() {
34
+ return this.baseDistributorClient.getVersion();
35
+ }
36
+
37
+ async getClaimFee(address: string, amount: bigint): Promise<bigint> {
38
+ const fee = await this.baseDistributorClient.getClaimFee(amount);
39
+ return fee;
40
+ }
41
+
42
+ async batchFetchClaimed(userClaimIds: string[]): Promise<boolean[]> {
43
+ const res = await this.baseDistributorClient.batchFetchClaimed(this.options.coinType, userClaimIds);
44
+ return res;
45
+ }
46
+
47
+ async getPaused() {
48
+ return this.baseDistributorClient.getPaused();
49
+ }
50
+
51
+ async getContractInfo() {
52
+ return this.baseDistributorClient.getDistributorInfo();
53
+ }
54
+
55
+ async claim(data: ISignatureClaimData[], recipient?: string) {
56
+ const { feeCollector } = await this.getContractInfo();
57
+ return this.feeDistributorClient.claimWithFee(this.options.coinType, feeCollector, data);
58
+ }
59
+ }