@xchainjs/xchain-sui 0.1.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/lib/client.d.ts +30 -0
- package/lib/const.d.ts +7 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.esm.js +410 -0
- package/lib/index.js +421 -0
- package/lib/types.d.ts +28 -0
- package/lib/utils.d.ts +3 -0
- package/package.json +44 -0
package/lib/client.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AssetInfo, BaseXChainClient, Fees, Network, PreparedTx, TxHash, TxHistoryParams } from '@xchainjs/xchain-client';
|
|
2
|
+
import { Address, TokenAsset } from '@xchainjs/xchain-util';
|
|
3
|
+
import { Balance, SUIClientParams, Tx, TxParams, TxsPage } from './types';
|
|
4
|
+
export declare class Client extends BaseXChainClient {
|
|
5
|
+
private explorerProviders;
|
|
6
|
+
private suiClient;
|
|
7
|
+
private clientUrls?;
|
|
8
|
+
constructor(params?: SUIClientParams);
|
|
9
|
+
setNetwork(network: Network): void;
|
|
10
|
+
getAssetInfo(): AssetInfo;
|
|
11
|
+
getExplorerUrl(): string;
|
|
12
|
+
getExplorerAddressUrl(address: Address): string;
|
|
13
|
+
getExplorerTxUrl(txID: TxHash): string;
|
|
14
|
+
getFullDerivationPath(walletIndex: number): string;
|
|
15
|
+
getAddressAsync(index?: number): Promise<string>;
|
|
16
|
+
getAddress(): string;
|
|
17
|
+
validateAddress(address: Address): boolean;
|
|
18
|
+
getBalance(address: Address, assets?: TokenAsset[]): Promise<Balance[]>;
|
|
19
|
+
getFees(): Promise<Fees>;
|
|
20
|
+
getTransactionData(txId: string): Promise<Tx>;
|
|
21
|
+
getTransactions(params?: TxHistoryParams): Promise<TxsPage>;
|
|
22
|
+
transfer({ walletIndex, recipient, asset, amount, memo, gasBudget }: TxParams): Promise<string>;
|
|
23
|
+
broadcastTx(txHex: string): Promise<TxHash>;
|
|
24
|
+
prepareTx({ walletIndex, memo, recipient, asset, amount, gasBudget, }: TxParams): Promise<PreparedTx>;
|
|
25
|
+
private getKeypair;
|
|
26
|
+
private getCoinDecimals;
|
|
27
|
+
private fetchAllTransactionBlocks;
|
|
28
|
+
private parseTransaction;
|
|
29
|
+
private coinTypeToSymbol;
|
|
30
|
+
}
|
package/lib/const.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Asset } from '@xchainjs/xchain-util';
|
|
2
|
+
import { SUIClientParams } from './types';
|
|
3
|
+
export declare const SUIChain: "SUI";
|
|
4
|
+
export declare const SUI_DECIMALS = 9;
|
|
5
|
+
export declare const SUIAsset: Asset;
|
|
6
|
+
export declare const SUI_TYPE_TAG = "0x2::sui::SUI";
|
|
7
|
+
export declare const defaultSuiParams: SUIClientParams;
|
package/lib/index.d.ts
ADDED
package/lib/index.esm.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { SuiClient } from '@mysten/sui/client';
|
|
2
|
+
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
|
|
3
|
+
import { Transaction } from '@mysten/sui/transactions';
|
|
4
|
+
import { isValidSuiAddress } from '@mysten/sui/utils';
|
|
5
|
+
import { ExplorerProvider, Network, BaseXChainClient, FeeType, FeeOption, TxType } from '@xchainjs/xchain-client';
|
|
6
|
+
import { getSeed } from '@xchainjs/xchain-crypto';
|
|
7
|
+
import { AssetType, baseAmount, assetFromStringEx, eqAsset, getContractAddressFromAsset } from '@xchainjs/xchain-util';
|
|
8
|
+
import slip10 from 'micro-key-producer/slip10.js';
|
|
9
|
+
|
|
10
|
+
/******************************************************************************
|
|
11
|
+
Copyright (c) Microsoft Corporation.
|
|
12
|
+
|
|
13
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
14
|
+
purpose with or without fee is hereby granted.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
17
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
18
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
19
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
20
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
21
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
22
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
23
|
+
***************************************************************************** */
|
|
24
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
function __awaiter(thisArg, _arguments, P, generator) {
|
|
28
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
29
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
30
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
31
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
32
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
33
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
38
|
+
var e = new Error(message);
|
|
39
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const SUIChain = 'SUI';
|
|
43
|
+
const SUI_DECIMALS = 9;
|
|
44
|
+
const SUIAsset = {
|
|
45
|
+
chain: SUIChain,
|
|
46
|
+
ticker: 'SUI',
|
|
47
|
+
symbol: 'SUI',
|
|
48
|
+
type: AssetType.NATIVE,
|
|
49
|
+
};
|
|
50
|
+
const SUI_TYPE_TAG = '0x2::sui::SUI';
|
|
51
|
+
const mainnetExplorer = new ExplorerProvider('https://suiscan.xyz/mainnet', 'https://suiscan.xyz/mainnet/account/%%ADDRESS%%', 'https://suiscan.xyz/mainnet/tx/%%TX_ID%%');
|
|
52
|
+
const defaultSuiParams = {
|
|
53
|
+
network: Network.Mainnet,
|
|
54
|
+
rootDerivationPaths: {
|
|
55
|
+
[Network.Mainnet]: "m/44'/784'/",
|
|
56
|
+
[Network.Testnet]: "m/44'/784'/",
|
|
57
|
+
[Network.Stagenet]: "m/44'/784'/",
|
|
58
|
+
},
|
|
59
|
+
explorerProviders: {
|
|
60
|
+
[Network.Mainnet]: mainnetExplorer,
|
|
61
|
+
[Network.Testnet]: new ExplorerProvider('https://suiscan.xyz/testnet', 'https://suiscan.xyz/testnet/account/%%ADDRESS%%', 'https://suiscan.xyz/testnet/tx/%%TX_ID%%'),
|
|
62
|
+
[Network.Stagenet]: mainnetExplorer,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const getDefaultClientUrl = (network) => {
|
|
67
|
+
const networkMap = {
|
|
68
|
+
[Network.Mainnet]: 'https://fullnode.mainnet.sui.io:443',
|
|
69
|
+
[Network.Stagenet]: 'https://fullnode.mainnet.sui.io:443',
|
|
70
|
+
[Network.Testnet]: 'https://fullnode.testnet.sui.io:443',
|
|
71
|
+
};
|
|
72
|
+
return networkMap[network];
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
class Client extends BaseXChainClient {
|
|
76
|
+
constructor(params = defaultSuiParams) {
|
|
77
|
+
var _a;
|
|
78
|
+
const mergedParams = Object.assign(Object.assign({}, defaultSuiParams), params);
|
|
79
|
+
super(SUIChain, mergedParams);
|
|
80
|
+
this.explorerProviders = mergedParams.explorerProviders;
|
|
81
|
+
this.clientUrls = mergedParams.clientUrls;
|
|
82
|
+
this.suiClient = new SuiClient({
|
|
83
|
+
url: ((_a = this.clientUrls) === null || _a === void 0 ? void 0 : _a[this.getNetwork()]) || getDefaultClientUrl(this.getNetwork()),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
setNetwork(network) {
|
|
87
|
+
var _a;
|
|
88
|
+
super.setNetwork(network);
|
|
89
|
+
this.suiClient = new SuiClient({
|
|
90
|
+
url: ((_a = this.clientUrls) === null || _a === void 0 ? void 0 : _a[this.getNetwork()]) || getDefaultClientUrl(this.getNetwork()),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
getAssetInfo() {
|
|
94
|
+
return {
|
|
95
|
+
asset: SUIAsset,
|
|
96
|
+
decimal: SUI_DECIMALS,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
getExplorerUrl() {
|
|
100
|
+
return this.explorerProviders[this.getNetwork()].getExplorerUrl();
|
|
101
|
+
}
|
|
102
|
+
getExplorerAddressUrl(address) {
|
|
103
|
+
return this.explorerProviders[this.getNetwork()].getExplorerAddressUrl(address);
|
|
104
|
+
}
|
|
105
|
+
getExplorerTxUrl(txID) {
|
|
106
|
+
return this.explorerProviders[this.getNetwork()].getExplorerTxUrl(txID);
|
|
107
|
+
}
|
|
108
|
+
getFullDerivationPath(walletIndex) {
|
|
109
|
+
if (!this.rootDerivationPaths) {
|
|
110
|
+
throw Error('Can not generate derivation path due to root derivation path is undefined');
|
|
111
|
+
}
|
|
112
|
+
return `${this.rootDerivationPaths[this.getNetwork()]}${walletIndex}'/0'`;
|
|
113
|
+
}
|
|
114
|
+
getAddressAsync(index) {
|
|
115
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
116
|
+
return this.getKeypair(index || 0).getPublicKey().toSuiAddress();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
getAddress() {
|
|
120
|
+
throw Error('Sync method not supported');
|
|
121
|
+
}
|
|
122
|
+
validateAddress(address) {
|
|
123
|
+
return isValidSuiAddress(address);
|
|
124
|
+
}
|
|
125
|
+
getBalance(address, assets) {
|
|
126
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
127
|
+
const balances = [];
|
|
128
|
+
// Get native SUI balance
|
|
129
|
+
const suiBalance = yield this.suiClient.getBalance({ owner: address });
|
|
130
|
+
balances.push({
|
|
131
|
+
asset: SUIAsset,
|
|
132
|
+
amount: baseAmount(suiBalance.totalBalance, SUI_DECIMALS),
|
|
133
|
+
});
|
|
134
|
+
// Get all coin balances if no specific assets requested, or filter by requested assets
|
|
135
|
+
const allBalances = yield this.suiClient.getAllBalances({ owner: address });
|
|
136
|
+
for (const coinBalance of allBalances) {
|
|
137
|
+
// Skip native SUI (already added)
|
|
138
|
+
if (coinBalance.coinType === SUI_TYPE_TAG)
|
|
139
|
+
continue;
|
|
140
|
+
const tokenAsset = assetFromStringEx(`SUI.${this.coinTypeToSymbol(coinBalance.coinType)}`);
|
|
141
|
+
if (assets && !assets.some((a) => eqAsset(a, tokenAsset)))
|
|
142
|
+
continue;
|
|
143
|
+
const decimals = yield this.getCoinDecimals(coinBalance.coinType);
|
|
144
|
+
balances.push({
|
|
145
|
+
asset: tokenAsset,
|
|
146
|
+
amount: baseAmount(coinBalance.totalBalance, decimals),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return balances;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
getFees() {
|
|
153
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
154
|
+
// SUI uses a gas-based fee model. Reference gas price from the network.
|
|
155
|
+
const gasPrice = yield this.suiClient.getReferenceGasPrice();
|
|
156
|
+
const baseFee = BigInt(gasPrice) * BigInt(2000); // Estimate for a simple transfer
|
|
157
|
+
return {
|
|
158
|
+
type: FeeType.FlatFee,
|
|
159
|
+
[FeeOption.Average]: baseAmount(baseFee.toString(), SUI_DECIMALS),
|
|
160
|
+
[FeeOption.Fast]: baseAmount(baseFee.toString(), SUI_DECIMALS),
|
|
161
|
+
[FeeOption.Fastest]: baseAmount(baseFee.toString(), SUI_DECIMALS),
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
getTransactionData(txId) {
|
|
166
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
167
|
+
const txResponse = yield this.suiClient.getTransactionBlock({
|
|
168
|
+
digest: txId,
|
|
169
|
+
options: {
|
|
170
|
+
showInput: true,
|
|
171
|
+
showEffects: true,
|
|
172
|
+
showBalanceChanges: true,
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
return yield this.parseTransaction(txResponse);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
getTransactions(params) {
|
|
179
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
180
|
+
const address = (params === null || params === void 0 ? void 0 : params.address) || (yield this.getAddressAsync());
|
|
181
|
+
const limit = (params === null || params === void 0 ? void 0 : params.limit) || 50;
|
|
182
|
+
// Fetch all pages for FromAddress
|
|
183
|
+
const fromResponses = yield this.fetchAllTransactionBlocks({ FromAddress: address }, limit);
|
|
184
|
+
// Fetch all pages for ToAddress
|
|
185
|
+
const toResponses = yield this.fetchAllTransactionBlocks({ ToAddress: address }, limit);
|
|
186
|
+
// Merge and deduplicate by digest
|
|
187
|
+
const seen = new Set();
|
|
188
|
+
const allTxResponses = [];
|
|
189
|
+
for (const tx of [...fromResponses, ...toResponses]) {
|
|
190
|
+
if (!seen.has(tx.digest)) {
|
|
191
|
+
seen.add(tx.digest);
|
|
192
|
+
allTxResponses.push(tx);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Sort by timestamp descending
|
|
196
|
+
allTxResponses.sort((a, b) => {
|
|
197
|
+
const timeA = Number(a.timestampMs || 0);
|
|
198
|
+
const timeB = Number(b.timestampMs || 0);
|
|
199
|
+
return timeB - timeA;
|
|
200
|
+
});
|
|
201
|
+
const txs = [];
|
|
202
|
+
for (const txResponse of allTxResponses) {
|
|
203
|
+
try {
|
|
204
|
+
txs.push(yield this.parseTransaction(txResponse));
|
|
205
|
+
}
|
|
206
|
+
catch (_a) {
|
|
207
|
+
// Skip unparseable transactions
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const offset = (params === null || params === void 0 ? void 0 : params.offset) || 0;
|
|
211
|
+
const paged = txs.slice(offset, offset + limit);
|
|
212
|
+
return {
|
|
213
|
+
txs: paged,
|
|
214
|
+
total: txs.length,
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
transfer(_a) {
|
|
219
|
+
return __awaiter(this, arguments, void 0, function* ({ walletIndex, recipient, asset, amount, memo, gasBudget }) {
|
|
220
|
+
var _b, _c, _d, _e;
|
|
221
|
+
if (memo)
|
|
222
|
+
throw Error('Memo is not supported for SUI transfers');
|
|
223
|
+
const keypair = this.getKeypair(walletIndex || 0);
|
|
224
|
+
const tx = new Transaction();
|
|
225
|
+
if (!asset || eqAsset(asset, SUIAsset)) {
|
|
226
|
+
// Native SUI transfer
|
|
227
|
+
const [coin] = tx.splitCoins(tx.gas, [amount.amount().toString()]);
|
|
228
|
+
tx.transferObjects([coin], recipient);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
// Token transfer
|
|
232
|
+
const coinType = getContractAddressFromAsset(asset);
|
|
233
|
+
const coins = yield this.suiClient.getCoins({
|
|
234
|
+
owner: keypair.getPublicKey().toSuiAddress(),
|
|
235
|
+
coinType,
|
|
236
|
+
});
|
|
237
|
+
if (coins.data.length === 0) {
|
|
238
|
+
throw Error('No coins found for the specified asset');
|
|
239
|
+
}
|
|
240
|
+
// Merge all coins into the first one if there are multiple
|
|
241
|
+
const primaryCoin = tx.object(coins.data[0].coinObjectId);
|
|
242
|
+
if (coins.data.length > 1) {
|
|
243
|
+
tx.mergeCoins(primaryCoin, coins.data.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
244
|
+
}
|
|
245
|
+
const [splitCoin] = tx.splitCoins(primaryCoin, [amount.amount().toString()]);
|
|
246
|
+
tx.transferObjects([splitCoin], recipient);
|
|
247
|
+
}
|
|
248
|
+
if (gasBudget) {
|
|
249
|
+
tx.setGasBudget(gasBudget.amount().toNumber());
|
|
250
|
+
}
|
|
251
|
+
const result = yield this.suiClient.signAndExecuteTransaction({
|
|
252
|
+
signer: keypair,
|
|
253
|
+
transaction: tx,
|
|
254
|
+
options: { showEffects: true },
|
|
255
|
+
});
|
|
256
|
+
if (((_c = (_b = result.effects) === null || _b === void 0 ? void 0 : _b.status) === null || _c === void 0 ? void 0 : _c.status) !== 'success') {
|
|
257
|
+
throw Error(`Transaction failed: ${((_e = (_d = result.effects) === null || _d === void 0 ? void 0 : _d.status) === null || _e === void 0 ? void 0 : _e.error) || 'unknown error'}`);
|
|
258
|
+
}
|
|
259
|
+
yield this.suiClient.waitForTransaction({ digest: result.digest });
|
|
260
|
+
return result.digest;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
broadcastTx(txHex) {
|
|
264
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
265
|
+
var _a, _b, _c, _d;
|
|
266
|
+
const txBytes = Uint8Array.from(Buffer.from(txHex, 'hex'));
|
|
267
|
+
const keypair = this.getKeypair(0);
|
|
268
|
+
const { signature, bytes } = yield keypair.signTransaction(txBytes);
|
|
269
|
+
const result = yield this.suiClient.executeTransactionBlock({
|
|
270
|
+
transactionBlock: bytes,
|
|
271
|
+
signature,
|
|
272
|
+
options: { showEffects: true },
|
|
273
|
+
});
|
|
274
|
+
if (((_b = (_a = result.effects) === null || _a === void 0 ? void 0 : _a.status) === null || _b === void 0 ? void 0 : _b.status) !== 'success') {
|
|
275
|
+
throw Error(`Transaction failed: ${((_d = (_c = result.effects) === null || _c === void 0 ? void 0 : _c.status) === null || _d === void 0 ? void 0 : _d.error) || 'unknown error'}`);
|
|
276
|
+
}
|
|
277
|
+
return result.digest;
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
prepareTx(_a) {
|
|
281
|
+
return __awaiter(this, arguments, void 0, function* ({ walletIndex, memo, recipient, asset, amount, gasBudget, }) {
|
|
282
|
+
if (memo)
|
|
283
|
+
throw Error('Memo is not supported for SUI transfers');
|
|
284
|
+
const sender = yield this.getAddressAsync(walletIndex !== null && walletIndex !== void 0 ? walletIndex : 0);
|
|
285
|
+
const tx = new Transaction();
|
|
286
|
+
tx.setSender(sender);
|
|
287
|
+
if (!asset || eqAsset(asset, SUIAsset)) {
|
|
288
|
+
const [coin] = tx.splitCoins(tx.gas, [amount.amount().toString()]);
|
|
289
|
+
tx.transferObjects([coin], recipient);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
const coinType = getContractAddressFromAsset(asset);
|
|
293
|
+
const coins = yield this.suiClient.getCoins({
|
|
294
|
+
owner: sender,
|
|
295
|
+
coinType,
|
|
296
|
+
});
|
|
297
|
+
if (coins.data.length === 0) {
|
|
298
|
+
throw Error('No coins found for the specified asset');
|
|
299
|
+
}
|
|
300
|
+
const primaryCoin = tx.object(coins.data[0].coinObjectId);
|
|
301
|
+
if (coins.data.length > 1) {
|
|
302
|
+
tx.mergeCoins(primaryCoin, coins.data.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
303
|
+
}
|
|
304
|
+
const [splitCoin] = tx.splitCoins(primaryCoin, [amount.amount().toString()]);
|
|
305
|
+
tx.transferObjects([splitCoin], recipient);
|
|
306
|
+
}
|
|
307
|
+
if (gasBudget) {
|
|
308
|
+
tx.setGasBudget(gasBudget.amount().toNumber());
|
|
309
|
+
}
|
|
310
|
+
const builtTx = yield tx.build({ client: this.suiClient });
|
|
311
|
+
return { rawUnsignedTx: Buffer.from(builtTx).toString('hex') };
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
getKeypair(index) {
|
|
315
|
+
if (!this.phrase)
|
|
316
|
+
throw new Error('Phrase must be provided');
|
|
317
|
+
const seed = getSeed(this.phrase);
|
|
318
|
+
const hd = slip10.fromMasterSeed(seed);
|
|
319
|
+
const derived = hd.derive(this.getFullDerivationPath(index));
|
|
320
|
+
return Ed25519Keypair.fromSecretKey(derived.privateKey);
|
|
321
|
+
}
|
|
322
|
+
getCoinDecimals(coinType) {
|
|
323
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
324
|
+
var _a;
|
|
325
|
+
if (coinType === SUI_TYPE_TAG)
|
|
326
|
+
return SUI_DECIMALS;
|
|
327
|
+
try {
|
|
328
|
+
const metadata = yield this.suiClient.getCoinMetadata({ coinType });
|
|
329
|
+
return (_a = metadata === null || metadata === void 0 ? void 0 : metadata.decimals) !== null && _a !== void 0 ? _a : SUI_DECIMALS;
|
|
330
|
+
}
|
|
331
|
+
catch (_b) {
|
|
332
|
+
return SUI_DECIMALS;
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
fetchAllTransactionBlocks(filter, maxResults) {
|
|
337
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
338
|
+
const results = [];
|
|
339
|
+
let cursor = undefined;
|
|
340
|
+
const pageSize = Math.min(maxResults, 50);
|
|
341
|
+
do {
|
|
342
|
+
const page = yield this.suiClient.queryTransactionBlocks({
|
|
343
|
+
filter,
|
|
344
|
+
options: {
|
|
345
|
+
showInput: true,
|
|
346
|
+
showEffects: true,
|
|
347
|
+
showBalanceChanges: true,
|
|
348
|
+
},
|
|
349
|
+
limit: pageSize,
|
|
350
|
+
cursor: cursor !== null && cursor !== void 0 ? cursor : undefined,
|
|
351
|
+
});
|
|
352
|
+
results.push(...page.data);
|
|
353
|
+
cursor = page.nextCursor;
|
|
354
|
+
if (!page.hasNextPage || results.length >= maxResults)
|
|
355
|
+
break;
|
|
356
|
+
} while (cursor);
|
|
357
|
+
return results.slice(0, maxResults);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
parseTransaction(txResponse) {
|
|
361
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
362
|
+
const from = [];
|
|
363
|
+
const to = [];
|
|
364
|
+
if (txResponse.balanceChanges) {
|
|
365
|
+
for (const change of txResponse.balanceChanges) {
|
|
366
|
+
const isSui = change.coinType === SUI_TYPE_TAG;
|
|
367
|
+
const decimals = yield this.getCoinDecimals(change.coinType);
|
|
368
|
+
const asset = isSui
|
|
369
|
+
? SUIAsset
|
|
370
|
+
: assetFromStringEx(`SUI.${this.coinTypeToSymbol(change.coinType)}`);
|
|
371
|
+
const changeAmount = BigInt(change.amount);
|
|
372
|
+
const ownerAddress = change.owner && typeof change.owner === 'object' && 'AddressOwner' in change.owner
|
|
373
|
+
? change.owner.AddressOwner
|
|
374
|
+
: '';
|
|
375
|
+
if (changeAmount < BigInt(0)) {
|
|
376
|
+
from.push({
|
|
377
|
+
from: ownerAddress,
|
|
378
|
+
amount: baseAmount((-changeAmount).toString(), decimals),
|
|
379
|
+
asset,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
else if (changeAmount > BigInt(0)) {
|
|
383
|
+
to.push({
|
|
384
|
+
to: ownerAddress,
|
|
385
|
+
amount: baseAmount(changeAmount.toString(), decimals),
|
|
386
|
+
asset,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
asset: SUIAsset,
|
|
393
|
+
date: new Date(Number(txResponse.timestampMs || 0)),
|
|
394
|
+
type: TxType.Transfer,
|
|
395
|
+
hash: txResponse.digest,
|
|
396
|
+
from,
|
|
397
|
+
to,
|
|
398
|
+
};
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
coinTypeToSymbol(coinType) {
|
|
402
|
+
// coinType format: "0xpackage::module::Type"
|
|
403
|
+
// Convert to symbol format for xchainjs: "TYPE-0xpackage::module::Type"
|
|
404
|
+
const parts = coinType.split('::');
|
|
405
|
+
const typeName = parts[parts.length - 1] || coinType;
|
|
406
|
+
return `${typeName}-${coinType}`;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export { Client, SUIAsset, SUIChain, SUI_DECIMALS, SUI_TYPE_TAG, defaultSuiParams };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var client = require('@mysten/sui/client');
|
|
4
|
+
var ed25519 = require('@mysten/sui/keypairs/ed25519');
|
|
5
|
+
var transactions = require('@mysten/sui/transactions');
|
|
6
|
+
var utils = require('@mysten/sui/utils');
|
|
7
|
+
var xchainClient = require('@xchainjs/xchain-client');
|
|
8
|
+
var xchainCrypto = require('@xchainjs/xchain-crypto');
|
|
9
|
+
var xchainUtil = require('@xchainjs/xchain-util');
|
|
10
|
+
var slip10 = require('micro-key-producer/slip10.js');
|
|
11
|
+
|
|
12
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
|
+
|
|
14
|
+
var slip10__default = /*#__PURE__*/_interopDefault(slip10);
|
|
15
|
+
|
|
16
|
+
/******************************************************************************
|
|
17
|
+
Copyright (c) Microsoft Corporation.
|
|
18
|
+
|
|
19
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
20
|
+
purpose with or without fee is hereby granted.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
23
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
24
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
25
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
26
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
27
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
28
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
29
|
+
***************************************************************************** */
|
|
30
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
function __awaiter(thisArg, _arguments, P, generator) {
|
|
34
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
35
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
36
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
37
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
38
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
39
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
44
|
+
var e = new Error(message);
|
|
45
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const SUIChain = 'SUI';
|
|
49
|
+
const SUI_DECIMALS = 9;
|
|
50
|
+
const SUIAsset = {
|
|
51
|
+
chain: SUIChain,
|
|
52
|
+
ticker: 'SUI',
|
|
53
|
+
symbol: 'SUI',
|
|
54
|
+
type: xchainUtil.AssetType.NATIVE,
|
|
55
|
+
};
|
|
56
|
+
const SUI_TYPE_TAG = '0x2::sui::SUI';
|
|
57
|
+
const mainnetExplorer = new xchainClient.ExplorerProvider('https://suiscan.xyz/mainnet', 'https://suiscan.xyz/mainnet/account/%%ADDRESS%%', 'https://suiscan.xyz/mainnet/tx/%%TX_ID%%');
|
|
58
|
+
const defaultSuiParams = {
|
|
59
|
+
network: xchainClient.Network.Mainnet,
|
|
60
|
+
rootDerivationPaths: {
|
|
61
|
+
[xchainClient.Network.Mainnet]: "m/44'/784'/",
|
|
62
|
+
[xchainClient.Network.Testnet]: "m/44'/784'/",
|
|
63
|
+
[xchainClient.Network.Stagenet]: "m/44'/784'/",
|
|
64
|
+
},
|
|
65
|
+
explorerProviders: {
|
|
66
|
+
[xchainClient.Network.Mainnet]: mainnetExplorer,
|
|
67
|
+
[xchainClient.Network.Testnet]: new xchainClient.ExplorerProvider('https://suiscan.xyz/testnet', 'https://suiscan.xyz/testnet/account/%%ADDRESS%%', 'https://suiscan.xyz/testnet/tx/%%TX_ID%%'),
|
|
68
|
+
[xchainClient.Network.Stagenet]: mainnetExplorer,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const getDefaultClientUrl = (network) => {
|
|
73
|
+
const networkMap = {
|
|
74
|
+
[xchainClient.Network.Mainnet]: 'https://fullnode.mainnet.sui.io:443',
|
|
75
|
+
[xchainClient.Network.Stagenet]: 'https://fullnode.mainnet.sui.io:443',
|
|
76
|
+
[xchainClient.Network.Testnet]: 'https://fullnode.testnet.sui.io:443',
|
|
77
|
+
};
|
|
78
|
+
return networkMap[network];
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
class Client extends xchainClient.BaseXChainClient {
|
|
82
|
+
constructor(params = defaultSuiParams) {
|
|
83
|
+
var _a;
|
|
84
|
+
const mergedParams = Object.assign(Object.assign({}, defaultSuiParams), params);
|
|
85
|
+
super(SUIChain, mergedParams);
|
|
86
|
+
this.explorerProviders = mergedParams.explorerProviders;
|
|
87
|
+
this.clientUrls = mergedParams.clientUrls;
|
|
88
|
+
this.suiClient = new client.SuiClient({
|
|
89
|
+
url: ((_a = this.clientUrls) === null || _a === void 0 ? void 0 : _a[this.getNetwork()]) || getDefaultClientUrl(this.getNetwork()),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
setNetwork(network) {
|
|
93
|
+
var _a;
|
|
94
|
+
super.setNetwork(network);
|
|
95
|
+
this.suiClient = new client.SuiClient({
|
|
96
|
+
url: ((_a = this.clientUrls) === null || _a === void 0 ? void 0 : _a[this.getNetwork()]) || getDefaultClientUrl(this.getNetwork()),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
getAssetInfo() {
|
|
100
|
+
return {
|
|
101
|
+
asset: SUIAsset,
|
|
102
|
+
decimal: SUI_DECIMALS,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
getExplorerUrl() {
|
|
106
|
+
return this.explorerProviders[this.getNetwork()].getExplorerUrl();
|
|
107
|
+
}
|
|
108
|
+
getExplorerAddressUrl(address) {
|
|
109
|
+
return this.explorerProviders[this.getNetwork()].getExplorerAddressUrl(address);
|
|
110
|
+
}
|
|
111
|
+
getExplorerTxUrl(txID) {
|
|
112
|
+
return this.explorerProviders[this.getNetwork()].getExplorerTxUrl(txID);
|
|
113
|
+
}
|
|
114
|
+
getFullDerivationPath(walletIndex) {
|
|
115
|
+
if (!this.rootDerivationPaths) {
|
|
116
|
+
throw Error('Can not generate derivation path due to root derivation path is undefined');
|
|
117
|
+
}
|
|
118
|
+
return `${this.rootDerivationPaths[this.getNetwork()]}${walletIndex}'/0'`;
|
|
119
|
+
}
|
|
120
|
+
getAddressAsync(index) {
|
|
121
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
122
|
+
return this.getKeypair(index || 0).getPublicKey().toSuiAddress();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
getAddress() {
|
|
126
|
+
throw Error('Sync method not supported');
|
|
127
|
+
}
|
|
128
|
+
validateAddress(address) {
|
|
129
|
+
return utils.isValidSuiAddress(address);
|
|
130
|
+
}
|
|
131
|
+
getBalance(address, assets) {
|
|
132
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
133
|
+
const balances = [];
|
|
134
|
+
// Get native SUI balance
|
|
135
|
+
const suiBalance = yield this.suiClient.getBalance({ owner: address });
|
|
136
|
+
balances.push({
|
|
137
|
+
asset: SUIAsset,
|
|
138
|
+
amount: xchainUtil.baseAmount(suiBalance.totalBalance, SUI_DECIMALS),
|
|
139
|
+
});
|
|
140
|
+
// Get all coin balances if no specific assets requested, or filter by requested assets
|
|
141
|
+
const allBalances = yield this.suiClient.getAllBalances({ owner: address });
|
|
142
|
+
for (const coinBalance of allBalances) {
|
|
143
|
+
// Skip native SUI (already added)
|
|
144
|
+
if (coinBalance.coinType === SUI_TYPE_TAG)
|
|
145
|
+
continue;
|
|
146
|
+
const tokenAsset = xchainUtil.assetFromStringEx(`SUI.${this.coinTypeToSymbol(coinBalance.coinType)}`);
|
|
147
|
+
if (assets && !assets.some((a) => xchainUtil.eqAsset(a, tokenAsset)))
|
|
148
|
+
continue;
|
|
149
|
+
const decimals = yield this.getCoinDecimals(coinBalance.coinType);
|
|
150
|
+
balances.push({
|
|
151
|
+
asset: tokenAsset,
|
|
152
|
+
amount: xchainUtil.baseAmount(coinBalance.totalBalance, decimals),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return balances;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
getFees() {
|
|
159
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
160
|
+
// SUI uses a gas-based fee model. Reference gas price from the network.
|
|
161
|
+
const gasPrice = yield this.suiClient.getReferenceGasPrice();
|
|
162
|
+
const baseFee = BigInt(gasPrice) * BigInt(2000); // Estimate for a simple transfer
|
|
163
|
+
return {
|
|
164
|
+
type: xchainClient.FeeType.FlatFee,
|
|
165
|
+
[xchainClient.FeeOption.Average]: xchainUtil.baseAmount(baseFee.toString(), SUI_DECIMALS),
|
|
166
|
+
[xchainClient.FeeOption.Fast]: xchainUtil.baseAmount(baseFee.toString(), SUI_DECIMALS),
|
|
167
|
+
[xchainClient.FeeOption.Fastest]: xchainUtil.baseAmount(baseFee.toString(), SUI_DECIMALS),
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
getTransactionData(txId) {
|
|
172
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
173
|
+
const txResponse = yield this.suiClient.getTransactionBlock({
|
|
174
|
+
digest: txId,
|
|
175
|
+
options: {
|
|
176
|
+
showInput: true,
|
|
177
|
+
showEffects: true,
|
|
178
|
+
showBalanceChanges: true,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
return yield this.parseTransaction(txResponse);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
getTransactions(params) {
|
|
185
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
186
|
+
const address = (params === null || params === void 0 ? void 0 : params.address) || (yield this.getAddressAsync());
|
|
187
|
+
const limit = (params === null || params === void 0 ? void 0 : params.limit) || 50;
|
|
188
|
+
// Fetch all pages for FromAddress
|
|
189
|
+
const fromResponses = yield this.fetchAllTransactionBlocks({ FromAddress: address }, limit);
|
|
190
|
+
// Fetch all pages for ToAddress
|
|
191
|
+
const toResponses = yield this.fetchAllTransactionBlocks({ ToAddress: address }, limit);
|
|
192
|
+
// Merge and deduplicate by digest
|
|
193
|
+
const seen = new Set();
|
|
194
|
+
const allTxResponses = [];
|
|
195
|
+
for (const tx of [...fromResponses, ...toResponses]) {
|
|
196
|
+
if (!seen.has(tx.digest)) {
|
|
197
|
+
seen.add(tx.digest);
|
|
198
|
+
allTxResponses.push(tx);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Sort by timestamp descending
|
|
202
|
+
allTxResponses.sort((a, b) => {
|
|
203
|
+
const timeA = Number(a.timestampMs || 0);
|
|
204
|
+
const timeB = Number(b.timestampMs || 0);
|
|
205
|
+
return timeB - timeA;
|
|
206
|
+
});
|
|
207
|
+
const txs = [];
|
|
208
|
+
for (const txResponse of allTxResponses) {
|
|
209
|
+
try {
|
|
210
|
+
txs.push(yield this.parseTransaction(txResponse));
|
|
211
|
+
}
|
|
212
|
+
catch (_a) {
|
|
213
|
+
// Skip unparseable transactions
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const offset = (params === null || params === void 0 ? void 0 : params.offset) || 0;
|
|
217
|
+
const paged = txs.slice(offset, offset + limit);
|
|
218
|
+
return {
|
|
219
|
+
txs: paged,
|
|
220
|
+
total: txs.length,
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
transfer(_a) {
|
|
225
|
+
return __awaiter(this, arguments, void 0, function* ({ walletIndex, recipient, asset, amount, memo, gasBudget }) {
|
|
226
|
+
var _b, _c, _d, _e;
|
|
227
|
+
if (memo)
|
|
228
|
+
throw Error('Memo is not supported for SUI transfers');
|
|
229
|
+
const keypair = this.getKeypair(walletIndex || 0);
|
|
230
|
+
const tx = new transactions.Transaction();
|
|
231
|
+
if (!asset || xchainUtil.eqAsset(asset, SUIAsset)) {
|
|
232
|
+
// Native SUI transfer
|
|
233
|
+
const [coin] = tx.splitCoins(tx.gas, [amount.amount().toString()]);
|
|
234
|
+
tx.transferObjects([coin], recipient);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// Token transfer
|
|
238
|
+
const coinType = xchainUtil.getContractAddressFromAsset(asset);
|
|
239
|
+
const coins = yield this.suiClient.getCoins({
|
|
240
|
+
owner: keypair.getPublicKey().toSuiAddress(),
|
|
241
|
+
coinType,
|
|
242
|
+
});
|
|
243
|
+
if (coins.data.length === 0) {
|
|
244
|
+
throw Error('No coins found for the specified asset');
|
|
245
|
+
}
|
|
246
|
+
// Merge all coins into the first one if there are multiple
|
|
247
|
+
const primaryCoin = tx.object(coins.data[0].coinObjectId);
|
|
248
|
+
if (coins.data.length > 1) {
|
|
249
|
+
tx.mergeCoins(primaryCoin, coins.data.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
250
|
+
}
|
|
251
|
+
const [splitCoin] = tx.splitCoins(primaryCoin, [amount.amount().toString()]);
|
|
252
|
+
tx.transferObjects([splitCoin], recipient);
|
|
253
|
+
}
|
|
254
|
+
if (gasBudget) {
|
|
255
|
+
tx.setGasBudget(gasBudget.amount().toNumber());
|
|
256
|
+
}
|
|
257
|
+
const result = yield this.suiClient.signAndExecuteTransaction({
|
|
258
|
+
signer: keypair,
|
|
259
|
+
transaction: tx,
|
|
260
|
+
options: { showEffects: true },
|
|
261
|
+
});
|
|
262
|
+
if (((_c = (_b = result.effects) === null || _b === void 0 ? void 0 : _b.status) === null || _c === void 0 ? void 0 : _c.status) !== 'success') {
|
|
263
|
+
throw Error(`Transaction failed: ${((_e = (_d = result.effects) === null || _d === void 0 ? void 0 : _d.status) === null || _e === void 0 ? void 0 : _e.error) || 'unknown error'}`);
|
|
264
|
+
}
|
|
265
|
+
yield this.suiClient.waitForTransaction({ digest: result.digest });
|
|
266
|
+
return result.digest;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
broadcastTx(txHex) {
|
|
270
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
271
|
+
var _a, _b, _c, _d;
|
|
272
|
+
const txBytes = Uint8Array.from(Buffer.from(txHex, 'hex'));
|
|
273
|
+
const keypair = this.getKeypair(0);
|
|
274
|
+
const { signature, bytes } = yield keypair.signTransaction(txBytes);
|
|
275
|
+
const result = yield this.suiClient.executeTransactionBlock({
|
|
276
|
+
transactionBlock: bytes,
|
|
277
|
+
signature,
|
|
278
|
+
options: { showEffects: true },
|
|
279
|
+
});
|
|
280
|
+
if (((_b = (_a = result.effects) === null || _a === void 0 ? void 0 : _a.status) === null || _b === void 0 ? void 0 : _b.status) !== 'success') {
|
|
281
|
+
throw Error(`Transaction failed: ${((_d = (_c = result.effects) === null || _c === void 0 ? void 0 : _c.status) === null || _d === void 0 ? void 0 : _d.error) || 'unknown error'}`);
|
|
282
|
+
}
|
|
283
|
+
return result.digest;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
prepareTx(_a) {
|
|
287
|
+
return __awaiter(this, arguments, void 0, function* ({ walletIndex, memo, recipient, asset, amount, gasBudget, }) {
|
|
288
|
+
if (memo)
|
|
289
|
+
throw Error('Memo is not supported for SUI transfers');
|
|
290
|
+
const sender = yield this.getAddressAsync(walletIndex !== null && walletIndex !== void 0 ? walletIndex : 0);
|
|
291
|
+
const tx = new transactions.Transaction();
|
|
292
|
+
tx.setSender(sender);
|
|
293
|
+
if (!asset || xchainUtil.eqAsset(asset, SUIAsset)) {
|
|
294
|
+
const [coin] = tx.splitCoins(tx.gas, [amount.amount().toString()]);
|
|
295
|
+
tx.transferObjects([coin], recipient);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
const coinType = xchainUtil.getContractAddressFromAsset(asset);
|
|
299
|
+
const coins = yield this.suiClient.getCoins({
|
|
300
|
+
owner: sender,
|
|
301
|
+
coinType,
|
|
302
|
+
});
|
|
303
|
+
if (coins.data.length === 0) {
|
|
304
|
+
throw Error('No coins found for the specified asset');
|
|
305
|
+
}
|
|
306
|
+
const primaryCoin = tx.object(coins.data[0].coinObjectId);
|
|
307
|
+
if (coins.data.length > 1) {
|
|
308
|
+
tx.mergeCoins(primaryCoin, coins.data.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
309
|
+
}
|
|
310
|
+
const [splitCoin] = tx.splitCoins(primaryCoin, [amount.amount().toString()]);
|
|
311
|
+
tx.transferObjects([splitCoin], recipient);
|
|
312
|
+
}
|
|
313
|
+
if (gasBudget) {
|
|
314
|
+
tx.setGasBudget(gasBudget.amount().toNumber());
|
|
315
|
+
}
|
|
316
|
+
const builtTx = yield tx.build({ client: this.suiClient });
|
|
317
|
+
return { rawUnsignedTx: Buffer.from(builtTx).toString('hex') };
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
getKeypair(index) {
|
|
321
|
+
if (!this.phrase)
|
|
322
|
+
throw new Error('Phrase must be provided');
|
|
323
|
+
const seed = xchainCrypto.getSeed(this.phrase);
|
|
324
|
+
const hd = slip10__default.default.fromMasterSeed(seed);
|
|
325
|
+
const derived = hd.derive(this.getFullDerivationPath(index));
|
|
326
|
+
return ed25519.Ed25519Keypair.fromSecretKey(derived.privateKey);
|
|
327
|
+
}
|
|
328
|
+
getCoinDecimals(coinType) {
|
|
329
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
330
|
+
var _a;
|
|
331
|
+
if (coinType === SUI_TYPE_TAG)
|
|
332
|
+
return SUI_DECIMALS;
|
|
333
|
+
try {
|
|
334
|
+
const metadata = yield this.suiClient.getCoinMetadata({ coinType });
|
|
335
|
+
return (_a = metadata === null || metadata === void 0 ? void 0 : metadata.decimals) !== null && _a !== void 0 ? _a : SUI_DECIMALS;
|
|
336
|
+
}
|
|
337
|
+
catch (_b) {
|
|
338
|
+
return SUI_DECIMALS;
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
fetchAllTransactionBlocks(filter, maxResults) {
|
|
343
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
344
|
+
const results = [];
|
|
345
|
+
let cursor = undefined;
|
|
346
|
+
const pageSize = Math.min(maxResults, 50);
|
|
347
|
+
do {
|
|
348
|
+
const page = yield this.suiClient.queryTransactionBlocks({
|
|
349
|
+
filter,
|
|
350
|
+
options: {
|
|
351
|
+
showInput: true,
|
|
352
|
+
showEffects: true,
|
|
353
|
+
showBalanceChanges: true,
|
|
354
|
+
},
|
|
355
|
+
limit: pageSize,
|
|
356
|
+
cursor: cursor !== null && cursor !== void 0 ? cursor : undefined,
|
|
357
|
+
});
|
|
358
|
+
results.push(...page.data);
|
|
359
|
+
cursor = page.nextCursor;
|
|
360
|
+
if (!page.hasNextPage || results.length >= maxResults)
|
|
361
|
+
break;
|
|
362
|
+
} while (cursor);
|
|
363
|
+
return results.slice(0, maxResults);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
parseTransaction(txResponse) {
|
|
367
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
368
|
+
const from = [];
|
|
369
|
+
const to = [];
|
|
370
|
+
if (txResponse.balanceChanges) {
|
|
371
|
+
for (const change of txResponse.balanceChanges) {
|
|
372
|
+
const isSui = change.coinType === SUI_TYPE_TAG;
|
|
373
|
+
const decimals = yield this.getCoinDecimals(change.coinType);
|
|
374
|
+
const asset = isSui
|
|
375
|
+
? SUIAsset
|
|
376
|
+
: xchainUtil.assetFromStringEx(`SUI.${this.coinTypeToSymbol(change.coinType)}`);
|
|
377
|
+
const changeAmount = BigInt(change.amount);
|
|
378
|
+
const ownerAddress = change.owner && typeof change.owner === 'object' && 'AddressOwner' in change.owner
|
|
379
|
+
? change.owner.AddressOwner
|
|
380
|
+
: '';
|
|
381
|
+
if (changeAmount < BigInt(0)) {
|
|
382
|
+
from.push({
|
|
383
|
+
from: ownerAddress,
|
|
384
|
+
amount: xchainUtil.baseAmount((-changeAmount).toString(), decimals),
|
|
385
|
+
asset,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
else if (changeAmount > BigInt(0)) {
|
|
389
|
+
to.push({
|
|
390
|
+
to: ownerAddress,
|
|
391
|
+
amount: xchainUtil.baseAmount(changeAmount.toString(), decimals),
|
|
392
|
+
asset,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
asset: SUIAsset,
|
|
399
|
+
date: new Date(Number(txResponse.timestampMs || 0)),
|
|
400
|
+
type: xchainClient.TxType.Transfer,
|
|
401
|
+
hash: txResponse.digest,
|
|
402
|
+
from,
|
|
403
|
+
to,
|
|
404
|
+
};
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
coinTypeToSymbol(coinType) {
|
|
408
|
+
// coinType format: "0xpackage::module::Type"
|
|
409
|
+
// Convert to symbol format for xchainjs: "TYPE-0xpackage::module::Type"
|
|
410
|
+
const parts = coinType.split('::');
|
|
411
|
+
const typeName = parts[parts.length - 1] || coinType;
|
|
412
|
+
return `${typeName}-${coinType}`;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
exports.Client = Client;
|
|
417
|
+
exports.SUIAsset = SUIAsset;
|
|
418
|
+
exports.SUIChain = SUIChain;
|
|
419
|
+
exports.SUI_DECIMALS = SUI_DECIMALS;
|
|
420
|
+
exports.SUI_TYPE_TAG = SUI_TYPE_TAG;
|
|
421
|
+
exports.defaultSuiParams = defaultSuiParams;
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Balance as BaseBalance, ExplorerProviders, Network, Tx as BaseTx, TxFrom as BaseTxFrom, TxParams as BaseTxParams, TxTo as BaseTxTo, TxsPage as BaseTxsPage, XChainClientParams } from '@xchainjs/xchain-client';
|
|
2
|
+
import { Asset, BaseAmount, TokenAsset } from '@xchainjs/xchain-util';
|
|
3
|
+
export type SUIClientParams = XChainClientParams & {
|
|
4
|
+
explorerProviders: ExplorerProviders;
|
|
5
|
+
clientUrls?: Record<Network, string>;
|
|
6
|
+
};
|
|
7
|
+
export type CompatibleAsset = Asset | TokenAsset;
|
|
8
|
+
export type Balance = BaseBalance & {
|
|
9
|
+
asset: CompatibleAsset;
|
|
10
|
+
};
|
|
11
|
+
export type TxParams = BaseTxParams & {
|
|
12
|
+
asset?: CompatibleAsset;
|
|
13
|
+
gasBudget?: BaseAmount;
|
|
14
|
+
};
|
|
15
|
+
export type TxFrom = BaseTxFrom & {
|
|
16
|
+
asset?: Asset | TokenAsset;
|
|
17
|
+
};
|
|
18
|
+
export type TxTo = BaseTxTo & {
|
|
19
|
+
asset?: Asset | TokenAsset;
|
|
20
|
+
};
|
|
21
|
+
export type Tx = BaseTx & {
|
|
22
|
+
asset: Asset | TokenAsset;
|
|
23
|
+
from: TxFrom[];
|
|
24
|
+
to: TxTo[];
|
|
25
|
+
};
|
|
26
|
+
export type TxsPage = BaseTxsPage & {
|
|
27
|
+
txs: Tx[];
|
|
28
|
+
};
|
package/lib/utils.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xchainjs/xchain-sui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Sui client for XChainJS",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"Sui",
|
|
7
|
+
"XChain"
|
|
8
|
+
],
|
|
9
|
+
"author": "THORChain",
|
|
10
|
+
"homepage": "https://github.com/xchainjs/xchainjs-lib",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"main": "lib/index.js",
|
|
13
|
+
"module": "lib/index.esm.js",
|
|
14
|
+
"typings": "lib/index.d.ts",
|
|
15
|
+
"directories": {
|
|
16
|
+
"lib": "lib",
|
|
17
|
+
"test": "__tests__"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"lib"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git@github.com:xchainjs/xchainjs-lib.git"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"clean": "rm -rf .turbo && rm -rf lib",
|
|
28
|
+
"build": "yarn clean && rollup -c --bundleConfigAsCjs",
|
|
29
|
+
"build:release": "yarn exec rm -rf release && yarn pack && yarn exec \"mkdir release && tar zxvf package.tgz --directory release && rm package.tgz\"",
|
|
30
|
+
"test": "jest",
|
|
31
|
+
"lint": "eslint --config ../../eslint.config.mjs \"{src,__tests__}/**/*.ts\" --fix --max-warnings 0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@mysten/sui": "^1.21.0",
|
|
35
|
+
"@xchainjs/xchain-client": "2.0.11",
|
|
36
|
+
"@xchainjs/xchain-crypto": "1.0.6",
|
|
37
|
+
"@xchainjs/xchain-util": "2.0.6",
|
|
38
|
+
"micro-key-producer": "^0.7.6"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"directory": "release/package"
|
|
43
|
+
}
|
|
44
|
+
}
|