@hivemind-os/collective-core 0.2.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.
Files changed (145) hide show
  1. package/.test-data/05a845c4-1682-41b9-97e5-65a5263156c0/spending.sqlite +0 -0
  2. package/.test-data/10e18ba5-98f0-42e0-8899-06a09459ae85/agents.sqlite +0 -0
  3. package/.test-data/20464456-23cb-4ff7-8df5-3c129fb95a90/agents.sqlite +0 -0
  4. package/.test-data/2e2ec66c-e8b4-43eb-945f-cca84ed0a0f6/agents.sqlite +0 -0
  5. package/.test-data/2f5ef9b7-01da-4ba0-a6fa-af8063a2567c/agents.sqlite +0 -0
  6. package/.test-data/3ac13cd5-84c9-4e71-8b89-6d3ef4dd819c/agents.sqlite +0 -0
  7. package/.test-data/40b7dd4a-fae0-4a5d-a6a0-64c9048af35e/agents.sqlite +0 -0
  8. package/.test-data/59511a04-42f8-4286-bb1e-733795e08749/agents.sqlite +0 -0
  9. package/.test-data/6778e8c5-6eb5-416d-8aca-9d559cdf83e4/agents.sqlite +0 -0
  10. package/.test-data/7648dc86-df90-4460-8f9c-55cdb481324b/agents.sqlite +0 -0
  11. package/.test-data/77162d98-6b22-41b1-a50f-536fab739f8a/agents.sqlite +0 -0
  12. package/.test-data/798dbdab-cbe7-4edd-8a5b-ae99c285fe17/agents.sqlite +0 -0
  13. package/.test-data/8033d6ac-8b85-454d-b708-00ac695b22f8/agents.sqlite +0 -0
  14. package/.test-data/9155a4f4-cda3-487c-9eed-921c82d7550f/agents.sqlite +0 -0
  15. package/.test-data/9bfeee53-c231-46c3-8c93-2180933f5d50/agents.sqlite +0 -0
  16. package/.test-data/a4c64287-79f6-46e9-847d-2803c63a74fd/agents.sqlite +0 -0
  17. package/.test-data/b8f58952-1ed8-46ff-abd7-21fb86e9457f/agents.sqlite +0 -0
  18. package/.test-data/c3060504-3187-41ed-8532-82332be48b0b/spending.sqlite +0 -0
  19. package/.test-data/cc471629-8006-4fc1-b8a1-399d2df2cc4e/agents.sqlite +0 -0
  20. package/.test-data/dbca3bef-397d-4bbc-bd4c-4f7b14103e04/spending.sqlite +0 -0
  21. package/.test-data/f1283dd1-6602-4de7-a050-16aac7abc288/agents.sqlite +0 -0
  22. package/.turbo/turbo-build.log +14 -0
  23. package/dist/index.d.ts +1675 -0
  24. package/dist/index.js +8006 -0
  25. package/dist/index.js.map +1 -0
  26. package/package.json +41 -0
  27. package/src/auth/device-flow.ts +108 -0
  28. package/src/auth/ed25519-provider.ts +43 -0
  29. package/src/auth/errors.ts +82 -0
  30. package/src/auth/evm-key.ts +55 -0
  31. package/src/auth/index.ts +8 -0
  32. package/src/auth/session-state.ts +25 -0
  33. package/src/auth/session-store.ts +510 -0
  34. package/src/auth/types.ts +81 -0
  35. package/src/auth/zklogin-provider.ts +902 -0
  36. package/src/blobstore/WALRUS_FINDINGS.md +284 -0
  37. package/src/blobstore/encrypted-store.ts +56 -0
  38. package/src/blobstore/fs-store.ts +91 -0
  39. package/src/blobstore/hybrid-store.ts +144 -0
  40. package/src/blobstore/index.ts +5 -0
  41. package/src/blobstore/interface.ts +33 -0
  42. package/src/blobstore/walrus-spike.ts +345 -0
  43. package/src/blobstore/walrus-store.ts +551 -0
  44. package/src/cache/agent-cache.ts +403 -0
  45. package/src/cache/index.ts +1 -0
  46. package/src/crypto/encryption.ts +152 -0
  47. package/src/crypto/index.ts +2 -0
  48. package/src/crypto/x25519.ts +41 -0
  49. package/src/dispute/client.ts +191 -0
  50. package/src/dispute/index.ts +1 -0
  51. package/src/events/index.ts +2 -0
  52. package/src/events/parser.ts +291 -0
  53. package/src/events/subscription.ts +131 -0
  54. package/src/evm/constants.ts +6 -0
  55. package/src/evm/index.ts +2 -0
  56. package/src/evm/wallet.ts +136 -0
  57. package/src/identity/did.ts +36 -0
  58. package/src/identity/index.ts +4 -0
  59. package/src/identity/keypair.ts +199 -0
  60. package/src/identity/signing.ts +28 -0
  61. package/src/index.ts +22 -0
  62. package/src/internal/parsing.ts +416 -0
  63. package/src/marketplace/client.ts +349 -0
  64. package/src/marketplace/index.ts +1 -0
  65. package/src/metering/hash-chain.ts +94 -0
  66. package/src/metering/index.ts +4 -0
  67. package/src/metering/meter.ts +80 -0
  68. package/src/metering/streaming.ts +196 -0
  69. package/src/metering/verification.ts +104 -0
  70. package/src/payment/index.ts +1 -0
  71. package/src/payment/rail-selector.ts +41 -0
  72. package/src/registry/client.ts +328 -0
  73. package/src/registry/index.ts +1 -0
  74. package/src/relay/consumer-client.ts +497 -0
  75. package/src/relay/index.ts +1 -0
  76. package/src/relay-registry/client.ts +295 -0
  77. package/src/relay-registry/discovery.ts +109 -0
  78. package/src/relay-registry/index.ts +2 -0
  79. package/src/reputation/anchor-client.ts +126 -0
  80. package/src/reputation/event-publisher.ts +67 -0
  81. package/src/reputation/index.ts +5 -0
  82. package/src/reputation/merkle.ts +79 -0
  83. package/src/reputation/score-calculator.ts +133 -0
  84. package/src/reputation/serialization.ts +37 -0
  85. package/src/reputation/store.ts +165 -0
  86. package/src/reputation/validation.ts +135 -0
  87. package/src/routing/circuit-breaker.ts +111 -0
  88. package/src/routing/fan-out.ts +266 -0
  89. package/src/routing/index.ts +4 -0
  90. package/src/routing/performance.ts +244 -0
  91. package/src/routing/selector.ts +225 -0
  92. package/src/spending/index.ts +1 -0
  93. package/src/spending/policy.ts +271 -0
  94. package/src/staking/client.ts +319 -0
  95. package/src/staking/index.ts +1 -0
  96. package/src/sui/client.ts +214 -0
  97. package/src/sui/index.ts +2 -0
  98. package/src/sui/tx-helpers.ts +1070 -0
  99. package/src/task/client.ts +215 -0
  100. package/src/task/index.ts +1 -0
  101. package/src/x402/client.ts +295 -0
  102. package/src/x402/index.ts +1 -0
  103. package/tests/auth/device-flow.test.ts +62 -0
  104. package/tests/auth/ed25519-provider.test.ts +24 -0
  105. package/tests/auth/evm-key.test.ts +31 -0
  106. package/tests/auth/session-store.test.ts +201 -0
  107. package/tests/auth/zklogin-provider.test.ts +366 -0
  108. package/tests/blobstore/encrypted-store.test.ts +78 -0
  109. package/tests/blobstore.test.ts +91 -0
  110. package/tests/cache.test.ts +124 -0
  111. package/tests/crypto/encryption.test.ts +70 -0
  112. package/tests/crypto/x25519.test.ts +47 -0
  113. package/tests/dispute/client.test.ts +238 -0
  114. package/tests/events.test.ts +202 -0
  115. package/tests/evm/wallet.test.ts +101 -0
  116. package/tests/hybrid-store.test.ts +121 -0
  117. package/tests/identity.test.ts +161 -0
  118. package/tests/marketplace.test.ts +308 -0
  119. package/tests/metering/hash-chain.test.ts +32 -0
  120. package/tests/metering/meter.test.ts +23 -0
  121. package/tests/metering/streaming.test.ts +52 -0
  122. package/tests/metering/verification.test.ts +27 -0
  123. package/tests/payment/rail-selector.test.ts +95 -0
  124. package/tests/registry.test.ts +183 -0
  125. package/tests/relay-consumer-client.test.ts +119 -0
  126. package/tests/relay-registry/client.test.ts +261 -0
  127. package/tests/reputation/event-publisher.test.ts +70 -0
  128. package/tests/reputation/merkle.test.ts +44 -0
  129. package/tests/reputation/score-calculator.test.ts +104 -0
  130. package/tests/reputation/store.test.ts +94 -0
  131. package/tests/routing/circuit-breaker.test.ts +45 -0
  132. package/tests/routing/fan-out.test.ts +123 -0
  133. package/tests/routing/performance.test.ts +49 -0
  134. package/tests/routing/selector.test.ts +114 -0
  135. package/tests/spending.test.ts +133 -0
  136. package/tests/staking/client.test.ts +286 -0
  137. package/tests/sui-client.test.ts +85 -0
  138. package/tests/task.test.ts +249 -0
  139. package/tests/tx-helpers.test.ts +70 -0
  140. package/tests/walrus-spike.test.ts +100 -0
  141. package/tests/walrus-store.test.ts +196 -0
  142. package/tests/x402/client.test.ts +116 -0
  143. package/tsconfig.json +9 -0
  144. package/tsup.config.ts +11 -0
  145. package/vitest.config.ts +8 -0
@@ -0,0 +1,319 @@
1
+ import type { StakePosition } from '@hivemind-os/collective-types';
2
+ import type { SuiEvent, SuiTransactionBlockResponse } from '@mysten/sui/client';
3
+ import type { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
4
+
5
+ import { isRecord, normalizeMoveValue, parseStakePositionFields } from '../internal/parsing.js';
6
+ import { MeshSuiClient } from '../sui/client.js';
7
+ import {
8
+ buildAddStakeTx,
9
+ buildDepositStakeTx,
10
+ buildSlashExpiredEscrowTx,
11
+ buildSlashNonDeliveryTx,
12
+ buildStartDeactivationTx,
13
+ buildWithdrawStakeTx,
14
+ } from '../sui/tx-helpers.js';
15
+
16
+ export interface StakingContractConfig {
17
+ packageId: string;
18
+ }
19
+
20
+ export const STAKING_COOLDOWN_MS = 604_800_000;
21
+
22
+ export class StakingClient {
23
+ constructor(
24
+ private readonly suiClient: MeshSuiClient,
25
+ private readonly config: StakingContractConfig,
26
+ ) {}
27
+
28
+ async depositStake(params: {
29
+ amountMist: bigint;
30
+ stakeType: 'agent' | 'relay';
31
+ signer: Ed25519Keypair;
32
+ }): Promise<{ stakeId: string; txDigest: string }> {
33
+ const tx = buildDepositStakeTx({
34
+ packageId: this.config.packageId,
35
+ amountMist: params.amountMist,
36
+ stakeType: params.stakeType,
37
+ });
38
+ const response = await this.suiClient.executeTransaction(tx, params.signer);
39
+ const stakeId = readStakeId(response, `${this.config.packageId}::staking::StakeDeposited`);
40
+ if (!stakeId) {
41
+ throw new Error('Unable to determine stake id from transaction response.');
42
+ }
43
+ return { stakeId, txDigest: response.digest };
44
+ }
45
+
46
+ async addStake(params: {
47
+ stakeId: string;
48
+ amountMist: bigint;
49
+ signer: Ed25519Keypair;
50
+ }): Promise<{ txDigest: string }> {
51
+ const tx = buildAddStakeTx({
52
+ packageId: this.config.packageId,
53
+ stakeId: params.stakeId,
54
+ amountMist: params.amountMist,
55
+ });
56
+ const response = await this.suiClient.executeTransaction(tx, params.signer);
57
+ return { txDigest: response.digest };
58
+ }
59
+
60
+ async startDeactivation(params: {
61
+ stakeId: string;
62
+ signer: Ed25519Keypair;
63
+ }): Promise<{ cooldownEndsAt: number; txDigest: string }> {
64
+ const tx = buildStartDeactivationTx({
65
+ packageId: this.config.packageId,
66
+ stakeId: params.stakeId,
67
+ });
68
+ const response = await this.suiClient.executeTransaction(tx, params.signer);
69
+ const event = findEvent(response.events, `${this.config.packageId}::staking::DeactivationStarted`);
70
+ return {
71
+ cooldownEndsAt: requireEventNumber(event, 'DeactivationStarted', 'cooldown_ends_at', 'cooldownEndsAt'),
72
+ txDigest: response.digest,
73
+ };
74
+ }
75
+
76
+ async withdrawStake(params: {
77
+ stakeId: string;
78
+ signer: Ed25519Keypair;
79
+ }): Promise<{ amountReturned: bigint; txDigest: string }> {
80
+ const tx = buildWithdrawStakeTx({
81
+ packageId: this.config.packageId,
82
+ stakeId: params.stakeId,
83
+ });
84
+ const response = await this.suiClient.executeTransaction(tx, params.signer);
85
+ const event = findEvent(response.events, `${this.config.packageId}::staking::StakeWithdrawn`);
86
+ return {
87
+ amountReturned: requireEventBigInt(event, 'StakeWithdrawn', 'amount'),
88
+ txDigest: response.digest,
89
+ };
90
+ }
91
+
92
+ async slashExpiredEscrow(params: {
93
+ stakeId: string;
94
+ taskId: string;
95
+ signer: Ed25519Keypair;
96
+ }): Promise<{ slashedAmount: bigint; txDigest: string }> {
97
+ const tx = buildSlashExpiredEscrowTx({
98
+ packageId: this.config.packageId,
99
+ stakeId: params.stakeId,
100
+ taskId: params.taskId,
101
+ });
102
+ const response = await this.suiClient.executeTransaction(tx, params.signer);
103
+ const event = findEvent(response.events, `${this.config.packageId}::staking::StakeSlashed`);
104
+ return {
105
+ slashedAmount: requireEventBigInt(event, 'StakeSlashed', 'amount'),
106
+ txDigest: response.digest,
107
+ };
108
+ }
109
+
110
+ async slashNonDelivery(params: {
111
+ stakeId: string;
112
+ taskId: string;
113
+ signer: Ed25519Keypair;
114
+ }): Promise<{ slashedAmount: bigint; txDigest: string }> {
115
+ const tx = buildSlashNonDeliveryTx({
116
+ packageId: this.config.packageId,
117
+ stakeId: params.stakeId,
118
+ taskId: params.taskId,
119
+ });
120
+ const response = await this.suiClient.executeTransaction(tx, params.signer);
121
+ const event = findEvent(response.events, `${this.config.packageId}::staking::StakeSlashed`);
122
+ return {
123
+ slashedAmount: requireEventBigInt(event, 'StakeSlashed', 'amount'),
124
+ txDigest: response.digest,
125
+ };
126
+ }
127
+
128
+ async getStakePosition(stakeId: string): Promise<StakePosition | null> {
129
+ try {
130
+ const object = await this.suiClient.getObject<Record<string, unknown>>(stakeId);
131
+ return parseStakePositionFields(object, stakeId);
132
+ } catch (error) {
133
+ if (isObjectMissingError(error)) {
134
+ return null;
135
+ }
136
+ throw error;
137
+ }
138
+ }
139
+
140
+ async getStakeByOwner(owner: string): Promise<StakePosition | null> {
141
+ const positions = await this.getStakeByOwners([owner]);
142
+ return positions.get(owner) ?? null;
143
+ }
144
+
145
+ async getStakeByOwners(owners: string[]): Promise<Map<string, StakePosition>> {
146
+ const ownerFilter = new Set(owners.map((owner) => owner.toLowerCase()));
147
+ if (ownerFilter.size === 0) {
148
+ return new Map();
149
+ }
150
+
151
+ const stakeEventType = `${this.config.packageId}::staking::StakeDeposited`;
152
+ const stakeIdsByOwner = new Map<string, Set<string>>();
153
+ let cursor = null;
154
+
155
+ do {
156
+ const page = await this.suiClient.queryEvents(stakeEventType, cursor, 100);
157
+ for (const event of page.events) {
158
+ const payload = normalizeEvent(event);
159
+ const eventOwner = asString(readEventField(payload, 'owner')).toLowerCase();
160
+ if (!ownerFilter.has(eventOwner)) {
161
+ continue;
162
+ }
163
+ const stakeId = asString(readEventField(payload, 'stake_id', 'stakeId'));
164
+ if (!stakeId) {
165
+ continue;
166
+ }
167
+ const existing = stakeIdsByOwner.get(eventOwner) ?? new Set<string>();
168
+ existing.add(stakeId);
169
+ stakeIdsByOwner.set(eventOwner, existing);
170
+ }
171
+ cursor = page.nextCursor;
172
+ if (!page.hasMore) {
173
+ break;
174
+ }
175
+ } while (cursor);
176
+
177
+ const normalizedResults = new Map<string, StakePosition>();
178
+ await Promise.all(
179
+ [...stakeIdsByOwner.entries()].map(async ([normalizedOwner, ids]) => {
180
+ const positions = await Promise.all([...ids].map(async (stakeId) => await this.getStakePosition(stakeId)));
181
+ const best = positions.filter((position): position is StakePosition => Boolean(position)).sort(compareStakePositions)[0];
182
+ if (best) {
183
+ normalizedResults.set(normalizedOwner, best);
184
+ }
185
+ }),
186
+ );
187
+
188
+ return new Map(
189
+ owners
190
+ .map((owner) => [owner, normalizedResults.get(owner.toLowerCase())] as const)
191
+ .filter((entry): entry is readonly [string, StakePosition] => Boolean(entry[1])),
192
+ );
193
+ }
194
+ }
195
+
196
+ function compareStakePositions(left: StakePosition, right: StakePosition): number {
197
+ return (
198
+ compareBoolean(left.isActive ?? false, right.isActive ?? false) ||
199
+ compareBoolean(left.meetsMinium ?? false, right.meetsMinium ?? false) ||
200
+ compareBigInt(left.balanceMist, right.balanceMist) ||
201
+ compareNumber(left.stakedAt, right.stakedAt)
202
+ );
203
+ }
204
+
205
+ function readStakeId(response: SuiTransactionBlockResponse, eventType: string): string | undefined {
206
+ const event = findEvent(response.events, eventType);
207
+ return asString(readEventField(event, 'stake_id', 'stakeId')) || extractObjectId(response, /::staking::StakePosition$/);
208
+ }
209
+
210
+ function extractObjectId(response: SuiTransactionBlockResponse, objectTypePattern: RegExp): string | undefined {
211
+ const change = (response.objectChanges as Array<Record<string, unknown>> | null | undefined)?.find(
212
+ (entry) =>
213
+ (entry.type === 'created' || entry.type === 'transferred' || entry.type === 'mutated') &&
214
+ typeof entry.objectType === 'string' &&
215
+ objectTypePattern.test(entry.objectType) &&
216
+ typeof entry.objectId === 'string',
217
+ );
218
+ return change?.objectId as string | undefined;
219
+ }
220
+
221
+ function findEvent(events: SuiTransactionBlockResponse['events'], eventType: string): Record<string, unknown> {
222
+ const payload = events
223
+ ?.map((event) => ({ type: event.type, payload: normalizeEvent(event) }))
224
+ .find((event) => event.type === eventType)?.payload;
225
+ return payload ?? {};
226
+ }
227
+
228
+ function normalizeEvent(event: SuiEvent): Record<string, unknown> {
229
+ const normalized = normalizeMoveValue(event.parsedJson);
230
+ return isRecord(normalized) ? normalized : {};
231
+ }
232
+
233
+ function readEventField(record: Record<string, unknown>, ...keys: string[]): unknown {
234
+ for (const key of keys) {
235
+ if (key in record) {
236
+ return record[key];
237
+ }
238
+ }
239
+ return undefined;
240
+ }
241
+
242
+ function asString(value: unknown): string {
243
+ return typeof value === 'string' ? value : '';
244
+ }
245
+
246
+ function asNumber(value: unknown): number {
247
+ if (typeof value === 'number') {
248
+ return value;
249
+ }
250
+ if (typeof value === 'bigint') {
251
+ return Number(value);
252
+ }
253
+ if (typeof value === 'string' && value.length > 0) {
254
+ return Number(value);
255
+ }
256
+ return 0;
257
+ }
258
+
259
+ function requireEventNumber(record: Record<string, unknown>, eventName: string, ...keys: string[]): number {
260
+ const raw = readEventField(record, ...keys);
261
+ if (raw == null || raw === '') {
262
+ throw new Error(`${eventName} event did not include a valid ${keys[0]}.`);
263
+ }
264
+ const value = asNumber(raw);
265
+ if (!Number.isSafeInteger(value) || value < 0) {
266
+ throw new Error(`${eventName} event did not include a valid ${keys[0]}.`);
267
+ }
268
+ return value;
269
+ }
270
+
271
+ function asBigInt(value: unknown): bigint {
272
+ if (typeof value === 'bigint') {
273
+ return value;
274
+ }
275
+ if (typeof value === 'number') {
276
+ return BigInt(value);
277
+ }
278
+ if (typeof value === 'string' && value.length > 0) {
279
+ return BigInt(value);
280
+ }
281
+ return 0n;
282
+ }
283
+
284
+ function requireEventBigInt(record: Record<string, unknown>, eventName: string, ...keys: string[]): bigint {
285
+ const raw = readEventField(record, ...keys);
286
+ if (raw == null || raw === '') {
287
+ throw new Error(`${eventName} event did not include ${keys[0]}.`);
288
+ }
289
+ try {
290
+ return asBigInt(raw);
291
+ } catch {
292
+ throw new Error(`${eventName} event did not include a valid ${keys[0]}.`);
293
+ }
294
+ }
295
+
296
+ function compareBoolean(left: boolean, right: boolean): number {
297
+ if (left === right) {
298
+ return 0;
299
+ }
300
+ return left ? -1 : 1;
301
+ }
302
+
303
+ function compareBigInt(left: bigint, right: bigint): number {
304
+ if (left === right) {
305
+ return 0;
306
+ }
307
+ return left > right ? -1 : 1;
308
+ }
309
+
310
+ function compareNumber(left: number, right: number): number {
311
+ if (left === right) {
312
+ return 0;
313
+ }
314
+ return left > right ? -1 : 1;
315
+ }
316
+
317
+ function isObjectMissingError(error: unknown): boolean {
318
+ return error instanceof Error && /not found|does not contain move object data/i.test(error.message);
319
+ }
@@ -0,0 +1 @@
1
+ export * from './client.js';
@@ -0,0 +1,214 @@
1
+ import pino from 'pino';
2
+
3
+ import type { NetworkConfig } from '@hivemind-os/collective-types';
4
+ import {
5
+ SuiClient,
6
+ type EventId as EventID,
7
+ type SuiEvent,
8
+ type SuiTransactionBlockResponse,
9
+ type SuiTransactionBlockResponseOptions,
10
+ } from '@mysten/sui/client';
11
+ import type { Signer } from '@mysten/sui/cryptography';
12
+ import { Transaction } from '@mysten/sui/transactions';
13
+
14
+ import { isRecord, normalizeMoveValue, normalizeObjectOwner } from '../internal/parsing.js';
15
+
16
+ const logger = pino({ name: '@hivemind-os/collective-core:sui' });
17
+ const MAX_TRANSACTION_ATTEMPTS = 3;
18
+
19
+ export interface ExecuteTransactionOptions {
20
+ requestType?: 'WaitForEffectsCert' | 'WaitForLocalExecution';
21
+ responseOptions?: SuiTransactionBlockResponseOptions;
22
+ timeout?: number;
23
+ pollInterval?: number;
24
+ }
25
+
26
+ export class SuiTransactionExecutionError extends Error {
27
+ readonly digest?: string;
28
+ readonly retryable: boolean;
29
+
30
+ constructor(message: string, options: { cause?: unknown; digest?: string; retryable?: boolean } = {}) {
31
+ super(message, { cause: options.cause });
32
+ this.name = 'SuiTransactionExecutionError';
33
+ this.digest = options.digest;
34
+ this.retryable = options.retryable ?? false;
35
+ }
36
+ }
37
+
38
+ export function createSuiClient(config: NetworkConfig): SuiClient {
39
+ return new SuiClient({ url: config.rpcUrl });
40
+ }
41
+
42
+ export class MeshSuiClient {
43
+ private readonly suiClient: SuiClient;
44
+
45
+ constructor(private readonly networkConfig: NetworkConfig) {
46
+ this.suiClient = createSuiClient(networkConfig);
47
+ }
48
+
49
+ async executeTransaction(
50
+ tx: Transaction,
51
+ keypair: Signer,
52
+ options: ExecuteTransactionOptions = {},
53
+ ): Promise<SuiTransactionBlockResponse> {
54
+ const responseOptions: SuiTransactionBlockResponseOptions = {
55
+ showEffects: true,
56
+ showEvents: true,
57
+ showObjectChanges: true,
58
+ ...options.responseOptions,
59
+ };
60
+
61
+ tx.setSenderIfNotSet(keypair.getPublicKey().toSuiAddress());
62
+
63
+ let lastError: SuiTransactionExecutionError | undefined;
64
+ for (let attempt = 1; attempt <= MAX_TRANSACTION_ATTEMPTS; attempt += 1) {
65
+ try {
66
+ const executed = await this.suiClient.signAndExecuteTransaction({
67
+ transaction: tx,
68
+ signer: keypair,
69
+ requestType: options.requestType ?? 'WaitForLocalExecution',
70
+ options: responseOptions,
71
+ });
72
+ const response = await this.suiClient.waitForTransaction({
73
+ digest: executed.digest,
74
+ options: responseOptions,
75
+ timeout: options.timeout,
76
+ pollInterval: options.pollInterval,
77
+ });
78
+ const failure = getExecutionFailure(response);
79
+ if (failure) {
80
+ throw failure;
81
+ }
82
+
83
+ return response;
84
+ } catch (error) {
85
+ const executionError = normalizeExecutionError(error);
86
+ lastError = executionError;
87
+ if (attempt >= MAX_TRANSACTION_ATTEMPTS || !executionError.retryable) {
88
+ throw executionError;
89
+ }
90
+
91
+ logger.warn(
92
+ { err: executionError, attempt, digest: executionError.digest },
93
+ 'Retrying Sui transaction after retryable failure.',
94
+ );
95
+ await delay(getRetryDelayMs(attempt));
96
+ }
97
+ }
98
+
99
+ throw lastError ?? new SuiTransactionExecutionError('Transaction execution failed.');
100
+ }
101
+
102
+ async getBalance(address: string): Promise<bigint> {
103
+ const balance = await this.suiClient.getBalance({ owner: address });
104
+ return BigInt(balance.totalBalance);
105
+ }
106
+
107
+ async queryEvents(
108
+ eventType: string,
109
+ cursor?: EventID | null,
110
+ limit?: number,
111
+ ): Promise<{ events: SuiEvent[]; nextCursor: EventID | null; hasMore: boolean }> {
112
+ const result = await this.suiClient.queryEvents({
113
+ query: { MoveEventType: eventType },
114
+ cursor: cursor ?? undefined,
115
+ limit,
116
+ order: 'ascending',
117
+ });
118
+
119
+ return {
120
+ events: result.data,
121
+ nextCursor: result.nextCursor ?? null,
122
+ hasMore: result.hasNextPage,
123
+ };
124
+ }
125
+
126
+ async getObject<T>(objectId: string): Promise<T> {
127
+ const response = await this.suiClient.getObject({
128
+ id: objectId,
129
+ options: {
130
+ showContent: true,
131
+ showOwner: true,
132
+ showType: true,
133
+ },
134
+ });
135
+
136
+ if (!response.data?.content || response.data.content.dataType !== 'moveObject') {
137
+ throw new Error(`Object ${objectId} was not found or does not contain Move object data.`);
138
+ }
139
+
140
+ const normalized = normalizeMoveValue(response.data.content.fields);
141
+ if (!isRecord(normalized)) {
142
+ throw new Error(`Object ${objectId} did not resolve to a field record.`);
143
+ }
144
+
145
+ return {
146
+ objectId: response.data.objectId,
147
+ objectType: response.data.type ?? undefined,
148
+ objectOwner: normalizeObjectOwner(response.data.owner),
149
+ ...normalized,
150
+ } as T;
151
+ }
152
+
153
+ get client(): SuiClient {
154
+ return this.suiClient;
155
+ }
156
+
157
+ get config(): NetworkConfig {
158
+ return this.networkConfig;
159
+ }
160
+ }
161
+
162
+ function getExecutionFailure(response: SuiTransactionBlockResponse): SuiTransactionExecutionError | null {
163
+ const status = response.effects?.status;
164
+ if (!status || status.status !== 'failure') {
165
+ return null;
166
+ }
167
+
168
+ const rawMessage = typeof status.error === 'string' && status.error.trim() ? status.error.trim() : 'unknown error';
169
+ return new SuiTransactionExecutionError(formatFailureMessage(rawMessage, response.digest), {
170
+ digest: response.digest,
171
+ retryable: isRetryableErrorMessage(rawMessage),
172
+ });
173
+ }
174
+
175
+ function normalizeExecutionError(error: unknown): SuiTransactionExecutionError {
176
+ if (error instanceof SuiTransactionExecutionError) {
177
+ return error;
178
+ }
179
+
180
+ const message = error instanceof Error ? error.message : String(error);
181
+ return new SuiTransactionExecutionError(formatFailureMessage(message), {
182
+ cause: error,
183
+ retryable: isRetryableErrorMessage(message),
184
+ });
185
+ }
186
+
187
+ function formatFailureMessage(message: string, digest?: string): string {
188
+ const prefix = digest ? `Sui transaction ${digest}` : 'Sui transaction';
189
+ if (/insufficient gas|no valid gas|gas balance|gas coin/i.test(message)) {
190
+ return `${prefix} failed due to insufficient gas: ${message}`;
191
+ }
192
+
193
+ if (/object.*(lock|conflict)|equivocat|shared object.*busy/i.test(message)) {
194
+ return `${prefix} failed due to an object lock or conflict: ${message}`;
195
+ }
196
+
197
+ return `${prefix} failed: ${message}`;
198
+ }
199
+
200
+ function isRetryableErrorMessage(message: string): boolean {
201
+ return /(timeout|temporar|429|5\d\d|fetch failed|network|connection reset|ECONNRESET|ETIMEDOUT|object.*(lock|conflict)|equivocat|busy)/i.test(
202
+ message,
203
+ );
204
+ }
205
+
206
+ function getRetryDelayMs(attempt: number): number {
207
+ return 500 * 2 ** (attempt - 1);
208
+ }
209
+
210
+ function delay(ms: number): Promise<void> {
211
+ return new Promise((resolvePromise) => {
212
+ setTimeout(resolvePromise, ms);
213
+ });
214
+ }
@@ -0,0 +1,2 @@
1
+ export * from './client.js';
2
+ export * from './tx-helpers.js';