@effectstream/sm 0.102.0 → 0.103.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/README.md +1 -1
- package/package.json +9 -9
- package/primitives/src/builtin.ts +21 -5
- package/primitives/src/grammar.ts +2 -0
- package/primitives/src/midnight-nullifier-and-commitment/midnight-nullifier-and-commitment-grammar.ts +37 -0
- package/primitives/src/{midnight-nullifier/midnight-nullifier.ts → midnight-nullifier-and-commitment/midnight-nullifier-and-commitment.ts} +37 -14
- package/primitives/src/midnight-unshielded-create/midnight-unshielded-create-grammar.ts +14 -1
- package/primitives/src/midnight-unshielded-create/midnight-unshielded-create.ts +4 -1
- package/primitives/src/midnight-unshielded-spend/midnight-unshielded-spend-grammar.ts +14 -1
- package/primitives/src/midnight-unshielded-spend/midnight-unshielded-spend.ts +4 -1
- package/primitives/src/mod.ts +9 -4
- package/primitives/src/solana-token-account/solana-token-account-grammar.ts +13 -0
- package/primitives/src/solana-token-account/solana-token-account-primitive.test.ts +117 -0
- package/primitives/src/solana-token-account/solana-token-account-primitive.ts +156 -0
- package/primitives/src/midnight-nullifier/midnight-nullifier-grammar.ts +0 -8
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ inferred at call sites rather than imported directly.
|
|
|
77
77
|
|
|
78
78
|
Subpath exports:
|
|
79
79
|
|
|
80
|
-
- `@effectstream/sm/builtin`: `
|
|
80
|
+
- `@effectstream/sm/builtin`: `PrimitiveTypeEVMERC20`, `PrimitiveTypeEVMERC721`, `PrimitiveTypeEVMERC1155`, `PrimitiveTypeEVMEffectstreamL2`, `PrimitiveTypeCardanoTransfer`, `PrimitiveTypeMidnightGeneric`, `PrimitiveTypeUtxorpcGeneric`, and 20+ more chain-specific event tags.
|
|
81
81
|
- `@effectstream/sm/grammar`: the underlying grammar/parsing utilities, also re-exported from `@effectstream/concise`.
|
|
82
82
|
|
|
83
83
|
## Examples
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effectstream/sm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.103.1",
|
|
4
4
|
"license": "(MIT OR Apache-2.0)",
|
|
5
5
|
"homepage": "https://effectstream.github.io/docs/",
|
|
6
6
|
"repository": {
|
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
"./grammar": "./primitives/src/grammar.ts"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@effectstream/concise": "0.
|
|
21
|
-
"@effectstream/config": "0.
|
|
22
|
-
"@effectstream/coroutine": "0.
|
|
23
|
-
"@effectstream/crypto": "0.
|
|
24
|
-
"@effectstream/db": "0.
|
|
25
|
-
"@effectstream/event-client": "0.
|
|
26
|
-
"@effectstream/log": "0.
|
|
27
|
-
"@effectstream/utils": "0.
|
|
20
|
+
"@effectstream/concise": "0.103.1",
|
|
21
|
+
"@effectstream/config": "0.103.1",
|
|
22
|
+
"@effectstream/coroutine": "0.103.1",
|
|
23
|
+
"@effectstream/crypto": "0.103.1",
|
|
24
|
+
"@effectstream/db": "0.103.1",
|
|
25
|
+
"@effectstream/event-client": "0.103.1",
|
|
26
|
+
"@effectstream/log": "0.103.1",
|
|
27
|
+
"@effectstream/utils": "0.103.1",
|
|
28
28
|
"pg": "8.21.0",
|
|
29
29
|
"pg-tx": "^1.0.1",
|
|
30
30
|
"@sinclair/typebox": "0.34.41",
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
// list of built-in primitives
|
|
2
2
|
// this list is exposed to the effectstream-sdk modules via the @effectstream/sm/builtin module
|
|
3
|
+
import {
|
|
4
|
+
SOLANA_PRIMITIVE_ACCOUNT_BALANCE,
|
|
5
|
+
SOLANA_PRIMITIVE_PROGRAM_LOG,
|
|
6
|
+
SOLANA_PRIMITIVE_TOKEN_ACCOUNT,
|
|
7
|
+
} from "@effectstream/config";
|
|
8
|
+
|
|
3
9
|
export const PrimitiveTypeMidnightGeneric = "Midnight:Generic" as const;
|
|
4
|
-
export const
|
|
10
|
+
export const PrimitiveTypeMidnightNullifierAndCommitment = "Midnight:NullifierAndCommitment" as const;
|
|
5
11
|
export const PrimitiveTypeMidnightUnshieldedSpend = "Midnight:UnshieldedSpend" as const;
|
|
6
12
|
export const PrimitiveTypeMidnightUnshieldedCreate = "Midnight:UnshieldedCreate" as const;
|
|
7
13
|
export const PrimitiveTypeMidnightZswapRoot = "Midnight:ZswapRoot" as const;
|
|
@@ -33,12 +39,21 @@ export const PrimitiveTypeNEARIntent = "NEAR:Intent" as const;
|
|
|
33
39
|
export const PrimitiveTypeNEARGeneric = "NEAR:Generic" as const;
|
|
34
40
|
export const PrimitiveTypeNEARAccountWatch = "NEAR:AccountWatch" as const;
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
// Solana's discriminators are defined in @effectstream/config, beside the
|
|
43
|
+
// `SolanaPrimitive` type they discriminate, because the sync fetcher dispatches on
|
|
44
|
+
// them and @effectstream/sync does not depend on @effectstream/sm. Re-exported here
|
|
45
|
+
// under the PrimitiveType* names so consumers keep importing them from
|
|
46
|
+
// @effectstream/sm/builtin like every other primitive.
|
|
47
|
+
// Aliased through local consts rather than `export { X as Y } from`, because the
|
|
48
|
+
// `BuiltInPrimitives` union below needs `typeof PrimitiveTypeSolana*` in scope and a
|
|
49
|
+
// bare re-export does not bind the name locally.
|
|
50
|
+
export const PrimitiveTypeSolanaProgramLog = SOLANA_PRIMITIVE_PROGRAM_LOG;
|
|
51
|
+
export const PrimitiveTypeSolanaAccountBalance = SOLANA_PRIMITIVE_ACCOUNT_BALANCE;
|
|
52
|
+
export const PrimitiveTypeSolanaTokenAccount = SOLANA_PRIMITIVE_TOKEN_ACCOUNT;
|
|
38
53
|
|
|
39
54
|
type BuiltInPrimitives =
|
|
40
55
|
typeof PrimitiveTypeMidnightGeneric |
|
|
41
|
-
typeof
|
|
56
|
+
typeof PrimitiveTypeMidnightNullifierAndCommitment |
|
|
42
57
|
typeof PrimitiveTypeMidnightUnshieldedSpend |
|
|
43
58
|
typeof PrimitiveTypeMidnightUnshieldedCreate |
|
|
44
59
|
typeof PrimitiveTypeMidnightZswapRoot |
|
|
@@ -63,7 +78,8 @@ type BuiltInPrimitives =
|
|
|
63
78
|
typeof PrimitiveTypeNEARGeneric |
|
|
64
79
|
typeof PrimitiveTypeNEARAccountWatch |
|
|
65
80
|
typeof PrimitiveTypeSolanaProgramLog |
|
|
66
|
-
typeof PrimitiveTypeSolanaAccountBalance
|
|
81
|
+
typeof PrimitiveTypeSolanaAccountBalance |
|
|
82
|
+
typeof PrimitiveTypeSolanaTokenAccount // |
|
|
67
83
|
// typeof PrimitiveTypeEVMGeneric
|
|
68
84
|
;
|
|
69
85
|
|
|
@@ -17,6 +17,7 @@ import { nearGenericGrammar } from "./near-generic/near-generic-grammar.ts";
|
|
|
17
17
|
import { nearAccountWatchGrammar } from "./near-account-watch/near-account-watch-grammar.ts";
|
|
18
18
|
import { solanaProgramLogGrammar } from "./solana-program-log/solana-program-log-grammar.ts";
|
|
19
19
|
import { solanaAccountBalanceGrammar } from "./solana-account-balance/solana-account-balance-grammar.ts";
|
|
20
|
+
import { solanaTokenAccountGrammar } from "./solana-token-account/solana-token-account-grammar.ts";
|
|
20
21
|
import { mintBurnGrammar } from "./cardano-mint-burn/mint-burn-grammar.ts";
|
|
21
22
|
import { transferGrammar } from "./cardano-transfer/transfer-grammar.ts";
|
|
22
23
|
import { poolDelegationGrammar } from "./cardano-pool-delegation/pool-delegation-grammar.ts";
|
|
@@ -42,6 +43,7 @@ export const builtinGrammars = {
|
|
|
42
43
|
nearAccountWatch: nearAccountWatchGrammar,
|
|
43
44
|
solanaProgramLog: solanaProgramLogGrammar,
|
|
44
45
|
solanaAccountBalance: solanaAccountBalanceGrammar,
|
|
46
|
+
solanaTokenAccount: solanaTokenAccountGrammar,
|
|
45
47
|
cardanoMintBurn: mintBurnGrammar,
|
|
46
48
|
cardanoTransfer: transferGrammar,
|
|
47
49
|
cardanoPoolDelegation: poolDelegationGrammar,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
|
|
3
|
+
export const midnightNullifierAndCommitmentGrammar = [
|
|
4
|
+
[
|
|
5
|
+
"payload",
|
|
6
|
+
Type.Union([
|
|
7
|
+
Type.Object({
|
|
8
|
+
kind: Type.Literal("nullifier"),
|
|
9
|
+
/** 32-byte nullifier, hex (no 0x prefix) */
|
|
10
|
+
nullifier: Type.String(),
|
|
11
|
+
/** 32-byte transaction hash, hex (no 0x prefix) */
|
|
12
|
+
txHash: Type.String(),
|
|
13
|
+
/** Indexer-assigned zswap ledger event id */
|
|
14
|
+
eventId: Type.Number(),
|
|
15
|
+
/** Logical segment the event was emitted in */
|
|
16
|
+
logicalSegment: Type.Number(),
|
|
17
|
+
/** Contract address that spent the coin, hex — absent for user coins */
|
|
18
|
+
contract: Type.Optional(Type.String()),
|
|
19
|
+
}),
|
|
20
|
+
Type.Object({
|
|
21
|
+
kind: Type.Literal("commitment"),
|
|
22
|
+
/** 32-byte coin commitment, hex (no 0x prefix) */
|
|
23
|
+
commitment: Type.String(),
|
|
24
|
+
/** Index of the commitment in the zswap Merkle tree (u64 as decimal string) */
|
|
25
|
+
mtIndex: Type.String(),
|
|
26
|
+
/** 32-byte transaction hash, hex (no 0x prefix) */
|
|
27
|
+
txHash: Type.String(),
|
|
28
|
+
/** Indexer-assigned zswap ledger event id */
|
|
29
|
+
eventId: Type.Number(),
|
|
30
|
+
/** Logical segment the event was emitted in */
|
|
31
|
+
logicalSegment: Type.Number(),
|
|
32
|
+
/** Contract address that received the coin, hex — absent for user coins */
|
|
33
|
+
contract: Type.Optional(Type.String()),
|
|
34
|
+
}),
|
|
35
|
+
]),
|
|
36
|
+
],
|
|
37
|
+
] as const;
|
|
@@ -16,28 +16,40 @@ import type {
|
|
|
16
16
|
ProtocolPrimitiveMap,
|
|
17
17
|
} from "@effectstream/config";
|
|
18
18
|
import type { SyncStateUpdateStream } from "@effectstream/coroutine";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { PrimitiveTypeMidnightNullifierAndCommitment } from "../builtin.ts";
|
|
20
|
+
import { midnightNullifierAndCommitmentGrammar } from "./midnight-nullifier-and-commitment-grammar.ts";
|
|
21
|
+
|
|
22
|
+
/** Which zswap ledger event kinds the primitive emits. */
|
|
23
|
+
export type MidnightZswapCapture = "nullifiers" | "commitments" | "both";
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
|
-
*
|
|
26
|
+
* MidnightNullifierAndCommitmentPrimitive
|
|
24
27
|
*
|
|
25
|
-
* Watches
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
+
* Watches ZswapInput (nullifier-spend) and ZswapOutput (commitment-create)
|
|
29
|
+
* events on the Midnight shielded ledger. Both event kinds arrive in the
|
|
30
|
+
* same indexer field (`zswapLedgerEvents`), so tracking both costs no extra
|
|
31
|
+
* indexer queries. One state-machine input is emitted per event observed in
|
|
32
|
+
* a block, regardless of which contract produced it. The `capture` option
|
|
33
|
+
* selects which kinds are emitted (default: both).
|
|
28
34
|
*
|
|
29
35
|
* Usage:
|
|
30
36
|
* stm.addStateTransition("myPrefix", function* (data) {
|
|
31
37
|
* const { payload } = data.parsedInput;
|
|
32
|
-
*
|
|
38
|
+
* if (payload.kind === "nullifier") {
|
|
39
|
+
* // { kind, nullifier, txHash, eventId, logicalSegment, contract? }
|
|
40
|
+
* } else {
|
|
41
|
+
* // { kind, commitment, mtIndex, txHash, eventId, logicalSegment, contract? }
|
|
42
|
+
* }
|
|
33
43
|
* });
|
|
34
44
|
*/
|
|
35
|
-
export class
|
|
45
|
+
export class MidnightNullifierAndCommitmentPrimitive extends Primitive<
|
|
36
46
|
ConfigSyncProtocolType.MIDNIGHT_PARALLEL,
|
|
37
|
-
typeof
|
|
47
|
+
typeof midnightNullifierAndCommitmentGrammar
|
|
38
48
|
> {
|
|
39
|
-
readonly internalTypeName =
|
|
40
|
-
override readonly grammar =
|
|
49
|
+
readonly internalTypeName = PrimitiveTypeMidnightNullifierAndCommitment;
|
|
50
|
+
override readonly grammar = midnightNullifierAndCommitmentGrammar;
|
|
51
|
+
|
|
52
|
+
readonly capture: MidnightZswapCapture;
|
|
41
53
|
|
|
42
54
|
override dynamicTables = undefined;
|
|
43
55
|
override getIntermediatePrefix(): string[] {
|
|
@@ -51,8 +63,11 @@ export class MidnightNullifierPrimitive extends Primitive<
|
|
|
51
63
|
instanceName: string;
|
|
52
64
|
startBlockHeight: number;
|
|
53
65
|
stateMachinePrefix: string;
|
|
66
|
+
/** Which zswap event kinds to emit. Default: "both". */
|
|
67
|
+
capture?: MidnightZswapCapture;
|
|
54
68
|
}) {
|
|
55
69
|
super(config);
|
|
70
|
+
this.capture = config.capture ?? "both";
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
override *getPayload(
|
|
@@ -65,12 +80,19 @@ export class MidnightNullifierPrimitive extends Primitive<
|
|
|
65
80
|
data: {
|
|
66
81
|
fromAddressAndType: AddressAndType;
|
|
67
82
|
stateMachinePayload:
|
|
68
|
-
| StaticDecode<
|
|
83
|
+
| StaticDecode<
|
|
84
|
+
CommandTuple<string, typeof midnightNullifierAndCommitmentGrammar>
|
|
85
|
+
>
|
|
69
86
|
| null;
|
|
70
|
-
accountingPayload: ParamToData<
|
|
87
|
+
accountingPayload: ParamToData<
|
|
88
|
+
typeof midnightNullifierAndCommitmentGrammar
|
|
89
|
+
>;
|
|
71
90
|
}[];
|
|
72
91
|
}> {
|
|
73
|
-
|
|
92
|
+
// The sync-protocol payload map types Midnight payloads as Record<string, any>;
|
|
93
|
+
// the fetcher's fetchZswapEvents builds exactly the grammar's payload shape.
|
|
94
|
+
const payload = primitiveTransactionData.output
|
|
95
|
+
.payload as ParamToData<typeof this.grammar>["payload"];
|
|
74
96
|
|
|
75
97
|
const accountingPayload: ParamToData<typeof this.grammar> = {
|
|
76
98
|
payload,
|
|
@@ -109,6 +131,7 @@ export class MidnightNullifierPrimitive extends Primitive<
|
|
|
109
131
|
type: this.internalTypeName,
|
|
110
132
|
startBlockHeight: this.startBlockHeight,
|
|
111
133
|
scheduledPrefix: this.stateMachinePrefix ?? "",
|
|
134
|
+
capture: this.capture,
|
|
112
135
|
} as const;
|
|
113
136
|
}
|
|
114
137
|
}
|
|
@@ -3,6 +3,19 @@ import { Type } from "@sinclair/typebox";
|
|
|
3
3
|
export const midnightUnshieldedCreateGrammar = [
|
|
4
4
|
[
|
|
5
5
|
"payload",
|
|
6
|
-
Type.
|
|
6
|
+
Type.Object({
|
|
7
|
+
/** Bech32m owner address of the created UTXO */
|
|
8
|
+
owner: Type.String(),
|
|
9
|
+
/** Hex-encoded hash of the creating intent */
|
|
10
|
+
intentHash: Type.String(),
|
|
11
|
+
/** Output index within the creating intent's offer */
|
|
12
|
+
outputIndex: Type.Number(),
|
|
13
|
+
/** u128 value as a decimal string */
|
|
14
|
+
value: Type.String(),
|
|
15
|
+
/** Hex-encoded serialized token type */
|
|
16
|
+
tokenType: Type.String(),
|
|
17
|
+
/** Hash of the creating transaction */
|
|
18
|
+
txHash: Type.String(),
|
|
19
|
+
}),
|
|
7
20
|
],
|
|
8
21
|
] as const;
|
|
@@ -77,7 +77,10 @@ export class MidnightUnshieldedCreatePrimitive extends Primitive<
|
|
|
77
77
|
accountingPayload: ParamToData<typeof midnightUnshieldedCreateGrammar>;
|
|
78
78
|
}[];
|
|
79
79
|
}> {
|
|
80
|
-
|
|
80
|
+
// The sync-protocol payload map types Midnight payloads as Record<string, any>;
|
|
81
|
+
// the fetcher's fetchUnshieldedCreates builds exactly the grammar's payload shape.
|
|
82
|
+
const payload = primitiveTransactionData.output
|
|
83
|
+
.payload as ParamToData<typeof this.grammar>["payload"];
|
|
81
84
|
|
|
82
85
|
const accountingPayload: ParamToData<typeof this.grammar> = {
|
|
83
86
|
payload,
|
|
@@ -3,6 +3,19 @@ import { Type } from "@sinclair/typebox";
|
|
|
3
3
|
export const midnightUnshieldedSpendGrammar = [
|
|
4
4
|
[
|
|
5
5
|
"payload",
|
|
6
|
-
Type.
|
|
6
|
+
Type.Object({
|
|
7
|
+
/** Bech32m owner address of the spent UTXO */
|
|
8
|
+
owner: Type.String(),
|
|
9
|
+
/** Hex-encoded hash of the intent that CREATED the spent UTXO */
|
|
10
|
+
intentHash: Type.String(),
|
|
11
|
+
/** Output index within the creating intent's offer */
|
|
12
|
+
outputIndex: Type.Number(),
|
|
13
|
+
/** u128 value as a decimal string */
|
|
14
|
+
value: Type.String(),
|
|
15
|
+
/** Hex-encoded serialized token type */
|
|
16
|
+
tokenType: Type.String(),
|
|
17
|
+
/** Hash of the spending transaction */
|
|
18
|
+
txHash: Type.String(),
|
|
19
|
+
}),
|
|
7
20
|
],
|
|
8
21
|
] as const;
|
|
@@ -71,7 +71,10 @@ export class MidnightUnshieldedSpendPrimitive extends Primitive<
|
|
|
71
71
|
accountingPayload: ParamToData<typeof midnightUnshieldedSpendGrammar>;
|
|
72
72
|
}[];
|
|
73
73
|
}> {
|
|
74
|
-
|
|
74
|
+
// The sync-protocol payload map types Midnight payloads as Record<string, any>;
|
|
75
|
+
// the fetcher's fetchUnshieldedSpends builds exactly the grammar's payload shape.
|
|
76
|
+
const payload = primitiveTransactionData.output
|
|
77
|
+
.payload as ParamToData<typeof this.grammar>["payload"];
|
|
75
78
|
|
|
76
79
|
const accountingPayload: ParamToData<typeof this.grammar> = {
|
|
77
80
|
payload,
|
package/primitives/src/mod.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PrimitiveTypeMidnightGeneric,
|
|
3
|
-
|
|
3
|
+
PrimitiveTypeMidnightNullifierAndCommitment,
|
|
4
4
|
PrimitiveTypeMidnightUnshieldedSpend,
|
|
5
5
|
PrimitiveTypeMidnightUnshieldedCreate,
|
|
6
6
|
PrimitiveTypeMidnightZswapRoot,
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
PrimitiveTypeNEARAccountWatch,
|
|
22
22
|
PrimitiveTypeSolanaProgramLog,
|
|
23
23
|
PrimitiveTypeSolanaAccountBalance,
|
|
24
|
+
PrimitiveTypeSolanaTokenAccount,
|
|
24
25
|
PrimitiveTypeCardanoMintBurn,
|
|
25
26
|
PrimitiveTypeCardanoTransfer,
|
|
26
27
|
PrimitiveTypeCardanoPoolDelegation,
|
|
@@ -30,7 +31,8 @@ import {
|
|
|
30
31
|
} from "./builtin.ts";
|
|
31
32
|
|
|
32
33
|
import { MidnightGenericPrimitive } from "./midnight-generic/midnight-genetic.ts";
|
|
33
|
-
import {
|
|
34
|
+
import { MidnightNullifierAndCommitmentPrimitive } from "./midnight-nullifier-and-commitment/midnight-nullifier-and-commitment.ts";
|
|
35
|
+
export type { MidnightZswapCapture } from "./midnight-nullifier-and-commitment/midnight-nullifier-and-commitment.ts";
|
|
34
36
|
import { MidnightUnshieldedSpendPrimitive } from "./midnight-unshielded-spend/midnight-unshielded-spend.ts";
|
|
35
37
|
import { MidnightUnshieldedCreatePrimitive } from "./midnight-unshielded-create/midnight-unshielded-create.ts";
|
|
36
38
|
import { MidnightZswapRootPrimitive } from "./midnight-zswap-root/midnight-zswap-root.ts";
|
|
@@ -51,6 +53,7 @@ import { NearGenericPrimitive } from "./near-generic/near-generic-primitive.ts";
|
|
|
51
53
|
import { NearAccountWatchPrimitive } from "./near-account-watch/near-account-watch-primitive.ts";
|
|
52
54
|
import { SolanaProgramLogPrimitive } from "./solana-program-log/solana-program-log-primitive.ts";
|
|
53
55
|
import { SolanaAccountBalancePrimitive } from "./solana-account-balance/solana-account-balance-primitive.ts";
|
|
56
|
+
import { SolanaTokenAccountPrimitive } from "./solana-token-account/solana-token-account-primitive.ts";
|
|
54
57
|
import { CardanoMintBurnPrimitive } from "./cardano-mint-burn/mint-burn-primitive.ts";
|
|
55
58
|
import { CardanoTransferPrimitive } from "./cardano-transfer/transfer-primitive.ts";
|
|
56
59
|
import { CardanoPoolDelegationPrimitive } from "./cardano-pool-delegation/pool-delegation-primitive.ts";
|
|
@@ -60,7 +63,7 @@ import { CardanoProjectedNftPrimitive } from "./cardano-projected-nft/projected-
|
|
|
60
63
|
|
|
61
64
|
const builtInPrimitivesMap = {
|
|
62
65
|
[PrimitiveTypeMidnightGeneric]: MidnightGenericPrimitive,
|
|
63
|
-
[
|
|
66
|
+
[PrimitiveTypeMidnightNullifierAndCommitment]: MidnightNullifierAndCommitmentPrimitive,
|
|
64
67
|
[PrimitiveTypeMidnightUnshieldedSpend]: MidnightUnshieldedSpendPrimitive,
|
|
65
68
|
[PrimitiveTypeMidnightUnshieldedCreate]: MidnightUnshieldedCreatePrimitive,
|
|
66
69
|
[PrimitiveTypeMidnightZswapRoot]: MidnightZswapRootPrimitive,
|
|
@@ -81,6 +84,7 @@ const builtInPrimitivesMap = {
|
|
|
81
84
|
[PrimitiveTypeNEARAccountWatch]: NearAccountWatchPrimitive,
|
|
82
85
|
[PrimitiveTypeSolanaProgramLog]: SolanaProgramLogPrimitive,
|
|
83
86
|
[PrimitiveTypeSolanaAccountBalance]: SolanaAccountBalancePrimitive,
|
|
87
|
+
[PrimitiveTypeSolanaTokenAccount]: SolanaTokenAccountPrimitive,
|
|
84
88
|
[PrimitiveTypeCardanoMintBurn]: CardanoMintBurnPrimitive,
|
|
85
89
|
[PrimitiveTypeCardanoTransfer]: CardanoTransferPrimitive,
|
|
86
90
|
[PrimitiveTypeCardanoPoolDelegation]: CardanoPoolDelegationPrimitive,
|
|
@@ -95,7 +99,7 @@ export {
|
|
|
95
99
|
|
|
96
100
|
// Built-in Primitives
|
|
97
101
|
MidnightGenericPrimitive,
|
|
98
|
-
|
|
102
|
+
MidnightNullifierAndCommitmentPrimitive,
|
|
99
103
|
MidnightUnshieldedSpendPrimitive,
|
|
100
104
|
MidnightUnshieldedCreatePrimitive,
|
|
101
105
|
MidnightZswapRootPrimitive,
|
|
@@ -116,6 +120,7 @@ export {
|
|
|
116
120
|
NearAccountWatchPrimitive,
|
|
117
121
|
SolanaProgramLogPrimitive,
|
|
118
122
|
SolanaAccountBalancePrimitive,
|
|
123
|
+
SolanaTokenAccountPrimitive,
|
|
119
124
|
CardanoMintBurnPrimitive,
|
|
120
125
|
CardanoTransferPrimitive,
|
|
121
126
|
CardanoPoolDelegationPrimitive,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
|
|
3
|
+
export const solanaTokenAccountGrammar = [
|
|
4
|
+
["tokenAccount", Type.String()],
|
|
5
|
+
["mint", Type.String()],
|
|
6
|
+
["owner", Type.String()],
|
|
7
|
+
// SPL amounts are u64 and do not survive a JS number. Carried as the raw base
|
|
8
|
+
// unit string, matching nep141Grammar's `amount` and erc20Grammar's `value`;
|
|
9
|
+
// `decimals` is what turns it into a display value.
|
|
10
|
+
["amount", Type.String()],
|
|
11
|
+
["decimals", Type.Number()],
|
|
12
|
+
["slot", Type.Number()],
|
|
13
|
+
] as const;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
// Via the barrel, not the file directly: mod.ts registers every primitive class, so
|
|
3
|
+
// importing the file first hits a TDZ error on the map entry. Same as
|
|
4
|
+
// solana-program-log-primitive.test.ts.
|
|
5
|
+
import { SolanaTokenAccountPrimitive } from "./../mod.ts";
|
|
6
|
+
import { PrimitiveRegistry } from "../../PrimitiveRegistry.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Unit coverage for the primitive half of SOLANA:TokenAccount — the config guard and
|
|
10
|
+
* the payload the state machine actually receives. The fetcher half (which balance
|
|
11
|
+
* records match, and lookup-table resolution) is covered in
|
|
12
|
+
* `sync/src/sync-protocols/solana/read-primitives.test.ts`.
|
|
13
|
+
*
|
|
14
|
+
* `getPayload` is a generator that never yields, so one `.next()` drives it to done.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const MINT = "Mint111111111111111111111111111111111111111";
|
|
18
|
+
const OWNER = "0wner11111111111111111111111111111111111111";
|
|
19
|
+
const TOKEN_ACCOUNT = "Tok111111111111111111111111111111111111111";
|
|
20
|
+
|
|
21
|
+
function makePrimitive(overrides: Record<string, unknown> = {}) {
|
|
22
|
+
// The Primitive constructor self-registers and PrimitiveRegistry.addPrimitive
|
|
23
|
+
// throws on a duplicate instanceName, so the singleton has to be cleared between
|
|
24
|
+
// constructions.
|
|
25
|
+
PrimitiveRegistry.primitives = {};
|
|
26
|
+
return new SolanaTokenAccountPrimitive({
|
|
27
|
+
instanceName: "WatchedToken",
|
|
28
|
+
startBlockHeight: 0,
|
|
29
|
+
mint: MINT,
|
|
30
|
+
stateMachinePrefix: "solana-token-account",
|
|
31
|
+
...overrides,
|
|
32
|
+
} as any);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function payloadOf(primitive: SolanaTokenAccountPrimitive, payload: unknown) {
|
|
36
|
+
const it = primitive.getPayload(0 as any, { output: { payload } } as any) as any;
|
|
37
|
+
const step = it.next();
|
|
38
|
+
expect(step.done).toBe(true);
|
|
39
|
+
return step.value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const balancePayload = {
|
|
43
|
+
tokenAccount: TOKEN_ACCOUNT,
|
|
44
|
+
mint: MINT,
|
|
45
|
+
owner: OWNER,
|
|
46
|
+
amount: "1500000000",
|
|
47
|
+
decimals: 9,
|
|
48
|
+
slot: 11,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
test("rejects a config with no mint, owner or tokenAccount", () => {
|
|
52
|
+
// Without a filter it would match every token balance on chain, which is never
|
|
53
|
+
// what was meant — so this fails at construction rather than flooding the STM.
|
|
54
|
+
expect(() =>
|
|
55
|
+
makePrimitive({ mint: undefined })
|
|
56
|
+
).toThrow(/needs at least one of/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("accepts a config filtered by owner alone", () => {
|
|
60
|
+
expect(() => makePrimitive({ mint: undefined, owner: OWNER })).not.toThrow();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("accepts a config filtered by tokenAccount alone", () => {
|
|
64
|
+
expect(() =>
|
|
65
|
+
makePrimitive({ mint: undefined, tokenAccount: TOKEN_ACCOUNT })
|
|
66
|
+
).not.toThrow();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("carries the raw u64 amount through as a string", () => {
|
|
70
|
+
// 2^63 - 1: a value that silently loses precision as a JS number, which is why
|
|
71
|
+
// the grammar types `amount` as a string.
|
|
72
|
+
const huge = "9223372036854775807";
|
|
73
|
+
const out = payloadOf(makePrimitive(), { ...balancePayload, amount: huge });
|
|
74
|
+
expect(out.data[0].accountingPayload.amount).toBe(huge);
|
|
75
|
+
expect(out.data[0].accountingPayload.decimals).toBe(9);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("attributes the event to the owner", () => {
|
|
79
|
+
const out = payloadOf(makePrimitive(), balancePayload);
|
|
80
|
+
expect(out.data[0].fromAddressAndType.address).toBe(OWNER);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("falls back to the token account when the RPC omitted the owner", () => {
|
|
84
|
+
// `owner` is optional in the RPC's token balance record. The balance is still
|
|
85
|
+
// valid state, so it is reported against the token account rather than dropped.
|
|
86
|
+
const out = payloadOf(makePrimitive(), { ...balancePayload, owner: undefined });
|
|
87
|
+
expect(out.data[0].fromAddressAndType.address).toBe(TOKEN_ACCOUNT);
|
|
88
|
+
expect(out.data[0].accountingPayload.owner).toBe("");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("builds a state machine payload prefixed by stateMachinePrefix", () => {
|
|
92
|
+
const out = payloadOf(makePrimitive(), balancePayload);
|
|
93
|
+
expect(out.data[0].stateMachinePayload).not.toBeNull();
|
|
94
|
+
expect(out.data[0].stateMachinePayload[0]).toBe("solana-token-account");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("emits no state machine payload when no prefix is configured", () => {
|
|
98
|
+
// The accounting row is still written; only STM delivery is skipped. This is the
|
|
99
|
+
// documented meaning of omitting the prefix.
|
|
100
|
+
const out = payloadOf(
|
|
101
|
+
makePrimitive({ stateMachinePrefix: undefined }),
|
|
102
|
+
balancePayload,
|
|
103
|
+
);
|
|
104
|
+
expect(out.data[0].stateMachinePayload).toBeNull();
|
|
105
|
+
expect(out.data[0].accountingPayload.mint).toBe(MINT);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("getConfig round-trips the state machine prefix under both names", () => {
|
|
109
|
+
// Every other builtin emits only `scheduledPrefix`, which the Primitive
|
|
110
|
+
// constructor does not read — so a config round-tripped through getConfig() loses
|
|
111
|
+
// its STM routing. Emitting both makes this primitive survive that trip.
|
|
112
|
+
const config = makePrimitive().getConfig() as Record<string, unknown>;
|
|
113
|
+
expect(config.stateMachinePrefix).toBe("solana-token-account");
|
|
114
|
+
expect(config.scheduledPrefix).toBe("solana-token-account");
|
|
115
|
+
expect(config.type).toBe("SOLANA:TokenAccount");
|
|
116
|
+
expect(config.mint).toBe(MINT);
|
|
117
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { StaticDecode } from "@sinclair/typebox";
|
|
2
|
+
import type {
|
|
3
|
+
ConfigSyncProtocolType,
|
|
4
|
+
FlattenSyncProtocolIOFor,
|
|
5
|
+
ProtocolPrimitiveMap,
|
|
6
|
+
} from "@effectstream/config";
|
|
7
|
+
import {
|
|
8
|
+
type AddressAndType,
|
|
9
|
+
AddressType,
|
|
10
|
+
type EffectstreamBlockNumber,
|
|
11
|
+
} from "@effectstream/utils";
|
|
12
|
+
import { type JsonObject, Primitive } from "@effectstream/sm";
|
|
13
|
+
import {
|
|
14
|
+
type CommandTuple,
|
|
15
|
+
generateRawStmInput,
|
|
16
|
+
type ParamToData,
|
|
17
|
+
} from "@effectstream/concise";
|
|
18
|
+
import type { StateUpdateStream } from "@effectstream/coroutine";
|
|
19
|
+
import { solanaTokenAccountGrammar } from "./solana-token-account-grammar.ts";
|
|
20
|
+
import { PrimitiveTypeSolanaTokenAccount } from "../builtin.ts";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Built-in primitive: tracks the SPL token balance of a watched token account per
|
|
24
|
+
* slot, derived from a transaction's `meta.postTokenBalances` by
|
|
25
|
+
* `SolanaFetcher.readPrimitives`.
|
|
26
|
+
*
|
|
27
|
+
* Narrowed by any combination of `mint`, `owner` and `tokenAccount`; at least one
|
|
28
|
+
* must be set. `tokenProgramId` optionally pins it to classic SPL Token or to
|
|
29
|
+
* Token-2022 rather than accepting both.
|
|
30
|
+
*
|
|
31
|
+
* KNOWN LIMITATION — this reports post-state balances only, matching
|
|
32
|
+
* SOLANA:AccountBalance. A token account that is *closed* appears in
|
|
33
|
+
* `preTokenBalances` and is absent from `postTokenBalances`, so closure produces no
|
|
34
|
+
* event rather than a zero one. Emitting a pre/post delta would cover it and is a
|
|
35
|
+
* deliberate non-goal here; see the Solana chain docs.
|
|
36
|
+
*/
|
|
37
|
+
export class SolanaTokenAccountPrimitive extends Primitive<
|
|
38
|
+
ConfigSyncProtocolType.SOLANA_RPC_PARALLEL,
|
|
39
|
+
typeof solanaTokenAccountGrammar
|
|
40
|
+
> {
|
|
41
|
+
readonly internalTypeName = PrimitiveTypeSolanaTokenAccount;
|
|
42
|
+
override grammar = solanaTokenAccountGrammar;
|
|
43
|
+
readonly mint: string | undefined;
|
|
44
|
+
readonly owner: string | undefined;
|
|
45
|
+
readonly tokenAccount: string | undefined;
|
|
46
|
+
readonly tokenProgramId: string | undefined;
|
|
47
|
+
|
|
48
|
+
constructor(config: {
|
|
49
|
+
instanceName: string;
|
|
50
|
+
startBlockHeight: number;
|
|
51
|
+
mint?: string;
|
|
52
|
+
owner?: string;
|
|
53
|
+
tokenAccount?: string;
|
|
54
|
+
tokenProgramId?: string;
|
|
55
|
+
stateMachinePrefix: string | undefined;
|
|
56
|
+
}) {
|
|
57
|
+
super(config);
|
|
58
|
+
// Rejected here rather than in the fetcher because this is the earliest point
|
|
59
|
+
// it can be caught: an entry with no filter at all would match every token
|
|
60
|
+
// balance in every transaction, which is never what was meant.
|
|
61
|
+
if (
|
|
62
|
+
config.mint == null && config.owner == null && config.tokenAccount == null
|
|
63
|
+
) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`[${PrimitiveTypeSolanaTokenAccount}] "${config.instanceName}" needs at least one of ` +
|
|
66
|
+
`mint, owner or tokenAccount. Without a filter it would match every token balance on chain.`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
this.mint = config.mint;
|
|
70
|
+
this.owner = config.owner;
|
|
71
|
+
this.tokenAccount = config.tokenAccount;
|
|
72
|
+
this.tokenProgramId = config.tokenProgramId;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
override *getPayload(
|
|
76
|
+
_: EffectstreamBlockNumber,
|
|
77
|
+
primitiveTransactionData: FlattenSyncProtocolIOFor<
|
|
78
|
+
ConfigSyncProtocolType.SOLANA_RPC_PARALLEL
|
|
79
|
+
>,
|
|
80
|
+
): StateUpdateStream<{
|
|
81
|
+
isBatched: boolean;
|
|
82
|
+
data: {
|
|
83
|
+
fromAddressAndType: AddressAndType;
|
|
84
|
+
stateMachinePayload:
|
|
85
|
+
| StaticDecode<CommandTuple<string, typeof solanaTokenAccountGrammar>>
|
|
86
|
+
| null;
|
|
87
|
+
accountingPayload: JsonObject;
|
|
88
|
+
}[];
|
|
89
|
+
}> {
|
|
90
|
+
const payload = primitiveTransactionData.output.payload as {
|
|
91
|
+
tokenAccount?: string;
|
|
92
|
+
mint?: string;
|
|
93
|
+
owner?: string;
|
|
94
|
+
amount?: string;
|
|
95
|
+
decimals?: number;
|
|
96
|
+
slot?: number;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const accountingPayload: ParamToData<typeof solanaTokenAccountGrammar> = {
|
|
100
|
+
tokenAccount: String(payload.tokenAccount ?? ""),
|
|
101
|
+
mint: String(payload.mint ?? this.mint ?? ""),
|
|
102
|
+
// `owner` is optional in the RPC's token balance record, so it can genuinely
|
|
103
|
+
// be absent. Empty string rather than a thrown error: the balance itself is
|
|
104
|
+
// still valid state, and the grammar requires a string.
|
|
105
|
+
owner: String(payload.owner ?? this.owner ?? ""),
|
|
106
|
+
amount: String(payload.amount ?? "0"),
|
|
107
|
+
decimals: Number(payload.decimals ?? 0),
|
|
108
|
+
slot: Number(payload.slot ?? 0),
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const stateMachinePayload = this.stateMachinePrefix
|
|
112
|
+
? generateRawStmInput(
|
|
113
|
+
this.grammar,
|
|
114
|
+
this.stateMachinePrefix,
|
|
115
|
+
accountingPayload,
|
|
116
|
+
)
|
|
117
|
+
: null;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
isBatched: false,
|
|
121
|
+
data: [
|
|
122
|
+
{
|
|
123
|
+
// The owner is the party whose holdings changed, so it is the meaningful
|
|
124
|
+
// actor. Falls back to the token account when the RPC omitted the owner.
|
|
125
|
+
fromAddressAndType: {
|
|
126
|
+
type: AddressType.SOLANA,
|
|
127
|
+
address: accountingPayload.owner || accountingPayload.tokenAccount,
|
|
128
|
+
},
|
|
129
|
+
accountingPayload,
|
|
130
|
+
stateMachinePayload,
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
override getConfig(): ProtocolPrimitiveMap[
|
|
137
|
+
ConfigSyncProtocolType.SOLANA_RPC_PARALLEL
|
|
138
|
+
] {
|
|
139
|
+
return {
|
|
140
|
+
name: this.instanceName,
|
|
141
|
+
type: this.internalTypeName,
|
|
142
|
+
startBlockHeight: this.startBlockHeight,
|
|
143
|
+
mint: this.mint,
|
|
144
|
+
owner: this.owner,
|
|
145
|
+
tokenAccount: this.tokenAccount,
|
|
146
|
+
tokenProgramId: this.tokenProgramId,
|
|
147
|
+
// Both names on purpose. Every other builtin emits only `scheduledPrefix`,
|
|
148
|
+
// which the `Primitive` constructor does not read — so a config round-tripped
|
|
149
|
+
// through getConfig() silently loses its state-machine routing. Emitting
|
|
150
|
+
// `stateMachinePrefix` too makes this primitive survive that trip, and
|
|
151
|
+
// `scheduledPrefix` stays for anything still reading the deprecated name.
|
|
152
|
+
stateMachinePrefix: this.stateMachinePrefix,
|
|
153
|
+
scheduledPrefix: this.stateMachinePrefix,
|
|
154
|
+
} as ProtocolPrimitiveMap[ConfigSyncProtocolType.SOLANA_RPC_PARALLEL];
|
|
155
|
+
}
|
|
156
|
+
}
|