@effectstream/sm 0.100.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 (60) hide show
  1. package/Stm.ts +88 -0
  2. package/delegate-wallet.ts +431 -0
  3. package/mod.ts +4 -0
  4. package/package.json +27 -0
  5. package/primitives/Primitive.ts +80 -0
  6. package/primitives/PrimitiveRegistry.ts +59 -0
  7. package/primitives/mod.ts +5 -0
  8. package/primitives/processing.ts +72 -0
  9. package/primitives/src/README.md +3 -0
  10. package/primitives/src/avail-generic/avail-generic-grammar.ts +5 -0
  11. package/primitives/src/avail-generic/avail-primitive.ts +103 -0
  12. package/primitives/src/bitcoin-address/bitcoin-grammar.ts +16 -0
  13. package/primitives/src/bitcoin-address/bitcoin-primitive.ts +113 -0
  14. package/primitives/src/builtin.ts +47 -0
  15. package/primitives/src/celestia-generic/celestia-generic-grammar.ts +5 -0
  16. package/primitives/src/celestia-generic/celestia-primitive.ts +97 -0
  17. package/primitives/src/evm-effectstream-l2/effectstream-l2-abi.ts +1 -0
  18. package/primitives/src/evm-effectstream-l2/effectstream-l2-primitive.ts +484 -0
  19. package/primitives/src/evm-erc1155/erc1155-abi.ts +1 -0
  20. package/primitives/src/evm-erc1155/erc1155-grammar.ts +13 -0
  21. package/primitives/src/evm-erc1155/erc1155-ivm.ts +104 -0
  22. package/primitives/src/evm-erc1155/erc1155-primitive.ts +242 -0
  23. package/primitives/src/evm-erc20/erc20-abi.ts +1 -0
  24. package/primitives/src/evm-erc20/erc20-grammar.ts +7 -0
  25. package/primitives/src/evm-erc20/erc20-ivm.ts +93 -0
  26. package/primitives/src/evm-erc20/erc20-primitive.test.ts +123 -0
  27. package/primitives/src/evm-erc20/erc20-primitive.ts +152 -0
  28. package/primitives/src/evm-erc721/erc721-abi.ts +1 -0
  29. package/primitives/src/evm-erc721/erc721-grammar.ts +9 -0
  30. package/primitives/src/evm-erc721/erc721-ivm.ts +85 -0
  31. package/primitives/src/evm-erc721/erc721-primitive.ts +149 -0
  32. package/primitives/src/evm-generic/evm-generic-primitive.ts +124 -0
  33. package/primitives/src/grammar.ts +34 -0
  34. package/primitives/src/midnight-generic/midnight-genetic-grammar.ts +8 -0
  35. package/primitives/src/midnight-generic/midnight-genetic.ts +271 -0
  36. package/primitives/src/midnight-nullifier/midnight-nullifier-grammar.ts +8 -0
  37. package/primitives/src/midnight-nullifier/midnight-nullifier.ts +114 -0
  38. package/primitives/src/mod.ts +81 -0
  39. package/primitives/src/near-account-watch/near-account-watch-grammar.ts +10 -0
  40. package/primitives/src/near-account-watch/near-account-watch-primitive.ts +106 -0
  41. package/primitives/src/near-generic/near-generic-grammar.ts +5 -0
  42. package/primitives/src/near-generic/near-generic-primitive.ts +105 -0
  43. package/primitives/src/near-intent/near-intent-grammar.ts +10 -0
  44. package/primitives/src/near-intent/near-intent-ivm.ts +71 -0
  45. package/primitives/src/near-intent/near-intent-primitive.ts +149 -0
  46. package/primitives/src/near-intent/near-intent-token-id-parser.ts +47 -0
  47. package/primitives/src/near-nep141/nep141-grammar.ts +7 -0
  48. package/primitives/src/near-nep141/nep141-ivm.ts +70 -0
  49. package/primitives/src/near-nep141/nep141-primitive.ts +117 -0
  50. package/primitives/src/near-nep171/nep171-grammar.ts +8 -0
  51. package/primitives/src/near-nep171/nep171-ivm.ts +71 -0
  52. package/primitives/src/near-nep171/nep171-primitive.ts +138 -0
  53. package/primitives/src/near-nep245/nep245-grammar.ts +11 -0
  54. package/primitives/src/near-nep245/nep245-ivm.ts +84 -0
  55. package/primitives/src/near-nep245/nep245-primitive.ts +147 -0
  56. package/primitives/src/utxorpc-generic/utxorpc-generic-grammar.ts +6 -0
  57. package/primitives/src/utxorpc-generic/utxorpc-generic.ts +88 -0
  58. package/primitives/types.ts +14 -0
  59. package/types.ts +28 -0
  60. package/verify-signature.ts +51 -0
package/Stm.ts ADDED
@@ -0,0 +1,88 @@
1
+ import {
2
+ type CommandTuples,
3
+ type FullJsonGrammar,
4
+ type GrammarDefinition,
5
+ parseStmInput,
6
+ toFullJsonGrammar,
7
+ toKeyedJsonGrammar,
8
+ } from "@effectstream/concise";
9
+ import type { AppEvents } from "./types.ts";
10
+ import type { Static, TSchema } from "@sinclair/typebox";
11
+ import type { BaseStfInput, BaseStfOutput } from "./types.ts";
12
+ import type { SyncStateUpdateStream } from "@effectstream/coroutine";
13
+
14
+ export type ParamToData<T extends readonly Readonly<[string, TSchema]>[]> = {
15
+ [K in T[number] as K[0]]: Static<K[1]>;
16
+ };
17
+ export type MessageListener<
18
+ Events extends AppEvents,
19
+ Params extends readonly Readonly<[string, TSchema]>[],
20
+ > = (
21
+ input: BaseStfInput & { parsedInput: ParamToData<Params> },
22
+ ) => SyncStateUpdateStream<void>;
23
+
24
+ export class Stm<
25
+ Grammar extends GrammarDefinition,
26
+ Events extends AppEvents,
27
+ > {
28
+ public readonly keyedJsonGrammar: CommandTuples<Grammar>;
29
+ public readonly fullJsonGrammar: FullJsonGrammar<Grammar>;
30
+
31
+ constructor(public readonly grammar: Grammar) {
32
+ // TODO: replace once TS5 decorators are better supported
33
+ this.addStateTransition.bind(this);
34
+ this.processInput.bind(this);
35
+
36
+ this.keyedJsonGrammar = toKeyedJsonGrammar(grammar);
37
+ this.fullJsonGrammar = toFullJsonGrammar(this.keyedJsonGrammar);
38
+ }
39
+
40
+ messageListeners = new Map<
41
+ string,
42
+ MessageListener<Events, readonly Readonly<[string, TSchema]>[]>
43
+ >();
44
+
45
+ addStateTransition<const Prefix extends keyof Grammar & string>(
46
+ prefix: Prefix,
47
+ call: MessageListener<Events, Grammar[Prefix]>,
48
+ ): void {
49
+ if (this.messageListeners.has(prefix)) {
50
+ throw new Error(
51
+ `Disallowed: duplicate listener for prefix ${prefix}. Duplicate prefixes can cause determinism issues`,
52
+ );
53
+ }
54
+ this.messageListeners.set(prefix, call);
55
+ }
56
+
57
+ *processInput(input: BaseStfInput): SyncStateUpdateStream<void> {
58
+ let prefix, data;
59
+ try {
60
+ const parsedInput = parseStmInput(
61
+ input.conciseInput,
62
+ this.grammar,
63
+ this.keyedJsonGrammar,
64
+ );
65
+ prefix = parsedInput.prefix;
66
+ data = parsedInput.data;
67
+ } catch (_e) {
68
+ if (_e instanceof Error) {
69
+ console.error(`[STM] Parsing error:`, _e.message);
70
+ }
71
+ console.error(
72
+ `[STM] Cannot parse the input with the known grammar:`,
73
+ input.conciseInput,
74
+ );
75
+ return;
76
+ }
77
+ const listener = this.messageListeners.get(prefix);
78
+ if (listener == null) {
79
+ console.error(
80
+ `Grammar match, but no prefix found with corresponding state transition: ${prefix}`,
81
+ );
82
+ return;
83
+ }
84
+
85
+ yield* listener({ ...input, parsedInput: data });
86
+ return;
87
+ }
88
+ }
@@ -0,0 +1,431 @@
1
+ import { type SyncStateUpdateStream, World } from "@effectstream/coroutine";
2
+ import {
3
+ accountMessages,
4
+ type BuiltinGrammar,
5
+ type BuiltinGrammarPrefix,
6
+ type ParseInputResult,
7
+ } from "@effectstream/concise";
8
+ import {
9
+ getAccountById,
10
+ getAddressByAccountId,
11
+ getAddressByAddress,
12
+ type IGetAddressByAddressResult,
13
+ newAccount,
14
+ newAddressWithId,
15
+ removeAddressAccount,
16
+ updateAddressAccount,
17
+ updatePrimaryAddress,
18
+ } from "@effectstream/db";
19
+ import {
20
+ AddressType,
21
+ type Signature,
22
+ TypeboxHelpers,
23
+ type WalletAddress,
24
+ } from "@effectstream/utils";
25
+ import { Value } from "@sinclair/typebox/value";
26
+ import { CryptoManager } from "@effectstream/crypto";
27
+
28
+ /**
29
+ * These messages come from unparsed sources, as the payload is a string format.
30
+ */
31
+ const formatWalletAddress = (
32
+ address: string,
33
+ addressType: AddressType
34
+ ): WalletAddress => {
35
+ const cryptoManager = CryptoManager.getCryptoManager(addressType);
36
+ return cryptoManager.decodeAddress(address);
37
+ };
38
+
39
+ export function* account_createAccount(
40
+ signerAddress: {
41
+ address: WalletAddress;
42
+ account_id: number | null;
43
+ },
44
+ input: ParseInputResult<
45
+ typeof BuiltinGrammar,
46
+ typeof BuiltinGrammarPrefix.createAccount
47
+ >
48
+ ): SyncStateUpdateStream<boolean> {
49
+ try {
50
+ // If signerAddress already has an account, return false
51
+ if (signerAddress.account_id !== null) {
52
+ console.error(
53
+ "Address already has an account:",
54
+ signerAddress.account_id
55
+ );
56
+ return false;
57
+ }
58
+
59
+ // Create a new account with this address as primary
60
+ const [newAccountResult] = yield* World.resolve(newAccount, {
61
+ primary_address: signerAddress.address,
62
+ });
63
+
64
+ // Update the address to link it to the new account
65
+ yield* World.resolve(updateAddressAccount, {
66
+ account_id: newAccountResult.id,
67
+ address: signerAddress.address,
68
+ });
69
+
70
+ return true;
71
+ } catch (error) {
72
+ console.error("Create account error:", error);
73
+ return false;
74
+ }
75
+ }
76
+
77
+ export function* account_linkAddress(
78
+ signerAddress: {
79
+ address: WalletAddress;
80
+ account_id: number | null;
81
+ },
82
+ input: ParseInputResult<
83
+ typeof BuiltinGrammar,
84
+ typeof BuiltinGrammarPrefix.linkAddress
85
+ >
86
+ ): SyncStateUpdateStream<boolean> {
87
+ try {
88
+ const account_id: number = input.data.account_id;
89
+ const primary_address_type: AddressType = input.data.primary_address_type;
90
+ const new_address_type: AddressType = input.data.new_address_type;
91
+ const signature_from_primary: Signature = input.data.signature_from_primary;
92
+ const new_address: WalletAddress = formatWalletAddress(
93
+ input.data.new_address,
94
+ input.data.new_address_type
95
+ );
96
+ const signature_from_new_address: Signature =
97
+ input.data.signature_from_new_address;
98
+ const is_new_primary: boolean = input.data.is_new_primary;
99
+
100
+ // Step 1: Get the account by account_id, it must exist
101
+ const [account] = yield* World.resolve(getAccountById, { account_id });
102
+ if (!account) {
103
+ console.error("Account not found:", account_id);
104
+ return false;
105
+ }
106
+ if (!account.primary_address) {
107
+ // no admin operations are possible without a primary address
108
+ console.error("Account has no primary address");
109
+ return false;
110
+ }
111
+
112
+ // Step 2: Check primary signature verification
113
+ // Verify signature from primary address
114
+ const primaryMessage = accountMessages.linkAccount(
115
+ account_id,
116
+ new_address,
117
+ is_new_primary
118
+ );
119
+
120
+ const primarySignatureValid = yield* verifySignature(
121
+ primary_address_type,
122
+ account.primary_address,
123
+ primaryMessage,
124
+ signature_from_primary
125
+ );
126
+
127
+ if (!primarySignatureValid) {
128
+ console.error("Invalid signature from primary");
129
+ return false;
130
+ }
131
+
132
+ // Step 3: Determine the new address and verify its signature if needed
133
+ // If new_address is different from signer, verify signature from new_address
134
+ const newAddressMessage = accountMessages.linkAccount(
135
+ account_id,
136
+ account.primary_address,
137
+ is_new_primary
138
+ );
139
+
140
+ const newAddressSignatureValid = yield* verifySignature(
141
+ new_address_type,
142
+ new_address,
143
+ newAddressMessage,
144
+ signature_from_new_address
145
+ );
146
+
147
+ if (!newAddressSignatureValid) {
148
+ console.error("Invalid signature from new address");
149
+ return false;
150
+ }
151
+
152
+ // Step 4: Check if target address already exists
153
+ const targetAddress = new_address || account.primary_address;
154
+ let [existingAddress] = yield* World.resolve(getAddressByAddress, {
155
+ address: targetAddress,
156
+ });
157
+ const targetAddressType = new_address
158
+ ? new_address_type
159
+ : account.address_type;
160
+
161
+ if (!existingAddress) {
162
+ // Create new address entry linked to this account
163
+ yield* World.resolve(newAddressWithId, {
164
+ address: targetAddress,
165
+ account_id,
166
+ address_type: targetAddressType,
167
+ });
168
+ [existingAddress] = yield* World.resolve(getAddressByAddress, {
169
+ address: targetAddress,
170
+ });
171
+ }
172
+
173
+ // Step 5:
174
+ // Update existing address to link to this account
175
+ yield* World.resolve(updateAddressAccount, {
176
+ account_id,
177
+ address: targetAddress,
178
+ });
179
+
180
+ // Step 5: Update primary address if needed
181
+ if (is_new_primary) {
182
+ yield* World.resolve(updatePrimaryAddress, {
183
+ account_id,
184
+ primary_address: targetAddress,
185
+ });
186
+ }
187
+
188
+ return true;
189
+ } catch (error) {
190
+ console.error("Link account error:", error);
191
+ return false;
192
+ }
193
+ }
194
+
195
+ function* verifySignature(
196
+ addressType: AddressType,
197
+ address: WalletAddress,
198
+ message: string,
199
+ signature: Signature
200
+ ): SyncStateUpdateStream<boolean> {
201
+ try {
202
+ const cryptoManager = CryptoManager.getCryptoManager(addressType);
203
+ const validSignature = yield* World.promise(
204
+ cryptoManager.verifySignature(address, message, signature)
205
+ );
206
+ return validSignature;
207
+ } catch (error) {
208
+ return false;
209
+ }
210
+ }
211
+
212
+ export function* account_unlinkAddressWithPrimary(
213
+ signerAddress: {
214
+ address: WalletAddress;
215
+ account_id: number | null;
216
+ },
217
+ input: ParseInputResult<
218
+ typeof BuiltinGrammar,
219
+ typeof BuiltinGrammarPrefix.unlinkAddress
220
+ >
221
+ ): SyncStateUpdateStream<boolean> {
222
+ try {
223
+ const account_id: number = input.data.account_id;
224
+ const primary_address_type: AddressType = input.data.primary_address_type;
225
+ const signature_from_primary: Signature = input.data.signature_from_primary;
226
+ const target_address: WalletAddress = formatWalletAddress(
227
+ input.data.target_address,
228
+ input.data.target_address_type
229
+ );
230
+ const new_primary_address: WalletAddress | null =
231
+ input.data.new_primary_address === "" ||
232
+ input.data.new_primary_address_type === -1
233
+ ? null
234
+ : formatWalletAddress(
235
+ input.data.new_primary_address,
236
+ input.data.new_primary_address_type
237
+ );
238
+
239
+ // Step 1: Check if account_id exists
240
+ const [account] = yield* World.resolve(getAccountById, { account_id });
241
+ if (!account) {
242
+ console.error("Account not found:", account_id);
243
+ return false;
244
+ }
245
+
246
+ // Determine the target address to unlink
247
+ const targetAddress = target_address || signerAddress.address;
248
+
249
+ // Step 2: Signature-based unlinking
250
+ if (!account.primary_address) {
251
+ console.error(
252
+ "Account has no primary address for signature verification"
253
+ );
254
+ return false;
255
+ }
256
+
257
+ // Verify signature from primary address
258
+ const primaryMessage = accountMessages.unlinkAccountWithPrimary(
259
+ account_id,
260
+ targetAddress,
261
+ new_primary_address
262
+ );
263
+
264
+ const primarySignatureValid = yield* verifySignature(
265
+ primary_address_type,
266
+ account.primary_address,
267
+ primaryMessage,
268
+ signature_from_primary
269
+ );
270
+
271
+ if (!primarySignatureValid) {
272
+ console.error("Invalid signature from primary");
273
+ return false;
274
+ }
275
+
276
+ // Check if the target_address exists and belongs to the account
277
+ const [addressToUnlink] = yield* World.resolve(getAddressByAddress, {
278
+ address: targetAddress,
279
+ });
280
+
281
+ if (!addressToUnlink) {
282
+ console.error("Address to unlink not found:", targetAddress);
283
+ return false;
284
+ }
285
+
286
+ if (addressToUnlink.account_id !== account_id) {
287
+ console.error("Address does not belong to the account:", targetAddress);
288
+ return false;
289
+ }
290
+
291
+ // Handle new primary address if specified
292
+ if (new_primary_address) {
293
+ const [newPrimaryAddress] = yield* World.resolve(getAddressByAddress, {
294
+ address: new_primary_address,
295
+ });
296
+
297
+ if (!newPrimaryAddress) {
298
+ console.error("New primary address not found:", new_primary_address);
299
+ return false;
300
+ }
301
+
302
+ if (newPrimaryAddress.account_id !== account_id) {
303
+ console.error(
304
+ "New primary address is already assigned to another account"
305
+ );
306
+ return false;
307
+ }
308
+
309
+ // Update the primary address
310
+ yield* World.resolve(updatePrimaryAddress, {
311
+ account_id,
312
+ primary_address: newPrimaryAddress.address,
313
+ });
314
+
315
+ yield* World.resolve(updateAddressAccount, {
316
+ account_id,
317
+ address: newPrimaryAddress.address,
318
+ });
319
+ } else if (account.primary_address === targetAddress) {
320
+ // If unlinking the primary address and no new primary specified, set to null
321
+ yield* World.resolve(updatePrimaryAddress, {
322
+ account_id,
323
+ primary_address: null as string | null,
324
+ });
325
+ }
326
+
327
+ // Unlink the address
328
+ yield* World.resolve(removeAddressAccount, {
329
+ address: targetAddress,
330
+ });
331
+
332
+ return true;
333
+ } catch (error) {
334
+ console.error("Unlink account with primary error:", error);
335
+ return false;
336
+ }
337
+ }
338
+
339
+ export function* account_unlinkAddressSelf(
340
+ signerAddress: {
341
+ address: WalletAddress;
342
+ account_id: number | null;
343
+ },
344
+ input: ParseInputResult<
345
+ typeof BuiltinGrammar,
346
+ typeof BuiltinGrammarPrefix.unlinkAddress
347
+ >
348
+ ): SyncStateUpdateStream<boolean> {
349
+ try {
350
+ const account_id: number = input.data.account_id;
351
+ const primary_address_type: AddressType = input.data.primary_address_type;
352
+
353
+ const target_address: WalletAddress | null =
354
+ input.data.target_address === "" || input.data.target_address_type === -1
355
+ ? null
356
+ : formatWalletAddress(
357
+ input.data.target_address,
358
+ input.data.target_address_type
359
+ );
360
+
361
+ // Step 1: Check if account_id exists
362
+ const [account] = yield* World.resolve(getAccountById, { account_id });
363
+ if (!account) {
364
+ console.error("Account not found:", account_id);
365
+ return false;
366
+ }
367
+
368
+ if (target_address && target_address !== signerAddress.address) {
369
+ console.error("Cannot unlink different address without signature");
370
+ return false;
371
+ }
372
+
373
+ // Determine the target address to unlink
374
+ const targetAddress = target_address || signerAddress.address;
375
+
376
+ // Check if the address belongs to the account
377
+ if (signerAddress.account_id !== account_id) {
378
+ console.error("Signer address does not belong to the account");
379
+ return false;
380
+ }
381
+
382
+ // If this was the primary address, set primary to null
383
+ if (account.primary_address === targetAddress) {
384
+ // Only allow unlinking if there are no other addresses in the account.
385
+ // The user should use the unlinkAddressWithPrimary instead, by providing the signature.
386
+ const otherAddresses = yield* World.resolve(getAddressByAccountId, {
387
+ account_id,
388
+ });
389
+ if (otherAddresses.length > 0) {
390
+ console.error(
391
+ "Account has other addresses, cannot unlink primary address"
392
+ );
393
+ return false;
394
+ }
395
+
396
+ yield* World.resolve(updatePrimaryAddress, {
397
+ account_id,
398
+ primary_address: null,
399
+ });
400
+ }
401
+
402
+ // Unlink the address
403
+ yield* World.resolve(removeAddressAccount, {
404
+ address: targetAddress,
405
+ });
406
+
407
+ return true;
408
+ } catch (error) {
409
+ console.error("Unlink account self error:", error);
410
+ return false;
411
+ }
412
+ }
413
+
414
+ export function* account_unlinkAddress(
415
+ signerAddress: {
416
+ address: WalletAddress;
417
+ account_id: number | null;
418
+ },
419
+ input: ParseInputResult<
420
+ typeof BuiltinGrammar,
421
+ typeof BuiltinGrammarPrefix.unlinkAddress
422
+ >
423
+ ): SyncStateUpdateStream<boolean> {
424
+ const { signature_from_primary } = input.data;
425
+
426
+ if (signature_from_primary) {
427
+ return yield* account_unlinkAddressWithPrimary(signerAddress, input);
428
+ } else {
429
+ return yield* account_unlinkAddressSelf(signerAddress, input);
430
+ }
431
+ }
package/mod.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./Stm.ts";
2
+ export * from "./primitives/mod.ts";
3
+ export * from "./types.ts";
4
+ export * from "./delegate-wallet.ts";
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@effectstream/sm",
3
+ "version": "0.100.0",
4
+ "license": "MIT",
5
+ "exports": {
6
+ ".": "./mod.ts",
7
+ "./builtin": "./primitives/src/builtin.ts",
8
+ "./grammar": "./primitives/src/grammar.ts"
9
+ },
10
+ "dependencies": {
11
+ "@effectstream/concise": "0.100.0",
12
+ "@effectstream/config": "0.100.0",
13
+ "@effectstream/coroutine": "0.100.0",
14
+ "@effectstream/crypto": "0.100.0",
15
+ "@effectstream/db": "0.100.0",
16
+ "@effectstream/log": "0.100.0",
17
+ "@effectstream/utils": "0.100.0",
18
+ "pg": "8.17.2",
19
+ "pg-tx": "^1.0.1",
20
+ "@sinclair/typebox": "0.34.41",
21
+ "assert-never": "^1.4.0",
22
+ "viem": "2.37.3",
23
+ "@pgtyped/runtime": "^2.4.2",
24
+ "@midnight-ntwrk/onchain-runtime": "npm:@midnight-ntwrk/onchain-runtime-v2@2.0.0",
25
+ "effection": "^3.5.0"
26
+ }
27
+ }
@@ -0,0 +1,80 @@
1
+ import type { AddressAndType, EffectstreamBlockNumber } from "@effectstream/utils";
2
+ import type { StaticDecode, TSchema } from "@sinclair/typebox";
3
+ import type { CommandTuple } from "@effectstream/concise";
4
+ import { PrimitiveRegistry } from "./PrimitiveRegistry.ts";
5
+ import type { StateUpdateStream } from "@effectstream/coroutine";
6
+ import type { JsonObject } from "./types.ts";
7
+ import type {
8
+ FlattenSyncProtocolIOFor,
9
+ ProtocolPrimitiveMap,
10
+ } from "@effectstream/config";
11
+ import type { AnyPrimitiveType } from "./src/builtin.ts";
12
+
13
+ /**
14
+ * Abstract Class for Effectstream Primitives
15
+ *
16
+ * This is the interface that needs to be implemented to register a new primitive.
17
+ * E.g., ERC721, ERC1155, etc,
18
+ */
19
+ export abstract class Primitive<
20
+ SyncProtocol extends keyof ProtocolPrimitiveMap,
21
+ TGrammar extends readonly Readonly<[string, TSchema]>[],
22
+ > {
23
+ // Instance defined unique name
24
+ public readonly instanceName: string;
25
+ // Start block height of the primitive
26
+ public readonly startBlockHeight: number;
27
+ // State machine prefix of the primitive
28
+ public readonly stateMachinePrefix: string | undefined;
29
+
30
+ constructor(config: {
31
+ instanceName: string,
32
+ startBlockHeight: number,
33
+ stateMachinePrefix: string | undefined,
34
+ }) {
35
+ this.instanceName = config.instanceName;
36
+ this.startBlockHeight = config.startBlockHeight;
37
+ this.stateMachinePrefix = config.stateMachinePrefix;
38
+ PrimitiveRegistry.addPrimitive(this);
39
+ }
40
+ // Primitive defined
41
+ // unique name for primitive definition as chain:protocol
42
+ abstract internalTypeName: AnyPrimitiveType;
43
+ // grammar for primitive
44
+ abstract grammar: TGrammar;
45
+
46
+ // Dynamic/ivm Table Global definitions
47
+ dynamicTables: undefined | ((name: string) => string) = undefined;
48
+ getIntermediatePrefix(): string[] {
49
+ return [];
50
+ }
51
+ getViewPrefix(): string[] {
52
+ return [];
53
+ }
54
+
55
+ // Return the config for the primitive.
56
+ abstract getConfig(): ProtocolPrimitiveMap[SyncProtocol];
57
+
58
+ // Arrow function to bind 'this', as this function is passed as a reference
59
+ public getDynamicTables = (name: string): string | undefined => {
60
+ return this.dynamicTables?.(name);
61
+ };
62
+
63
+ // This returns the payload in the state machine format.
64
+ // e.g., [stateMachinePrefix, v1, v2, v3]
65
+ abstract getPayload(
66
+ effectstream_block_height: EffectstreamBlockNumber,
67
+ primitiveTransactionData: FlattenSyncProtocolIOFor<SyncProtocol>,
68
+ ): StateUpdateStream<{
69
+ isBatched: boolean;
70
+ data: {
71
+ fromAddressAndType: AddressAndType;
72
+ stateMachinePayload:
73
+ | StaticDecode<
74
+ CommandTuple<string, TGrammar>
75
+ >
76
+ | null;
77
+ accountingPayload: JsonObject;
78
+ }[];
79
+ }>;
80
+ }
@@ -0,0 +1,59 @@
1
+ import type { ProtocolPrimitiveMap } from "@effectstream/config";
2
+ import type { Primitive } from "./Primitive.ts";
3
+
4
+ /**
5
+ * Registry for Effectstream Primitives
6
+ *
7
+ * This is a singleton that stores all the primitives that have been registered.
8
+ * They can be retrieved by their instance-name.
9
+ */
10
+ type UnknownPrimitive = any;
11
+ type UnknownSyncProtocol = any;
12
+ // @effectstream/db packages cannot import this package.
13
+ // so we communicate via the globalThis object.
14
+ (globalThis as any).EFFECTSTREAM_REGISTRY =
15
+ (globalThis as any).EFFECTSTREAM_REGISTRY ??
16
+ ({} as Record<
17
+ string,
18
+ Primitive<UnknownSyncProtocol, UnknownPrimitive>
19
+ >);
20
+
21
+ export class PrimitiveRegistry {
22
+ // Singleton to manage the primitives
23
+ static primitives: Record<
24
+ string,
25
+ Primitive<UnknownSyncProtocol, UnknownPrimitive>
26
+ > = (globalThis as any).EFFECTSTREAM_REGISTRY;
27
+
28
+ static getPrimitive<
29
+ SyncProtocol extends keyof ProtocolPrimitiveMap = UnknownSyncProtocol,
30
+ >(
31
+ instanceName: string,
32
+ ): Primitive<SyncProtocol, UnknownPrimitive> {
33
+ const primitive = this.primitives[instanceName];
34
+ if (!primitive) {
35
+ // This should never happen.
36
+ throw new Error(`Primitive ${instanceName} not found`);
37
+ }
38
+ return primitive;
39
+ }
40
+
41
+ static getPrimitiveByType<
42
+ SyncProtocol extends keyof ProtocolPrimitiveMap = UnknownSyncProtocol,
43
+ >(
44
+ type: string,
45
+ ): Primitive<SyncProtocol, UnknownPrimitive> | undefined {
46
+ return Object.values(this.primitives).find((primitive) =>
47
+ primitive.internalTypeName === type
48
+ );
49
+ }
50
+
51
+ static addPrimitive(
52
+ primitive: Primitive<UnknownSyncProtocol, UnknownPrimitive>,
53
+ ) {
54
+ if (this.primitives[primitive.instanceName]) {
55
+ throw new Error(`Primitive ${primitive.instanceName} already exists`);
56
+ }
57
+ this.primitives[primitive.instanceName] = primitive;
58
+ }
59
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./processing.ts";
2
+ export * from "./Primitive.ts";
3
+ export * from "./PrimitiveRegistry.ts";
4
+ export * from "./types.ts";
5
+ export * from "./src/mod.ts";