@dynamic-labs-wallet/sui 0.0.0-beta-191.1

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/index.cjs.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./src/index";
package/index.cjs.js ADDED
@@ -0,0 +1,392 @@
1
+ 'use strict';
2
+
3
+ var browser = require('@dynamic-labs-wallet/browser');
4
+ var ed25519 = require('@mysten/sui/keypairs/ed25519');
5
+ var cryptography = require('@mysten/sui/cryptography');
6
+ var converter = require('bech32-converting');
7
+ var verify = require('@mysten/sui/verify');
8
+ var bcs = require('@mysten/sui/bcs');
9
+ var blake2b = require('@noble/hashes/blake2b');
10
+
11
+ function _extends() {
12
+ _extends = Object.assign || function assign(target) {
13
+ for(var i = 1; i < arguments.length; i++){
14
+ var source = arguments[i];
15
+ for(var key in source)if (Object.prototype.hasOwnProperty.call(source, key)) target[key] = source[key];
16
+ }
17
+ return target;
18
+ };
19
+ return _extends.apply(this, arguments);
20
+ }
21
+
22
+ const formatMessage = (message, intentScope)=>{
23
+ if (intentScope === 'TransactionData') {
24
+ const txBytes = Uint8Array.from(Buffer.from(message, 'hex'));
25
+ const intentMessage = cryptography.messageWithIntent(intentScope, txBytes);
26
+ return blake2b.blake2b(intentMessage, {
27
+ dkLen: 32
28
+ });
29
+ } else {
30
+ const encodedMessage = new TextEncoder().encode(message);
31
+ const serializedMessage = bcs.bcs.vector(bcs.bcs.u8()).serialize(encodedMessage).toBytes();
32
+ const intentMessage = cryptography.messageWithIntent(intentScope, serializedMessage);
33
+ return blake2b.blake2b(intentMessage, {
34
+ dkLen: 32
35
+ });
36
+ }
37
+ };
38
+
39
+ class DynamicSuiWalletClient extends browser.DynamicWalletClient {
40
+ async createWalletAccount({ thresholdSignatureScheme, password = undefined, onError, signedSessionId }) {
41
+ try {
42
+ let ceremonyCeremonyCompleteResolver;
43
+ const ceremonyCompletePromise = new Promise((resolve)=>{
44
+ ceremonyCeremonyCompleteResolver = resolve;
45
+ });
46
+ // Generate key shares for given threshold signature scheme (TSS)
47
+ const { rawPublicKey, clientKeyShares } = await this.keyGen({
48
+ chainName: this.chainName,
49
+ thresholdSignatureScheme,
50
+ onError,
51
+ onCeremonyComplete: (accountAddress, walletId)=>{
52
+ // update wallet map
53
+ this.walletMap[accountAddress] = _extends({}, this.walletMap[accountAddress] || {}, {
54
+ accountAddress: accountAddress,
55
+ walletId,
56
+ chainName: this.chainName,
57
+ thresholdSignatureScheme,
58
+ clientKeySharesBackupInfo: browser.getClientKeyShareBackupInfo()
59
+ });
60
+ this.logger.debug('walletMap updated for wallet', {
61
+ context: {
62
+ accountAddress,
63
+ walletId,
64
+ walletMap: this.walletMap
65
+ }
66
+ });
67
+ ceremonyCeremonyCompleteResolver(undefined);
68
+ }
69
+ });
70
+ // Wait for the ceremony to complete before proceeding
71
+ await ceremonyCompletePromise;
72
+ if (!rawPublicKey || !clientKeyShares) {
73
+ throw new Error(browser.ERROR_KEYGEN_FAILED);
74
+ }
75
+ const { accountAddress, publicKeyHex } = this.deriveAccountAddress({
76
+ rawPublicKey: rawPublicKey
77
+ });
78
+ // Update client key shares in wallet map
79
+ // warning: this might result in race condition if `onCeremonyComplete` executes at the same time
80
+ await this.setClientKeySharesToLocalStorage({
81
+ accountAddress,
82
+ clientKeyShares,
83
+ overwriteOrMerge: 'overwrite'
84
+ });
85
+ await this.storeEncryptedBackupByWalletWithRetry({
86
+ accountAddress,
87
+ clientKeyShares,
88
+ password,
89
+ signedSessionId
90
+ });
91
+ return {
92
+ accountAddress,
93
+ rawPublicKey: rawPublicKey,
94
+ publicKeyHex
95
+ };
96
+ } catch (error) {
97
+ this.logger.error(browser.ERROR_CREATE_WALLET_ACCOUNT, error);
98
+ throw new Error(browser.ERROR_CREATE_WALLET_ACCOUNT);
99
+ }
100
+ }
101
+ async getRawPublicKeyFromClientKeyShares({ chainName, clientKeyShare }) {
102
+ const chainConfig = browser.getMPCChainConfig(chainName);
103
+ const derivationPath = new Uint32Array(chainConfig.derivationPath);
104
+ const rawPublicKey = await this.derivePublicKey({
105
+ chainName,
106
+ keyShare: clientKeyShare,
107
+ derivationPath
108
+ });
109
+ return rawPublicKey;
110
+ }
111
+ /**
112
+ * Format Ed25519 signature to string that satisfies Sui signature standard
113
+ */ async formatSignature(signatureEd25519, accountAddress) {
114
+ try {
115
+ // get public key from keyshare
116
+ // TODO: handle this more gracefully from the client key shares if possible
117
+ const clientKeyShares = await this.getClientKeySharesFromLocalStorage({
118
+ accountAddress
119
+ });
120
+ const rawPublicKey = await this.getRawPublicKeyFromClientKeyShares({
121
+ chainName: this.chainName,
122
+ clientKeyShare: clientKeyShares[0]
123
+ });
124
+ const rawPublicKeyBytes = Uint8Array.from(Buffer.from(rawPublicKey, 'hex'));
125
+ const suiPublicKey = new ed25519.Ed25519PublicKey(rawPublicKeyBytes);
126
+ const serializedSignature = cryptography.toSerializedSignature({
127
+ signature: signatureEd25519,
128
+ signatureScheme: 'ED25519',
129
+ publicKey: suiPublicKey
130
+ });
131
+ return serializedSignature;
132
+ } catch (error) {
133
+ this.logger.error('Error formatting signature:', error);
134
+ throw error;
135
+ }
136
+ }
137
+ async verifyMessageSignature({ message, signature, accountAddress }) {
138
+ try {
139
+ const messageBytes = new TextEncoder().encode(message);
140
+ const verifiedPublicKey = await verify.verifyPersonalMessageSignature(messageBytes, signature);
141
+ const isVerified = verifiedPublicKey.toSuiAddress().toLowerCase() === accountAddress.toLowerCase();
142
+ if (!isVerified) {
143
+ throw new Error(browser.ERROR_VERIFY_MESSAGE_SIGNATURE);
144
+ }
145
+ } catch (error) {
146
+ this.logger.error('Error verifying signature:', error);
147
+ throw error;
148
+ }
149
+ }
150
+ async verifyTransactionSignature({ transaction, signature, senderAddress }) {
151
+ try {
152
+ const txBytes = Uint8Array.from(Buffer.from(transaction, 'hex'));
153
+ const verifiedPublicKey = await verify.verifyTransactionSignature(txBytes, signature);
154
+ const isVerified = verifiedPublicKey.toSuiAddress().toLowerCase() === senderAddress.toLowerCase();
155
+ if (!isVerified) {
156
+ throw new Error(browser.ERROR_VERIFY_TRANSACTION_SIGNATURE);
157
+ }
158
+ } catch (error) {
159
+ this.logger.error('Error verifying signature:', error);
160
+ throw error;
161
+ }
162
+ }
163
+ async signMessage({ message, accountAddress, password = undefined, signedSessionId }) {
164
+ if (!accountAddress) {
165
+ throw new Error(browser.ERROR_ACCOUNT_ADDRESS_REQUIRED);
166
+ }
167
+ try {
168
+ const formattedMessage = formatMessage(message, 'PersonalMessage');
169
+ const signatureEd25519 = await this.sign({
170
+ message: formattedMessage,
171
+ accountAddress: accountAddress,
172
+ chainName: this.chainName,
173
+ password,
174
+ signedSessionId
175
+ });
176
+ const formattedSignature = await this.formatSignature(signatureEd25519, accountAddress);
177
+ await this.verifyMessageSignature({
178
+ message,
179
+ signature: formattedSignature,
180
+ accountAddress
181
+ });
182
+ return formattedSignature;
183
+ } catch (error) {
184
+ this.logger.error(browser.ERROR_SIGN_MESSAGE, error);
185
+ throw new Error(browser.ERROR_SIGN_MESSAGE);
186
+ }
187
+ }
188
+ async signTransaction({ transaction, senderAddress, password = undefined, signedSessionId }) {
189
+ if (!senderAddress) {
190
+ throw new Error(browser.ERROR_ACCOUNT_ADDRESS_REQUIRED);
191
+ }
192
+ try {
193
+ const formattedMessage = formatMessage(transaction, 'TransactionData');
194
+ const signatureEd25519 = await this.sign({
195
+ message: formattedMessage,
196
+ accountAddress: senderAddress,
197
+ chainName: this.chainName,
198
+ password,
199
+ signedSessionId
200
+ });
201
+ const formattedSignature = await this.formatSignature(signatureEd25519, senderAddress);
202
+ await this.verifyTransactionSignature({
203
+ transaction,
204
+ signature: formattedSignature,
205
+ senderAddress
206
+ });
207
+ return formattedSignature;
208
+ } catch (error) {
209
+ this.logger.error('Error signing message:', error);
210
+ throw error;
211
+ }
212
+ }
213
+ deriveAccountAddress({ rawPublicKey }) {
214
+ const pubKeyBytes = Buffer.from(rawPublicKey, 'hex');
215
+ const publicKey = new ed25519.Ed25519PublicKey(pubKeyBytes);
216
+ const accountAddress = publicKey.toSuiAddress();
217
+ return {
218
+ accountAddress,
219
+ publicKeyHex: rawPublicKey
220
+ };
221
+ }
222
+ /**
223
+ * Converts a Sui private key from Bech32 format to a 64-character hex string.
224
+ * The output is compatible with RFC8032 Ed25519 private key format.
225
+ *
226
+ * @param suiPrivateKey - The Sui private key in Bech32 format starting with "suiprivkey1"
227
+ * @returns An object containing the private key and the private key bytes
228
+ * @throws Error if the input is not a valid Sui private key format
229
+ */ convertSuiPrivateKey(suiPrivateKey) {
230
+ if (!suiPrivateKey.startsWith('suiprivkey1')) {
231
+ this.logger.debug('Sui private key not in Bech32 format');
232
+ return {
233
+ privateKey: suiPrivateKey,
234
+ privateKeyBytes: Buffer.from(suiPrivateKey, 'hex')
235
+ };
236
+ }
237
+ try {
238
+ const suiConverter = converter('suiprivkey');
239
+ const hexKey = suiConverter.toHex(suiPrivateKey);
240
+ let cleanHex = hexKey.startsWith('0x00') ? hexKey.slice(4) : hexKey.startsWith('0x') ? hexKey.slice(2) : hexKey;
241
+ if (cleanHex.length > 64) {
242
+ cleanHex = cleanHex.slice(cleanHex.length - 64);
243
+ }
244
+ if (cleanHex.length !== 64) {
245
+ throw new Error(`Invalid output: Expected 64 characters, got ${cleanHex.length}`);
246
+ }
247
+ return {
248
+ privateKey: cleanHex.toLowerCase(),
249
+ privateKeyBytes: Buffer.from(cleanHex, 'hex')
250
+ };
251
+ } catch (error) {
252
+ if (error instanceof Error) {
253
+ throw new Error(`Failed to convert Sui private key: ${error.message}`);
254
+ }
255
+ throw new Error('Failed to convert Sui private key: Unknown error');
256
+ }
257
+ }
258
+ /**
259
+ * Gets the public key for a given private key
260
+ * @param privateKeyBytes A Buffer containing the Ed25519 private key bytes
261
+ * @returns The public key (Sui address) derived from the private key
262
+ */ getPublicKeyFromPrivateKey(privateKeyBytes) {
263
+ try {
264
+ const keypair = ed25519.Ed25519Keypair.fromSecretKey(privateKeyBytes);
265
+ const publicKey = keypair.getPublicKey();
266
+ const publicKeyBase58 = publicKey.toSuiAddress();
267
+ return publicKeyBase58;
268
+ } catch (error) {
269
+ this.logger.error('Unable to derive public key from private key. Check private key format', error instanceof Error ? error.message : 'Unknown error');
270
+ throw error;
271
+ }
272
+ }
273
+ /**
274
+ * Imports the private key for a given account address
275
+ *
276
+ * @param privateKey The private key to import, accepts both Bech32 and hex formats
277
+ * @param chainName The chain name to import the private key for
278
+ * @param thresholdSignatureScheme The threshold signature scheme to use
279
+ * @param password The password for encrypted backup shares
280
+ * @returns The account address, raw public key, and client key shares
281
+ */ async importPrivateKey({ privateKey, chainName, thresholdSignatureScheme, password = undefined, onError, signedSessionId }) {
282
+ try {
283
+ let ceremonyCeremonyCompleteResolver;
284
+ const ceremonyCompletePromise = new Promise((resolve)=>{
285
+ ceremonyCeremonyCompleteResolver = resolve;
286
+ });
287
+ const { privateKey: formattedPrivateKey, privateKeyBytes } = await this.convertSuiPrivateKey(privateKey);
288
+ const publicKey = this.getPublicKeyFromPrivateKey(privateKeyBytes);
289
+ const { rawPublicKey, clientKeyShares } = await this.importRawPrivateKey({
290
+ chainName,
291
+ privateKey: formattedPrivateKey,
292
+ thresholdSignatureScheme,
293
+ onError: (error)=>{
294
+ this.logger.error(browser.ERROR_IMPORT_PRIVATE_KEY, error);
295
+ onError == null ? void 0 : onError(error);
296
+ },
297
+ onCeremonyComplete: (accountAddress, walletId)=>{
298
+ // update wallet map
299
+ this.walletMap[accountAddress] = _extends({}, this.walletMap[accountAddress] || {}, {
300
+ accountAddress,
301
+ walletId,
302
+ chainName: this.chainName,
303
+ thresholdSignatureScheme,
304
+ clientKeySharesBackupInfo: browser.getClientKeyShareBackupInfo()
305
+ });
306
+ this.logger.debug('walletMap updated for wallet', {
307
+ context: {
308
+ accountAddress,
309
+ walletId,
310
+ walletMap: this.walletMap
311
+ }
312
+ });
313
+ ceremonyCeremonyCompleteResolver(undefined);
314
+ }
315
+ });
316
+ // Wait for the ceremony to complete before proceeding
317
+ await ceremonyCompletePromise;
318
+ if (!rawPublicKey || !clientKeyShares) {
319
+ throw new Error(browser.ERROR_IMPORT_PRIVATE_KEY);
320
+ }
321
+ const { accountAddress } = await this.deriveAccountAddress({
322
+ rawPublicKey: rawPublicKey
323
+ });
324
+ if (accountAddress !== publicKey) {
325
+ throw new Error(`Public key mismatch: derived address ${accountAddress} !== public key ${publicKey}`);
326
+ }
327
+ // Update client key shares in wallet map
328
+ // warning: this might result in race condition if `onCeremonyComplete` executes at the same time
329
+ await this.setClientKeySharesToLocalStorage({
330
+ accountAddress,
331
+ clientKeyShares,
332
+ overwriteOrMerge: 'overwrite'
333
+ });
334
+ await this.storeEncryptedBackupByWalletWithRetry({
335
+ accountAddress,
336
+ clientKeyShares,
337
+ password,
338
+ signedSessionId
339
+ });
340
+ return {
341
+ accountAddress,
342
+ rawPublicKey: rawPublicKey,
343
+ clientKeyShares
344
+ };
345
+ } catch (error) {
346
+ this.logger.error(browser.ERROR_IMPORT_PRIVATE_KEY, error);
347
+ throw new Error(browser.ERROR_IMPORT_PRIVATE_KEY);
348
+ }
349
+ }
350
+ /**
351
+ * Exports the private key for a given account address
352
+ *
353
+ * @param accountAddress The account address to export the private key for
354
+ * @param password The password for encrypted backup shares
355
+ * @returns The private key in hex format
356
+ */ async exportPrivateKey({ accountAddress, password = undefined, signedSessionId }) {
357
+ try {
358
+ const { derivedPrivateKey } = await this.exportKey({
359
+ accountAddress,
360
+ chainName: this.chainName,
361
+ password,
362
+ signedSessionId
363
+ });
364
+ if (!derivedPrivateKey) {
365
+ throw new Error('Derived private key is undefined');
366
+ }
367
+ const privateScalarHex = derivedPrivateKey.slice(0, 64);
368
+ return privateScalarHex;
369
+ } catch (error) {
370
+ this.logger.error(browser.ERROR_EXPORT_PRIVATE_KEY, error);
371
+ throw new Error(browser.ERROR_EXPORT_PRIVATE_KEY);
372
+ }
373
+ }
374
+ async getSuiWallets() {
375
+ const wallets = await this.getWallets();
376
+ const suiWallets = wallets.filter((wallet)=>wallet.chainName === 'sui');
377
+ return suiWallets;
378
+ }
379
+ constructor({ environmentId, authToken, baseApiUrl, baseMPCRelayApiUrl, baseClientRelayApiUrl, storageKey, debug }){
380
+ super({
381
+ environmentId,
382
+ authToken,
383
+ baseApiUrl,
384
+ baseMPCRelayApiUrl,
385
+ storageKey,
386
+ debug,
387
+ baseClientRelayApiUrl
388
+ }), this.chainName = 'SUI';
389
+ }
390
+ }
391
+
392
+ exports.DynamicSuiWalletClient = DynamicSuiWalletClient;
package/index.esm.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./src/index";
package/index.esm.js ADDED
@@ -0,0 +1,390 @@
1
+ import { DynamicWalletClient, getClientKeyShareBackupInfo, ERROR_KEYGEN_FAILED, ERROR_CREATE_WALLET_ACCOUNT, getMPCChainConfig, ERROR_VERIFY_MESSAGE_SIGNATURE, ERROR_VERIFY_TRANSACTION_SIGNATURE, ERROR_ACCOUNT_ADDRESS_REQUIRED, ERROR_SIGN_MESSAGE, ERROR_IMPORT_PRIVATE_KEY, ERROR_EXPORT_PRIVATE_KEY } from '@dynamic-labs-wallet/browser';
2
+ import { Ed25519PublicKey, Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
3
+ import { messageWithIntent, toSerializedSignature } from '@mysten/sui/cryptography';
4
+ import converter from 'bech32-converting';
5
+ import { verifyPersonalMessageSignature, verifyTransactionSignature } from '@mysten/sui/verify';
6
+ import { bcs } from '@mysten/sui/bcs';
7
+ import { blake2b } from '@noble/hashes/blake2b';
8
+
9
+ function _extends() {
10
+ _extends = Object.assign || function assign(target) {
11
+ for(var i = 1; i < arguments.length; i++){
12
+ var source = arguments[i];
13
+ for(var key in source)if (Object.prototype.hasOwnProperty.call(source, key)) target[key] = source[key];
14
+ }
15
+ return target;
16
+ };
17
+ return _extends.apply(this, arguments);
18
+ }
19
+
20
+ const formatMessage = (message, intentScope)=>{
21
+ if (intentScope === 'TransactionData') {
22
+ const txBytes = Uint8Array.from(Buffer.from(message, 'hex'));
23
+ const intentMessage = messageWithIntent(intentScope, txBytes);
24
+ return blake2b(intentMessage, {
25
+ dkLen: 32
26
+ });
27
+ } else {
28
+ const encodedMessage = new TextEncoder().encode(message);
29
+ const serializedMessage = bcs.vector(bcs.u8()).serialize(encodedMessage).toBytes();
30
+ const intentMessage = messageWithIntent(intentScope, serializedMessage);
31
+ return blake2b(intentMessage, {
32
+ dkLen: 32
33
+ });
34
+ }
35
+ };
36
+
37
+ class DynamicSuiWalletClient extends DynamicWalletClient {
38
+ async createWalletAccount({ thresholdSignatureScheme, password = undefined, onError, signedSessionId }) {
39
+ try {
40
+ let ceremonyCeremonyCompleteResolver;
41
+ const ceremonyCompletePromise = new Promise((resolve)=>{
42
+ ceremonyCeremonyCompleteResolver = resolve;
43
+ });
44
+ // Generate key shares for given threshold signature scheme (TSS)
45
+ const { rawPublicKey, clientKeyShares } = await this.keyGen({
46
+ chainName: this.chainName,
47
+ thresholdSignatureScheme,
48
+ onError,
49
+ onCeremonyComplete: (accountAddress, walletId)=>{
50
+ // update wallet map
51
+ this.walletMap[accountAddress] = _extends({}, this.walletMap[accountAddress] || {}, {
52
+ accountAddress: accountAddress,
53
+ walletId,
54
+ chainName: this.chainName,
55
+ thresholdSignatureScheme,
56
+ clientKeySharesBackupInfo: getClientKeyShareBackupInfo()
57
+ });
58
+ this.logger.debug('walletMap updated for wallet', {
59
+ context: {
60
+ accountAddress,
61
+ walletId,
62
+ walletMap: this.walletMap
63
+ }
64
+ });
65
+ ceremonyCeremonyCompleteResolver(undefined);
66
+ }
67
+ });
68
+ // Wait for the ceremony to complete before proceeding
69
+ await ceremonyCompletePromise;
70
+ if (!rawPublicKey || !clientKeyShares) {
71
+ throw new Error(ERROR_KEYGEN_FAILED);
72
+ }
73
+ const { accountAddress, publicKeyHex } = this.deriveAccountAddress({
74
+ rawPublicKey: rawPublicKey
75
+ });
76
+ // Update client key shares in wallet map
77
+ // warning: this might result in race condition if `onCeremonyComplete` executes at the same time
78
+ await this.setClientKeySharesToLocalStorage({
79
+ accountAddress,
80
+ clientKeyShares,
81
+ overwriteOrMerge: 'overwrite'
82
+ });
83
+ await this.storeEncryptedBackupByWalletWithRetry({
84
+ accountAddress,
85
+ clientKeyShares,
86
+ password,
87
+ signedSessionId
88
+ });
89
+ return {
90
+ accountAddress,
91
+ rawPublicKey: rawPublicKey,
92
+ publicKeyHex
93
+ };
94
+ } catch (error) {
95
+ this.logger.error(ERROR_CREATE_WALLET_ACCOUNT, error);
96
+ throw new Error(ERROR_CREATE_WALLET_ACCOUNT);
97
+ }
98
+ }
99
+ async getRawPublicKeyFromClientKeyShares({ chainName, clientKeyShare }) {
100
+ const chainConfig = getMPCChainConfig(chainName);
101
+ const derivationPath = new Uint32Array(chainConfig.derivationPath);
102
+ const rawPublicKey = await this.derivePublicKey({
103
+ chainName,
104
+ keyShare: clientKeyShare,
105
+ derivationPath
106
+ });
107
+ return rawPublicKey;
108
+ }
109
+ /**
110
+ * Format Ed25519 signature to string that satisfies Sui signature standard
111
+ */ async formatSignature(signatureEd25519, accountAddress) {
112
+ try {
113
+ // get public key from keyshare
114
+ // TODO: handle this more gracefully from the client key shares if possible
115
+ const clientKeyShares = await this.getClientKeySharesFromLocalStorage({
116
+ accountAddress
117
+ });
118
+ const rawPublicKey = await this.getRawPublicKeyFromClientKeyShares({
119
+ chainName: this.chainName,
120
+ clientKeyShare: clientKeyShares[0]
121
+ });
122
+ const rawPublicKeyBytes = Uint8Array.from(Buffer.from(rawPublicKey, 'hex'));
123
+ const suiPublicKey = new Ed25519PublicKey(rawPublicKeyBytes);
124
+ const serializedSignature = toSerializedSignature({
125
+ signature: signatureEd25519,
126
+ signatureScheme: 'ED25519',
127
+ publicKey: suiPublicKey
128
+ });
129
+ return serializedSignature;
130
+ } catch (error) {
131
+ this.logger.error('Error formatting signature:', error);
132
+ throw error;
133
+ }
134
+ }
135
+ async verifyMessageSignature({ message, signature, accountAddress }) {
136
+ try {
137
+ const messageBytes = new TextEncoder().encode(message);
138
+ const verifiedPublicKey = await verifyPersonalMessageSignature(messageBytes, signature);
139
+ const isVerified = verifiedPublicKey.toSuiAddress().toLowerCase() === accountAddress.toLowerCase();
140
+ if (!isVerified) {
141
+ throw new Error(ERROR_VERIFY_MESSAGE_SIGNATURE);
142
+ }
143
+ } catch (error) {
144
+ this.logger.error('Error verifying signature:', error);
145
+ throw error;
146
+ }
147
+ }
148
+ async verifyTransactionSignature({ transaction, signature, senderAddress }) {
149
+ try {
150
+ const txBytes = Uint8Array.from(Buffer.from(transaction, 'hex'));
151
+ const verifiedPublicKey = await verifyTransactionSignature(txBytes, signature);
152
+ const isVerified = verifiedPublicKey.toSuiAddress().toLowerCase() === senderAddress.toLowerCase();
153
+ if (!isVerified) {
154
+ throw new Error(ERROR_VERIFY_TRANSACTION_SIGNATURE);
155
+ }
156
+ } catch (error) {
157
+ this.logger.error('Error verifying signature:', error);
158
+ throw error;
159
+ }
160
+ }
161
+ async signMessage({ message, accountAddress, password = undefined, signedSessionId }) {
162
+ if (!accountAddress) {
163
+ throw new Error(ERROR_ACCOUNT_ADDRESS_REQUIRED);
164
+ }
165
+ try {
166
+ const formattedMessage = formatMessage(message, 'PersonalMessage');
167
+ const signatureEd25519 = await this.sign({
168
+ message: formattedMessage,
169
+ accountAddress: accountAddress,
170
+ chainName: this.chainName,
171
+ password,
172
+ signedSessionId
173
+ });
174
+ const formattedSignature = await this.formatSignature(signatureEd25519, accountAddress);
175
+ await this.verifyMessageSignature({
176
+ message,
177
+ signature: formattedSignature,
178
+ accountAddress
179
+ });
180
+ return formattedSignature;
181
+ } catch (error) {
182
+ this.logger.error(ERROR_SIGN_MESSAGE, error);
183
+ throw new Error(ERROR_SIGN_MESSAGE);
184
+ }
185
+ }
186
+ async signTransaction({ transaction, senderAddress, password = undefined, signedSessionId }) {
187
+ if (!senderAddress) {
188
+ throw new Error(ERROR_ACCOUNT_ADDRESS_REQUIRED);
189
+ }
190
+ try {
191
+ const formattedMessage = formatMessage(transaction, 'TransactionData');
192
+ const signatureEd25519 = await this.sign({
193
+ message: formattedMessage,
194
+ accountAddress: senderAddress,
195
+ chainName: this.chainName,
196
+ password,
197
+ signedSessionId
198
+ });
199
+ const formattedSignature = await this.formatSignature(signatureEd25519, senderAddress);
200
+ await this.verifyTransactionSignature({
201
+ transaction,
202
+ signature: formattedSignature,
203
+ senderAddress
204
+ });
205
+ return formattedSignature;
206
+ } catch (error) {
207
+ this.logger.error('Error signing message:', error);
208
+ throw error;
209
+ }
210
+ }
211
+ deriveAccountAddress({ rawPublicKey }) {
212
+ const pubKeyBytes = Buffer.from(rawPublicKey, 'hex');
213
+ const publicKey = new Ed25519PublicKey(pubKeyBytes);
214
+ const accountAddress = publicKey.toSuiAddress();
215
+ return {
216
+ accountAddress,
217
+ publicKeyHex: rawPublicKey
218
+ };
219
+ }
220
+ /**
221
+ * Converts a Sui private key from Bech32 format to a 64-character hex string.
222
+ * The output is compatible with RFC8032 Ed25519 private key format.
223
+ *
224
+ * @param suiPrivateKey - The Sui private key in Bech32 format starting with "suiprivkey1"
225
+ * @returns An object containing the private key and the private key bytes
226
+ * @throws Error if the input is not a valid Sui private key format
227
+ */ convertSuiPrivateKey(suiPrivateKey) {
228
+ if (!suiPrivateKey.startsWith('suiprivkey1')) {
229
+ this.logger.debug('Sui private key not in Bech32 format');
230
+ return {
231
+ privateKey: suiPrivateKey,
232
+ privateKeyBytes: Buffer.from(suiPrivateKey, 'hex')
233
+ };
234
+ }
235
+ try {
236
+ const suiConverter = converter('suiprivkey');
237
+ const hexKey = suiConverter.toHex(suiPrivateKey);
238
+ let cleanHex = hexKey.startsWith('0x00') ? hexKey.slice(4) : hexKey.startsWith('0x') ? hexKey.slice(2) : hexKey;
239
+ if (cleanHex.length > 64) {
240
+ cleanHex = cleanHex.slice(cleanHex.length - 64);
241
+ }
242
+ if (cleanHex.length !== 64) {
243
+ throw new Error(`Invalid output: Expected 64 characters, got ${cleanHex.length}`);
244
+ }
245
+ return {
246
+ privateKey: cleanHex.toLowerCase(),
247
+ privateKeyBytes: Buffer.from(cleanHex, 'hex')
248
+ };
249
+ } catch (error) {
250
+ if (error instanceof Error) {
251
+ throw new Error(`Failed to convert Sui private key: ${error.message}`);
252
+ }
253
+ throw new Error('Failed to convert Sui private key: Unknown error');
254
+ }
255
+ }
256
+ /**
257
+ * Gets the public key for a given private key
258
+ * @param privateKeyBytes A Buffer containing the Ed25519 private key bytes
259
+ * @returns The public key (Sui address) derived from the private key
260
+ */ getPublicKeyFromPrivateKey(privateKeyBytes) {
261
+ try {
262
+ const keypair = Ed25519Keypair.fromSecretKey(privateKeyBytes);
263
+ const publicKey = keypair.getPublicKey();
264
+ const publicKeyBase58 = publicKey.toSuiAddress();
265
+ return publicKeyBase58;
266
+ } catch (error) {
267
+ this.logger.error('Unable to derive public key from private key. Check private key format', error instanceof Error ? error.message : 'Unknown error');
268
+ throw error;
269
+ }
270
+ }
271
+ /**
272
+ * Imports the private key for a given account address
273
+ *
274
+ * @param privateKey The private key to import, accepts both Bech32 and hex formats
275
+ * @param chainName The chain name to import the private key for
276
+ * @param thresholdSignatureScheme The threshold signature scheme to use
277
+ * @param password The password for encrypted backup shares
278
+ * @returns The account address, raw public key, and client key shares
279
+ */ async importPrivateKey({ privateKey, chainName, thresholdSignatureScheme, password = undefined, onError, signedSessionId }) {
280
+ try {
281
+ let ceremonyCeremonyCompleteResolver;
282
+ const ceremonyCompletePromise = new Promise((resolve)=>{
283
+ ceremonyCeremonyCompleteResolver = resolve;
284
+ });
285
+ const { privateKey: formattedPrivateKey, privateKeyBytes } = await this.convertSuiPrivateKey(privateKey);
286
+ const publicKey = this.getPublicKeyFromPrivateKey(privateKeyBytes);
287
+ const { rawPublicKey, clientKeyShares } = await this.importRawPrivateKey({
288
+ chainName,
289
+ privateKey: formattedPrivateKey,
290
+ thresholdSignatureScheme,
291
+ onError: (error)=>{
292
+ this.logger.error(ERROR_IMPORT_PRIVATE_KEY, error);
293
+ onError == null ? void 0 : onError(error);
294
+ },
295
+ onCeremonyComplete: (accountAddress, walletId)=>{
296
+ // update wallet map
297
+ this.walletMap[accountAddress] = _extends({}, this.walletMap[accountAddress] || {}, {
298
+ accountAddress,
299
+ walletId,
300
+ chainName: this.chainName,
301
+ thresholdSignatureScheme,
302
+ clientKeySharesBackupInfo: getClientKeyShareBackupInfo()
303
+ });
304
+ this.logger.debug('walletMap updated for wallet', {
305
+ context: {
306
+ accountAddress,
307
+ walletId,
308
+ walletMap: this.walletMap
309
+ }
310
+ });
311
+ ceremonyCeremonyCompleteResolver(undefined);
312
+ }
313
+ });
314
+ // Wait for the ceremony to complete before proceeding
315
+ await ceremonyCompletePromise;
316
+ if (!rawPublicKey || !clientKeyShares) {
317
+ throw new Error(ERROR_IMPORT_PRIVATE_KEY);
318
+ }
319
+ const { accountAddress } = await this.deriveAccountAddress({
320
+ rawPublicKey: rawPublicKey
321
+ });
322
+ if (accountAddress !== publicKey) {
323
+ throw new Error(`Public key mismatch: derived address ${accountAddress} !== public key ${publicKey}`);
324
+ }
325
+ // Update client key shares in wallet map
326
+ // warning: this might result in race condition if `onCeremonyComplete` executes at the same time
327
+ await this.setClientKeySharesToLocalStorage({
328
+ accountAddress,
329
+ clientKeyShares,
330
+ overwriteOrMerge: 'overwrite'
331
+ });
332
+ await this.storeEncryptedBackupByWalletWithRetry({
333
+ accountAddress,
334
+ clientKeyShares,
335
+ password,
336
+ signedSessionId
337
+ });
338
+ return {
339
+ accountAddress,
340
+ rawPublicKey: rawPublicKey,
341
+ clientKeyShares
342
+ };
343
+ } catch (error) {
344
+ this.logger.error(ERROR_IMPORT_PRIVATE_KEY, error);
345
+ throw new Error(ERROR_IMPORT_PRIVATE_KEY);
346
+ }
347
+ }
348
+ /**
349
+ * Exports the private key for a given account address
350
+ *
351
+ * @param accountAddress The account address to export the private key for
352
+ * @param password The password for encrypted backup shares
353
+ * @returns The private key in hex format
354
+ */ async exportPrivateKey({ accountAddress, password = undefined, signedSessionId }) {
355
+ try {
356
+ const { derivedPrivateKey } = await this.exportKey({
357
+ accountAddress,
358
+ chainName: this.chainName,
359
+ password,
360
+ signedSessionId
361
+ });
362
+ if (!derivedPrivateKey) {
363
+ throw new Error('Derived private key is undefined');
364
+ }
365
+ const privateScalarHex = derivedPrivateKey.slice(0, 64);
366
+ return privateScalarHex;
367
+ } catch (error) {
368
+ this.logger.error(ERROR_EXPORT_PRIVATE_KEY, error);
369
+ throw new Error(ERROR_EXPORT_PRIVATE_KEY);
370
+ }
371
+ }
372
+ async getSuiWallets() {
373
+ const wallets = await this.getWallets();
374
+ const suiWallets = wallets.filter((wallet)=>wallet.chainName === 'sui');
375
+ return suiWallets;
376
+ }
377
+ constructor({ environmentId, authToken, baseApiUrl, baseMPCRelayApiUrl, baseClientRelayApiUrl, storageKey, debug }){
378
+ super({
379
+ environmentId,
380
+ authToken,
381
+ baseApiUrl,
382
+ baseMPCRelayApiUrl,
383
+ storageKey,
384
+ debug,
385
+ baseClientRelayApiUrl
386
+ }), this.chainName = 'SUI';
387
+ }
388
+ }
389
+
390
+ export { DynamicSuiWalletClient };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@dynamic-labs-wallet/sui",
3
+ "version": "0.0.0-beta-191.1",
4
+ "license": "MIT",
5
+ "dependencies": {
6
+ "@dynamic-labs-wallet/browser": "0.0.0-beta-191.1",
7
+ "@mysten/sui": "1.26.0",
8
+ "@noble/hashes": "1.7.1",
9
+ "bech32-converting": "^1.0.9"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "nx": {
15
+ "sourceRoot": "packages/sui/src",
16
+ "projectType": "library",
17
+ "name": "sui",
18
+ "targets": {
19
+ "build": {}
20
+ }
21
+ },
22
+ "main": "./index.cjs.js",
23
+ "module": "./index.esm.js",
24
+ "types": "./index.esm.d.ts",
25
+ "exports": {
26
+ "./package.json": "./package.json",
27
+ ".": {
28
+ "types": "./index.esm.d.ts",
29
+ "import": "./index.esm.js",
30
+ "require": "./index.cjs.js",
31
+ "default": "./index.cjs.js"
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,96 @@
1
+ import { ClientKeyShare, DynamicWalletClient, ThresholdSignatureScheme, DynamicWalletClientProps } from '@dynamic-labs-wallet/browser';
2
+ export declare class DynamicSuiWalletClient extends DynamicWalletClient {
3
+ readonly chainName = "SUI";
4
+ constructor({ environmentId, authToken, baseApiUrl, baseMPCRelayApiUrl, baseClientRelayApiUrl, storageKey, debug, }: DynamicWalletClientProps);
5
+ createWalletAccount({ thresholdSignatureScheme, password, onError, signedSessionId, }: {
6
+ thresholdSignatureScheme: ThresholdSignatureScheme;
7
+ password?: string;
8
+ onError?: (error: Error) => void;
9
+ signedSessionId?: string;
10
+ }): Promise<{
11
+ accountAddress: string;
12
+ publicKeyHex: string;
13
+ rawPublicKey: string | undefined;
14
+ }>;
15
+ getRawPublicKeyFromClientKeyShares({ chainName, clientKeyShare, }: {
16
+ chainName: string;
17
+ clientKeyShare: ClientKeyShare;
18
+ }): Promise<any>;
19
+ /**
20
+ * Format Ed25519 signature to string that satisfies Sui signature standard
21
+ */
22
+ private formatSignature;
23
+ private verifyMessageSignature;
24
+ private verifyTransactionSignature;
25
+ signMessage({ message, accountAddress, password, signedSessionId, }: {
26
+ message: string;
27
+ accountAddress: string;
28
+ password?: string;
29
+ signedSessionId?: string;
30
+ }): Promise<string>;
31
+ signTransaction({ transaction, senderAddress, password, signedSessionId, }: {
32
+ transaction: string;
33
+ senderAddress: string;
34
+ password?: string;
35
+ signedSessionId?: string;
36
+ }): Promise<string>;
37
+ deriveAccountAddress({ rawPublicKey }: {
38
+ rawPublicKey: string;
39
+ }): {
40
+ accountAddress: string;
41
+ publicKeyHex: string;
42
+ };
43
+ /**
44
+ * Converts a Sui private key from Bech32 format to a 64-character hex string.
45
+ * The output is compatible with RFC8032 Ed25519 private key format.
46
+ *
47
+ * @param suiPrivateKey - The Sui private key in Bech32 format starting with "suiprivkey1"
48
+ * @returns An object containing the private key and the private key bytes
49
+ * @throws Error if the input is not a valid Sui private key format
50
+ */
51
+ convertSuiPrivateKey(suiPrivateKey: string): {
52
+ privateKey: string;
53
+ privateKeyBytes: Buffer;
54
+ };
55
+ /**
56
+ * Gets the public key for a given private key
57
+ * @param privateKeyBytes A Buffer containing the Ed25519 private key bytes
58
+ * @returns The public key (Sui address) derived from the private key
59
+ */
60
+ getPublicKeyFromPrivateKey(privateKeyBytes: Buffer): string;
61
+ /**
62
+ * Imports the private key for a given account address
63
+ *
64
+ * @param privateKey The private key to import, accepts both Bech32 and hex formats
65
+ * @param chainName The chain name to import the private key for
66
+ * @param thresholdSignatureScheme The threshold signature scheme to use
67
+ * @param password The password for encrypted backup shares
68
+ * @returns The account address, raw public key, and client key shares
69
+ */
70
+ importPrivateKey({ privateKey, chainName, thresholdSignatureScheme, password, onError, signedSessionId, }: {
71
+ privateKey: string;
72
+ chainName: string;
73
+ thresholdSignatureScheme: ThresholdSignatureScheme;
74
+ password?: string;
75
+ onError?: (error: Error) => void;
76
+ signedSessionId?: string;
77
+ }): Promise<{
78
+ accountAddress: string;
79
+ rawPublicKey: string | undefined;
80
+ clientKeyShares: ClientKeyShare[];
81
+ }>;
82
+ /**
83
+ * Exports the private key for a given account address
84
+ *
85
+ * @param accountAddress The account address to export the private key for
86
+ * @param password The password for encrypted backup shares
87
+ * @returns The private key in hex format
88
+ */
89
+ exportPrivateKey({ accountAddress, password, signedSessionId, }: {
90
+ accountAddress: string;
91
+ password?: string;
92
+ signedSessionId?: string;
93
+ }): Promise<string>;
94
+ getSuiWallets(): Promise<any>;
95
+ }
96
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ export declare const ERROR_KEYGEN_FAILED = "Error with keygen";
2
+ export declare const ERROR_CREATE_WALLET_ACCOUNT = "Error creating sui wallet account";
3
+ export declare const ERROR_IMPORT_PRIVATE_KEY = "Error importing private key";
4
+ export declare const ERROR_EXPORT_PRIVATE_KEY = "Error exporting private key";
5
+ export declare const ERROR_SIGN_MESSAGE = "Error signing message";
6
+ export declare const ERROR_ACCOUNT_ADDRESS_REQUIRED = "Account address is required";
7
+ export declare const ERROR_VERIFY_MESSAGE_SIGNATURE = "Error verifying message signature";
8
+ export declare const ERROR_VERIFY_TRANSACTION_SIGNATURE = "Error verifying transaction signature";
9
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/client/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mBAAmB,sBAAsB,CAAC;AAEvD,eAAO,MAAM,2BAA2B,sCAAsC,CAAC;AAE/E,eAAO,MAAM,wBAAwB,gCAAgC,CAAC;AAEtE,eAAO,MAAM,wBAAwB,gCAAgC,CAAC;AAEtE,eAAO,MAAM,kBAAkB,0BAA0B,CAAC;AAE1D,eAAO,MAAM,8BAA8B,gCAAgC,CAAC;AAE5E,eAAO,MAAM,8BAA8B,sCACN,CAAC;AAEtC,eAAO,MAAM,kCAAkC,0CACN,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from './client';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC"}
package/src/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './client';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../packages/src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC"}
package/src/utils.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { IntentScope } from '@mysten/sui/cryptography';
2
+ export declare const formatMessage: (message: string, intentScope: IntentScope) => Uint8Array;
3
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../packages/src/utils.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAqB,MAAM,0BAA0B,CAAC;AAG1E,eAAO,MAAM,aAAa,YACf,MAAM,eACF,WAAW,KACvB,UAeF,CAAC"}