suidouble 2.5.0 → 2.16.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/.claude/settings.local.json +7 -0
- package/README.md +222 -131
- package/index.js +0 -2
- package/lib/SuiCliCommands.js +18 -25
- package/lib/SuiCoin.js +79 -137
- package/lib/SuiCoins.js +41 -29
- package/lib/SuiCommonMethods.js +40 -3
- package/lib/SuiEvent.js +54 -6
- package/lib/SuiInBrowser.js +143 -15
- package/lib/SuiInBrowserAdapter.js +164 -37
- package/lib/SuiLocalTestValidator.js +76 -14
- package/lib/SuiMaster.js +335 -139
- package/lib/SuiMemoryObjectStorage.js +66 -73
- package/lib/SuiObject.js +128 -153
- package/lib/SuiPackage.js +292 -187
- package/lib/SuiPackageModule.js +176 -221
- package/lib/SuiPaginatedResponse.js +288 -25
- package/lib/SuiPseudoRandomAddress.js +29 -2
- package/lib/SuiTransaction.js +115 -70
- package/lib/SuiUtils.js +179 -127
- package/package.json +29 -13
- package/test/build_modules.test.js +41 -0
- package/test/coins.test.js +17 -16
- package/test/custom_transaction.test.js +167 -0
- package/test/event_listeners.test.js +171 -0
- package/test/failed_transaction.test.js +184 -0
- package/test/name_service.test.js +28 -0
- package/test/owned_objects.test.js +148 -0
- package/test/rpc.test.js +3 -6
- package/test/sui_in_browser.test.js +2 -2
- package/test/sui_master_basic.test.js +4 -5
- package/test/sui_master_onlocal.test.js +84 -22
- package/test/sui_object_properties.test.js +85 -0
- package/tsconfig.json +15 -0
- package/types/index.d.ts +15 -0
- package/types/lib/SuiCliCommands.d.ts +6 -0
- package/types/lib/SuiCoin.d.ts +183 -0
- package/types/lib/SuiCoins.d.ts +93 -0
- package/types/lib/SuiCommonMethods.d.ts +37 -0
- package/types/lib/SuiEvent.d.ts +95 -0
- package/types/lib/SuiInBrowser.d.ts +189 -0
- package/types/lib/SuiInBrowserAdapter.d.ts +167 -0
- package/types/lib/SuiLocalTestValidator.d.ts +92 -0
- package/types/lib/SuiMaster.d.ts +333 -0
- package/types/lib/SuiMemoryObjectStorage.d.ts +96 -0
- package/types/lib/SuiObject.d.ts +135 -0
- package/types/lib/SuiPackage.d.ts +233 -0
- package/types/lib/SuiPackageModule.d.ts +139 -0
- package/types/lib/SuiPaginatedResponse.d.ts +148 -0
- package/types/lib/SuiPseudoRandomAddress.d.ts +33 -0
- package/types/lib/SuiTransaction.d.ts +92 -0
- package/types/lib/SuiUtils.d.ts +152 -0
- package/types/lib/data/icons.d.ts +12 -0
- package/lib/SuiTestScenario.js +0 -169
- package/test/sui_test_scenario.test.js +0 -61
|
@@ -2,38 +2,84 @@ import SuiCommonMethods from './SuiCommonMethods.js';
|
|
|
2
2
|
import SuiEvent from './SuiEvent.js';
|
|
3
3
|
import SuiTransaction from './SuiTransaction.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @import { SuiClientTypes } from "@mysten/sui/client"
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {import('./SuiObject.js').default} SuiObject
|
|
11
|
+
* @typedef {import('./SuiMaster.js').default} SuiMaster
|
|
12
|
+
* @typedef {{ filter?: object, limit?: number }} GraphqlEventsParams
|
|
13
|
+
* @typedef {{ owner: string, type?: string, limit?: number }} GraphqlOwnedObjectsParams
|
|
14
|
+
* @typedef {{ filter?: object, limit?: number }} GraphqlTransactionsParams
|
|
15
|
+
* @typedef {SuiClientTypes.ListOwnedObjectsOptions<SuiClientTypes.ObjectInclude>
|
|
16
|
+
* | SuiClientTypes.ListCoinsOptions
|
|
17
|
+
* | SuiClientTypes.ListDynamicFieldsOptions
|
|
18
|
+
* | GraphqlEventsParams
|
|
19
|
+
* | GraphqlOwnedObjectsParams
|
|
20
|
+
* | GraphqlTransactionsParams } SuiPaginatedResponseParams
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @template {SuiObject | SuiTransaction | SuiEvent} T
|
|
25
|
+
*/
|
|
5
26
|
export default class SuiPaginatedResponse extends SuiCommonMethods {
|
|
6
|
-
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {Object} params
|
|
30
|
+
* @param {SuiMaster} params.suiMaster
|
|
31
|
+
* @param {SuiPaginatedResponseParams} params.params
|
|
32
|
+
* @param {string} [params.method]
|
|
33
|
+
* @param {string} [params.order]
|
|
34
|
+
* @param {boolean} [params.debug]
|
|
35
|
+
*/
|
|
36
|
+
constructor(params) {
|
|
7
37
|
super(params);
|
|
8
38
|
|
|
9
39
|
this._suiMaster = params.suiMaster;
|
|
10
40
|
if (!this._suiMaster) {
|
|
11
|
-
throw new Error('suiMaster is
|
|
41
|
+
throw new Error('suiMaster is required for SuiPaginatedResponse');
|
|
12
42
|
}
|
|
13
43
|
|
|
14
44
|
this._method = params.method;
|
|
45
|
+
|
|
46
|
+
/** @type {SuiPaginatedResponseParams} */
|
|
15
47
|
this._params = params.params;
|
|
16
|
-
this._order = params.order
|
|
48
|
+
this._order = SuiPaginatedResponse._normalizeOrder(params.order);
|
|
17
49
|
|
|
18
50
|
this._hasNextPage = true;
|
|
19
51
|
this._nextCursor = null;
|
|
20
52
|
|
|
53
|
+
/** @type {T[]} */
|
|
21
54
|
this._data = [];
|
|
22
55
|
}
|
|
23
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Accept 'asc' | 'ascending' | 'desc' | 'descending' (case-insensitive).
|
|
59
|
+
* Defaults to 'descending' (newest first).
|
|
60
|
+
* @param {?string} input
|
|
61
|
+
* @returns {'ascending' | 'descending'}
|
|
62
|
+
*/
|
|
63
|
+
static _normalizeOrder(input) {
|
|
64
|
+
if (!input) return 'descending';
|
|
65
|
+
const v = (''+input).toLowerCase();
|
|
66
|
+
if (v === 'asc' || v === 'ascending') return 'ascending';
|
|
67
|
+
if (v === 'desc' || v === 'descending') return 'descending';
|
|
68
|
+
return 'descending';
|
|
69
|
+
}
|
|
70
|
+
|
|
24
71
|
/**
|
|
25
72
|
* Simple itterator to go over all list of items, not caring about pagination/cursors etc. It fetches next page when needed
|
|
26
73
|
* Optional maxLimit second parameter to stop when reached count
|
|
27
|
-
* @param {
|
|
28
|
-
* @param {Number} maxLimit
|
|
74
|
+
* @param {(item: T) => (void | Promise<void>)} callbackFunc
|
|
75
|
+
* @param {Number} maxLimit
|
|
29
76
|
*/
|
|
30
77
|
async forEach(callbackFunc, maxLimit = null) {
|
|
31
78
|
let curN = 0;
|
|
32
79
|
do {
|
|
33
80
|
for (const item of this._data) {
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
}
|
|
81
|
+
if (maxLimit && curN >= maxLimit) break;
|
|
82
|
+
await callbackFunc(item);
|
|
37
83
|
curN++;
|
|
38
84
|
}
|
|
39
85
|
} while( (!maxLimit || curN < maxLimit) && (await this.nextPage()) );
|
|
@@ -43,6 +89,7 @@ export default class SuiPaginatedResponse extends SuiCommonMethods {
|
|
|
43
89
|
return this._hasNextPage;
|
|
44
90
|
}
|
|
45
91
|
|
|
92
|
+
/** @returns {T[]} */
|
|
46
93
|
get data() {
|
|
47
94
|
return this._data;
|
|
48
95
|
}
|
|
@@ -56,8 +103,17 @@ export default class SuiPaginatedResponse extends SuiCommonMethods {
|
|
|
56
103
|
}
|
|
57
104
|
|
|
58
105
|
async fetch(params = {}) {
|
|
59
|
-
|
|
60
|
-
|
|
106
|
+
if (this._method === 'graphqlEvents') {
|
|
107
|
+
return await this._fetchEventsGraphql(params);
|
|
108
|
+
}
|
|
109
|
+
if (this._method === 'graphqlOwnedObjects') {
|
|
110
|
+
return await this._fetchOwnedObjectsGraphql(params);
|
|
111
|
+
}
|
|
112
|
+
if (this._method === 'graphqlTransactions') {
|
|
113
|
+
return await this._fetchTransactionsGraphql(params);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const paramsCopy = /** @type {any} */ (Object.assign({}, this._params));
|
|
61
117
|
|
|
62
118
|
if (params.cursor) {
|
|
63
119
|
paramsCopy.cursor = params.cursor;
|
|
@@ -65,34 +121,241 @@ export default class SuiPaginatedResponse extends SuiCommonMethods {
|
|
|
65
121
|
paramsCopy.order = this._order;
|
|
66
122
|
|
|
67
123
|
const response = await this._suiMaster.client[this._method](paramsCopy);
|
|
68
|
-
let responseCount = 0;
|
|
69
|
-
if (response.data && response.data.length) {
|
|
70
|
-
responseCount = response.data.length;
|
|
71
|
-
}
|
|
72
124
|
|
|
73
125
|
if (response.hasNextPage) {
|
|
74
126
|
this._hasNextPage = true;
|
|
75
|
-
this._nextCursor = response.
|
|
127
|
+
this._nextCursor = response.cursor ?? null;
|
|
76
128
|
} else {
|
|
77
129
|
this._hasNextPage = false;
|
|
78
130
|
this._nextCursor = null;
|
|
79
131
|
}
|
|
80
132
|
|
|
81
|
-
this.
|
|
82
|
-
|
|
83
|
-
if (this._method === 'queryEvents') {
|
|
84
|
-
// convert data to SuiEvent instances
|
|
85
|
-
this._data = response.data.map((raw)=>(new SuiEvent({data: raw, suiMaster: this._suiMaster, debug: this._debug})));
|
|
86
|
-
} else if (this._method === 'queryTransactionBlocks') {
|
|
87
|
-
// convert data to SuiTransaction instances
|
|
88
|
-
this._data = response.data.map((raw)=>(new SuiTransaction({data: raw, suiMaster: this._suiMaster, debug: this._debug})));
|
|
89
|
-
} else if (this._method === 'getOwnedObjects') {
|
|
133
|
+
if (this._method === 'listOwnedObjects') {
|
|
90
134
|
// convert data to SuiObject instances
|
|
91
|
-
this._data = response.
|
|
135
|
+
this._data = response.objects.map((raw)=>(new (this._suiMaster.SuiObject)({suiMaster: this._suiMaster, debug: this._debug, raw: raw})));
|
|
136
|
+
} else if (this._method === 'listDynamicFields') {
|
|
137
|
+
this._data = response.dynamicFields || [];
|
|
92
138
|
} else {
|
|
93
139
|
this._data = response.data;
|
|
94
140
|
}
|
|
95
141
|
|
|
96
142
|
return this._data;
|
|
97
143
|
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Shared Relay-style GraphQL pagination:
|
|
147
|
+
* - ascending → `first` + `after`, `hasNextPage` / `endCursor`
|
|
148
|
+
* - descending → `last` + `before`, `hasPreviousPage` / `startCursor`
|
|
149
|
+
* `last` returns nodes ascending too, so descending callers get a reversed slice.
|
|
150
|
+
*
|
|
151
|
+
* @param {Object} config
|
|
152
|
+
* @param {string} config.query - GraphQL query declaring $first/$after/$last/$before + any extra vars
|
|
153
|
+
* @param {Object} [config.variables] - extra variables merged into the request (e.g. filter, owner)
|
|
154
|
+
* @param {(data: any) => any} config.extractConnection - pull the Connection (pageInfo + nodes) out of response.data
|
|
155
|
+
* @param {(node: any) => T} config.mapNode - turn a node into the wrapped T instance
|
|
156
|
+
* @param {string} [config.errorLabel='GraphQL query'] - prefix for errors
|
|
157
|
+
* @param {Object} [params]
|
|
158
|
+
* @param {?string} [params.cursor]
|
|
159
|
+
*/
|
|
160
|
+
async _fetchGraphqlPaginated(config, params = {}) {
|
|
161
|
+
const pageSize = this._params.limit || 50;
|
|
162
|
+
const cursor = params.cursor || null;
|
|
163
|
+
const ascending = this._order === 'ascending';
|
|
164
|
+
|
|
165
|
+
const pageVars = ascending
|
|
166
|
+
? { first: pageSize, after: cursor, last: null, before: null }
|
|
167
|
+
: { last: pageSize, before: cursor, first: null, after: null };
|
|
168
|
+
|
|
169
|
+
const response = await this._suiMaster.graphqlClient.query({
|
|
170
|
+
query: config.query,
|
|
171
|
+
variables: { ...(config.variables || {}), ...pageVars },
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (response.errors && response.errors.length) {
|
|
175
|
+
const label = config.errorLabel || 'GraphQL query';
|
|
176
|
+
throw new Error(`${label} failed: ` + response.errors.map((e) => e.message).join('; '));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const connection = config.extractConnection(response.data);
|
|
180
|
+
if (!connection) {
|
|
181
|
+
this._hasNextPage = false;
|
|
182
|
+
this._nextCursor = null;
|
|
183
|
+
this._data = [];
|
|
184
|
+
return this._data;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const pageInfo = connection.pageInfo || {};
|
|
188
|
+
if (ascending) {
|
|
189
|
+
this._hasNextPage = !!pageInfo.hasNextPage;
|
|
190
|
+
this._nextCursor = pageInfo.hasNextPage ? pageInfo.endCursor : null;
|
|
191
|
+
} else {
|
|
192
|
+
this._hasNextPage = !!pageInfo.hasPreviousPage;
|
|
193
|
+
this._nextCursor = pageInfo.hasPreviousPage ? pageInfo.startCursor : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const nodes = connection.nodes || [];
|
|
197
|
+
const ordered = ascending ? nodes : nodes.slice().reverse();
|
|
198
|
+
|
|
199
|
+
this._data = ordered.map(config.mapNode);
|
|
200
|
+
return this._data;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Paginate the GraphQL `events` query. `this._params` = { filter, limit } where
|
|
205
|
+
* `filter` is a GraphQL `EventFilter` (module/type/sender/checkpoint fields).
|
|
206
|
+
*/
|
|
207
|
+
async _fetchEventsGraphql(params = {}) {
|
|
208
|
+
const query = `
|
|
209
|
+
query Events($filter: EventFilter, $first: Int, $after: String, $last: Int, $before: String) {
|
|
210
|
+
events(filter: $filter, first: $first, after: $after, last: $last, before: $before) {
|
|
211
|
+
pageInfo { hasNextPage endCursor hasPreviousPage startCursor }
|
|
212
|
+
nodes {
|
|
213
|
+
sequenceNumber
|
|
214
|
+
timestamp
|
|
215
|
+
sender { address }
|
|
216
|
+
transaction { digest }
|
|
217
|
+
contents {
|
|
218
|
+
type { repr }
|
|
219
|
+
json
|
|
220
|
+
bcs
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
`;
|
|
226
|
+
|
|
227
|
+
return await this._fetchGraphqlPaginated({
|
|
228
|
+
query,
|
|
229
|
+
variables: { filter: /** @type {any} */ (this._params).filter || {} },
|
|
230
|
+
extractConnection: (data) => data?.events,
|
|
231
|
+
mapNode: (node) => /** @type {any} */ (new SuiEvent({
|
|
232
|
+
suiMaster: this._suiMaster,
|
|
233
|
+
debug: this._debug,
|
|
234
|
+
data: {
|
|
235
|
+
type: node.contents?.type?.repr,
|
|
236
|
+
parsedJson: node.contents?.json,
|
|
237
|
+
timestampMs: node.timestamp ? new Date(node.timestamp).getTime() : null,
|
|
238
|
+
sender: node.sender?.address,
|
|
239
|
+
transactionDigest: node.transaction?.digest,
|
|
240
|
+
sequenceNumber: node.sequenceNumber,
|
|
241
|
+
bcs: node.contents?.bcs,
|
|
242
|
+
},
|
|
243
|
+
})),
|
|
244
|
+
errorLabel: 'GraphQL events query',
|
|
245
|
+
}, params);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Paginate `address(address).objects(filter)` via GraphQL. `this._params` = { owner, type, limit }
|
|
250
|
+
* where `type` is a GraphQL `ObjectFilter.type` string.
|
|
251
|
+
*/
|
|
252
|
+
async _fetchOwnedObjectsGraphql(params = {}) {
|
|
253
|
+
const p = /** @type {any} */ (this._params);
|
|
254
|
+
const owner = p.owner;
|
|
255
|
+
const filter = p.type ? { type: p.type } : null;
|
|
256
|
+
|
|
257
|
+
const query = `
|
|
258
|
+
query OwnedObjects($owner: SuiAddress!, $filter: ObjectFilter, $first: Int, $after: String, $last: Int, $before: String) {
|
|
259
|
+
address(address: $owner) {
|
|
260
|
+
objects(first: $first, after: $after, last: $last, before: $before, filter: $filter) {
|
|
261
|
+
pageInfo { hasNextPage endCursor hasPreviousPage startCursor }
|
|
262
|
+
nodes {
|
|
263
|
+
address
|
|
264
|
+
version
|
|
265
|
+
digest
|
|
266
|
+
contents {
|
|
267
|
+
type { repr }
|
|
268
|
+
json
|
|
269
|
+
}
|
|
270
|
+
owner {
|
|
271
|
+
__typename
|
|
272
|
+
... on AddressOwner { address { address } }
|
|
273
|
+
... on ObjectOwner { address { address } }
|
|
274
|
+
... on ConsensusAddressOwner { address { address } }
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
`;
|
|
281
|
+
|
|
282
|
+
return await this._fetchGraphqlPaginated({
|
|
283
|
+
query,
|
|
284
|
+
variables: { owner, filter },
|
|
285
|
+
extractConnection: (data) => data?.address?.objects,
|
|
286
|
+
mapNode: (node) => /** @type {any} */ (new (this._suiMaster.SuiObject)({
|
|
287
|
+
suiMaster: this._suiMaster,
|
|
288
|
+
debug: this._debug,
|
|
289
|
+
raw: /** @type {any} */ ({
|
|
290
|
+
objectId: node.address,
|
|
291
|
+
version: node.version,
|
|
292
|
+
type: node.contents?.type?.repr,
|
|
293
|
+
owner: SuiPaginatedResponse._normalizeGraphqlOwner(node.owner),
|
|
294
|
+
json: node.contents?.json,
|
|
295
|
+
}),
|
|
296
|
+
})),
|
|
297
|
+
errorLabel: 'GraphQL owned-objects query',
|
|
298
|
+
}, params);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Paginate the GraphQL root `transactions(filter: TransactionFilter)` query.
|
|
303
|
+
* `this._params` = { filter, limit } where `filter` is a GraphQL `TransactionFilter`
|
|
304
|
+
* (`sentAddress`, `affectedAddress`, `affectedObject`, `function`, checkpoint bounds, `kind`).
|
|
305
|
+
*/
|
|
306
|
+
async _fetchTransactionsGraphql(params = {}) {
|
|
307
|
+
const query = `
|
|
308
|
+
query Transactions($filter: TransactionFilter, $first: Int, $after: String, $last: Int, $before: String) {
|
|
309
|
+
transactions(filter: $filter, first: $first, after: $after, last: $last, before: $before) {
|
|
310
|
+
pageInfo { hasNextPage endCursor hasPreviousPage startCursor }
|
|
311
|
+
nodes {
|
|
312
|
+
digest
|
|
313
|
+
sender { address }
|
|
314
|
+
effects {
|
|
315
|
+
status
|
|
316
|
+
timestamp
|
|
317
|
+
epoch { epochId }
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
`;
|
|
323
|
+
|
|
324
|
+
return await this._fetchGraphqlPaginated({
|
|
325
|
+
query,
|
|
326
|
+
variables: { filter: /** @type {any} */ (this._params).filter || {} },
|
|
327
|
+
extractConnection: (data) => data?.transactions,
|
|
328
|
+
mapNode: (node) => /** @type {any} */ (new SuiTransaction({
|
|
329
|
+
suiMaster: this._suiMaster,
|
|
330
|
+
debug: this._debug,
|
|
331
|
+
data: /** @type {any} */ ({
|
|
332
|
+
digest: node.digest,
|
|
333
|
+
epoch: node.effects?.epoch?.epochId,
|
|
334
|
+
timestampMs: node.effects?.timestamp ? new Date(node.effects.timestamp).getTime() : null,
|
|
335
|
+
status: { success: node.effects?.status === 'SUCCESS' },
|
|
336
|
+
sender: node.sender?.address,
|
|
337
|
+
}),
|
|
338
|
+
})),
|
|
339
|
+
errorLabel: 'GraphQL transactions query',
|
|
340
|
+
}, params);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Map a GraphQL Owner union node to the gRPC-style owner shape SuiObject consumes.
|
|
345
|
+
*/
|
|
346
|
+
static _normalizeGraphqlOwner(owner) {
|
|
347
|
+
if (!owner || !owner.__typename) return null;
|
|
348
|
+
if (owner.__typename === 'AddressOwner') {
|
|
349
|
+
return { AddressOwner: owner.address?.address || null };
|
|
350
|
+
}
|
|
351
|
+
if (owner.__typename === 'ObjectOwner') {
|
|
352
|
+
return { ObjectOwner: owner.address?.address || null };
|
|
353
|
+
}
|
|
354
|
+
if (owner.__typename === 'ConsensusAddressOwner') {
|
|
355
|
+
return { ConsensusAddressOwner: owner.address?.address || null };
|
|
356
|
+
}
|
|
357
|
+
if (owner.__typename === 'Shared') return { Shared: {} };
|
|
358
|
+
if (owner.__typename === 'Immutable') return 'Immutable';
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
98
361
|
};
|
|
@@ -2,12 +2,40 @@ import { entropyToMnemonic } from '@scure/bip39';
|
|
|
2
2
|
import { wordlist} from '@scure/bip39/wordlists/english';
|
|
3
3
|
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Deterministically derives an Ed25519 keypair (and thus a Sui address) from an arbitrary
|
|
7
|
+
* string seed. Useful for tests and local dev where you need a stable, human-readable
|
|
8
|
+
* "name" instead of a raw private key.
|
|
9
|
+
*
|
|
10
|
+
* The derivation is deterministic but **not cryptographically secure** — the hash function
|
|
11
|
+
* is intentionally simple. Do not use for production key management.
|
|
12
|
+
*/
|
|
5
13
|
export default class SuiPseudoRandomAddress {
|
|
14
|
+
/**
|
|
15
|
+
* Derive an Ed25519 keypair from an arbitrary string seed.
|
|
16
|
+
* The same string always produces the same keypair (and Sui address).
|
|
17
|
+
*
|
|
18
|
+
* @param {string} as - any string seed, e.g. `'alice'`, `'test-wallet-1'`
|
|
19
|
+
* @returns {Ed25519Keypair}
|
|
20
|
+
*/
|
|
6
21
|
static stringToKeyPair(as) {
|
|
7
22
|
const pseudoRandomPhrase = SuiPseudoRandomAddress.stringToPhrase(as);
|
|
8
23
|
return Ed25519Keypair.deriveKeypair(pseudoRandomPhrase);
|
|
9
24
|
}
|
|
10
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Convert an arbitrary string seed into a BIP-39 mnemonic phrase by mapping the string
|
|
28
|
+
* to 32 pseudo-random bytes and running them through `entropyToMnemonic`.
|
|
29
|
+
*
|
|
30
|
+
* The byte derivation:
|
|
31
|
+
* 1. Repeats the string until it is at least 32 chars (appending `*"` so `'test'` and
|
|
32
|
+
* `'testtest'` produce different entropy).
|
|
33
|
+
* 2. Maps each character to its char-code.
|
|
34
|
+
* 3. Folds any bytes beyond position 31 back into the first 32 via modular addition.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} as - any string seed
|
|
37
|
+
* @returns {string} 24-word BIP-39 mnemonic derived from the seed
|
|
38
|
+
*/
|
|
11
39
|
static stringToPhrase(as) {
|
|
12
40
|
let asToHash = `${as}`;
|
|
13
41
|
// calculate very simple 32 bytes hash of a string
|
|
@@ -22,9 +50,8 @@ export default class SuiPseudoRandomAddress {
|
|
|
22
50
|
asBytes[addToPos] = (asBytes[addToPos] + asBytes[i]) % 256;
|
|
23
51
|
}
|
|
24
52
|
}
|
|
25
|
-
const asUint8Array = new Uint8Array(32);
|
|
53
|
+
const asUint8Array = new Uint8Array(32);
|
|
26
54
|
asUint8Array.set(asBytes.slice(0,32));
|
|
27
|
-
// console.log(asUint8Array);
|
|
28
55
|
// use @scure/bip39 to get mnemonic out of pseudo-random array:
|
|
29
56
|
const phrase = entropyToMnemonic(asUint8Array, wordlist);
|
|
30
57
|
|