@effectstream/sm 0.101.1 → 0.103.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.
- package/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/package.json +10 -10
- package/primitives/Primitive.ts +4 -1
- package/primitives/src/builtin.ts +24 -3
- package/primitives/src/grammar.ts +8 -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-token-mint/midnight-token-mint-grammar.ts +12 -4
- package/primitives/src/midnight-token-mint/midnight-token-mint-ivm.ts +105 -0
- package/primitives/src/midnight-token-mint/midnight-token-mint.test.ts +150 -0
- package/primitives/src/midnight-token-mint/midnight-token-mint.ts +80 -23
- 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 +17 -4
- package/primitives/src/solana-account-balance/solana-account-balance-grammar.ts +7 -0
- package/primitives/src/solana-account-balance/solana-account-balance-primitive.ts +108 -0
- package/primitives/src/solana-program-log/solana-program-log-grammar.ts +7 -0
- package/primitives/src/solana-program-log/solana-program-log-primitive.test.ts +128 -0
- package/primitives/src/solana-program-log/solana-program-log-primitive.ts +108 -0
- 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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
import { run } from "effection";
|
|
3
|
+
import type { ConfigSyncProtocolType, FlattenSyncProtocolIOFor } from "@effectstream/config";
|
|
4
|
+
import { SolanaProgramLogPrimitive } from "./../mod.ts";
|
|
5
|
+
import { PrimitiveRegistry } from "../../PrimitiveRegistry.ts";
|
|
6
|
+
|
|
7
|
+
const PROGRAM_ID = "11111111111111111111111111111111";
|
|
8
|
+
const PrimitiveTypeSolanaProgramLog = "SOLANA:ProgramLog";
|
|
9
|
+
|
|
10
|
+
function cleanup() {
|
|
11
|
+
PrimitiveRegistry.primitives = {};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
test("SolanaProgramLogPrimitive - initializes correctly", () => {
|
|
15
|
+
cleanup();
|
|
16
|
+
const primitive = new SolanaProgramLogPrimitive({
|
|
17
|
+
instanceName: "spl",
|
|
18
|
+
startBlockHeight: 0,
|
|
19
|
+
programId: PROGRAM_ID,
|
|
20
|
+
stateMachinePrefix: "solana-program-log",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(primitive.instanceName).toEqual("spl");
|
|
24
|
+
expect(primitive.startBlockHeight).toEqual(0);
|
|
25
|
+
expect(primitive.programId).toEqual(PROGRAM_ID);
|
|
26
|
+
expect(primitive.internalTypeName).toEqual(PrimitiveTypeSolanaProgramLog);
|
|
27
|
+
|
|
28
|
+
const config = primitive.getConfig();
|
|
29
|
+
expect(config.name).toEqual("spl");
|
|
30
|
+
expect(config.type).toEqual(PrimitiveTypeSolanaProgramLog);
|
|
31
|
+
expect(config.programId).toEqual(PROGRAM_ID);
|
|
32
|
+
expect(config.scheduledPrefix).toEqual("solana-program-log");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("SolanaProgramLogPrimitive - getPayload generates correct state update", async () => {
|
|
36
|
+
cleanup();
|
|
37
|
+
const primitive = new SolanaProgramLogPrimitive({
|
|
38
|
+
instanceName: "spl",
|
|
39
|
+
startBlockHeight: 0,
|
|
40
|
+
programId: PROGRAM_ID,
|
|
41
|
+
stateMachinePrefix: "solana-program-log",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const mockTxData = {
|
|
45
|
+
syncProtocol: {
|
|
46
|
+
name: "parallelSolanaRPC",
|
|
47
|
+
blockNumber: 42,
|
|
48
|
+
transactionHash: "5x…signature",
|
|
49
|
+
contractAddress: PROGRAM_ID,
|
|
50
|
+
},
|
|
51
|
+
output: {
|
|
52
|
+
payloadType: "solana:transaction",
|
|
53
|
+
payload: {
|
|
54
|
+
programId: PROGRAM_ID,
|
|
55
|
+
slot: 42,
|
|
56
|
+
logMessages: ["Program log: hi", "Program log: bye"],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
} as unknown as FlattenSyncProtocolIOFor<
|
|
60
|
+
ConfigSyncProtocolType.SOLANA_RPC_PARALLEL
|
|
61
|
+
>;
|
|
62
|
+
|
|
63
|
+
await run(function* () {
|
|
64
|
+
const generator = primitive.getPayload(123, mockTxData);
|
|
65
|
+
const result = generator.next().value;
|
|
66
|
+
|
|
67
|
+
if (!result) throw new Error("No payload generated");
|
|
68
|
+
if (!("isBatched" in result)) {
|
|
69
|
+
throw new Error("Result is not a SyncStateUpdate");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
expect(result.isBatched).toEqual(false);
|
|
73
|
+
expect(result.data.length).toEqual(1);
|
|
74
|
+
|
|
75
|
+
const item = result.data[0];
|
|
76
|
+
expect(item.accountingPayload.slot).toEqual(42);
|
|
77
|
+
expect(item.accountingPayload.programId).toEqual(PROGRAM_ID);
|
|
78
|
+
expect(item.accountingPayload.logMessages).toEqual([
|
|
79
|
+
"Program log: hi",
|
|
80
|
+
"Program log: bye",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
expect(Array.isArray(item.stateMachinePayload)).toEqual(true);
|
|
84
|
+
// @ts-ignore: safe to access
|
|
85
|
+
expect(item.stateMachinePayload?.[0]).toEqual("solana-program-log");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("SolanaProgramLogPrimitive - getPayload skips state machine payload if no prefix", async () => {
|
|
90
|
+
cleanup();
|
|
91
|
+
const primitive = new SolanaProgramLogPrimitive({
|
|
92
|
+
instanceName: "spl",
|
|
93
|
+
startBlockHeight: 0,
|
|
94
|
+
programId: PROGRAM_ID,
|
|
95
|
+
stateMachinePrefix: undefined,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const mockTxData = {
|
|
99
|
+
syncProtocol: {
|
|
100
|
+
name: "parallelSolanaRPC",
|
|
101
|
+
blockNumber: 7,
|
|
102
|
+
transactionHash: "sig",
|
|
103
|
+
contractAddress: PROGRAM_ID,
|
|
104
|
+
},
|
|
105
|
+
output: {
|
|
106
|
+
payload: {
|
|
107
|
+
programId: PROGRAM_ID,
|
|
108
|
+
slot: 7,
|
|
109
|
+
logMessages: ["Program log: x"],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
} as unknown as FlattenSyncProtocolIOFor<
|
|
113
|
+
ConfigSyncProtocolType.SOLANA_RPC_PARALLEL
|
|
114
|
+
>;
|
|
115
|
+
|
|
116
|
+
await run(function* () {
|
|
117
|
+
const generator = primitive.getPayload(123, mockTxData);
|
|
118
|
+
const result = generator.next().value;
|
|
119
|
+
|
|
120
|
+
if (!result) throw new Error("No payload generated");
|
|
121
|
+
if (!("isBatched" in result)) {
|
|
122
|
+
throw new Error("Result is not a SyncStateUpdate");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const item = result.data[0];
|
|
126
|
+
expect(item.stateMachinePayload).toEqual(null);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { StaticDecode } from "@sinclair/typebox";
|
|
2
|
+
import {
|
|
3
|
+
type ConfigSyncProtocolType,
|
|
4
|
+
type FlattenSyncProtocolIOFor,
|
|
5
|
+
type 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 { solanaProgramLogGrammar } from "./solana-program-log-grammar.ts";
|
|
20
|
+
import { PrimitiveTypeSolanaProgramLog } from "../builtin.ts";
|
|
21
|
+
|
|
22
|
+
export class SolanaProgramLogPrimitive extends Primitive<
|
|
23
|
+
ConfigSyncProtocolType.SOLANA_RPC_PARALLEL,
|
|
24
|
+
typeof solanaProgramLogGrammar
|
|
25
|
+
> {
|
|
26
|
+
readonly internalTypeName = PrimitiveTypeSolanaProgramLog;
|
|
27
|
+
override grammar = solanaProgramLogGrammar;
|
|
28
|
+
readonly programId: string;
|
|
29
|
+
readonly eventType: string | undefined;
|
|
30
|
+
|
|
31
|
+
constructor(config: {
|
|
32
|
+
instanceName: string;
|
|
33
|
+
startBlockHeight: number;
|
|
34
|
+
programId: string;
|
|
35
|
+
eventType?: string;
|
|
36
|
+
stateMachinePrefix: string | undefined;
|
|
37
|
+
}) {
|
|
38
|
+
super(config);
|
|
39
|
+
this.programId = config.programId;
|
|
40
|
+
this.eventType = config.eventType;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
override *getPayload(
|
|
44
|
+
_: EffectstreamBlockNumber,
|
|
45
|
+
primitiveTransactionData: FlattenSyncProtocolIOFor<
|
|
46
|
+
ConfigSyncProtocolType.SOLANA_RPC_PARALLEL
|
|
47
|
+
>,
|
|
48
|
+
): StateUpdateStream<{
|
|
49
|
+
isBatched: boolean;
|
|
50
|
+
data: {
|
|
51
|
+
fromAddressAndType: AddressAndType;
|
|
52
|
+
stateMachinePayload:
|
|
53
|
+
| StaticDecode<CommandTuple<string, typeof solanaProgramLogGrammar>>
|
|
54
|
+
| null;
|
|
55
|
+
accountingPayload: JsonObject;
|
|
56
|
+
}[];
|
|
57
|
+
}> {
|
|
58
|
+
const { programId, slot, logMessages } =
|
|
59
|
+
primitiveTransactionData.output.payload as {
|
|
60
|
+
programId: string;
|
|
61
|
+
slot: number;
|
|
62
|
+
logMessages: string[];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const accountingPayload: ParamToData<typeof solanaProgramLogGrammar> = {
|
|
66
|
+
slot,
|
|
67
|
+
programId,
|
|
68
|
+
logMessages,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const stateMachinePayload = this.stateMachinePrefix
|
|
72
|
+
? generateRawStmInput(
|
|
73
|
+
this.grammar,
|
|
74
|
+
this.stateMachinePrefix,
|
|
75
|
+
accountingPayload,
|
|
76
|
+
)
|
|
77
|
+
: null;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
isBatched: false,
|
|
81
|
+
data: [
|
|
82
|
+
{
|
|
83
|
+
fromAddressAndType: {
|
|
84
|
+
type: AddressType.SOLANA,
|
|
85
|
+
address: String(
|
|
86
|
+
primitiveTransactionData.syncProtocol.contractAddress ?? "",
|
|
87
|
+
),
|
|
88
|
+
},
|
|
89
|
+
accountingPayload,
|
|
90
|
+
stateMachinePayload,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
override getConfig(): ProtocolPrimitiveMap[
|
|
97
|
+
ConfigSyncProtocolType.SOLANA_RPC_PARALLEL
|
|
98
|
+
] {
|
|
99
|
+
return {
|
|
100
|
+
name: this.instanceName,
|
|
101
|
+
type: this.internalTypeName,
|
|
102
|
+
startBlockHeight: this.startBlockHeight,
|
|
103
|
+
programId: this.programId,
|
|
104
|
+
eventType: this.eventType,
|
|
105
|
+
scheduledPrefix: this.stateMachinePrefix,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -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
|
+
}
|