@moneypot/hub 1.4.4 → 1.4.6
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/dist/src/db/types.d.ts +15 -2
- package/dist/src/hash-chain/db-hash-chain.d.ts +2 -1
- package/dist/src/hash-chain/db-hash-chain.js +6 -3
- package/dist/src/hash-chain/plugins/hub-create-hash-chain.js +4 -22
- package/dist/src/pg-versions/008-outcome-bet-hash.sql +54 -0
- package/dist/src/plugins/hub-make-outcome-bet.d.ts +7 -13
- package/dist/src/plugins/hub-make-outcome-bet.js +16 -11
- package/package.json +1 -1
package/dist/src/db/types.d.ts
CHANGED
|
@@ -119,7 +119,6 @@ export type DbHashChain = {
|
|
|
119
119
|
user_id: string;
|
|
120
120
|
experience_id: string;
|
|
121
121
|
casino_id: string;
|
|
122
|
-
client_seed: string;
|
|
123
122
|
max_iterations: number;
|
|
124
123
|
current_iteration: number;
|
|
125
124
|
active: boolean;
|
|
@@ -136,9 +135,23 @@ export type DbHash = {
|
|
|
136
135
|
hash_chain_id: string;
|
|
137
136
|
iteration: number;
|
|
138
137
|
digest: Uint8Array;
|
|
138
|
+
client_seed: string;
|
|
139
139
|
metadata: Record<string, unknown>;
|
|
140
140
|
};
|
|
141
|
-
export type
|
|
141
|
+
export type DbOutcome = {
|
|
142
142
|
weight: number;
|
|
143
143
|
profit: number;
|
|
144
144
|
};
|
|
145
|
+
export type DbOutcomeBet = {
|
|
146
|
+
id: string;
|
|
147
|
+
hash_id: DbHash["id"];
|
|
148
|
+
kind: string;
|
|
149
|
+
wager: number;
|
|
150
|
+
currency_key: string;
|
|
151
|
+
profit: number;
|
|
152
|
+
user_id: string;
|
|
153
|
+
casino_id: string;
|
|
154
|
+
experience_id: string;
|
|
155
|
+
outcomes: DbOutcome[];
|
|
156
|
+
outcome_idx: number;
|
|
157
|
+
};
|
|
@@ -6,10 +6,11 @@ export declare function dbLockHubHashChain(pgClient: PgClientInTransaction, { us
|
|
|
6
6
|
casinoId: DbCasino["id"];
|
|
7
7
|
hashChainId: DbHashChain["id"];
|
|
8
8
|
}): Promise<DbHashChain | null>;
|
|
9
|
-
export declare function dbInsertHubHash(pgClient: PgClientInTransaction, { hashChainId, kind, digest, iteration, metadata, }: {
|
|
9
|
+
export declare function dbInsertHubHash(pgClient: PgClientInTransaction, { hashChainId, kind, digest, iteration, clientSeed, metadata, }: {
|
|
10
10
|
hashChainId: DbHashChain["id"];
|
|
11
11
|
kind: DbHash["kind"];
|
|
12
12
|
digest: DbHash["digest"];
|
|
13
13
|
iteration: number;
|
|
14
|
+
clientSeed?: string;
|
|
14
15
|
metadata?: DbHash["metadata"];
|
|
15
16
|
}): Promise<DbHash>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DbHashKind, } from "../db/types.js";
|
|
1
2
|
import { exactlyOneRow, maybeOneRow, } from "../db/index.js";
|
|
2
3
|
import { assert } from "tsafe";
|
|
3
4
|
export async function dbLockHubHashChain(pgClient, { userId, experienceId, casinoId, hashChainId, }) {
|
|
@@ -17,18 +18,20 @@ export async function dbLockHubHashChain(pgClient, { userId, experienceId, casin
|
|
|
17
18
|
.then(maybeOneRow)
|
|
18
19
|
.then((row) => row ?? null);
|
|
19
20
|
}
|
|
20
|
-
export async function dbInsertHubHash(pgClient, { hashChainId, kind, digest, iteration, metadata = {}, }) {
|
|
21
|
+
export async function dbInsertHubHash(pgClient, { hashChainId, kind, digest, iteration, clientSeed, metadata = {}, }) {
|
|
21
22
|
assert(pgClient._inTransaction, "dbInsertHash must be called in a transaction");
|
|
23
|
+
assert(kind === DbHashKind.INTERMEDIATE && typeof clientSeed === "string", "clientSeed must be provided for INTERMEDIATE hashes");
|
|
22
24
|
return pgClient
|
|
23
25
|
.query(`
|
|
24
|
-
INSERT INTO hub.hash (hash_chain_id, kind, digest, iteration, metadata)
|
|
25
|
-
VALUES ($1, $2, $3, $4, $5)
|
|
26
|
+
INSERT INTO hub.hash (hash_chain_id, kind, digest, iteration, client_seed, metadata)
|
|
27
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
26
28
|
RETURNING *
|
|
27
29
|
`, [
|
|
28
30
|
hashChainId,
|
|
29
31
|
kind,
|
|
30
32
|
digest,
|
|
31
33
|
iteration,
|
|
34
|
+
clientSeed,
|
|
32
35
|
metadata,
|
|
33
36
|
])
|
|
34
37
|
.then(exactlyOneRow);
|
|
@@ -1,47 +1,31 @@
|
|
|
1
1
|
import { context, object, sideEffect } from "@moneypot/hub/grafast";
|
|
2
2
|
import { gql, makeExtendSchemaPlugin } from "@moneypot/hub/graphile";
|
|
3
3
|
import { GraphQLError } from "graphql";
|
|
4
|
-
import { z } from "zod";
|
|
5
4
|
import { PgAdvisoryLock } from "../../pg-advisory-lock.js";
|
|
6
5
|
import { exactlyOneRow, superuserPool, withPgPoolTransaction, } from "@moneypot/hub/db";
|
|
7
6
|
import * as HashCommon from "../get-hash.js";
|
|
8
7
|
import { DbHashKind } from "../../db/types.js";
|
|
9
8
|
import * as config from "../../config.js";
|
|
10
|
-
const InputSchema = z.object({
|
|
11
|
-
clientSeed: z.string(),
|
|
12
|
-
});
|
|
13
9
|
export const HubCreateHashChainPlugin = makeExtendSchemaPlugin((build) => {
|
|
14
10
|
const hashChainTable = build.input.pgRegistry.pgResources.hub_hash_chain;
|
|
15
11
|
return {
|
|
16
12
|
typeDefs: gql `
|
|
17
|
-
input HubCreateHashChainInput {
|
|
18
|
-
clientSeed: String!
|
|
19
|
-
}
|
|
20
|
-
|
|
21
13
|
type HubCreateHashChainPayload {
|
|
22
14
|
hashChain: HubHashChain!
|
|
23
15
|
}
|
|
24
16
|
|
|
25
17
|
extend type Mutation {
|
|
26
|
-
hubCreateHashChain
|
|
27
|
-
input: HubCreateHashChainInput!
|
|
28
|
-
): HubCreateHashChainPayload!
|
|
18
|
+
hubCreateHashChain: HubCreateHashChainPayload!
|
|
29
19
|
}
|
|
30
20
|
`,
|
|
31
21
|
plans: {
|
|
32
22
|
Mutation: {
|
|
33
|
-
hubCreateHashChain: (
|
|
23
|
+
hubCreateHashChain: () => {
|
|
34
24
|
const $identity = context().get("identity");
|
|
35
|
-
const $hashChainId = sideEffect([$
|
|
25
|
+
const $hashChainId = sideEffect([$identity], ([identity]) => {
|
|
36
26
|
if (identity?.kind !== "user") {
|
|
37
27
|
throw new GraphQLError("Unauthorized");
|
|
38
28
|
}
|
|
39
|
-
const result = InputSchema.safeParse(rawInput);
|
|
40
|
-
if (!result.success) {
|
|
41
|
-
const message = result.error.errors[0].message;
|
|
42
|
-
throw new GraphQLError(message);
|
|
43
|
-
}
|
|
44
|
-
const { clientSeed } = result.data;
|
|
45
29
|
return withPgPoolTransaction(superuserPool, async (pgClient) => {
|
|
46
30
|
await PgAdvisoryLock.forNewHashChain(pgClient, {
|
|
47
31
|
userId: identity.session.user_id,
|
|
@@ -66,18 +50,16 @@ export const HubCreateHashChainPlugin = makeExtendSchemaPlugin((build) => {
|
|
|
66
50
|
user_id,
|
|
67
51
|
experience_id,
|
|
68
52
|
casino_id,
|
|
69
|
-
client_seed,
|
|
70
53
|
active,
|
|
71
54
|
max_iteration,
|
|
72
55
|
current_iteration
|
|
73
56
|
)
|
|
74
|
-
VALUES ($1, $2, $3,
|
|
57
|
+
VALUES ($1, $2, $3, true, $4, $4)
|
|
75
58
|
RETURNING *
|
|
76
59
|
`, [
|
|
77
60
|
identity.session.user_id,
|
|
78
61
|
identity.session.experience_id,
|
|
79
62
|
identity.session.casino_id,
|
|
80
|
-
clientSeed,
|
|
81
63
|
config.HASHCHAINSERVER_MAX_ITERATIONS,
|
|
82
64
|
])
|
|
83
65
|
.then(exactlyOneRow);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
-- Migration: Add hash_id to hub.outcome_bet table
|
|
2
|
+
|
|
3
|
+
-- Step 1: Add the hash_id column (nullable initially)
|
|
4
|
+
ALTER TABLE hub.outcome_bet
|
|
5
|
+
ADD COLUMN hash_id uuid REFERENCES hub.hash(id);
|
|
6
|
+
|
|
7
|
+
-- Step 2: Index
|
|
8
|
+
CREATE INDEX outcome_bet_hash_id_idx ON hub.outcome_bet(hash_id);
|
|
9
|
+
|
|
10
|
+
-- Step 3: Update existing rows to set hash_id based on hash_chain_id
|
|
11
|
+
-- This uses a window function to assign row numbers within each hash_chain_id group
|
|
12
|
+
-- ordered by the bet creation time (id)
|
|
13
|
+
WITH bet_iterations AS (
|
|
14
|
+
SELECT
|
|
15
|
+
ob.id,
|
|
16
|
+
ob.hash_chain_id,
|
|
17
|
+
hc.max_iteration,
|
|
18
|
+
-- Row number starting from 1 for each hash_chain_id group
|
|
19
|
+
-- Ordered by id (which is uuid v7, so it's time-ordered)
|
|
20
|
+
ROW_NUMBER() OVER (PARTITION BY ob.hash_chain_id ORDER BY ob.id) as bet_sequence
|
|
21
|
+
FROM hub.outcome_bet ob
|
|
22
|
+
JOIN hub.hash_chain hc ON hc.id = ob.hash_chain_id
|
|
23
|
+
),
|
|
24
|
+
hash_mapping AS (
|
|
25
|
+
SELECT
|
|
26
|
+
bi.id as bet_id,
|
|
27
|
+
h.id as hash_id
|
|
28
|
+
FROM bet_iterations bi
|
|
29
|
+
JOIN hub.hash h ON
|
|
30
|
+
h.hash_chain_id = bi.hash_chain_id AND
|
|
31
|
+
-- Calculate the iteration: max_iteration - bet_sequence
|
|
32
|
+
h.iteration = bi.max_iteration - bi.bet_sequence
|
|
33
|
+
WHERE h.kind = 'INTERMEDIATE'
|
|
34
|
+
)
|
|
35
|
+
UPDATE hub.outcome_bet ob
|
|
36
|
+
SET hash_id = hm.hash_id
|
|
37
|
+
FROM hash_mapping hm
|
|
38
|
+
WHERE ob.id = hm.bet_id;
|
|
39
|
+
|
|
40
|
+
-- Step 4: Make hash_id NOT NULL after populating it
|
|
41
|
+
ALTER TABLE hub.outcome_bet ALTER COLUMN hash_id SET NOT NULL;
|
|
42
|
+
|
|
43
|
+
-- Step 5: Drop hash chain id
|
|
44
|
+
ALTER TABLE hub.outcome_bet DROP COLUMN hash_chain_id;
|
|
45
|
+
|
|
46
|
+
-- Step 5: Add client_seed column to hash table
|
|
47
|
+
ALTER TABLE hub.hash ADD COLUMN client_seed text null;
|
|
48
|
+
|
|
49
|
+
-- Step 6: Update hash table with client_seed from hash_chain for each outcome_bet
|
|
50
|
+
UPDATE hub.hash h
|
|
51
|
+
SET client_seed = hc.client_seed
|
|
52
|
+
FROM hub.outcome_bet ob
|
|
53
|
+
JOIN hub.hash_chain hc ON hc.id = ob.hash_chain_id
|
|
54
|
+
WHERE h.id = ob.hash_id;
|
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { DbOutcome } from "../db/index.js";
|
|
2
3
|
import { Result } from "../util.js";
|
|
3
|
-
declare const OutcomeSchema: z.ZodObject<{
|
|
4
|
-
weight: z.ZodNumber;
|
|
5
|
-
profit: z.ZodNumber;
|
|
6
|
-
}, "strict", z.ZodTypeAny, {
|
|
7
|
-
profit: number;
|
|
8
|
-
weight: number;
|
|
9
|
-
}, {
|
|
10
|
-
profit: number;
|
|
11
|
-
weight: number;
|
|
12
|
-
}>;
|
|
13
4
|
declare const InputSchema: z.ZodObject<{
|
|
14
5
|
kind: z.ZodString;
|
|
6
|
+
clientSeed: z.ZodString;
|
|
15
7
|
wager: z.ZodNumber;
|
|
16
8
|
currency: z.ZodString;
|
|
17
9
|
outcomes: z.ZodEffects<z.ZodEffects<z.ZodArray<z.ZodObject<{
|
|
@@ -42,6 +34,7 @@ declare const InputSchema: z.ZodObject<{
|
|
|
42
34
|
currency: string;
|
|
43
35
|
hashChainId: string;
|
|
44
36
|
kind: string;
|
|
37
|
+
clientSeed: string;
|
|
45
38
|
wager: number;
|
|
46
39
|
outcomes: {
|
|
47
40
|
profit: number;
|
|
@@ -52,6 +45,7 @@ declare const InputSchema: z.ZodObject<{
|
|
|
52
45
|
currency: string;
|
|
53
46
|
hashChainId: string;
|
|
54
47
|
kind: string;
|
|
48
|
+
clientSeed: string;
|
|
55
49
|
wager: number;
|
|
56
50
|
outcomes: {
|
|
57
51
|
profit: number;
|
|
@@ -60,20 +54,20 @@ declare const InputSchema: z.ZodObject<{
|
|
|
60
54
|
metadata?: Record<string, unknown> | undefined;
|
|
61
55
|
}>;
|
|
62
56
|
type Input = z.infer<typeof InputSchema>;
|
|
63
|
-
type Outcome = z.infer<typeof OutcomeSchema>;
|
|
64
57
|
type Metadata = NonNullable<Input["metadata"]>;
|
|
65
58
|
type FinalizeMetadataData = {
|
|
66
59
|
wager: number;
|
|
67
60
|
currencyKey: string;
|
|
68
61
|
clientSeed: string;
|
|
69
62
|
hash: Uint8Array;
|
|
70
|
-
outcomes:
|
|
63
|
+
outcomes: DbOutcome[];
|
|
71
64
|
outcomeIdx: number;
|
|
65
|
+
roll: number;
|
|
72
66
|
};
|
|
73
67
|
export type OutcomeBetConfig = {
|
|
74
68
|
houseEdge: number;
|
|
75
69
|
saveOutcomes: boolean;
|
|
76
|
-
allowLossBeyondWager
|
|
70
|
+
allowLossBeyondWager?: boolean;
|
|
77
71
|
initializeMetadataFromUntrustedUserInput?: (input: Input) => Result<Metadata, string>;
|
|
78
72
|
finalizeMetadata?: (validatedMetadata: Metadata, data: FinalizeMetadataData) => Metadata;
|
|
79
73
|
};
|
|
@@ -27,6 +27,7 @@ const OutcomeSchema = z
|
|
|
27
27
|
const InputSchema = z
|
|
28
28
|
.object({
|
|
29
29
|
kind: z.string(),
|
|
30
|
+
clientSeed: z.string(),
|
|
30
31
|
wager: z
|
|
31
32
|
.number()
|
|
32
33
|
.int("Wager must be an integer")
|
|
@@ -53,7 +54,7 @@ const BetConfigsSchema = z.record(BetKindSchema, z.object({
|
|
|
53
54
|
.gte(0, "House edge must be >= 0")
|
|
54
55
|
.lte(1, "House edge must be <= 1"),
|
|
55
56
|
saveOutcomes: z.boolean(),
|
|
56
|
-
allowLossBeyondWager: z.boolean().
|
|
57
|
+
allowLossBeyondWager: z.boolean().default(false),
|
|
57
58
|
initializeMetadataFromUntrustedUserInput: z
|
|
58
59
|
.function()
|
|
59
60
|
.optional(),
|
|
@@ -77,6 +78,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
77
78
|
currency: String!
|
|
78
79
|
outcomes: [HubOutcomeInput!]!
|
|
79
80
|
hashChainId: UUID!
|
|
81
|
+
clientSeed: String!
|
|
80
82
|
metadata: JSON
|
|
81
83
|
}
|
|
82
84
|
|
|
@@ -230,11 +232,12 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
230
232
|
throw new Error(`Unknown bet hash result: ${_exhaustiveCheck}`);
|
|
231
233
|
}
|
|
232
234
|
}
|
|
233
|
-
await dbInsertHubHash(pgClient, {
|
|
235
|
+
const dbHash = await dbInsertHubHash(pgClient, {
|
|
234
236
|
hashChainId: dbHashChain.id,
|
|
235
237
|
kind: DbHashKind.INTERMEDIATE,
|
|
236
238
|
digest: betHashResult.hash,
|
|
237
239
|
iteration: betHashIteration,
|
|
240
|
+
clientSeed: input.clientSeed,
|
|
238
241
|
});
|
|
239
242
|
const result = await pgClient.query(`
|
|
240
243
|
UPDATE hub.hash_chain
|
|
@@ -246,13 +249,13 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
246
249
|
}
|
|
247
250
|
const finalHash = (() => {
|
|
248
251
|
const serverHash = betHashResult.hash;
|
|
249
|
-
const clientSeed =
|
|
252
|
+
const clientSeed = input.clientSeed;
|
|
250
253
|
const finalHash = createHmac("sha256", serverHash)
|
|
251
254
|
.update(clientSeed)
|
|
252
255
|
.digest();
|
|
253
256
|
return finalHash;
|
|
254
257
|
})();
|
|
255
|
-
const { outcome, outcomeIdx } = pickRandomOutcome({
|
|
258
|
+
const { outcome, outcomeIdx, roll } = pickRandomOutcome({
|
|
256
259
|
outcomes: input.outcomes,
|
|
257
260
|
hash: finalHash,
|
|
258
261
|
});
|
|
@@ -293,10 +296,11 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
293
296
|
const immutableData = structuredClone({
|
|
294
297
|
wager: input.wager,
|
|
295
298
|
currencyKey: dbCurrency.key,
|
|
296
|
-
clientSeed:
|
|
299
|
+
clientSeed: input.clientSeed,
|
|
297
300
|
hash: betHashResult.hash,
|
|
298
301
|
outcomes: input.outcomes,
|
|
299
302
|
outcomeIdx,
|
|
303
|
+
roll,
|
|
300
304
|
});
|
|
301
305
|
const finalizedMetadata = betConfig.finalizeMetadata
|
|
302
306
|
? betConfig.finalizeMetadata(initializedMetadata, immutableData)
|
|
@@ -306,7 +310,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
306
310
|
wager: input.wager,
|
|
307
311
|
profit: outcome.profit,
|
|
308
312
|
currency_key: dbCurrency.key,
|
|
309
|
-
|
|
313
|
+
hash_id: dbHash.id,
|
|
310
314
|
user_id: session.user_id,
|
|
311
315
|
casino_id: session.casino_id,
|
|
312
316
|
experience_id: session.experience_id,
|
|
@@ -328,7 +332,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
328
332
|
user_id,
|
|
329
333
|
casino_id,
|
|
330
334
|
experience_id,
|
|
331
|
-
|
|
335
|
+
hash_id,
|
|
332
336
|
kind,
|
|
333
337
|
currency_key,
|
|
334
338
|
wager,
|
|
@@ -344,7 +348,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
344
348
|
newBet.user_id,
|
|
345
349
|
newBet.casino_id,
|
|
346
350
|
newBet.experience_id,
|
|
347
|
-
newBet.
|
|
351
|
+
newBet.hash_id,
|
|
348
352
|
newBet.kind,
|
|
349
353
|
newBet.currency_key,
|
|
350
354
|
newBet.wager,
|
|
@@ -402,18 +406,19 @@ function pickRandomOutcome({ outcomes, hash, }) {
|
|
|
402
406
|
}));
|
|
403
407
|
const totalProb = outcomesWithProbability.reduce((sum, outcome) => sum + outcome.probability, 0);
|
|
404
408
|
assert(Math.abs(totalProb - 1.0) < FLOAT_EPSILON, "Probabilities must sum to ~1");
|
|
405
|
-
const
|
|
409
|
+
const roll = normalizeHash(hash);
|
|
406
410
|
let cumulativeProb = 0;
|
|
407
411
|
for (let i = 0; i < outcomesWithProbability.length; i++) {
|
|
408
412
|
const outcome = outcomesWithProbability[i];
|
|
409
413
|
cumulativeProb += outcome.probability;
|
|
410
|
-
if (
|
|
411
|
-
return { outcome, outcomeIdx: i };
|
|
414
|
+
if (roll <= cumulativeProb) {
|
|
415
|
+
return { outcome, outcomeIdx: i, roll };
|
|
412
416
|
}
|
|
413
417
|
}
|
|
414
418
|
return {
|
|
415
419
|
outcome: outcomes[outcomes.length - 1],
|
|
416
420
|
outcomeIdx: outcomes.length - 1,
|
|
421
|
+
roll,
|
|
417
422
|
};
|
|
418
423
|
}
|
|
419
424
|
async function finishHashChainInBackground({ hashChainId, }) {
|