@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,191 @@
1
+ import type { Dispute, NetworkConfig } 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, parseDisputeFields } from '../internal/parsing.js';
6
+ import { MeshSuiClient } from '../sui/client.js';
7
+ import {
8
+ buildAcceptResolutionTx,
9
+ buildArbitrateDisputeTx,
10
+ buildOpenDisputeTx,
11
+ buildRespondToDisputeTx,
12
+ } from '../sui/tx-helpers.js';
13
+
14
+ export class DisputeClient {
15
+ constructor(
16
+ private readonly suiClient: MeshSuiClient,
17
+ private readonly config: Pick<NetworkConfig, 'packageId'>,
18
+ ) {}
19
+
20
+ async openDispute(params: {
21
+ taskId: string;
22
+ evidenceBlobId: string;
23
+ proposedSplitMist: bigint;
24
+ arbitratorAddress?: string;
25
+ signer: Ed25519Keypair;
26
+ }): Promise<{ disputeId: string; txDigest: string }> {
27
+ const tx = buildOpenDisputeTx({
28
+ packageId: this.config.packageId,
29
+ taskId: params.taskId,
30
+ evidenceBlobId: params.evidenceBlobId,
31
+ proposedSplitMist: params.proposedSplitMist,
32
+ arbitratorAddress: params.arbitratorAddress,
33
+ });
34
+ const response = await this.suiClient.executeTransaction(tx, params.signer);
35
+ const disputeId =
36
+ extractObjectId(response, /::dispute::Dispute$/)
37
+ ?? asString(readEventField(findEvent(response.events, `${this.config.packageId}::dispute::DisputeOpened`), 'dispute_id', 'disputeId'));
38
+ if (!disputeId) {
39
+ throw new Error('Unable to determine dispute id from transaction response.');
40
+ }
41
+ return { disputeId, txDigest: response.digest };
42
+ }
43
+
44
+ async respondToDispute(params: {
45
+ disputeId: string;
46
+ evidenceBlobId: string;
47
+ proposedSplitMist: bigint;
48
+ signer: Ed25519Keypair;
49
+ }): Promise<{ txDigest: string }> {
50
+ const tx = buildRespondToDisputeTx({
51
+ packageId: this.config.packageId,
52
+ disputeId: params.disputeId,
53
+ evidenceBlobId: params.evidenceBlobId,
54
+ proposedSplitMist: params.proposedSplitMist,
55
+ });
56
+ const response = await this.suiClient.executeTransaction(tx, params.signer);
57
+ return { txDigest: response.digest };
58
+ }
59
+
60
+ async acceptResolution(params: {
61
+ disputeId: string;
62
+ taskId: string;
63
+ signer: Ed25519Keypair;
64
+ }): Promise<{ requesterAmount: bigint; providerAmount: bigint; txDigest: string }> {
65
+ const tx = buildAcceptResolutionTx({
66
+ packageId: this.config.packageId,
67
+ disputeId: params.disputeId,
68
+ taskId: params.taskId,
69
+ });
70
+ const response = await this.suiClient.executeTransaction(tx, params.signer);
71
+ const event = findEvent(response.events, `${this.config.packageId}::dispute::DisputeMutuallyResolved`);
72
+ return {
73
+ requesterAmount: requireEventBigInt(event, 'DisputeMutuallyResolved', 'requester_amount', 'requesterAmount'),
74
+ providerAmount: requireEventBigInt(event, 'DisputeMutuallyResolved', 'provider_amount', 'providerAmount'),
75
+ txDigest: response.digest,
76
+ };
77
+ }
78
+
79
+ async arbitrate(params: {
80
+ disputeId: string;
81
+ taskId: string;
82
+ rulingSplitMist: bigint;
83
+ signer: Ed25519Keypair;
84
+ }): Promise<{ txDigest: string }> {
85
+ const tx = buildArbitrateDisputeTx({
86
+ packageId: this.config.packageId,
87
+ disputeId: params.disputeId,
88
+ taskId: params.taskId,
89
+ rulingSplitMist: params.rulingSplitMist,
90
+ });
91
+ const response = await this.suiClient.executeTransaction(tx, params.signer);
92
+ return { txDigest: response.digest };
93
+ }
94
+
95
+ async getDispute(disputeId: string): Promise<Dispute | null> {
96
+ try {
97
+ const object = await this.suiClient.getObject<Record<string, unknown>>(disputeId);
98
+ return parseDisputeFields(object, disputeId);
99
+ } catch (error) {
100
+ if (isObjectMissingError(error)) {
101
+ return null;
102
+ }
103
+ throw error;
104
+ }
105
+ }
106
+
107
+ async getDisputeByTask(taskId: string): Promise<Dispute | null> {
108
+ const eventType = `${this.config.packageId}::dispute::DisputeOpened`;
109
+ let cursor = null;
110
+
111
+ do {
112
+ const page = await this.suiClient.queryEvents(eventType, cursor, 100);
113
+ const matchedEvent = page.events.find((event) => asString(readEventField(normalizeEvent(event), 'task_id', 'taskId')) === taskId);
114
+ if (matchedEvent) {
115
+ const disputeId = asString(readEventField(normalizeEvent(matchedEvent), 'dispute_id', 'disputeId'));
116
+ return disputeId ? await this.getDispute(disputeId) : null;
117
+ }
118
+ cursor = page.nextCursor;
119
+ if (!page.hasMore) {
120
+ break;
121
+ }
122
+ } while (cursor);
123
+
124
+ return null;
125
+ }
126
+ }
127
+
128
+ function extractObjectId(response: SuiTransactionBlockResponse, objectTypePattern: RegExp): string | undefined {
129
+ const change = (response.objectChanges as Array<Record<string, unknown>> | null | undefined)?.find(
130
+ (entry) =>
131
+ (entry.type === 'created' || entry.type === 'transferred' || entry.type === 'mutated')
132
+ && typeof entry.objectType === 'string'
133
+ && objectTypePattern.test(entry.objectType)
134
+ && typeof entry.objectId === 'string',
135
+ );
136
+ return change?.objectId as string | undefined;
137
+ }
138
+
139
+ function findEvent(events: SuiTransactionBlockResponse['events'], eventType: string): Record<string, unknown> {
140
+ const payload = events
141
+ ?.map((event) => ({ type: event.type, payload: normalizeEvent(event) }))
142
+ .find((event) => event.type === eventType)?.payload;
143
+ return payload ?? {};
144
+ }
145
+
146
+ function normalizeEvent(event: SuiEvent): Record<string, unknown> {
147
+ const normalized = normalizeMoveValue(event.parsedJson);
148
+ return isRecord(normalized) ? normalized : {};
149
+ }
150
+
151
+ function readEventField(record: Record<string, unknown>, ...keys: string[]): unknown {
152
+ for (const key of keys) {
153
+ if (key in record) {
154
+ return record[key];
155
+ }
156
+ }
157
+ return undefined;
158
+ }
159
+
160
+ function asString(value: unknown): string {
161
+ return typeof value === 'string' ? value : '';
162
+ }
163
+
164
+ function asBigInt(value: unknown): bigint {
165
+ if (typeof value === 'bigint') {
166
+ return value;
167
+ }
168
+ if (typeof value === 'number') {
169
+ return BigInt(value);
170
+ }
171
+ if (typeof value === 'string' && value.length > 0) {
172
+ return BigInt(value);
173
+ }
174
+ return 0n;
175
+ }
176
+
177
+ function requireEventBigInt(record: Record<string, unknown>, eventName: string, ...keys: string[]): bigint {
178
+ const raw = readEventField(record, ...keys);
179
+ if (raw == null || raw === '') {
180
+ throw new Error(`${eventName} event did not include ${keys[0]}.`);
181
+ }
182
+ try {
183
+ return asBigInt(raw);
184
+ } catch {
185
+ throw new Error(`${eventName} event did not include a valid ${keys[0]}.`);
186
+ }
187
+ }
188
+
189
+ function isObjectMissingError(error: unknown): boolean {
190
+ return error instanceof Error && /not found|does not contain move object data/i.test(error.message);
191
+ }
@@ -0,0 +1 @@
1
+ export * from './client.js';
@@ -0,0 +1,2 @@
1
+ export * from './subscription.js';
2
+ export * from './parser.js';
@@ -0,0 +1,291 @@
1
+ import { BidStatus, PaymentScheme, RelayNodeStatus, TaskStatus, type MeshEvent } from '@hivemind-os/collective-types';
2
+ import type { SuiEvent } from '@mysten/sui/client';
3
+
4
+ import { isRecord, parseAgentCardFields, parseBidFields, parseRelayNodeFields, parseTaskFields } from '../internal/parsing.js';
5
+
6
+ export function parseRawEvent(rawEvent: SuiEvent, packageId: string): MeshEvent | null {
7
+ if (!rawEvent.type.startsWith(`${packageId}::`)) {
8
+ return null;
9
+ }
10
+
11
+ if (!isRecord(rawEvent.parsedJson)) {
12
+ return null;
13
+ }
14
+
15
+ const payload = rawEvent.parsedJson;
16
+ const timestampMs = Number(rawEvent.timestampMs ?? 0);
17
+ const base = {
18
+ packageId,
19
+ txDigest: rawEvent.id.txDigest,
20
+ timestampMs,
21
+ };
22
+
23
+ switch (rawEvent.type) {
24
+ case `${packageId}::registry::AgentRegistered`: {
25
+ const agent = parseAgentCardFields(payload);
26
+ return {
27
+ ...base,
28
+ type: 'agent.registered',
29
+ registryId: '',
30
+ agent,
31
+ };
32
+ }
33
+ case `${packageId}::registry::AgentUpdated`: {
34
+ const agent = parseAgentCardFields(payload);
35
+ return {
36
+ ...base,
37
+ type: 'agent.updated',
38
+ agent,
39
+ previousVersion: Math.max(agent.version - 1, 0),
40
+ };
41
+ }
42
+ case `${packageId}::registry::AgentDeactivated`: {
43
+ const agent = parseAgentCardFields(payload);
44
+ return {
45
+ ...base,
46
+ type: 'agent.deactivated',
47
+ agentId: agent.id,
48
+ owner: agent.owner,
49
+ deactivatedAt: timestampMs,
50
+ };
51
+ }
52
+ case `${packageId}::task::TaskPosted`: {
53
+ return {
54
+ ...base,
55
+ type: 'task.posted',
56
+ task: parseTaskFields(payload),
57
+ };
58
+ }
59
+ case `${packageId}::task::TaskAccepted`: {
60
+ return {
61
+ ...base,
62
+ type: 'task.accepted',
63
+ taskId: asString(payload.task_id, payload.taskId),
64
+ requester: asString(payload.requester),
65
+ provider: asString(payload.provider),
66
+ price: asBigInt(payload.price),
67
+ acceptedAt: asNumber(payload.accepted_at, timestampMs),
68
+ status: TaskStatus.ACCEPTED,
69
+ };
70
+ }
71
+ case `${packageId}::task::TaskCompleted`: {
72
+ return {
73
+ ...base,
74
+ type: 'task.completed',
75
+ taskId: asString(payload.task_id, payload.taskId),
76
+ provider: asString(payload.provider),
77
+ resultBlobId: bytesToString(payload.result_blob_id ?? payload.resultBlobId),
78
+ price: asBigInt(payload.price),
79
+ paymentScheme: asOptionalPaymentScheme(payload.payment_scheme ?? payload.paymentScheme),
80
+ meteredUnits: asOptionalNumber(payload.metered_units ?? payload.meteredUnits),
81
+ verificationHash: bytesToHex(payload.verification_hash ?? payload.verificationHash),
82
+ completedAt: asNumber(payload.completed_at, timestampMs),
83
+ status: TaskStatus.COMPLETED,
84
+ };
85
+ }
86
+ case `${packageId}::task::TaskPaymentReleased`: {
87
+ return {
88
+ ...base,
89
+ type: 'task.released',
90
+ taskId: asString(payload.task_id, payload.taskId),
91
+ requester: asString(payload.requester),
92
+ provider: asString(payload.provider),
93
+ price: asBigInt(payload.price),
94
+ refundAmount: asOptionalBigInt(payload.refund_amount ?? payload.refundAmount),
95
+ releasedAt: timestampMs,
96
+ status: TaskStatus.RELEASED,
97
+ };
98
+ }
99
+ case `${packageId}::task::TaskDisputed`: {
100
+ return {
101
+ ...base,
102
+ type: 'task.disputed',
103
+ taskId: asString(payload.task_id, payload.taskId),
104
+ requester: asString(payload.requester),
105
+ provider: asOptionalString(payload.provider),
106
+ disputedAt: asNumber(payload.disputed_at, timestampMs),
107
+ status: TaskStatus.DISPUTED,
108
+ };
109
+ }
110
+ case `${packageId}::task::TaskCancelled`:
111
+ case `${packageId}::task::TaskExpiredRefunded`: {
112
+ return {
113
+ ...base,
114
+ type: 'task.cancelled',
115
+ taskId: asString(payload.task_id, payload.taskId),
116
+ requester: asString(payload.requester),
117
+ cancelledAt: timestampMs,
118
+ status: TaskStatus.CANCELLED,
119
+ };
120
+ }
121
+ case `${packageId}::marketplace::BidPlaced`: {
122
+ return {
123
+ ...base,
124
+ type: 'bid.placed',
125
+ bid: parseBidFields(payload),
126
+ };
127
+ }
128
+ case `${packageId}::marketplace::BidAccepted`: {
129
+ return {
130
+ ...base,
131
+ type: 'bid.accepted',
132
+ bidId: asString(payload.bid_id, payload.bidId),
133
+ taskId: asString(payload.task_id, payload.taskId),
134
+ requester: asString(payload.requester),
135
+ bidder: asString(payload.bidder),
136
+ bidPrice: asBigInt(payload.bid_price ?? payload.bidPrice),
137
+ refundedAmount: asBigInt(payload.refunded_amount ?? payload.refundedAmount),
138
+ acceptedAt: asNumber(payload.accepted_at, timestampMs),
139
+ status: BidStatus.ACCEPTED,
140
+ };
141
+ }
142
+ case `${packageId}::marketplace::BidWithdrawn`: {
143
+ return {
144
+ ...base,
145
+ type: 'bid.withdrawn',
146
+ bidId: asString(payload.bid_id, payload.bidId),
147
+ taskId: asString(payload.task_id, payload.taskId),
148
+ bidder: asString(payload.bidder),
149
+ withdrawnAt: timestampMs,
150
+ status: BidStatus.WITHDRAWN,
151
+ };
152
+ }
153
+ case `${packageId}::marketplace::BidRejected`: {
154
+ return {
155
+ ...base,
156
+ type: 'bid.rejected',
157
+ bidId: asString(payload.bid_id, payload.bidId),
158
+ taskId: asString(payload.task_id, payload.taskId),
159
+ requester: asString(payload.requester),
160
+ bidder: asString(payload.bidder),
161
+ rejectedAt: timestampMs,
162
+ status: BidStatus.REJECTED,
163
+ };
164
+ }
165
+ case `${packageId}::relay_registry::RelayRegistered`: {
166
+ return {
167
+ ...base,
168
+ type: 'relay.registered',
169
+ relay: parseRelayNodeFields(payload),
170
+ };
171
+ }
172
+ case `${packageId}::relay_registry::RelayHeartbeat`: {
173
+ return {
174
+ ...base,
175
+ type: 'relay.heartbeat',
176
+ relayId: asString(payload.relay_id, payload.relayId),
177
+ operator: asString(payload.operator),
178
+ lastHeartbeat: asNumber(payload.last_heartbeat ?? payload.lastHeartbeat, timestampMs),
179
+ };
180
+ }
181
+ case `${packageId}::relay_registry::RelayDeactivated`: {
182
+ return {
183
+ ...base,
184
+ type: 'relay.deactivated',
185
+ relayId: asString(payload.relay_id, payload.relayId),
186
+ operator: asString(payload.operator),
187
+ status: RelayNodeStatus.INACTIVE,
188
+ };
189
+ }
190
+ case `${packageId}::relay_registry::RelaySlashed`: {
191
+ return {
192
+ ...base,
193
+ type: 'relay.slashed',
194
+ relayId: asString(payload.relay_id, payload.relayId),
195
+ operator: asString(payload.operator),
196
+ status: RelayNodeStatus.SLASHED,
197
+ };
198
+ }
199
+ default:
200
+ return null;
201
+ }
202
+ }
203
+
204
+ function asString(...values: unknown[]): string {
205
+ const match = values.find((value) => typeof value === 'string');
206
+ return typeof match === 'string' ? match : '';
207
+ }
208
+
209
+ function asOptionalString(value: unknown): string | undefined {
210
+ const normalized = asString(value);
211
+ return normalized.length > 0 ? normalized : undefined;
212
+ }
213
+
214
+ function asNumber(value: unknown, fallback = 0): number {
215
+ if (typeof value === 'number') {
216
+ return value;
217
+ }
218
+
219
+ if (typeof value === 'string' && value.length > 0) {
220
+ return Number(value);
221
+ }
222
+
223
+ if (typeof value === 'bigint') {
224
+ return Number(value);
225
+ }
226
+
227
+ return fallback;
228
+ }
229
+
230
+ function asOptionalNumber(value: unknown): number | undefined {
231
+ if (value === undefined || value === null || value === '') {
232
+ return undefined;
233
+ }
234
+ return asNumber(value);
235
+ }
236
+
237
+ function asBigInt(value: unknown): bigint {
238
+ if (typeof value === 'bigint') {
239
+ return value;
240
+ }
241
+ if (typeof value === 'number') {
242
+ return BigInt(value);
243
+ }
244
+ if (typeof value === 'string' && value.length > 0) {
245
+ return BigInt(value);
246
+ }
247
+ return 0n;
248
+ }
249
+
250
+ function asOptionalBigInt(value: unknown): bigint | undefined {
251
+ if (value === undefined || value === null || value === '') {
252
+ return undefined;
253
+ }
254
+ return asBigInt(value);
255
+ }
256
+
257
+ function asOptionalPaymentScheme(value: unknown): PaymentScheme | undefined {
258
+ if (value === undefined || value === null || value === '') {
259
+ return undefined;
260
+ }
261
+ const parsed = asNumber(value);
262
+ switch (parsed) {
263
+ case 0:
264
+ return PaymentScheme.EXACT;
265
+ case 1:
266
+ return PaymentScheme.UPTO;
267
+ case 2:
268
+ return PaymentScheme.STREAM;
269
+ default:
270
+ return undefined;
271
+ }
272
+ }
273
+
274
+ function bytesToString(value: unknown): string {
275
+ if (typeof value === 'string') {
276
+ return value;
277
+ }
278
+
279
+ if (Array.isArray(value) && value.every((entry) => typeof entry === 'number')) {
280
+ return new TextDecoder().decode(new Uint8Array(value));
281
+ }
282
+
283
+ return '';
284
+ }
285
+
286
+ function bytesToHex(value: unknown): string | undefined {
287
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === 'number')) {
288
+ return undefined;
289
+ }
290
+ return Buffer.from(value).toString('hex');
291
+ }
@@ -0,0 +1,131 @@
1
+ import Database from 'better-sqlite3';
2
+ import pino from 'pino';
3
+
4
+ import type { EventId as EventID, SuiEvent } from '@mysten/sui/client';
5
+
6
+ import { MeshSuiClient } from '../sui/client.js';
7
+
8
+ const logger = pino({ name: '@hivemind-os/collective-core:events' });
9
+
10
+ export interface CursorStore {
11
+ getCursor(eventType: string): Promise<EventID | null>;
12
+ setCursor(eventType: string, cursor: EventID): Promise<void>;
13
+ }
14
+
15
+ export class EventSubscription {
16
+ private running = false;
17
+ private timer?: NodeJS.Timeout;
18
+ private cursor: EventID | null = null;
19
+ private polling = false;
20
+
21
+ constructor(
22
+ private readonly params: {
23
+ suiClient: MeshSuiClient;
24
+ eventType: string;
25
+ pollIntervalMs?: number;
26
+ onEvent: (event: SuiEvent) => Promise<void>;
27
+ onError?: (error: unknown) => void;
28
+ cursorStore: CursorStore;
29
+ },
30
+ ) {}
31
+
32
+ start(): void {
33
+ if (this.running) {
34
+ return;
35
+ }
36
+
37
+ this.running = true;
38
+ void this.poll();
39
+ }
40
+
41
+ stop(): void {
42
+ this.running = false;
43
+ if (this.timer) {
44
+ clearTimeout(this.timer);
45
+ this.timer = undefined;
46
+ }
47
+ }
48
+
49
+ isRunning(): boolean {
50
+ return this.running;
51
+ }
52
+
53
+ private async poll(): Promise<void> {
54
+ if (!this.running || this.polling) {
55
+ return;
56
+ }
57
+
58
+ this.polling = true;
59
+ let nextDelay = this.params.pollIntervalMs ?? 5_000;
60
+
61
+ try {
62
+ if (!this.cursor) {
63
+ this.cursor = await this.params.cursorStore.getCursor(this.params.eventType);
64
+ }
65
+
66
+ const page = await this.params.suiClient.queryEvents(
67
+ this.params.eventType,
68
+ this.cursor,
69
+ 100,
70
+ );
71
+
72
+ for (const event of page.events) {
73
+ await this.params.onEvent(event);
74
+ this.cursor = event.id;
75
+ await this.params.cursorStore.setCursor(this.params.eventType, event.id);
76
+ }
77
+
78
+ if (page.hasMore) {
79
+ nextDelay = 0;
80
+ }
81
+ } catch (error) {
82
+ logger.error({ err: error, eventType: this.params.eventType }, 'Event polling failed.');
83
+ this.params.onError?.(error);
84
+ } finally {
85
+ this.polling = false;
86
+ if (this.running) {
87
+ this.timer = setTimeout(() => {
88
+ void this.poll();
89
+ }, nextDelay);
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ export class SqliteCursorStore implements CursorStore {
96
+ private readonly db: Database.Database;
97
+
98
+ constructor(dbPath: string) {
99
+ this.db = new Database(dbPath);
100
+ this.db.exec(`
101
+ CREATE TABLE IF NOT EXISTS event_cursors (
102
+ event_type TEXT PRIMARY KEY,
103
+ cursor_json TEXT NOT NULL,
104
+ updated_at INTEGER NOT NULL
105
+ );
106
+ `);
107
+ }
108
+
109
+ async getCursor(eventType: string): Promise<EventID | null> {
110
+ const row = this.db
111
+ .prepare('SELECT cursor_json FROM event_cursors WHERE event_type = ?')
112
+ .get(eventType) as { cursor_json: string } | undefined;
113
+
114
+ return row ? (JSON.parse(row.cursor_json) as EventID) : null;
115
+ }
116
+
117
+ async setCursor(eventType: string, cursor: EventID): Promise<void> {
118
+ this.db
119
+ .prepare(
120
+ `INSERT INTO event_cursors (event_type, cursor_json, updated_at)
121
+ VALUES (?, ?, ?)
122
+ ON CONFLICT(event_type)
123
+ DO UPDATE SET cursor_json = excluded.cursor_json, updated_at = excluded.updated_at`,
124
+ )
125
+ .run(eventType, JSON.stringify(cursor), Date.now());
126
+ }
127
+
128
+ close(): void {
129
+ this.db.close();
130
+ }
131
+ }
@@ -0,0 +1,6 @@
1
+ export const USDC_ADDRESS = {
2
+ base: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
3
+ 'base-sepolia': '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
4
+ } as const;
5
+
6
+ export const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3';
@@ -0,0 +1,2 @@
1
+ export * from './constants.js';
2
+ export * from './wallet.js';