@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.
- package/Stm.ts +88 -0
- package/delegate-wallet.ts +431 -0
- package/mod.ts +4 -0
- package/package.json +27 -0
- package/primitives/Primitive.ts +80 -0
- package/primitives/PrimitiveRegistry.ts +59 -0
- package/primitives/mod.ts +5 -0
- package/primitives/processing.ts +72 -0
- package/primitives/src/README.md +3 -0
- package/primitives/src/avail-generic/avail-generic-grammar.ts +5 -0
- package/primitives/src/avail-generic/avail-primitive.ts +103 -0
- package/primitives/src/bitcoin-address/bitcoin-grammar.ts +16 -0
- package/primitives/src/bitcoin-address/bitcoin-primitive.ts +113 -0
- package/primitives/src/builtin.ts +47 -0
- package/primitives/src/celestia-generic/celestia-generic-grammar.ts +5 -0
- package/primitives/src/celestia-generic/celestia-primitive.ts +97 -0
- package/primitives/src/evm-effectstream-l2/effectstream-l2-abi.ts +1 -0
- package/primitives/src/evm-effectstream-l2/effectstream-l2-primitive.ts +484 -0
- package/primitives/src/evm-erc1155/erc1155-abi.ts +1 -0
- package/primitives/src/evm-erc1155/erc1155-grammar.ts +13 -0
- package/primitives/src/evm-erc1155/erc1155-ivm.ts +104 -0
- package/primitives/src/evm-erc1155/erc1155-primitive.ts +242 -0
- package/primitives/src/evm-erc20/erc20-abi.ts +1 -0
- package/primitives/src/evm-erc20/erc20-grammar.ts +7 -0
- package/primitives/src/evm-erc20/erc20-ivm.ts +93 -0
- package/primitives/src/evm-erc20/erc20-primitive.test.ts +123 -0
- package/primitives/src/evm-erc20/erc20-primitive.ts +152 -0
- package/primitives/src/evm-erc721/erc721-abi.ts +1 -0
- package/primitives/src/evm-erc721/erc721-grammar.ts +9 -0
- package/primitives/src/evm-erc721/erc721-ivm.ts +85 -0
- package/primitives/src/evm-erc721/erc721-primitive.ts +149 -0
- package/primitives/src/evm-generic/evm-generic-primitive.ts +124 -0
- package/primitives/src/grammar.ts +34 -0
- package/primitives/src/midnight-generic/midnight-genetic-grammar.ts +8 -0
- package/primitives/src/midnight-generic/midnight-genetic.ts +271 -0
- package/primitives/src/midnight-nullifier/midnight-nullifier-grammar.ts +8 -0
- package/primitives/src/midnight-nullifier/midnight-nullifier.ts +114 -0
- package/primitives/src/mod.ts +81 -0
- package/primitives/src/near-account-watch/near-account-watch-grammar.ts +10 -0
- package/primitives/src/near-account-watch/near-account-watch-primitive.ts +106 -0
- package/primitives/src/near-generic/near-generic-grammar.ts +5 -0
- package/primitives/src/near-generic/near-generic-primitive.ts +105 -0
- package/primitives/src/near-intent/near-intent-grammar.ts +10 -0
- package/primitives/src/near-intent/near-intent-ivm.ts +71 -0
- package/primitives/src/near-intent/near-intent-primitive.ts +149 -0
- package/primitives/src/near-intent/near-intent-token-id-parser.ts +47 -0
- package/primitives/src/near-nep141/nep141-grammar.ts +7 -0
- package/primitives/src/near-nep141/nep141-ivm.ts +70 -0
- package/primitives/src/near-nep141/nep141-primitive.ts +117 -0
- package/primitives/src/near-nep171/nep171-grammar.ts +8 -0
- package/primitives/src/near-nep171/nep171-ivm.ts +71 -0
- package/primitives/src/near-nep171/nep171-primitive.ts +138 -0
- package/primitives/src/near-nep245/nep245-grammar.ts +11 -0
- package/primitives/src/near-nep245/nep245-ivm.ts +84 -0
- package/primitives/src/near-nep245/nep245-primitive.ts +147 -0
- package/primitives/src/utxorpc-generic/utxorpc-generic-grammar.ts +6 -0
- package/primitives/src/utxorpc-generic/utxorpc-generic.ts +88 -0
- package/primitives/types.ts +14 -0
- package/types.ts +28 -0
- 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
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
|
+
}
|