@sign-global/tokentable-core 1.14.2 → 2.0.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 +15 -0
- package/dist/index.d.mts +6 -5
- package/dist/index.d.ts +6 -5
- package/dist/index.js +69 -84
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +69 -84
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -4
- package/src/constants/network.ts +4 -5
- package/src/contracts/sui/BaseDistributorClient.ts +22 -38
- package/src/contracts/sui/DistributorWithFeeClient.ts +8 -8
- package/src/contracts/sui/SuiContractClient.ts +43 -37
- package/src/contracts/sui/__tests__/SuiContractClient.test.ts +16 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sign-global/tokentable-core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -36,9 +36,8 @@
|
|
|
36
36
|
"bignumber.js": "^9.1.2",
|
|
37
37
|
"dayjs": "^1.11.13",
|
|
38
38
|
"nanoid": "^5.0.9",
|
|
39
|
-
"@mysten/
|
|
40
|
-
"@mysten/
|
|
41
|
-
"@mysten/wallet-standard": "^0.16.10",
|
|
39
|
+
"@mysten/sui": "^2.28.0",
|
|
40
|
+
"@mysten/wallet-standard": "^0.21.21",
|
|
42
41
|
"@aptos-labs/ts-sdk": "^4.0.0",
|
|
43
42
|
"@aptos-labs/wallet-adapter-react": "^7.0.4"
|
|
44
43
|
},
|
package/src/constants/network.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getFullnodeUrl } from '@mysten/sui/client';
|
|
2
1
|
import { CHAIN } from '@tonconnect/sdk';
|
|
3
2
|
import { defineChain } from 'viem';
|
|
4
3
|
|
|
@@ -163,10 +162,10 @@ export const suiTestNet = {
|
|
|
163
162
|
},
|
|
164
163
|
rpcUrls: {
|
|
165
164
|
public: {
|
|
166
|
-
http: [
|
|
165
|
+
http: ['https://fullnode.testnet.sui.io:443']
|
|
167
166
|
},
|
|
168
167
|
default: {
|
|
169
|
-
http: [
|
|
168
|
+
http: ['https://fullnode.testnet.sui.io:443']
|
|
170
169
|
}
|
|
171
170
|
},
|
|
172
171
|
blockExplorers: {
|
|
@@ -189,10 +188,10 @@ export const suiMainNet = {
|
|
|
189
188
|
},
|
|
190
189
|
rpcUrls: {
|
|
191
190
|
public: {
|
|
192
|
-
http: [
|
|
191
|
+
http: ['https://fullnode.mainnet.sui.io:443']
|
|
193
192
|
},
|
|
194
193
|
default: {
|
|
195
|
-
http: [
|
|
194
|
+
http: ['https://fullnode.mainnet.sui.io:443']
|
|
196
195
|
}
|
|
197
196
|
},
|
|
198
197
|
blockExplorers: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SuiContractClientBase, SuiContractInfo } from './SuiContractClient';
|
|
2
2
|
import { Transaction } from '@mysten/sui/transactions';
|
|
3
3
|
import { bcs } from '@mysten/sui/bcs';
|
|
4
|
+
import { fromBase64 } from '@mysten/sui/utils';
|
|
4
5
|
|
|
5
6
|
export class SuiBaseDistributorClient extends SuiContractClientBase {
|
|
6
7
|
constructor(info: SuiContractInfo) {
|
|
@@ -20,19 +21,14 @@ export class SuiBaseDistributorClient extends SuiContractClientBase {
|
|
|
20
21
|
|
|
21
22
|
const result = await this.signAndExecute(tx);
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
);
|
|
28
|
-
if (createdObj) {
|
|
29
|
-
distributorId = createdObj.reference.objectId;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
24
|
+
// 新建的 shared object 即 distributor
|
|
25
|
+
const createdObj = result.effects?.changedObjects.find(
|
|
26
|
+
(obj) => obj.idOperation === 'Created' && obj.outputOwner?.$kind === 'Shared'
|
|
27
|
+
);
|
|
32
28
|
|
|
33
29
|
return {
|
|
34
30
|
txResult: result,
|
|
35
|
-
distributorId
|
|
31
|
+
distributorId: createdObj?.objectId
|
|
36
32
|
};
|
|
37
33
|
}
|
|
38
34
|
|
|
@@ -69,20 +65,20 @@ export class SuiBaseDistributorClient extends SuiContractClientBase {
|
|
|
69
65
|
}
|
|
70
66
|
|
|
71
67
|
async getDistributorInfo() {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
const { object } = await this.client.getObject({
|
|
69
|
+
objectId: this.distributorId!,
|
|
70
|
+
include: { json: true }
|
|
75
71
|
});
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
const match = content?.type.match(/<(.+)>$/);
|
|
72
|
+
const fields = object.json as any;
|
|
73
|
+
const match = object.type.match(/<(.+)>$/);
|
|
79
74
|
const coinType = match ? match[1] : '';
|
|
80
75
|
return {
|
|
81
76
|
startTime: fields?.start_time,
|
|
82
77
|
endTime: fields?.end_time,
|
|
83
|
-
signer: fields?.
|
|
78
|
+
signer: fields?.authorized_public_key,
|
|
84
79
|
ownerCapId: fields?.owner_cap_id,
|
|
85
|
-
|
|
80
|
+
// gRPC JSON 中 vector<u8> 以 base64 返回
|
|
81
|
+
version: fields?.version ? new TextDecoder().decode(fromBase64(fields.version)) : undefined,
|
|
86
82
|
feeCollector: fields?.fee_collector_config,
|
|
87
83
|
token: coinType,
|
|
88
84
|
tokenBalance: fields?.token_balance,
|
|
@@ -113,42 +109,30 @@ export class SuiBaseDistributorClient extends SuiContractClientBase {
|
|
|
113
109
|
|
|
114
110
|
async batchFetchClaimed(coinType: string, claimIds: string[]): Promise<boolean[]> {
|
|
115
111
|
const claimIdsBytes = claimIds.map((id) => Array.from(new TextEncoder().encode(id)));
|
|
116
|
-
const res = await this.readMoveFunction
|
|
117
|
-
'get_claim_status',
|
|
118
|
-
[coinType],
|
|
119
|
-
[this.distributorId!, claimIdsBytes]
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
const [data, type] = res;
|
|
112
|
+
const res = await this.readMoveFunction('get_claim_status', [coinType], [this.distributorId!, claimIdsBytes]);
|
|
123
113
|
|
|
124
114
|
// 使用 BCS 解码 vector<bool>
|
|
125
|
-
|
|
126
|
-
// data 通常是 Uint8Array 格式
|
|
127
|
-
const uint8Array = new Uint8Array(data);
|
|
128
|
-
return Array.from(bcs.vector(bcs.bool()).parse(uint8Array));
|
|
129
|
-
}
|
|
130
|
-
return [];
|
|
115
|
+
return bcs.vector(bcs.bool()).parse(res);
|
|
131
116
|
}
|
|
132
117
|
|
|
133
118
|
async getDistributorId(projectId: string, projectRegistryId: string) {
|
|
134
|
-
const
|
|
119
|
+
const res = await this.readMoveFunction(
|
|
135
120
|
'get_distributor_by_project_id',
|
|
136
121
|
[],
|
|
137
122
|
[projectRegistryId, Array.from(Buffer.from(projectId, 'utf8'))]
|
|
138
123
|
);
|
|
139
|
-
|
|
124
|
+
// 使用 BCS 解码 Option<ID>
|
|
125
|
+
return bcs.option(bcs.Address).parse(res);
|
|
140
126
|
}
|
|
141
127
|
|
|
142
128
|
async getOwnerCapId() {
|
|
143
|
-
const objects = await this.client.
|
|
129
|
+
const { objects } = await this.client.listOwnedObjects({
|
|
144
130
|
owner: this.wallet.accounts[0].address,
|
|
145
|
-
|
|
146
|
-
StructType: `${this.packageId}::ownable::OwnerCap`
|
|
147
|
-
}
|
|
131
|
+
type: `${this.packageId}::ownable::OwnerCap`
|
|
148
132
|
});
|
|
149
133
|
|
|
150
134
|
// 获取 OwnerCap 对象ID
|
|
151
|
-
const ownerCapId = objects
|
|
135
|
+
const ownerCapId = objects[0]?.objectId;
|
|
152
136
|
|
|
153
137
|
return ownerCapId;
|
|
154
138
|
}
|
|
@@ -12,27 +12,27 @@ export class SuiDistributorWithFeeClient extends SuiContractClientBase {
|
|
|
12
12
|
const totalFees = BigInt(totalFeesStr);
|
|
13
13
|
|
|
14
14
|
// 获取用户的 coins
|
|
15
|
-
const coins = await this.client.
|
|
15
|
+
const coins = await this.client.listCoins({
|
|
16
16
|
owner: this.wallet.accounts[0].address,
|
|
17
17
|
coinType: feeTokenType
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
// 找到足够余额的单个 coin
|
|
21
|
-
const suitableCoin = coins.
|
|
21
|
+
const suitableCoin = coins.objects.find((coin) => BigInt(coin.balance) >= totalFees);
|
|
22
22
|
|
|
23
23
|
if (suitableCoin) {
|
|
24
24
|
// 如果coin的余额正好等于所需费用,直接使用
|
|
25
25
|
if (BigInt(suitableCoin.balance) === totalFees) {
|
|
26
|
-
return tx.object(suitableCoin.
|
|
26
|
+
return tx.object(suitableCoin.objectId);
|
|
27
27
|
}
|
|
28
28
|
// 如果coin的余额大于所需费用,需要分割coin
|
|
29
|
-
const [feeCoin] = tx.splitCoins(tx.object(suitableCoin.
|
|
29
|
+
const [feeCoin] = tx.splitCoins(tx.object(suitableCoin.objectId), [tx.pure(bcs.u64().serialize(totalFees))]);
|
|
30
30
|
return feeCoin;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// 如果没有单个足够的 coin,需要合并多个 coins
|
|
34
34
|
// 这里可以实现 coin 合并逻辑或抛出错误
|
|
35
|
-
throw new Error(`Insufficient balance. Required: ${totalFeesStr}, Available coins: ${coins.
|
|
35
|
+
throw new Error(`Insufficient balance. Required: ${totalFeesStr}, Available coins: ${coins.objects.length}`);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
async claimWithFee(coinType: string, feeCollector: string, data: ISignatureClaimData[]) {
|
|
@@ -73,13 +73,13 @@ export class SuiDistributorWithFeeClient extends SuiContractClientBase {
|
|
|
73
73
|
const feeTokenType = '0x2::sui::SUI'; // 或其他费用代币类型
|
|
74
74
|
|
|
75
75
|
// 在splitCoins之前添加余额检查
|
|
76
|
-
const gasBalance = await this.client.getBalance({
|
|
76
|
+
const { balance: gasBalance } = await this.client.getBalance({
|
|
77
77
|
owner: this.wallet.accounts[0].address,
|
|
78
78
|
coinType: feeTokenType
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
if (BigInt(gasBalance.
|
|
82
|
-
throw new Error(`Insufficient SUI balance. Required: ${totalFees}, Available: ${gasBalance.
|
|
81
|
+
if (BigInt(gasBalance.balance) < totalFees) {
|
|
82
|
+
throw new Error(`Insufficient SUI balance. Required: ${totalFees}, Available: ${gasBalance.balance}`);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
const [feeCoin] = tx.splitCoins(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SuiGrpcClient } from '@mysten/sui/grpc';
|
|
2
2
|
import { bcs } from '@mysten/sui/bcs';
|
|
3
3
|
import { Transaction } from '@mysten/sui/transactions';
|
|
4
|
+
import { fromBase64 } from '@mysten/sui/utils';
|
|
4
5
|
import { signTransaction, Wallet } from '@mysten/wallet-standard';
|
|
5
6
|
|
|
6
7
|
export interface SuiContractInfo {
|
|
@@ -9,6 +10,7 @@ export interface SuiContractInfo {
|
|
|
9
10
|
walletClient: Wallet;
|
|
10
11
|
coinType: string;
|
|
11
12
|
distributorId?: string;
|
|
13
|
+
/** gRPC endpoint, e.g. https://fullnode.mainnet.sui.io:443 */
|
|
12
14
|
chainRpc?: string;
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -18,7 +20,7 @@ export abstract class SuiContractClientBase {
|
|
|
18
20
|
protected moduleName: string;
|
|
19
21
|
protected distributorId?: string;
|
|
20
22
|
protected wallet: Wallet;
|
|
21
|
-
protected client:
|
|
23
|
+
protected client: SuiGrpcClient;
|
|
22
24
|
|
|
23
25
|
constructor(contractInfo: SuiContractInfo & { moduleName: string }) {
|
|
24
26
|
this.packageId = contractInfo.packageId;
|
|
@@ -26,7 +28,10 @@ export abstract class SuiContractClientBase {
|
|
|
26
28
|
this.moduleName = contractInfo.moduleName;
|
|
27
29
|
this.distributorId = contractInfo.distributorId;
|
|
28
30
|
this.wallet = contractInfo.walletClient;
|
|
29
|
-
this.client = new
|
|
31
|
+
this.client = new SuiGrpcClient({
|
|
32
|
+
network: this.chainId,
|
|
33
|
+
baseUrl: contractInfo.chainRpc || `https://fullnode.${this.chainId}.sui.io:443`
|
|
34
|
+
});
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
async signAndExecute(tx: Transaction) {
|
|
@@ -77,35 +82,36 @@ export abstract class SuiContractClientBase {
|
|
|
77
82
|
});
|
|
78
83
|
|
|
79
84
|
// 发送交易
|
|
80
|
-
const result = await this.client.
|
|
81
|
-
|
|
82
|
-
signature,
|
|
83
|
-
|
|
84
|
-
showEffects: true,
|
|
85
|
-
showEvents: true
|
|
86
|
-
}
|
|
85
|
+
const result = await this.client.executeTransaction({
|
|
86
|
+
transaction: fromBase64(bytes),
|
|
87
|
+
signatures: [signature],
|
|
88
|
+
include: { effects: true, events: true }
|
|
87
89
|
});
|
|
88
90
|
|
|
91
|
+
if (result.$kind === 'FailedTransaction') {
|
|
92
|
+
throw new Error(result.FailedTransaction.status.error?.message ?? 'Transaction failed');
|
|
93
|
+
}
|
|
94
|
+
|
|
89
95
|
// 等待交易确认
|
|
90
|
-
await this.waitForTx(result.digest);
|
|
96
|
+
await this.waitForTx(result.Transaction.digest);
|
|
91
97
|
|
|
92
|
-
return result;
|
|
98
|
+
return result.Transaction;
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
protected async waitForTx(digest: string) {
|
|
96
102
|
return this.client.waitForTransaction({
|
|
97
103
|
digest,
|
|
98
|
-
|
|
104
|
+
include: { effects: true, events: true }
|
|
99
105
|
});
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
/**
|
|
103
|
-
*
|
|
109
|
+
* 读取合约返回值(只读调用),返回第一个返回值的 BCS 字节
|
|
104
110
|
* @param func Move function 名
|
|
105
111
|
* @param typeArgs 类型参数
|
|
106
112
|
* @param args Move function 参数
|
|
107
113
|
*/
|
|
108
|
-
async readMoveFunction
|
|
114
|
+
async readMoveFunction(func: string, typeArgs: string[] = [], args: any[] = []): Promise<Uint8Array> {
|
|
109
115
|
const DEFAULT_SENDER = '0x0000000000000000000000000000000000000000000000000000000000000001';
|
|
110
116
|
const sender = this.wallet?.accounts?.[0]?.address || DEFAULT_SENDER;
|
|
111
117
|
if (!sender) throw new Error('No wallet account selected');
|
|
@@ -122,55 +128,55 @@ export abstract class SuiContractClientBase {
|
|
|
122
128
|
return tx.pure(arg); // 纯量 u64、address 等
|
|
123
129
|
})
|
|
124
130
|
});
|
|
131
|
+
tx.setSender(sender);
|
|
125
132
|
|
|
126
|
-
//
|
|
127
|
-
const res = await this.client.
|
|
128
|
-
|
|
129
|
-
|
|
133
|
+
// simulateTransaction 不消耗 Gas;关闭 checks 以允许调用非 entry 函数
|
|
134
|
+
const res = await this.client.simulateTransaction({
|
|
135
|
+
transaction: tx,
|
|
136
|
+
checksEnabled: false,
|
|
137
|
+
include: { commandResults: true }
|
|
130
138
|
});
|
|
131
139
|
|
|
132
140
|
console.log(res, 'res');
|
|
133
141
|
|
|
134
|
-
if (
|
|
142
|
+
if (res.$kind === 'FailedTransaction') {
|
|
143
|
+
throw new Error(`Failed to read ${func}: ${res.FailedTransaction.status.error?.message}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const returnValue = res.commandResults?.[0]?.returnValues?.[0]?.bcs;
|
|
147
|
+
if (!returnValue) {
|
|
135
148
|
throw new Error(`Failed to read ${func}`);
|
|
136
149
|
}
|
|
137
150
|
|
|
138
|
-
return
|
|
151
|
+
return returnValue;
|
|
139
152
|
}
|
|
140
153
|
|
|
141
154
|
/**
|
|
142
155
|
* 查询 Distributor MoveObject 的 fields
|
|
143
156
|
*/
|
|
144
157
|
async getObjectFields(objectId: string) {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
158
|
+
const { object } = await this.client.getObject({
|
|
159
|
+
objectId,
|
|
160
|
+
include: { json: true }
|
|
148
161
|
});
|
|
149
162
|
|
|
150
|
-
if (
|
|
163
|
+
if (!object.json) {
|
|
151
164
|
throw new Error(`Object ${objectId} is not a moveObject`);
|
|
152
165
|
}
|
|
153
166
|
|
|
154
|
-
return
|
|
167
|
+
return object.json;
|
|
155
168
|
}
|
|
156
169
|
|
|
157
170
|
async getSharedObjectRef(tx: Transaction, objectId: string, mutable: boolean) {
|
|
158
|
-
const
|
|
159
|
-
id: objectId,
|
|
160
|
-
options: { showOwner: true }
|
|
161
|
-
});
|
|
171
|
+
const { object } = await this.client.getObject({ objectId });
|
|
162
172
|
|
|
163
|
-
console.log(
|
|
164
|
-
|
|
165
|
-
if (!obj.data) {
|
|
166
|
-
throw new Error(`Object ${objectId} not found`);
|
|
167
|
-
}
|
|
173
|
+
console.log(object, 'obj');
|
|
168
174
|
|
|
169
|
-
if (
|
|
175
|
+
if (object.owner.$kind !== 'Shared') {
|
|
170
176
|
throw new Error(`Object ${objectId} is not a shared object`);
|
|
171
177
|
}
|
|
172
178
|
|
|
173
|
-
const initialSharedVersion =
|
|
179
|
+
const initialSharedVersion = object.owner.Shared.initialSharedVersion;
|
|
174
180
|
|
|
175
181
|
return tx.sharedObjectRef({
|
|
176
182
|
objectId,
|
|
@@ -2,13 +2,12 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
const suiClientArgs = vi.fn();
|
|
4
4
|
|
|
5
|
-
vi.mock('@mysten/sui/
|
|
6
|
-
|
|
7
|
-
constructor(args: { network: string;
|
|
5
|
+
vi.mock('@mysten/sui/grpc', () => ({
|
|
6
|
+
SuiGrpcClient: class {
|
|
7
|
+
constructor(args: { network: string; baseUrl: string }) {
|
|
8
8
|
suiClientArgs(args);
|
|
9
9
|
}
|
|
10
|
-
}
|
|
11
|
-
getFullnodeUrl: (network: string) => `https://fullnode.${network}.sui.io:443`
|
|
10
|
+
}
|
|
12
11
|
}));
|
|
13
12
|
|
|
14
13
|
import { SuiContractClientBase, SuiContractInfo } from '../SuiContractClient';
|
|
@@ -29,11 +28,11 @@ describe('SuiContractClientBase RPC selection', () => {
|
|
|
29
28
|
});
|
|
30
29
|
|
|
31
30
|
it('uses chainRpc when provided', () => {
|
|
32
|
-
new TestClient({ ...baseInfo, chainRpc: 'https://sui-mainnet.
|
|
31
|
+
new TestClient({ ...baseInfo, chainRpc: 'https://sui-mainnet.grpc.example.com:443' });
|
|
33
32
|
|
|
34
33
|
expect(suiClientArgs).toHaveBeenCalledWith({
|
|
35
34
|
network: 'mainnet',
|
|
36
|
-
|
|
35
|
+
baseUrl: 'https://sui-mainnet.grpc.example.com:443'
|
|
37
36
|
});
|
|
38
37
|
});
|
|
39
38
|
|
|
@@ -42,7 +41,16 @@ describe('SuiContractClientBase RPC selection', () => {
|
|
|
42
41
|
|
|
43
42
|
expect(suiClientArgs).toHaveBeenCalledWith({
|
|
44
43
|
network: 'mainnet',
|
|
45
|
-
|
|
44
|
+
baseUrl: 'https://fullnode.mainnet.sui.io:443'
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('uses the testnet fullnode for testnet', () => {
|
|
49
|
+
new TestClient({ ...baseInfo, chainId: 'testnet' });
|
|
50
|
+
|
|
51
|
+
expect(suiClientArgs).toHaveBeenCalledWith({
|
|
52
|
+
network: 'testnet',
|
|
53
|
+
baseUrl: 'https://fullnode.testnet.sui.io:443'
|
|
46
54
|
});
|
|
47
55
|
});
|
|
48
56
|
});
|