@movebridge/core 0.2.0 → 0.2.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/dist/index.d.mts CHANGED
@@ -288,6 +288,9 @@ declare class TransactionBuilder {
288
288
  * @returns Transaction hash
289
289
  * @throws MovementError with code WALLET_NOT_CONNECTED if no wallet connected
290
290
  * @throws MovementError with code TRANSACTION_FAILED if submission fails
291
+ *
292
+ * Note: Transaction failures do NOT affect wallet connection state.
293
+ * The wallet remains connected even if a transaction fails.
291
294
  */
292
295
  signAndSubmit(payload: TransactionPayload): Promise<string>;
293
296
  /**
@@ -489,6 +492,17 @@ declare class EventListener {
489
492
  * ```
490
493
  */
491
494
  subscribe(subscription: EventSubscriptionConfig | LegacyEventSubscription): string;
495
+ /**
496
+ * Parses a legacy event handle into account address and event type
497
+ * @internal
498
+ */
499
+ private parseEventHandle;
500
+ /**
501
+ * Validates event type format
502
+ * Expected format: address::module::EventType
503
+ * @internal
504
+ */
505
+ private isValidEventType;
492
506
  /**
493
507
  * Unsubscribes from events
494
508
  * @param subscriptionId - Subscription ID to remove
@@ -514,6 +528,48 @@ declare class EventListener {
514
528
  * @internal
515
529
  */
516
530
  private pollEvents;
531
+ /**
532
+ * Fetches events from the blockchain
533
+ * Handles different API response formats and fallback methods
534
+ * @internal
535
+ */
536
+ private fetchEvents;
537
+ /**
538
+ * Fetches events using the event handle API
539
+ * @internal
540
+ */
541
+ private fetchEventsViaEventHandle;
542
+ /**
543
+ * Fetches events by checking account resources
544
+ * This is a fallback method when indexer is not available
545
+ * @internal
546
+ */
547
+ private fetchEventsViaResources;
548
+ /**
549
+ * Gets the fullnode URL from the Aptos client config
550
+ * @internal
551
+ */
552
+ private getFullnodeUrl;
553
+ /**
554
+ * Sorts events by sequence number in ascending order
555
+ * @internal
556
+ */
557
+ private sortEventsBySequence;
558
+ /**
559
+ * Parses sequence number from various formats
560
+ * @internal
561
+ */
562
+ private parseSequenceNumber;
563
+ /**
564
+ * Normalizes event data to a consistent format
565
+ * @internal
566
+ */
567
+ private normalizeEventData;
568
+ /**
569
+ * Safely invokes a callback without letting errors propagate
570
+ * @internal
571
+ */
572
+ private safeInvokeCallback;
517
573
  }
518
574
 
519
575
  /**
package/dist/index.d.ts CHANGED
@@ -288,6 +288,9 @@ declare class TransactionBuilder {
288
288
  * @returns Transaction hash
289
289
  * @throws MovementError with code WALLET_NOT_CONNECTED if no wallet connected
290
290
  * @throws MovementError with code TRANSACTION_FAILED if submission fails
291
+ *
292
+ * Note: Transaction failures do NOT affect wallet connection state.
293
+ * The wallet remains connected even if a transaction fails.
291
294
  */
292
295
  signAndSubmit(payload: TransactionPayload): Promise<string>;
293
296
  /**
@@ -489,6 +492,17 @@ declare class EventListener {
489
492
  * ```
490
493
  */
491
494
  subscribe(subscription: EventSubscriptionConfig | LegacyEventSubscription): string;
495
+ /**
496
+ * Parses a legacy event handle into account address and event type
497
+ * @internal
498
+ */
499
+ private parseEventHandle;
500
+ /**
501
+ * Validates event type format
502
+ * Expected format: address::module::EventType
503
+ * @internal
504
+ */
505
+ private isValidEventType;
492
506
  /**
493
507
  * Unsubscribes from events
494
508
  * @param subscriptionId - Subscription ID to remove
@@ -514,6 +528,48 @@ declare class EventListener {
514
528
  * @internal
515
529
  */
516
530
  private pollEvents;
531
+ /**
532
+ * Fetches events from the blockchain
533
+ * Handles different API response formats and fallback methods
534
+ * @internal
535
+ */
536
+ private fetchEvents;
537
+ /**
538
+ * Fetches events using the event handle API
539
+ * @internal
540
+ */
541
+ private fetchEventsViaEventHandle;
542
+ /**
543
+ * Fetches events by checking account resources
544
+ * This is a fallback method when indexer is not available
545
+ * @internal
546
+ */
547
+ private fetchEventsViaResources;
548
+ /**
549
+ * Gets the fullnode URL from the Aptos client config
550
+ * @internal
551
+ */
552
+ private getFullnodeUrl;
553
+ /**
554
+ * Sorts events by sequence number in ascending order
555
+ * @internal
556
+ */
557
+ private sortEventsBySequence;
558
+ /**
559
+ * Parses sequence number from various formats
560
+ * @internal
561
+ */
562
+ private parseSequenceNumber;
563
+ /**
564
+ * Normalizes event data to a consistent format
565
+ * @internal
566
+ */
567
+ private normalizeEventData;
568
+ /**
569
+ * Safely invokes a callback without letting errors propagate
570
+ * @internal
571
+ */
572
+ private safeInvokeCallback;
517
573
  }
518
574
 
519
575
  /**
package/dist/index.js CHANGED
@@ -63,7 +63,7 @@ var NETWORK_CONFIG = {
63
63
  testnet: {
64
64
  chainId: 250,
65
65
  rpcUrl: "https://testnet.movementnetwork.xyz/v1",
66
- indexerUrl: null,
66
+ indexerUrl: "https://hasura.testnet.movementnetwork.xyz/v1/graphql",
67
67
  explorerUrl: "https://explorer.movementnetwork.xyz/?network=bardock+testnet",
68
68
  faucetUrl: "https://faucet.testnet.movementnetwork.xyz/"
69
69
  }
@@ -281,20 +281,54 @@ var SUPPORTED_WALLETS = {
281
281
  };
282
282
  var STORAGE_KEY = "movebridge:lastWallet";
283
283
  function normalizeHash(data) {
284
+ if (data === null || data === void 0) {
285
+ throw new Error("Invalid hash: received null or undefined");
286
+ }
284
287
  if (typeof data === "string") {
285
- return data.startsWith("0x") ? data : `0x${data}`;
288
+ const trimmed = data.trim();
289
+ if (!trimmed) {
290
+ throw new Error("Invalid hash: received empty string");
291
+ }
292
+ return trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`;
286
293
  }
287
294
  if (data instanceof Uint8Array) {
295
+ if (data.length === 0) {
296
+ throw new Error("Invalid hash: received empty Uint8Array");
297
+ }
288
298
  return "0x" + Array.from(data).map((b) => b.toString(16).padStart(2, "0")).join("");
289
299
  }
290
- if (data && typeof data === "object" && "hash" in data) {
291
- return normalizeHash(data.hash);
300
+ if (data instanceof ArrayBuffer) {
301
+ const arr = new Uint8Array(data);
302
+ if (arr.length === 0) {
303
+ throw new Error("Invalid hash: received empty ArrayBuffer");
304
+ }
305
+ return "0x" + Array.from(arr).map((b) => b.toString(16).padStart(2, "0")).join("");
292
306
  }
293
- if (data && typeof data.toString === "function") {
294
- const str = data.toString();
295
- return str.startsWith("0x") ? str : `0x${str}`;
307
+ if (data && typeof data === "object") {
308
+ if ("hash" in data) {
309
+ return normalizeHash(data.hash);
310
+ }
311
+ if ("txnHash" in data) {
312
+ return normalizeHash(data.txnHash);
313
+ }
314
+ if ("transactionHash" in data) {
315
+ return normalizeHash(data.transactionHash);
316
+ }
317
+ if ("output" in data) {
318
+ return normalizeHash(data.output);
319
+ }
320
+ if (typeof data.toString === "function") {
321
+ const str = data.toString();
322
+ if (str !== "[object Object]") {
323
+ return str.startsWith("0x") ? str : `0x${str}`;
324
+ }
325
+ }
296
326
  }
297
- return String(data);
327
+ const strValue = String(data);
328
+ if (strValue === "[object Object]" || !strValue) {
329
+ throw new Error("Invalid hash: could not extract hash from response");
330
+ }
331
+ return strValue.startsWith("0x") ? strValue : `0x${strValue}`;
298
332
  }
299
333
  function toHexString(data) {
300
334
  if (typeof data === "string") return data;
@@ -310,13 +344,32 @@ function extractUserResponse(response) {
310
344
  if (!response) {
311
345
  throw new Error("Empty response from wallet");
312
346
  }
347
+ if (typeof response === "string" || typeof response === "number") {
348
+ return response;
349
+ }
350
+ if (response instanceof Uint8Array) {
351
+ return response;
352
+ }
313
353
  const resp = response;
314
- if (resp.status === "rejected") {
354
+ if (resp.status === "rejected" || resp.status === "Rejected") {
315
355
  throw new Error("User rejected the request");
316
356
  }
357
+ if (resp.status === "error" || resp.error) {
358
+ const errorMsg = resp.error || resp.message || "Transaction failed";
359
+ throw new Error(String(errorMsg));
360
+ }
317
361
  if (resp.args !== void 0) {
318
362
  return resp.args;
319
363
  }
364
+ if (resp.status === "approved" && resp.output !== void 0) {
365
+ return resp.output;
366
+ }
367
+ if (resp.result !== void 0) {
368
+ return resp.result;
369
+ }
370
+ if (resp.data !== void 0) {
371
+ return resp.data;
372
+ }
320
373
  return response;
321
374
  }
322
375
  function createStandardAdapter(wallet) {
@@ -350,15 +403,67 @@ function createStandardAdapter(wallet) {
350
403
  if (!signTxFeature) {
351
404
  throw new Error("Wallet does not support signAndSubmitTransaction");
352
405
  }
353
- const response = await signTxFeature.signAndSubmitTransaction(payload);
354
- const result = extractUserResponse(response);
355
- let hash;
356
- if (result && typeof result === "object" && "hash" in result) {
357
- hash = normalizeHash(result.hash);
406
+ const walletName = wallet.name.toLowerCase();
407
+ const isOKX = walletName.includes("okx");
408
+ let txPayload;
409
+ if (isOKX) {
410
+ txPayload = {
411
+ type: "entry_function_payload",
412
+ function: payload.payload.function,
413
+ type_arguments: payload.payload.typeArguments,
414
+ arguments: payload.payload.functionArguments
415
+ };
358
416
  } else {
359
- hash = normalizeHash(result);
417
+ txPayload = payload;
418
+ }
419
+ let response;
420
+ try {
421
+ response = await signTxFeature.signAndSubmitTransaction(txPayload);
422
+ } catch (firstError) {
423
+ if (isOKX) {
424
+ try {
425
+ response = await signTxFeature.signAndSubmitTransaction(payload);
426
+ } catch {
427
+ throw firstError;
428
+ }
429
+ } else {
430
+ try {
431
+ const legacyPayload = {
432
+ type: "entry_function_payload",
433
+ function: payload.payload.function,
434
+ type_arguments: payload.payload.typeArguments,
435
+ arguments: payload.payload.functionArguments
436
+ };
437
+ response = await signTxFeature.signAndSubmitTransaction(legacyPayload);
438
+ } catch {
439
+ throw firstError;
440
+ }
441
+ }
442
+ }
443
+ const result = extractUserResponse(response);
444
+ try {
445
+ if (typeof result === "string") {
446
+ return { hash: normalizeHash(result) };
447
+ }
448
+ if (result && typeof result === "object") {
449
+ const hashObj = result;
450
+ if (hashObj.hash !== void 0) {
451
+ return { hash: normalizeHash(hashObj.hash) };
452
+ }
453
+ if (hashObj.txnHash !== void 0) {
454
+ return { hash: normalizeHash(hashObj.txnHash) };
455
+ }
456
+ if (hashObj.transactionHash !== void 0) {
457
+ return { hash: normalizeHash(hashObj.transactionHash) };
458
+ }
459
+ return { hash: normalizeHash(result) };
460
+ }
461
+ return { hash: normalizeHash(result) };
462
+ } catch (error) {
463
+ throw new Error(
464
+ `Failed to extract transaction hash from wallet response: ${error instanceof Error ? error.message : "Unknown error"}`
465
+ );
360
466
  }
361
- return { hash };
362
467
  },
363
468
  async signTransaction(payload) {
364
469
  if (!signOnlyFeature) {
@@ -633,11 +738,14 @@ var TransactionBuilder = class {
633
738
  * @returns Transaction hash
634
739
  * @throws MovementError with code WALLET_NOT_CONNECTED if no wallet connected
635
740
  * @throws MovementError with code TRANSACTION_FAILED if submission fails
741
+ *
742
+ * Note: Transaction failures do NOT affect wallet connection state.
743
+ * The wallet remains connected even if a transaction fails.
636
744
  */
637
745
  async signAndSubmit(payload) {
638
746
  const adapter = this.walletManager.getAdapter();
639
- const state = this.walletManager.getState();
640
- if (!adapter || !state.connected) {
747
+ const stateBefore = this.walletManager.getState();
748
+ if (!adapter || !stateBefore.connected) {
641
749
  throw Errors.walletNotConnected();
642
750
  }
643
751
  try {
@@ -650,7 +758,13 @@ var TransactionBuilder = class {
650
758
  });
651
759
  return result.hash;
652
760
  } catch (error) {
653
- throw wrapError(error, "TRANSACTION_FAILED", "Failed to sign and submit transaction");
761
+ const errorMessage = error instanceof Error ? error.message : String(error);
762
+ const isUserRejection = errorMessage.toLowerCase().includes("rejected") || errorMessage.toLowerCase().includes("cancelled") || errorMessage.toLowerCase().includes("canceled") || errorMessage.toLowerCase().includes("denied");
763
+ throw wrapError(
764
+ error,
765
+ "TRANSACTION_FAILED",
766
+ isUserRejection ? "Transaction was rejected by user" : "Failed to sign and submit transaction"
767
+ );
654
768
  }
655
769
  }
656
770
  /**
@@ -876,13 +990,15 @@ var EventListener = class {
876
990
  accountAddress = subscription.accountAddress;
877
991
  eventType = subscription.eventType;
878
992
  } else {
879
- const parts = subscription.eventHandle.split("::");
880
- if (parts.length >= 3 && parts[0]) {
881
- accountAddress = parts[0];
882
- eventType = subscription.eventHandle;
883
- } else {
884
- accountAddress = subscription.eventHandle;
885
- eventType = subscription.eventHandle;
993
+ const parsed = this.parseEventHandle(subscription.eventHandle);
994
+ accountAddress = parsed.accountAddress;
995
+ eventType = parsed.eventType;
996
+ }
997
+ if (!this.isValidEventType(eventType)) {
998
+ if (process.env.NODE_ENV === "development") {
999
+ console.warn(
1000
+ `[EventListener] Event type "${eventType}" may not be in the expected format (address::module::EventType). Subscription will proceed but may not receive events.`
1001
+ );
886
1002
  }
887
1003
  }
888
1004
  const internalSub = {
@@ -902,6 +1018,41 @@ var EventListener = class {
902
1018
  });
903
1019
  return subscriptionId;
904
1020
  }
1021
+ /**
1022
+ * Parses a legacy event handle into account address and event type
1023
+ * @internal
1024
+ */
1025
+ parseEventHandle(eventHandle) {
1026
+ const parts = eventHandle.split("::");
1027
+ if (parts.length >= 3 && parts[0]) {
1028
+ return {
1029
+ accountAddress: parts[0],
1030
+ eventType: eventHandle
1031
+ };
1032
+ }
1033
+ return {
1034
+ accountAddress: eventHandle,
1035
+ eventType: eventHandle
1036
+ };
1037
+ }
1038
+ /**
1039
+ * Validates event type format
1040
+ * Expected format: address::module::EventType
1041
+ * @internal
1042
+ */
1043
+ isValidEventType(eventType) {
1044
+ const parts = eventType.split("::");
1045
+ if (parts.length < 3) {
1046
+ return false;
1047
+ }
1048
+ const address = parts[0];
1049
+ if (!address || !address.startsWith("0x")) {
1050
+ return false;
1051
+ }
1052
+ const module2 = parts[1];
1053
+ const eventName = parts.slice(2).join("::");
1054
+ return Boolean(module2 && eventName);
1055
+ }
905
1056
  /**
906
1057
  * Unsubscribes from events
907
1058
  * @param subscriptionId - Subscription ID to remove
@@ -948,36 +1099,200 @@ var EventListener = class {
948
1099
  if (!subscription) {
949
1100
  return;
950
1101
  }
1102
+ try {
1103
+ const events = await this.fetchEvents(
1104
+ subscription.accountAddress,
1105
+ subscription.eventType
1106
+ );
1107
+ const sortedEvents = this.sortEventsBySequence(events);
1108
+ for (const event of sortedEvents) {
1109
+ const sequenceNumber = this.parseSequenceNumber(event.sequence_number);
1110
+ if (sequenceNumber > subscription.lastSequenceNumber) {
1111
+ const contractEvent = {
1112
+ type: event.type || subscription.eventType,
1113
+ sequenceNumber: String(event.sequence_number),
1114
+ data: this.normalizeEventData(event.data)
1115
+ };
1116
+ this.safeInvokeCallback(subscription.callback, contractEvent);
1117
+ subscription.lastSequenceNumber = sequenceNumber;
1118
+ }
1119
+ }
1120
+ } catch (error) {
1121
+ if (process.env.NODE_ENV === "development") {
1122
+ console.warn(`[EventListener] Polling error for ${subscriptionId}:`, error);
1123
+ }
1124
+ }
1125
+ }
1126
+ /**
1127
+ * Fetches events from the blockchain
1128
+ * Handles different API response formats and fallback methods
1129
+ * @internal
1130
+ */
1131
+ async fetchEvents(accountAddress, eventType) {
951
1132
  try {
952
1133
  const events = await this.aptosClient.getAccountEventsByEventType({
953
- accountAddress: subscription.accountAddress,
954
- eventType: subscription.eventType,
1134
+ accountAddress,
1135
+ eventType,
955
1136
  options: {
956
1137
  limit: 25,
957
1138
  orderBy: [{ sequence_number: "desc" }]
958
1139
  }
959
1140
  });
960
- const sortedEvents = [...events].sort((a, b) => {
961
- const seqA = BigInt(a.sequence_number);
962
- const seqB = BigInt(b.sequence_number);
963
- return seqA < seqB ? -1 : seqA > seqB ? 1 : 0;
964
- });
965
- for (const event of sortedEvents) {
966
- const sequenceNumber = BigInt(event.sequence_number);
967
- if (sequenceNumber > subscription.lastSequenceNumber) {
968
- const contractEvent = {
969
- type: event.type,
970
- sequenceNumber: String(event.sequence_number),
971
- data: event.data
972
- };
973
- try {
974
- subscription.callback(contractEvent);
975
- } catch {
1141
+ return events.map((e) => ({
1142
+ type: e.type,
1143
+ sequence_number: e.sequence_number,
1144
+ data: e.data
1145
+ }));
1146
+ } catch (indexerError) {
1147
+ try {
1148
+ const events = await this.fetchEventsViaEventHandle(accountAddress, eventType);
1149
+ return events;
1150
+ } catch (handleError) {
1151
+ try {
1152
+ const events = await this.fetchEventsViaResources(accountAddress, eventType);
1153
+ return events;
1154
+ } catch (resourceError) {
1155
+ if (process.env.NODE_ENV === "development") {
1156
+ console.warn(
1157
+ `[EventListener] Failed to fetch events for ${eventType}:`,
1158
+ { indexerError, handleError, resourceError }
1159
+ );
976
1160
  }
977
- subscription.lastSequenceNumber = sequenceNumber;
1161
+ return [];
978
1162
  }
979
1163
  }
1164
+ }
1165
+ }
1166
+ /**
1167
+ * Fetches events using the event handle API
1168
+ * @internal
1169
+ */
1170
+ async fetchEventsViaEventHandle(accountAddress, eventType) {
1171
+ const parts = eventType.split("::");
1172
+ if (parts.length < 3) {
1173
+ throw new Error("Invalid event type format");
1174
+ }
1175
+ const structName = parts.slice(2).join("::");
1176
+ const eventHandleMappings = {
1177
+ "DepositEvent": { resource: "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>", field: "deposit_events" },
1178
+ "WithdrawEvent": { resource: "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>", field: "withdraw_events" }
1179
+ };
1180
+ const mapping = eventHandleMappings[structName];
1181
+ if (!mapping) {
1182
+ throw new Error(`Unknown event type: ${structName}`);
1183
+ }
1184
+ const events = await this.aptosClient.getEventsByEventHandle({
1185
+ accountAddress,
1186
+ eventHandleStruct: mapping.resource,
1187
+ fieldName: mapping.field,
1188
+ options: { limit: 25 }
1189
+ });
1190
+ return events.map((e) => ({
1191
+ type: e.type || eventType,
1192
+ sequence_number: e.sequence_number,
1193
+ data: e.data
1194
+ }));
1195
+ }
1196
+ /**
1197
+ * Fetches events by checking account resources
1198
+ * This is a fallback method when indexer is not available
1199
+ * @internal
1200
+ */
1201
+ async fetchEventsViaResources(accountAddress, eventType) {
1202
+ const resources = await this.aptosClient.getAccountResources({
1203
+ accountAddress
1204
+ });
1205
+ const coinStore = resources.find(
1206
+ (r) => r.type === "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>"
1207
+ );
1208
+ if (!coinStore) {
1209
+ return [];
1210
+ }
1211
+ const data = coinStore.data;
1212
+ const isDeposit = eventType.includes("Deposit");
1213
+ const eventHandle = isDeposit ? data.deposit_events : data.withdraw_events;
1214
+ if (!eventHandle) {
1215
+ return [];
1216
+ }
1217
+ try {
1218
+ const creationNum = eventHandle.guid.id.creation_num;
1219
+ const addr = eventHandle.guid.id.addr;
1220
+ const response = await fetch(
1221
+ `${this.getFullnodeUrl()}/accounts/${accountAddress}/events/${addr}/${creationNum}?limit=25`
1222
+ );
1223
+ if (!response.ok) {
1224
+ throw new Error(`HTTP ${response.status}`);
1225
+ }
1226
+ const events = await response.json();
1227
+ return events.map((e) => ({
1228
+ type: e.type || eventType,
1229
+ sequence_number: e.sequence_number,
1230
+ data: e.data
1231
+ }));
980
1232
  } catch {
1233
+ return [];
1234
+ }
1235
+ }
1236
+ /**
1237
+ * Gets the fullnode URL from the Aptos client config
1238
+ * @internal
1239
+ */
1240
+ getFullnodeUrl() {
1241
+ const config = this.aptosClient.config;
1242
+ return config?.fullnode || "https://testnet.movementnetwork.xyz/v1";
1243
+ }
1244
+ /**
1245
+ * Sorts events by sequence number in ascending order
1246
+ * @internal
1247
+ */
1248
+ sortEventsBySequence(events) {
1249
+ return [...events].sort((a, b) => {
1250
+ const seqA = this.parseSequenceNumber(a.sequence_number);
1251
+ const seqB = this.parseSequenceNumber(b.sequence_number);
1252
+ return seqA < seqB ? -1 : seqA > seqB ? 1 : 0;
1253
+ });
1254
+ }
1255
+ /**
1256
+ * Parses sequence number from various formats
1257
+ * @internal
1258
+ */
1259
+ parseSequenceNumber(value) {
1260
+ if (typeof value === "bigint") {
1261
+ return value;
1262
+ }
1263
+ if (typeof value === "number") {
1264
+ return BigInt(value);
1265
+ }
1266
+ try {
1267
+ return BigInt(value);
1268
+ } catch {
1269
+ return BigInt(0);
1270
+ }
1271
+ }
1272
+ /**
1273
+ * Normalizes event data to a consistent format
1274
+ * @internal
1275
+ */
1276
+ normalizeEventData(data) {
1277
+ if (data === null || data === void 0) {
1278
+ return {};
1279
+ }
1280
+ if (typeof data === "object") {
1281
+ return data;
1282
+ }
1283
+ return { value: data };
1284
+ }
1285
+ /**
1286
+ * Safely invokes a callback without letting errors propagate
1287
+ * @internal
1288
+ */
1289
+ safeInvokeCallback(callback, event) {
1290
+ try {
1291
+ callback(event);
1292
+ } catch (error) {
1293
+ if (process.env.NODE_ENV === "development") {
1294
+ console.warn("[EventListener] Callback error:", error);
1295
+ }
981
1296
  }
982
1297
  }
983
1298
  };
package/dist/index.mjs CHANGED
@@ -12,7 +12,7 @@ var NETWORK_CONFIG = {
12
12
  testnet: {
13
13
  chainId: 250,
14
14
  rpcUrl: "https://testnet.movementnetwork.xyz/v1",
15
- indexerUrl: null,
15
+ indexerUrl: "https://hasura.testnet.movementnetwork.xyz/v1/graphql",
16
16
  explorerUrl: "https://explorer.movementnetwork.xyz/?network=bardock+testnet",
17
17
  faucetUrl: "https://faucet.testnet.movementnetwork.xyz/"
18
18
  }
@@ -230,20 +230,54 @@ var SUPPORTED_WALLETS = {
230
230
  };
231
231
  var STORAGE_KEY = "movebridge:lastWallet";
232
232
  function normalizeHash(data) {
233
+ if (data === null || data === void 0) {
234
+ throw new Error("Invalid hash: received null or undefined");
235
+ }
233
236
  if (typeof data === "string") {
234
- return data.startsWith("0x") ? data : `0x${data}`;
237
+ const trimmed = data.trim();
238
+ if (!trimmed) {
239
+ throw new Error("Invalid hash: received empty string");
240
+ }
241
+ return trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`;
235
242
  }
236
243
  if (data instanceof Uint8Array) {
244
+ if (data.length === 0) {
245
+ throw new Error("Invalid hash: received empty Uint8Array");
246
+ }
237
247
  return "0x" + Array.from(data).map((b) => b.toString(16).padStart(2, "0")).join("");
238
248
  }
239
- if (data && typeof data === "object" && "hash" in data) {
240
- return normalizeHash(data.hash);
249
+ if (data instanceof ArrayBuffer) {
250
+ const arr = new Uint8Array(data);
251
+ if (arr.length === 0) {
252
+ throw new Error("Invalid hash: received empty ArrayBuffer");
253
+ }
254
+ return "0x" + Array.from(arr).map((b) => b.toString(16).padStart(2, "0")).join("");
241
255
  }
242
- if (data && typeof data.toString === "function") {
243
- const str = data.toString();
244
- return str.startsWith("0x") ? str : `0x${str}`;
256
+ if (data && typeof data === "object") {
257
+ if ("hash" in data) {
258
+ return normalizeHash(data.hash);
259
+ }
260
+ if ("txnHash" in data) {
261
+ return normalizeHash(data.txnHash);
262
+ }
263
+ if ("transactionHash" in data) {
264
+ return normalizeHash(data.transactionHash);
265
+ }
266
+ if ("output" in data) {
267
+ return normalizeHash(data.output);
268
+ }
269
+ if (typeof data.toString === "function") {
270
+ const str = data.toString();
271
+ if (str !== "[object Object]") {
272
+ return str.startsWith("0x") ? str : `0x${str}`;
273
+ }
274
+ }
245
275
  }
246
- return String(data);
276
+ const strValue = String(data);
277
+ if (strValue === "[object Object]" || !strValue) {
278
+ throw new Error("Invalid hash: could not extract hash from response");
279
+ }
280
+ return strValue.startsWith("0x") ? strValue : `0x${strValue}`;
247
281
  }
248
282
  function toHexString(data) {
249
283
  if (typeof data === "string") return data;
@@ -259,13 +293,32 @@ function extractUserResponse(response) {
259
293
  if (!response) {
260
294
  throw new Error("Empty response from wallet");
261
295
  }
296
+ if (typeof response === "string" || typeof response === "number") {
297
+ return response;
298
+ }
299
+ if (response instanceof Uint8Array) {
300
+ return response;
301
+ }
262
302
  const resp = response;
263
- if (resp.status === "rejected") {
303
+ if (resp.status === "rejected" || resp.status === "Rejected") {
264
304
  throw new Error("User rejected the request");
265
305
  }
306
+ if (resp.status === "error" || resp.error) {
307
+ const errorMsg = resp.error || resp.message || "Transaction failed";
308
+ throw new Error(String(errorMsg));
309
+ }
266
310
  if (resp.args !== void 0) {
267
311
  return resp.args;
268
312
  }
313
+ if (resp.status === "approved" && resp.output !== void 0) {
314
+ return resp.output;
315
+ }
316
+ if (resp.result !== void 0) {
317
+ return resp.result;
318
+ }
319
+ if (resp.data !== void 0) {
320
+ return resp.data;
321
+ }
269
322
  return response;
270
323
  }
271
324
  function createStandardAdapter(wallet) {
@@ -299,15 +352,67 @@ function createStandardAdapter(wallet) {
299
352
  if (!signTxFeature) {
300
353
  throw new Error("Wallet does not support signAndSubmitTransaction");
301
354
  }
302
- const response = await signTxFeature.signAndSubmitTransaction(payload);
303
- const result = extractUserResponse(response);
304
- let hash;
305
- if (result && typeof result === "object" && "hash" in result) {
306
- hash = normalizeHash(result.hash);
355
+ const walletName = wallet.name.toLowerCase();
356
+ const isOKX = walletName.includes("okx");
357
+ let txPayload;
358
+ if (isOKX) {
359
+ txPayload = {
360
+ type: "entry_function_payload",
361
+ function: payload.payload.function,
362
+ type_arguments: payload.payload.typeArguments,
363
+ arguments: payload.payload.functionArguments
364
+ };
307
365
  } else {
308
- hash = normalizeHash(result);
366
+ txPayload = payload;
367
+ }
368
+ let response;
369
+ try {
370
+ response = await signTxFeature.signAndSubmitTransaction(txPayload);
371
+ } catch (firstError) {
372
+ if (isOKX) {
373
+ try {
374
+ response = await signTxFeature.signAndSubmitTransaction(payload);
375
+ } catch {
376
+ throw firstError;
377
+ }
378
+ } else {
379
+ try {
380
+ const legacyPayload = {
381
+ type: "entry_function_payload",
382
+ function: payload.payload.function,
383
+ type_arguments: payload.payload.typeArguments,
384
+ arguments: payload.payload.functionArguments
385
+ };
386
+ response = await signTxFeature.signAndSubmitTransaction(legacyPayload);
387
+ } catch {
388
+ throw firstError;
389
+ }
390
+ }
391
+ }
392
+ const result = extractUserResponse(response);
393
+ try {
394
+ if (typeof result === "string") {
395
+ return { hash: normalizeHash(result) };
396
+ }
397
+ if (result && typeof result === "object") {
398
+ const hashObj = result;
399
+ if (hashObj.hash !== void 0) {
400
+ return { hash: normalizeHash(hashObj.hash) };
401
+ }
402
+ if (hashObj.txnHash !== void 0) {
403
+ return { hash: normalizeHash(hashObj.txnHash) };
404
+ }
405
+ if (hashObj.transactionHash !== void 0) {
406
+ return { hash: normalizeHash(hashObj.transactionHash) };
407
+ }
408
+ return { hash: normalizeHash(result) };
409
+ }
410
+ return { hash: normalizeHash(result) };
411
+ } catch (error) {
412
+ throw new Error(
413
+ `Failed to extract transaction hash from wallet response: ${error instanceof Error ? error.message : "Unknown error"}`
414
+ );
309
415
  }
310
- return { hash };
311
416
  },
312
417
  async signTransaction(payload) {
313
418
  if (!signOnlyFeature) {
@@ -582,11 +687,14 @@ var TransactionBuilder = class {
582
687
  * @returns Transaction hash
583
688
  * @throws MovementError with code WALLET_NOT_CONNECTED if no wallet connected
584
689
  * @throws MovementError with code TRANSACTION_FAILED if submission fails
690
+ *
691
+ * Note: Transaction failures do NOT affect wallet connection state.
692
+ * The wallet remains connected even if a transaction fails.
585
693
  */
586
694
  async signAndSubmit(payload) {
587
695
  const adapter = this.walletManager.getAdapter();
588
- const state = this.walletManager.getState();
589
- if (!adapter || !state.connected) {
696
+ const stateBefore = this.walletManager.getState();
697
+ if (!adapter || !stateBefore.connected) {
590
698
  throw Errors.walletNotConnected();
591
699
  }
592
700
  try {
@@ -599,7 +707,13 @@ var TransactionBuilder = class {
599
707
  });
600
708
  return result.hash;
601
709
  } catch (error) {
602
- throw wrapError(error, "TRANSACTION_FAILED", "Failed to sign and submit transaction");
710
+ const errorMessage = error instanceof Error ? error.message : String(error);
711
+ const isUserRejection = errorMessage.toLowerCase().includes("rejected") || errorMessage.toLowerCase().includes("cancelled") || errorMessage.toLowerCase().includes("canceled") || errorMessage.toLowerCase().includes("denied");
712
+ throw wrapError(
713
+ error,
714
+ "TRANSACTION_FAILED",
715
+ isUserRejection ? "Transaction was rejected by user" : "Failed to sign and submit transaction"
716
+ );
603
717
  }
604
718
  }
605
719
  /**
@@ -825,13 +939,15 @@ var EventListener = class {
825
939
  accountAddress = subscription.accountAddress;
826
940
  eventType = subscription.eventType;
827
941
  } else {
828
- const parts = subscription.eventHandle.split("::");
829
- if (parts.length >= 3 && parts[0]) {
830
- accountAddress = parts[0];
831
- eventType = subscription.eventHandle;
832
- } else {
833
- accountAddress = subscription.eventHandle;
834
- eventType = subscription.eventHandle;
942
+ const parsed = this.parseEventHandle(subscription.eventHandle);
943
+ accountAddress = parsed.accountAddress;
944
+ eventType = parsed.eventType;
945
+ }
946
+ if (!this.isValidEventType(eventType)) {
947
+ if (process.env.NODE_ENV === "development") {
948
+ console.warn(
949
+ `[EventListener] Event type "${eventType}" may not be in the expected format (address::module::EventType). Subscription will proceed but may not receive events.`
950
+ );
835
951
  }
836
952
  }
837
953
  const internalSub = {
@@ -851,6 +967,41 @@ var EventListener = class {
851
967
  });
852
968
  return subscriptionId;
853
969
  }
970
+ /**
971
+ * Parses a legacy event handle into account address and event type
972
+ * @internal
973
+ */
974
+ parseEventHandle(eventHandle) {
975
+ const parts = eventHandle.split("::");
976
+ if (parts.length >= 3 && parts[0]) {
977
+ return {
978
+ accountAddress: parts[0],
979
+ eventType: eventHandle
980
+ };
981
+ }
982
+ return {
983
+ accountAddress: eventHandle,
984
+ eventType: eventHandle
985
+ };
986
+ }
987
+ /**
988
+ * Validates event type format
989
+ * Expected format: address::module::EventType
990
+ * @internal
991
+ */
992
+ isValidEventType(eventType) {
993
+ const parts = eventType.split("::");
994
+ if (parts.length < 3) {
995
+ return false;
996
+ }
997
+ const address = parts[0];
998
+ if (!address || !address.startsWith("0x")) {
999
+ return false;
1000
+ }
1001
+ const module = parts[1];
1002
+ const eventName = parts.slice(2).join("::");
1003
+ return Boolean(module && eventName);
1004
+ }
854
1005
  /**
855
1006
  * Unsubscribes from events
856
1007
  * @param subscriptionId - Subscription ID to remove
@@ -897,36 +1048,200 @@ var EventListener = class {
897
1048
  if (!subscription) {
898
1049
  return;
899
1050
  }
1051
+ try {
1052
+ const events = await this.fetchEvents(
1053
+ subscription.accountAddress,
1054
+ subscription.eventType
1055
+ );
1056
+ const sortedEvents = this.sortEventsBySequence(events);
1057
+ for (const event of sortedEvents) {
1058
+ const sequenceNumber = this.parseSequenceNumber(event.sequence_number);
1059
+ if (sequenceNumber > subscription.lastSequenceNumber) {
1060
+ const contractEvent = {
1061
+ type: event.type || subscription.eventType,
1062
+ sequenceNumber: String(event.sequence_number),
1063
+ data: this.normalizeEventData(event.data)
1064
+ };
1065
+ this.safeInvokeCallback(subscription.callback, contractEvent);
1066
+ subscription.lastSequenceNumber = sequenceNumber;
1067
+ }
1068
+ }
1069
+ } catch (error) {
1070
+ if (process.env.NODE_ENV === "development") {
1071
+ console.warn(`[EventListener] Polling error for ${subscriptionId}:`, error);
1072
+ }
1073
+ }
1074
+ }
1075
+ /**
1076
+ * Fetches events from the blockchain
1077
+ * Handles different API response formats and fallback methods
1078
+ * @internal
1079
+ */
1080
+ async fetchEvents(accountAddress, eventType) {
900
1081
  try {
901
1082
  const events = await this.aptosClient.getAccountEventsByEventType({
902
- accountAddress: subscription.accountAddress,
903
- eventType: subscription.eventType,
1083
+ accountAddress,
1084
+ eventType,
904
1085
  options: {
905
1086
  limit: 25,
906
1087
  orderBy: [{ sequence_number: "desc" }]
907
1088
  }
908
1089
  });
909
- const sortedEvents = [...events].sort((a, b) => {
910
- const seqA = BigInt(a.sequence_number);
911
- const seqB = BigInt(b.sequence_number);
912
- return seqA < seqB ? -1 : seqA > seqB ? 1 : 0;
913
- });
914
- for (const event of sortedEvents) {
915
- const sequenceNumber = BigInt(event.sequence_number);
916
- if (sequenceNumber > subscription.lastSequenceNumber) {
917
- const contractEvent = {
918
- type: event.type,
919
- sequenceNumber: String(event.sequence_number),
920
- data: event.data
921
- };
922
- try {
923
- subscription.callback(contractEvent);
924
- } catch {
1090
+ return events.map((e) => ({
1091
+ type: e.type,
1092
+ sequence_number: e.sequence_number,
1093
+ data: e.data
1094
+ }));
1095
+ } catch (indexerError) {
1096
+ try {
1097
+ const events = await this.fetchEventsViaEventHandle(accountAddress, eventType);
1098
+ return events;
1099
+ } catch (handleError) {
1100
+ try {
1101
+ const events = await this.fetchEventsViaResources(accountAddress, eventType);
1102
+ return events;
1103
+ } catch (resourceError) {
1104
+ if (process.env.NODE_ENV === "development") {
1105
+ console.warn(
1106
+ `[EventListener] Failed to fetch events for ${eventType}:`,
1107
+ { indexerError, handleError, resourceError }
1108
+ );
925
1109
  }
926
- subscription.lastSequenceNumber = sequenceNumber;
1110
+ return [];
927
1111
  }
928
1112
  }
1113
+ }
1114
+ }
1115
+ /**
1116
+ * Fetches events using the event handle API
1117
+ * @internal
1118
+ */
1119
+ async fetchEventsViaEventHandle(accountAddress, eventType) {
1120
+ const parts = eventType.split("::");
1121
+ if (parts.length < 3) {
1122
+ throw new Error("Invalid event type format");
1123
+ }
1124
+ const structName = parts.slice(2).join("::");
1125
+ const eventHandleMappings = {
1126
+ "DepositEvent": { resource: "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>", field: "deposit_events" },
1127
+ "WithdrawEvent": { resource: "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>", field: "withdraw_events" }
1128
+ };
1129
+ const mapping = eventHandleMappings[structName];
1130
+ if (!mapping) {
1131
+ throw new Error(`Unknown event type: ${structName}`);
1132
+ }
1133
+ const events = await this.aptosClient.getEventsByEventHandle({
1134
+ accountAddress,
1135
+ eventHandleStruct: mapping.resource,
1136
+ fieldName: mapping.field,
1137
+ options: { limit: 25 }
1138
+ });
1139
+ return events.map((e) => ({
1140
+ type: e.type || eventType,
1141
+ sequence_number: e.sequence_number,
1142
+ data: e.data
1143
+ }));
1144
+ }
1145
+ /**
1146
+ * Fetches events by checking account resources
1147
+ * This is a fallback method when indexer is not available
1148
+ * @internal
1149
+ */
1150
+ async fetchEventsViaResources(accountAddress, eventType) {
1151
+ const resources = await this.aptosClient.getAccountResources({
1152
+ accountAddress
1153
+ });
1154
+ const coinStore = resources.find(
1155
+ (r) => r.type === "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>"
1156
+ );
1157
+ if (!coinStore) {
1158
+ return [];
1159
+ }
1160
+ const data = coinStore.data;
1161
+ const isDeposit = eventType.includes("Deposit");
1162
+ const eventHandle = isDeposit ? data.deposit_events : data.withdraw_events;
1163
+ if (!eventHandle) {
1164
+ return [];
1165
+ }
1166
+ try {
1167
+ const creationNum = eventHandle.guid.id.creation_num;
1168
+ const addr = eventHandle.guid.id.addr;
1169
+ const response = await fetch(
1170
+ `${this.getFullnodeUrl()}/accounts/${accountAddress}/events/${addr}/${creationNum}?limit=25`
1171
+ );
1172
+ if (!response.ok) {
1173
+ throw new Error(`HTTP ${response.status}`);
1174
+ }
1175
+ const events = await response.json();
1176
+ return events.map((e) => ({
1177
+ type: e.type || eventType,
1178
+ sequence_number: e.sequence_number,
1179
+ data: e.data
1180
+ }));
929
1181
  } catch {
1182
+ return [];
1183
+ }
1184
+ }
1185
+ /**
1186
+ * Gets the fullnode URL from the Aptos client config
1187
+ * @internal
1188
+ */
1189
+ getFullnodeUrl() {
1190
+ const config = this.aptosClient.config;
1191
+ return config?.fullnode || "https://testnet.movementnetwork.xyz/v1";
1192
+ }
1193
+ /**
1194
+ * Sorts events by sequence number in ascending order
1195
+ * @internal
1196
+ */
1197
+ sortEventsBySequence(events) {
1198
+ return [...events].sort((a, b) => {
1199
+ const seqA = this.parseSequenceNumber(a.sequence_number);
1200
+ const seqB = this.parseSequenceNumber(b.sequence_number);
1201
+ return seqA < seqB ? -1 : seqA > seqB ? 1 : 0;
1202
+ });
1203
+ }
1204
+ /**
1205
+ * Parses sequence number from various formats
1206
+ * @internal
1207
+ */
1208
+ parseSequenceNumber(value) {
1209
+ if (typeof value === "bigint") {
1210
+ return value;
1211
+ }
1212
+ if (typeof value === "number") {
1213
+ return BigInt(value);
1214
+ }
1215
+ try {
1216
+ return BigInt(value);
1217
+ } catch {
1218
+ return BigInt(0);
1219
+ }
1220
+ }
1221
+ /**
1222
+ * Normalizes event data to a consistent format
1223
+ * @internal
1224
+ */
1225
+ normalizeEventData(data) {
1226
+ if (data === null || data === void 0) {
1227
+ return {};
1228
+ }
1229
+ if (typeof data === "object") {
1230
+ return data;
1231
+ }
1232
+ return { value: data };
1233
+ }
1234
+ /**
1235
+ * Safely invokes a callback without letting errors propagate
1236
+ * @internal
1237
+ */
1238
+ safeInvokeCallback(callback, event) {
1239
+ try {
1240
+ callback(event);
1241
+ } catch (error) {
1242
+ if (process.env.NODE_ENV === "development") {
1243
+ console.warn("[EventListener] Callback error:", error);
1244
+ }
930
1245
  }
931
1246
  }
932
1247
  };
package/package.json CHANGED
@@ -1,55 +1,55 @@
1
1
  {
2
- "name": "@movebridge/core",
3
- "version": "0.2.0",
4
- "description": "Core SDK for Movement Network - wallet management, transactions, and contract interactions",
5
- "main": "./dist/index.js",
6
- "module": "./dist/index.mjs",
7
- "types": "./dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "import": "./dist/index.mjs",
12
- "require": "./dist/index.js"
2
+ "name": "@movebridge/core",
3
+ "version": "0.2.1",
4
+ "description": "Core SDK for Movement Network - wallet management, transactions, and contract interactions",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
20
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
21
+ "clean": "rm -rf dist",
22
+ "typecheck": "tsc --noEmit",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest"
25
+ },
26
+ "dependencies": {
27
+ "@aptos-labs/ts-sdk": "^1.26.0",
28
+ "@aptos-labs/wallet-standard": "^0.2.0",
29
+ "eventemitter3": "^5.0.1"
30
+ },
31
+ "devDependencies": {
32
+ "tsup": "^8.0.1",
33
+ "typescript": "^5.3.2"
34
+ },
35
+ "peerDependencies": {},
36
+ "keywords": [
37
+ "movement",
38
+ "aptos",
39
+ "blockchain",
40
+ "sdk",
41
+ "wallet",
42
+ "web3"
43
+ ],
44
+ "author": "Aqila Rifti",
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/AqilaRifti/MoveBridge",
49
+ "directory": "packages/core"
50
+ },
51
+ "homepage": "https://github.com/AqilaRifti/MoveBridge#readme",
52
+ "bugs": {
53
+ "url": "https://github.com/AqilaRifti/MoveBridge/issues"
13
54
  }
14
- },
15
- "files": [
16
- "dist"
17
- ],
18
- "dependencies": {
19
- "@aptos-labs/ts-sdk": "^1.26.0",
20
- "@aptos-labs/wallet-standard": "^0.2.0",
21
- "eventemitter3": "^5.0.1"
22
- },
23
- "devDependencies": {
24
- "tsup": "^8.0.1",
25
- "typescript": "^5.3.2"
26
- },
27
- "peerDependencies": {},
28
- "keywords": [
29
- "movement",
30
- "aptos",
31
- "blockchain",
32
- "sdk",
33
- "wallet",
34
- "web3"
35
- ],
36
- "author": "Aqila Rifti",
37
- "license": "MIT",
38
- "repository": {
39
- "type": "git",
40
- "url": "https://github.com/AqilaRifti/MoveBridge",
41
- "directory": "packages/core"
42
- },
43
- "homepage": "https://github.com/AqilaRifti/MoveBridge#readme",
44
- "bugs": {
45
- "url": "https://github.com/AqilaRifti/MoveBridge/issues"
46
- },
47
- "scripts": {
48
- "build": "tsup src/index.ts --format cjs,esm --dts --clean",
49
- "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
50
- "clean": "rm -rf dist",
51
- "typecheck": "tsc --noEmit",
52
- "test": "vitest run",
53
- "test:watch": "vitest"
54
- }
55
55
  }
package/LICENSE DELETED
File without changes