@effectstream/utils 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/package.json +28 -0
- package/src/binary-search.test.ts +38 -0
- package/src/binary-search.ts +45 -0
- package/src/captcha.ts +21 -0
- package/src/concurrency/condVar.ts +52 -0
- package/src/concurrency/latch.test.ts +51 -0
- package/src/concurrency/latch.ts +61 -0
- package/src/concurrency/mod.ts +4 -0
- package/src/concurrency/retry.test.ts +59 -0
- package/src/concurrency/retry.ts +67 -0
- package/src/concurrency/utils.ts +33 -0
- package/src/config.ts +484 -0
- package/src/constants.ts +10 -0
- package/src/decorators/bound.ts +22 -0
- package/src/decorators/mod.ts +1 -0
- package/src/mod.ts +8 -0
- package/src/runtime-spawn.ts +188 -0
- package/src/runtime.ts +89 -0
- package/src/types/json-query.ts +36 -0
- package/src/types/misc.ts +173 -0
- package/src/types/mod.ts +6 -0
- package/src/types/nominal.ts +168 -0
- package/src/types/typebox-helpers.ts +446 -0
- package/src/types/utils.ts +60 -0
- package/src/types/validators/algorand.ts +120 -0
- package/src/types/validators/cardano.ts +139 -0
- package/src/types/validators/mina.ts +54 -0
- package/src/types/validators/minicbor.ts +468 -0
- package/src/types/validators/mod.ts +2 -0
- package/src/types/validators/utils.ts +34 -0
- package/src/viem.ts +41 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { FormatRegistry, type Kind, Type } from "@sinclair/typebox";
|
|
2
|
+
import type {
|
|
3
|
+
NumberOptions,
|
|
4
|
+
RegExpOptions,
|
|
5
|
+
SchemaOptions,
|
|
6
|
+
Static,
|
|
7
|
+
StaticDecode,
|
|
8
|
+
TNull,
|
|
9
|
+
TObject,
|
|
10
|
+
TRegExp,
|
|
11
|
+
TSchema,
|
|
12
|
+
TString,
|
|
13
|
+
TTransform,
|
|
14
|
+
TUnion,
|
|
15
|
+
TUnsafe,
|
|
16
|
+
} from "@sinclair/typebox";
|
|
17
|
+
import { Value, ValueErrorType } from "@sinclair/typebox/value";
|
|
18
|
+
import type { AbiEvent } from "abitype";
|
|
19
|
+
import type { Mutable, Satisfies, TypeErrorMessage } from "./misc.ts";
|
|
20
|
+
import type * as Nominal from "./nominal.ts";
|
|
21
|
+
import * as ss58 from "@subsquid/ss58-codec";
|
|
22
|
+
import bs58check from "bs58check";
|
|
23
|
+
import { bech32 } from "bech32";
|
|
24
|
+
import {
|
|
25
|
+
verifyAlgorandAddress,
|
|
26
|
+
verifyAlgorandBlockHash,
|
|
27
|
+
verifyAlgorandTransactionHash,
|
|
28
|
+
} from "./validators/algorand.ts";
|
|
29
|
+
import {
|
|
30
|
+
verifyMinaAddress,
|
|
31
|
+
verifyMinaBlockHash,
|
|
32
|
+
verifyMinaTransactionHash,
|
|
33
|
+
} from "./validators/mina.ts";
|
|
34
|
+
import {
|
|
35
|
+
verifyCardanoByronAddress,
|
|
36
|
+
verifyCardanoShelleyAddress,
|
|
37
|
+
} from "./validators/cardano.ts";
|
|
38
|
+
|
|
39
|
+
export type MaybeStaticDecode<T extends never | TSchema> = T extends TSchema
|
|
40
|
+
? StaticDecode<T>
|
|
41
|
+
: never;
|
|
42
|
+
|
|
43
|
+
// IMPORTANT
|
|
44
|
+
// These types are used in the on-chain messages.
|
|
45
|
+
// Do not change or reuse the numeric values.
|
|
46
|
+
export enum AddressType {
|
|
47
|
+
NONE = -1,
|
|
48
|
+
EVM = 0,
|
|
49
|
+
CARDANO = 1,
|
|
50
|
+
SUBSTRATE = 2,
|
|
51
|
+
ALGORAND = 3,
|
|
52
|
+
MINA = 4,
|
|
53
|
+
MIDNIGHT = 5,
|
|
54
|
+
AVAIL = 6,
|
|
55
|
+
POLKADOT = 7,
|
|
56
|
+
NEAR = 8,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function tryDecode<T>(
|
|
60
|
+
val: string,
|
|
61
|
+
validator: (address: string) => T,
|
|
62
|
+
checkResult: (result: T) => boolean = () => true,
|
|
63
|
+
): boolean {
|
|
64
|
+
try {
|
|
65
|
+
return checkResult(validator(val));
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
FormatRegistry.Set(
|
|
71
|
+
"algorand-address",
|
|
72
|
+
(val) => tryDecode(val, verifyAlgorandAddress),
|
|
73
|
+
);
|
|
74
|
+
FormatRegistry.Set(
|
|
75
|
+
"algorand-blockhash",
|
|
76
|
+
(val) => tryDecode(val, verifyAlgorandBlockHash),
|
|
77
|
+
);
|
|
78
|
+
FormatRegistry.Set(
|
|
79
|
+
"algorand-txid",
|
|
80
|
+
(val) => tryDecode(val, verifyAlgorandTransactionHash),
|
|
81
|
+
);
|
|
82
|
+
FormatRegistry.Set("mina-address", (val) => tryDecode(val, verifyMinaAddress));
|
|
83
|
+
FormatRegistry.Set(
|
|
84
|
+
"mina-blockhash",
|
|
85
|
+
(val) => tryDecode(val, verifyMinaBlockHash),
|
|
86
|
+
);
|
|
87
|
+
FormatRegistry.Set(
|
|
88
|
+
"mina-txid",
|
|
89
|
+
(val) => tryDecode(val, verifyMinaTransactionHash),
|
|
90
|
+
);
|
|
91
|
+
FormatRegistry.Set(
|
|
92
|
+
"cardano-address",
|
|
93
|
+
(val) =>
|
|
94
|
+
tryDecode(val, verifyCardanoByronAddress) ||
|
|
95
|
+
tryDecode(val, verifyCardanoShelleyAddress),
|
|
96
|
+
);
|
|
97
|
+
FormatRegistry.Set("ss58", (val) => tryDecode(val, ss58.decode));
|
|
98
|
+
FormatRegistry.Set("bs58check", (val) => tryDecode(val, bs58check.decode));
|
|
99
|
+
FormatRegistry.Set("bech32", (val) =>
|
|
100
|
+
// note: increase max bech32 limit since some cryptocurrencies need it
|
|
101
|
+
tryDecode(val, (val) => bech32.decode(val, Number.MAX_SAFE_INTEGER / 2)));
|
|
102
|
+
|
|
103
|
+
function forceLowercase<T extends TString | TRegExp>(schema: T): TTransform<T> {
|
|
104
|
+
return Type.Transform(schema)
|
|
105
|
+
.Decode((value) => (value as any).toLowerCase())
|
|
106
|
+
.Encode((value) => value.toLowerCase());
|
|
107
|
+
}
|
|
108
|
+
function forceUppercase<T extends TString | TRegExp>(schema: T): TTransform<T> {
|
|
109
|
+
return Type.Transform(schema)
|
|
110
|
+
.Decode((value) => (value as any).toUpperCase())
|
|
111
|
+
.Encode((value) => value.toUpperCase());
|
|
112
|
+
}
|
|
113
|
+
export const TypeboxHelpers = {
|
|
114
|
+
Uint256: Type.Transform(Type.BigInt({
|
|
115
|
+
maximum:
|
|
116
|
+
115792089237316195423570985008687907853269984665640564039457584007913129639935n,
|
|
117
|
+
minimum: 0n,
|
|
118
|
+
}))
|
|
119
|
+
.Decode((value) => value.toString())
|
|
120
|
+
.Encode((value) => BigInt(value)),
|
|
121
|
+
BlockNumber: (options?: NumberOptions) =>
|
|
122
|
+
Type.Unsafe<Nominal.BlockNumber>(Type.Number(options)),
|
|
123
|
+
AbsoluteSlotNumber: (options?: NumberOptions) =>
|
|
124
|
+
Type.Unsafe<Nominal.AbsoluteSlotNumber>(Type.Number(options)),
|
|
125
|
+
EpochNumber: (options?: NumberOptions) =>
|
|
126
|
+
Type.Unsafe<Nominal.EpochNumber>(Type.Number(options)),
|
|
127
|
+
Algorand: {
|
|
128
|
+
BlockHash: Type.Unsafe<Nominal.AlgorandBlockHash>(
|
|
129
|
+
Type.String({ format: "algorand-blockhash" }),
|
|
130
|
+
),
|
|
131
|
+
TxHash: Type.Unsafe<Nominal.AlgorandTxHash>(
|
|
132
|
+
forceUppercase(Type.String({ format: "algorand-txid" })),
|
|
133
|
+
),
|
|
134
|
+
Address: Type.Unsafe<Nominal.AlgorandAddress>(
|
|
135
|
+
forceUppercase(Type.String({ format: "algorand-address" })),
|
|
136
|
+
),
|
|
137
|
+
},
|
|
138
|
+
Substrate: {
|
|
139
|
+
Address: Type.Unsafe<Nominal.SubstrateAddress>(
|
|
140
|
+
Type.String({ format: "ss58" }),
|
|
141
|
+
),
|
|
142
|
+
},
|
|
143
|
+
Avail: {
|
|
144
|
+
BlockHash: Type.Unsafe<Nominal.AvailBlockHash>(
|
|
145
|
+
forceLowercase(Type.RegExp(/^0x[a-fA-F0-9]{64}$/)),
|
|
146
|
+
),
|
|
147
|
+
Address: Type.Unsafe<Nominal.AvailAddress>(Type.String({ format: "ss58" })),
|
|
148
|
+
// recall: no good concept of tx hash on Substrate chains
|
|
149
|
+
},
|
|
150
|
+
Cardano: {
|
|
151
|
+
BlockHash: Type.Unsafe<Nominal.CardanoBlockHash>(
|
|
152
|
+
forceLowercase(Type.RegExp(/^[a-fA-F0-9]{64}$/)),
|
|
153
|
+
),
|
|
154
|
+
TxHash: Type.Unsafe<Nominal.CardanoTxHash>(
|
|
155
|
+
forceLowercase(Type.RegExp(/^[a-fA-F0-9]{64}$/)),
|
|
156
|
+
),
|
|
157
|
+
// TODO: this fails on Byron-era addresses
|
|
158
|
+
Address: Type.Unsafe<Nominal.CardanoAddress>(
|
|
159
|
+
Type.String({ format: "cardano-address" }),
|
|
160
|
+
),
|
|
161
|
+
Credential: Type.Unsafe<Nominal.CardanoCredential>(
|
|
162
|
+
forceLowercase(Type.RegExp(/^[a-fA-F0-9]{64}$/)),
|
|
163
|
+
),
|
|
164
|
+
// TODO: there are two common poolIDs. regex, and poolID bech32
|
|
165
|
+
PoolId: Type.Unsafe<Nominal.CardanoPoolId>(forceLowercase(Type.String())),
|
|
166
|
+
PolicyId: Type.Unsafe<Nominal.CardanoPolicyId>(
|
|
167
|
+
forceLowercase(Type.RegExp(/^[a-fA-F0-9]{56}$/)),
|
|
168
|
+
),
|
|
169
|
+
AssetName: Type.Unsafe<Nominal.CardanoAssetName>(
|
|
170
|
+
forceLowercase(Type.RegExp(/^[a-fA-F0-9]{64}$/)),
|
|
171
|
+
),
|
|
172
|
+
// TODO: bech32
|
|
173
|
+
Cip14Fingerprint: Type.Unsafe<Nominal.CardanoCip14Fingerprint>(
|
|
174
|
+
Type.String(),
|
|
175
|
+
),
|
|
176
|
+
AmountLovelace: Type.Unsafe<Nominal.CardanoAmountLovelace>(
|
|
177
|
+
Type.RegExp(/^\d+$/),
|
|
178
|
+
),
|
|
179
|
+
},
|
|
180
|
+
Evm: {
|
|
181
|
+
BlockHash: Type.Unsafe<Nominal.EvmBlockHash>(
|
|
182
|
+
forceLowercase(Type.RegExp(/^0x[a-fA-F0-9]{64}$/)),
|
|
183
|
+
),
|
|
184
|
+
TxHash: Type.Unsafe<Nominal.EvmTxHash>(
|
|
185
|
+
forceLowercase(Type.RegExp(/^0x[a-fA-F0-9]{64}$/)),
|
|
186
|
+
),
|
|
187
|
+
Address: Type.Unsafe<Nominal.EvmAddress>(
|
|
188
|
+
forceLowercase(Type.RegExp(/^0x[a-fA-F0-9]{40}$/)),
|
|
189
|
+
),
|
|
190
|
+
Selector: Type.Unsafe<Nominal.Evm4ByteSelector>(
|
|
191
|
+
forceLowercase(Type.RegExp(/^0x[a-fA-F0-9]{8}$/)),
|
|
192
|
+
),
|
|
193
|
+
FullSelector: Type.Unsafe<Nominal.EvmSelector>(
|
|
194
|
+
forceLowercase(Type.RegExp(/^0x[a-fA-F0-9]{64}$/)),
|
|
195
|
+
),
|
|
196
|
+
Signature: Type.Unsafe<Nominal.Signature>(
|
|
197
|
+
Type.RegExp(/^0x[a-fA-F0-9]{130}$/),
|
|
198
|
+
),
|
|
199
|
+
PrivateKey: Type.Unsafe<Nominal.EvmPrivateKey>(
|
|
200
|
+
Type.RegExp(/^0x[a-fA-F0-9]{64}$/),
|
|
201
|
+
),
|
|
202
|
+
},
|
|
203
|
+
Midnight: {
|
|
204
|
+
BlockHash: Type.Unsafe<Nominal.MidnightBlockHash>(
|
|
205
|
+
forceLowercase(Type.RegExp(/^0x[a-fA-F0-9]{64}$/)),
|
|
206
|
+
),
|
|
207
|
+
TxHash: Type.Unsafe<Nominal.MidnightTxHash>(
|
|
208
|
+
forceLowercase(Type.RegExp(/^0x[a-fA-F0-9]{64}$/)),
|
|
209
|
+
),
|
|
210
|
+
// TODO Temporal placeholder for midnight address
|
|
211
|
+
Address: Type.Unsafe<Nominal.MidnightAddress>(
|
|
212
|
+
forceLowercase(Type.RegExp(/^mn_(shield-addr|addr|dust-addr|shield-esk|shield-cpk)_(test|dev|undeployed)[a-zA-Z0-9]+$/)),
|
|
213
|
+
),
|
|
214
|
+
// TODO Temporal placeholder for midnight signature
|
|
215
|
+
Signature: Type.Unsafe<Nominal.MidnightSignature>(
|
|
216
|
+
Type.RegExp(/^.*$/),
|
|
217
|
+
),
|
|
218
|
+
},
|
|
219
|
+
Mina: {
|
|
220
|
+
BlockHash: Type.Unsafe<Nominal.MinaBlockHash>(
|
|
221
|
+
Type.String({ format: "mina-blockhash" }),
|
|
222
|
+
),
|
|
223
|
+
TxHash: Type.Unsafe<Nominal.MinaTxHash>(
|
|
224
|
+
Type.String({ format: "mina-txid" }),
|
|
225
|
+
),
|
|
226
|
+
Address: Type.Unsafe<Nominal.MinaAddress>(
|
|
227
|
+
Type.String({ format: "mina-address" }),
|
|
228
|
+
),
|
|
229
|
+
},
|
|
230
|
+
// TODO: PLACEHOLDER TYPES FOR POLKADOT
|
|
231
|
+
Polkadot: {
|
|
232
|
+
BlockHash: Type.Unsafe<Nominal.PolkadotBlockHash>(
|
|
233
|
+
Type.String({ format: "polkadot-blockhash" }),
|
|
234
|
+
),
|
|
235
|
+
TxHash: Type.Unsafe<Nominal.PolkadotTxHash>(
|
|
236
|
+
Type.String({ format: "polkadot-txid" }),
|
|
237
|
+
),
|
|
238
|
+
Address: Type.Unsafe<Nominal.PolkadotAddress>(Type.String({ format: "ss58" })),
|
|
239
|
+
},
|
|
240
|
+
Near: {
|
|
241
|
+
// NEAR account IDs: named (a-z0-9._-) up to 64 chars, or implicit (64 hex chars)
|
|
242
|
+
Address: Type.Unsafe<Nominal.NearAddress>(Type.String()),
|
|
243
|
+
},
|
|
244
|
+
Caip2: Type.Unsafe<Nominal.Caip2>(Type.String()),
|
|
245
|
+
WalletAddress: () =>
|
|
246
|
+
Type.Union(AddressTypebox as Mutable<typeof AddressTypebox>),
|
|
247
|
+
IntervalMs: (options?: NumberOptions) =>
|
|
248
|
+
Type.Unsafe<Nominal.IntervalMs>(Type.Number(options)),
|
|
249
|
+
IntervalSec: (options?: NumberOptions) =>
|
|
250
|
+
Type.Unsafe<Nominal.IntervalSec>(Type.Number(options)),
|
|
251
|
+
TimestampMs: (options?: NumberOptions) =>
|
|
252
|
+
Type.Unsafe<Nominal.TimestampMs>(Type.Number(options)),
|
|
253
|
+
TimestampMsStr: Type.Unsafe<Nominal.TimestampMsStr>(Type.RegExp(/^\d+$/)),
|
|
254
|
+
HexString0x: (options?: RegExpOptions) =>
|
|
255
|
+
Type.Unsafe<Nominal.HexString0x>(Type.RegExp(/^0x[a-fA-F0-9]+$/, options)),
|
|
256
|
+
HexStringNo0x: (options?: RegExpOptions) =>
|
|
257
|
+
Type.Unsafe<Nominal.HexStringNo0x>(Type.RegExp(/^[a-fA-F0-9]+$/, options)),
|
|
258
|
+
UnknownFormat: Type.Unsafe<Nominal.UnknownFormat>(Type.String()),
|
|
259
|
+
Lowercase: forceLowercase(Type.String()),
|
|
260
|
+
TrueOrFalse: Type.Transform(
|
|
261
|
+
Type.Union([Type.Literal("T"), Type.Literal("F")]),
|
|
262
|
+
)
|
|
263
|
+
.Decode((value) => value === "T")
|
|
264
|
+
.Encode((value) => (value ? "T" : "F")),
|
|
265
|
+
Nullable: <T extends TSchema>(
|
|
266
|
+
schema: T,
|
|
267
|
+
options?: SchemaOptions,
|
|
268
|
+
): TUnion<[T, TNull]> => Type.Union([schema, Type.Null()], options),
|
|
269
|
+
SerializeObjAsJson: <
|
|
270
|
+
T,
|
|
271
|
+
StringSchema extends TSchema & { [Kind]: string } = TString,
|
|
272
|
+
>(): TTransform<TUnsafe<T>, Static<StringSchema>> =>
|
|
273
|
+
Type.Transform(Type.Unsafe<T>(Type.Any()))
|
|
274
|
+
.Decode((x) => JSON.stringify(x) as Static<StringSchema>)
|
|
275
|
+
.Encode((x) => JSON.parse(x as string) as T),
|
|
276
|
+
SerializeJsonAsObj: <
|
|
277
|
+
T,
|
|
278
|
+
StringSchema extends TSchema & { [Kind]: string } = TString,
|
|
279
|
+
>(
|
|
280
|
+
schema: StringSchema,
|
|
281
|
+
): TTransform<StringSchema, T> =>
|
|
282
|
+
Type.Transform(schema)
|
|
283
|
+
.Decode((x) => JSON.parse(x as string) as T)
|
|
284
|
+
.Encode((x: T) => JSON.stringify(x) as Static<StringSchema>),
|
|
285
|
+
// TODO: maybe improve with a typebox-specific EVM ABI wrapper
|
|
286
|
+
// abitype library comes with a ZOD verifier, and that can be ported to typebox
|
|
287
|
+
EvmAbiEvent: Type.Unsafe<AbiEvent>(Type.Any()),
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
export const AddressValidator = {
|
|
291
|
+
[AddressType.NONE]: Type.Literal("0x0"),
|
|
292
|
+
[AddressType.EVM]: TypeboxHelpers.Evm.Address,
|
|
293
|
+
[AddressType.CARDANO]: TypeboxHelpers.Cardano.Address,
|
|
294
|
+
[AddressType.SUBSTRATE]: TypeboxHelpers.Substrate.Address,
|
|
295
|
+
[AddressType.AVAIL]: TypeboxHelpers.Avail.Address,
|
|
296
|
+
[AddressType.ALGORAND]: TypeboxHelpers.Algorand.Address,
|
|
297
|
+
[AddressType.MINA]: TypeboxHelpers.Mina.Address,
|
|
298
|
+
[AddressType.MIDNIGHT]: TypeboxHelpers.Midnight.Address,
|
|
299
|
+
[AddressType.POLKADOT]: TypeboxHelpers.Polkadot.Address,
|
|
300
|
+
[AddressType.NEAR]: TypeboxHelpers.Near.Address,
|
|
301
|
+
} as const satisfies Record<AddressType, TSchema>;
|
|
302
|
+
|
|
303
|
+
export const AddressTypebox = [
|
|
304
|
+
Type.Object({
|
|
305
|
+
type: Type.Literal(AddressType.NONE),
|
|
306
|
+
address: Type.Literal("0x0"),
|
|
307
|
+
}),
|
|
308
|
+
Type.Object({
|
|
309
|
+
type: Type.Literal(AddressType.EVM),
|
|
310
|
+
address: TypeboxHelpers.Evm.Address,
|
|
311
|
+
}),
|
|
312
|
+
Type.Object({
|
|
313
|
+
type: Type.Literal(AddressType.CARDANO),
|
|
314
|
+
address: TypeboxHelpers.Cardano.Address,
|
|
315
|
+
}),
|
|
316
|
+
Type.Object({
|
|
317
|
+
type: Type.Literal(AddressType.SUBSTRATE),
|
|
318
|
+
address: TypeboxHelpers.Substrate.Address,
|
|
319
|
+
}),
|
|
320
|
+
Type.Object({
|
|
321
|
+
type: Type.Literal(AddressType.AVAIL),
|
|
322
|
+
address: TypeboxHelpers.Avail.Address,
|
|
323
|
+
}),
|
|
324
|
+
Type.Object({
|
|
325
|
+
type: Type.Literal(AddressType.ALGORAND),
|
|
326
|
+
address: TypeboxHelpers.Algorand.Address,
|
|
327
|
+
}),
|
|
328
|
+
Type.Object({
|
|
329
|
+
type: Type.Literal(AddressType.MINA),
|
|
330
|
+
address: TypeboxHelpers.Mina.Address,
|
|
331
|
+
}),
|
|
332
|
+
Type.Object({
|
|
333
|
+
type: Type.Literal(AddressType.MIDNIGHT),
|
|
334
|
+
address: TypeboxHelpers.Midnight.Address,
|
|
335
|
+
}),
|
|
336
|
+
Type.Object({
|
|
337
|
+
type: Type.Literal(AddressType.POLKADOT),
|
|
338
|
+
address: TypeboxHelpers.Polkadot.Address,
|
|
339
|
+
}),
|
|
340
|
+
Type.Object({
|
|
341
|
+
type: Type.Literal(AddressType.NEAR),
|
|
342
|
+
address: TypeboxHelpers.Near.Address,
|
|
343
|
+
}),
|
|
344
|
+
] as const;
|
|
345
|
+
true satisfies Satisfies<
|
|
346
|
+
[(typeof AddressTypebox)[number]["static"]["type"]],
|
|
347
|
+
[AddressType]
|
|
348
|
+
>;
|
|
349
|
+
|
|
350
|
+
type AddressTypeMap = {
|
|
351
|
+
[K in keyof typeof AddressValidator]: StaticDecode<
|
|
352
|
+
(typeof AddressValidator)[K]
|
|
353
|
+
>;
|
|
354
|
+
};
|
|
355
|
+
export type AddressAndType = {
|
|
356
|
+
[K in keyof AddressTypeMap]: {
|
|
357
|
+
type: K;
|
|
358
|
+
address: AddressTypeMap[K];
|
|
359
|
+
};
|
|
360
|
+
}[keyof AddressTypeMap];
|
|
361
|
+
|
|
362
|
+
export function checkOrError<T extends TSchema>(
|
|
363
|
+
name: string | undefined,
|
|
364
|
+
structure: T,
|
|
365
|
+
config: unknown,
|
|
366
|
+
): StaticDecode<T> {
|
|
367
|
+
// 1) Check if there are any errors since Value.Decode doesn't give error messages
|
|
368
|
+
{
|
|
369
|
+
const errors = Array.from(Value.Errors(structure, config)).filter(
|
|
370
|
+
// there are many useless errors in this library
|
|
371
|
+
// ex: 1st error: "foo" should be "bar" in struct Foo
|
|
372
|
+
// 2nd error: struct Foo is invalid inside struct Config
|
|
373
|
+
// in this case, the 2nd error is useless as we only care about the 1st error
|
|
374
|
+
// However, we always want to show the error if for some reason it's the only error
|
|
375
|
+
(error) =>
|
|
376
|
+
error.type !== ValueErrorType.Intersect &&
|
|
377
|
+
error.type !== ValueErrorType.Union,
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
for (const error of errors) {
|
|
381
|
+
console.error({
|
|
382
|
+
name: name ?? "Schema root",
|
|
383
|
+
path: error.path,
|
|
384
|
+
valueProvided: error.value,
|
|
385
|
+
message: error.message,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
if (errors.length > 1) {
|
|
389
|
+
throw new Error(`Schema field missing or invalid. See above for error.`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const decoded = Value.Decode(structure, config);
|
|
394
|
+
return decoded;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
type TObjectToTuple<
|
|
398
|
+
Obj extends TObject<any>,
|
|
399
|
+
Keys extends (keyof Obj["properties"])[],
|
|
400
|
+
> = {
|
|
401
|
+
[K in keyof Keys]: [
|
|
402
|
+
Keys[K],
|
|
403
|
+
Obj extends TObject<Record<Keys[K] & string, infer V extends TSchema>> ? V
|
|
404
|
+
: never,
|
|
405
|
+
];
|
|
406
|
+
};
|
|
407
|
+
export function pick<const Keys extends string[]>(
|
|
408
|
+
keys: Keys,
|
|
409
|
+
): {
|
|
410
|
+
from: <Obj extends TObject<Record<Keys[number], any>>>(
|
|
411
|
+
obj: Obj,
|
|
412
|
+
) => TObjectToTuple<Obj, Keys>;
|
|
413
|
+
} {
|
|
414
|
+
return {
|
|
415
|
+
from: (obj) => {
|
|
416
|
+
const result = [];
|
|
417
|
+
for (const key of keys) {
|
|
418
|
+
result.push([key, (obj.properties as any)[key]]);
|
|
419
|
+
}
|
|
420
|
+
return result as any;
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
type HasAllProperties<
|
|
426
|
+
T extends TObject<any>,
|
|
427
|
+
K extends string[],
|
|
428
|
+
> = keyof T["properties"] extends K[number] ? T
|
|
429
|
+
: TypeErrorMessage<
|
|
430
|
+
`Missing properties: ${string & Exclude<keyof T["properties"], K[number]>}`
|
|
431
|
+
>;
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Same as `pick`, but requires that you include ALL properties and not just a subset of them
|
|
435
|
+
*/
|
|
436
|
+
export function pickAll<const Keys extends string[]>(
|
|
437
|
+
keys: Keys,
|
|
438
|
+
): {
|
|
439
|
+
from: <Obj extends TObject<Record<Keys[number], any>>>(
|
|
440
|
+
obj: HasAllProperties<Obj, Keys>,
|
|
441
|
+
) => TObjectToTuple<Obj, Keys>;
|
|
442
|
+
} {
|
|
443
|
+
return {
|
|
444
|
+
from: (obj) => pick(keys).from(obj as any),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { HexString0x, HexStringNo0x } from "./nominal.ts";
|
|
2
|
+
|
|
3
|
+
export type Json = null | boolean | number | string | Json[] | {
|
|
4
|
+
[key: string]: Json;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* This is a patch for a limitation in Typescript
|
|
9
|
+
* When writing functions with return types that are conditional (ex: (): T extends ... ? Foo : Bar
|
|
10
|
+
* It can't easily know type narrowing in function body leads to satisfying return type constraints
|
|
11
|
+
*
|
|
12
|
+
* Typically, this is solved by using an `as` when using `return`:
|
|
13
|
+
* ```
|
|
14
|
+
* function foo<T>(foo: T): Result<T> {
|
|
15
|
+
* if (foo === ...) {
|
|
16
|
+
* return { ... } as Result<T>; // we have to cast (`as`) to make it work
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
* but this loses some type safety (in hides any error in the `{ ... }`
|
|
21
|
+
*
|
|
22
|
+
* To solve this, this function checks that
|
|
23
|
+
* the input is valid for *some* instance of the generic
|
|
24
|
+
* and then casts it to a more general instance
|
|
25
|
+
*
|
|
26
|
+
* TODO: we can probably delete this once Typescript 5.8 is released
|
|
27
|
+
* https://github.com/microsoft/TypeScript/pull/56941
|
|
28
|
+
*/
|
|
29
|
+
export function narrowResult<
|
|
30
|
+
const General,
|
|
31
|
+
const Input extends General,
|
|
32
|
+
const Output extends General,
|
|
33
|
+
>(result: Input): Output {
|
|
34
|
+
return result as unknown as Output;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function strip0x(str: string): HexStringNo0x {
|
|
38
|
+
if (str.startsWith("0x")) {
|
|
39
|
+
return str.substring(2);
|
|
40
|
+
}
|
|
41
|
+
return str;
|
|
42
|
+
}
|
|
43
|
+
export function add0x(str: string): HexString0x {
|
|
44
|
+
if (str.startsWith("0x")) {
|
|
45
|
+
return str as `0x{string}`;
|
|
46
|
+
}
|
|
47
|
+
return `0x${str}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Typed version of Object.keys()
|
|
52
|
+
*
|
|
53
|
+
* careful: this is dangerous.
|
|
54
|
+
* Only use when you're sure there are no other key types added dynamically
|
|
55
|
+
*/
|
|
56
|
+
export function keysOf<Key extends number | string>(
|
|
57
|
+
obj: Record<Key, unknown>,
|
|
58
|
+
): Key[] {
|
|
59
|
+
return Object.keys(obj) as Key[];
|
|
60
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import base32 from "hi-base32";
|
|
2
|
+
import sha512 from "js-sha512";
|
|
3
|
+
import { Base64 } from "js-base64";
|
|
4
|
+
import { CHECKSUM_ERROR_MSG, MALFORMED_ERROR_MSG } from "./utils.ts";
|
|
5
|
+
|
|
6
|
+
const BLOCK_HASH_BYTE_LENGTH = 32;
|
|
7
|
+
const TX_ID_BYTE_LENGTH = 32;
|
|
8
|
+
const ADDRESS_BYTE_LENGTH = 36;
|
|
9
|
+
const CHECKSUM_BYTE_LENGTH = 4;
|
|
10
|
+
const ADDRESS_LENGTH = 58;
|
|
11
|
+
const HASH_BYTES_LENGTH = 32;
|
|
12
|
+
const PUBLIC_KEY_LENGTH = 32;
|
|
13
|
+
|
|
14
|
+
const MALFORMED_BLOCK_HASH_ERROR_MSG = MALFORMED_ERROR_MSG("block hash");
|
|
15
|
+
const MALFORMED_TX_ID_ERROR_MSG = MALFORMED_ERROR_MSG("tx id");
|
|
16
|
+
const MALFORMED_ADDRESS_ERROR_MSG = MALFORMED_ERROR_MSG("address");
|
|
17
|
+
const CHECKSUM_ADDRESS_ERROR_MSG = CHECKSUM_ERROR_MSG("address");
|
|
18
|
+
|
|
19
|
+
function checksumFromPublicKey(pk: Uint8Array): Uint8Array {
|
|
20
|
+
return Uint8Array.from(
|
|
21
|
+
sha512.sha512_256.array(pk).slice(
|
|
22
|
+
HASH_BYTES_LENGTH - CHECKSUM_BYTE_LENGTH,
|
|
23
|
+
HASH_BYTES_LENGTH,
|
|
24
|
+
),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Taken from https://github.com/algorand/js-algorand-sdk/blob/6973ff583b243ddb0632e91f4c0383021430a789/src/encoding/address.ts#L88
|
|
30
|
+
*
|
|
31
|
+
* For regular addresses,
|
|
32
|
+
* base32 encoding of a 32-byte ed25519 public key and a 4-byte checksum (the last 4 bytes of the SHA-512/256 hash of the public key)
|
|
33
|
+
* For multisig addresses,
|
|
34
|
+
* instead of a 32-byte public key, it’s the hash of some fields that specify the details of the multisig account.
|
|
35
|
+
* more specifically, it’s the SHA-512/256 hash of (a version byte, a threshold, and the public keys of all possible signers).
|
|
36
|
+
* The checksum is computed the same way as for regular addresses.
|
|
37
|
+
*
|
|
38
|
+
* Example address: JY2FRXQP7Q6SYH7QE2HF2XWNE644V6KUH3PYC4SYWPUSEATTDJSNUHMHR4
|
|
39
|
+
*/
|
|
40
|
+
export function verifyAlgorandAddress(address: string): boolean {
|
|
41
|
+
if (typeof address !== "string") {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`${MALFORMED_ADDRESS_ERROR_MSG}: expected string, got ${typeof address}, ${address}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (address.length !== ADDRESS_LENGTH) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`${MALFORMED_ADDRESS_ERROR_MSG}: expected length ${ADDRESS_LENGTH}, got ${address.length}: ${address}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// try to decode
|
|
53
|
+
const decoded = base32.decode.asBytes(address);
|
|
54
|
+
// Sanity check
|
|
55
|
+
if (decoded.length !== ADDRESS_BYTE_LENGTH) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`${MALFORMED_ADDRESS_ERROR_MSG}: expected byte length ${ADDRESS_BYTE_LENGTH}, got ${decoded.length}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Find publickey and checksum
|
|
62
|
+
const pk = new Uint8Array(
|
|
63
|
+
decoded.slice(0, ADDRESS_BYTE_LENGTH - CHECKSUM_BYTE_LENGTH),
|
|
64
|
+
);
|
|
65
|
+
const cs = new Uint8Array(
|
|
66
|
+
decoded.slice(PUBLIC_KEY_LENGTH, ADDRESS_BYTE_LENGTH),
|
|
67
|
+
);
|
|
68
|
+
const checksum = checksumFromPublicKey(pk);
|
|
69
|
+
// Check if the checksum and the address are equal
|
|
70
|
+
if (!arrayEqual(checksum, cs)) throw new Error(CHECKSUM_ADDRESS_ERROR_MSG);
|
|
71
|
+
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Note: strangely, there are 3 common ways to encode Algorand block hashes
|
|
77
|
+
* 1. (rpc) base64
|
|
78
|
+
* 2. (algokit json) blk-${base32} (stripping padding)
|
|
79
|
+
* 3. (msgpck) base32 (unsure about padding)
|
|
80
|
+
*
|
|
81
|
+
* Example block hash: 3KYSrPgi16Cl3iLu3r+GaeZkBBrgNFbMB60jsrJtTdI=
|
|
82
|
+
*/
|
|
83
|
+
export function verifyAlgorandBlockHash(hash: string): boolean {
|
|
84
|
+
// TODO: length check?
|
|
85
|
+
const decoded = Base64.toUint8Array(
|
|
86
|
+
"3KYSrPgi16Cl3iLu3r+GaeZkBBrgNFbMB60jsrJtTdI=",
|
|
87
|
+
);
|
|
88
|
+
// Sanity check
|
|
89
|
+
if (decoded.length !== TX_ID_BYTE_LENGTH) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`${MALFORMED_TX_ID_ERROR_MSG}: expected byte length ${TX_ID_BYTE_LENGTH}, got ${decoded.length}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Taken from https://github.com/algorand/js-algorand-sdk/blob/6973ff583b243ddb0632e91f4c0383021430a789/src/transaction.ts#L1102
|
|
99
|
+
*
|
|
100
|
+
* base32 string encoding (stripping padding) of a "rawTxId" (SHA512/256 digest)
|
|
101
|
+
*
|
|
102
|
+
* Example tx hash: IJFAHCDFJ25GBPPNXXZ67KR2IQMZ5XGMAFWCKXIETKY6WV4OR6YQ
|
|
103
|
+
*/
|
|
104
|
+
export function verifyAlgorandTransactionHash(hash: string): boolean {
|
|
105
|
+
const decoded = base32.decode.asBytes(hash);
|
|
106
|
+
// Sanity check
|
|
107
|
+
if (decoded.length !== BLOCK_HASH_BYTE_LENGTH) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`${MALFORMED_BLOCK_HASH_ERROR_MSG}: expected byte length ${MALFORMED_BLOCK_HASH_ERROR_MSG}, got ${decoded.length}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function arrayEqual<T>(a: ArrayLike<T>, b: ArrayLike<T>): boolean {
|
|
116
|
+
if (a.length !== b.length) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return Array.from(a).every((val, i) => val === b[i]);
|
|
120
|
+
}
|