@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sign-global/tokentable-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"vitest": "^1.0.0",
|
|
16
|
+
"@vitest/ui": "^1.0.0",
|
|
17
|
+
"jsdom": "^23.0.0"
|
|
18
|
+
},
|
|
14
19
|
"dependencies": {
|
|
15
20
|
"@tonconnect/sdk": "^3.0.6",
|
|
16
21
|
"viem": "^2.0.0",
|
|
@@ -30,7 +35,10 @@
|
|
|
30
35
|
"@solana/wallet-adapter-react": "^0.15.0",
|
|
31
36
|
"bignumber.js": "^9.1.2",
|
|
32
37
|
"dayjs": "^1.11.13",
|
|
33
|
-
"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"
|
|
34
42
|
},
|
|
35
43
|
"publishConfig": {
|
|
36
44
|
"access": "public"
|
|
@@ -39,6 +47,8 @@
|
|
|
39
47
|
"build": "tsup",
|
|
40
48
|
"build:watch": "tsup --watch",
|
|
41
49
|
"clean": "rm -rf dist",
|
|
42
|
-
"test": "
|
|
50
|
+
"test": "vitest",
|
|
51
|
+
"test:run": "vitest run",
|
|
52
|
+
"test:watch": "vitest --watch"
|
|
43
53
|
}
|
|
44
54
|
}
|
|
@@ -12,7 +12,16 @@ import {
|
|
|
12
12
|
berachainTestnet,
|
|
13
13
|
arbitrum
|
|
14
14
|
} from 'viem/chains';
|
|
15
|
-
import {
|
|
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 = {
|
|
@@ -49,8 +58,8 @@ export const ChainConfig = {
|
|
|
49
58
|
rpcUrl: 'https://zetachain-mainnet.g.alchemy.com/v2/1tmk0FrhwTJHXh0XIyae7_SXw1lDCjgs'
|
|
50
59
|
},
|
|
51
60
|
[arbitrum.id]: {
|
|
52
|
-
contractAddress: '
|
|
53
|
-
rpcUrl:
|
|
61
|
+
contractAddress: '0xeD09c23677D02f4f486E55f0f081cfc114EEa53b',
|
|
62
|
+
rpcUrl: 'https://arb-mainnet.g.alchemy.com/v2/udrqNPSB6i5n5L6QSM31Ng72h_hFOrVT'
|
|
54
63
|
},
|
|
55
64
|
[berachainTestnet.id]: {
|
|
56
65
|
contractAddress: '',
|
|
@@ -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
|
|
package/src/constants/network.ts
CHANGED
|
@@ -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
|
+
};
|
|
@@ -160,22 +160,27 @@ export class AirdropService {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
async getClaimFee(address: string, amount: bigint): Promise<bigint | undefined> {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
const feeClient = this.getFeeClient(feeCollector as string);
|
|
168
|
-
const fee = await feeClient.getFee(address, amount);
|
|
169
|
-
const version = await this.getVersion();
|
|
170
|
-
if (version === AirdropVersionEnum.V3) {
|
|
171
|
-
const threshold = await this.getFeelessThreshold();
|
|
172
|
-
if (isGreaterThan(Number(amount), Number(threshold))) {
|
|
173
|
-
return fee;
|
|
174
|
-
} else {
|
|
163
|
+
try {
|
|
164
|
+
const feeCollector = await this.feeCollectors(this.options.contractAddress as string);
|
|
165
|
+
if (!feeCollector || feeCollector === ZERO_ADDRESS) {
|
|
175
166
|
return undefined;
|
|
176
167
|
}
|
|
168
|
+
const feeClient = this.getFeeClient(feeCollector as string);
|
|
169
|
+
const fee = await feeClient.getFee(address, amount);
|
|
170
|
+
const version = await this.getVersion();
|
|
171
|
+
if (version === AirdropVersionEnum.V3) {
|
|
172
|
+
const threshold = await this.getFeelessThreshold();
|
|
173
|
+
if (isGreaterThan(Number(amount), Number(threshold))) {
|
|
174
|
+
return fee;
|
|
175
|
+
} else {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return fee;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error('Failed to get claim fee:', error);
|
|
182
|
+
throw new Error(`Failed to calculate claim fee: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
177
183
|
}
|
|
178
|
-
return fee;
|
|
179
184
|
}
|
|
180
185
|
|
|
181
186
|
async getKycThreshold() {
|
package/src/contracts/index.ts
CHANGED
|
@@ -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
|
+
}
|