@suigar/mcp 0.1.0 → 0.2.0-beta.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/LICENSE +201 -0
  3. package/README.md +79 -110
  4. package/dist/app/index.html +181 -0
  5. package/dist/bin.mjs +3 -7
  6. package/dist/bin.mjs.map +1 -0
  7. package/dist/client.d.mts +41 -11
  8. package/dist/client.d.mts.map +1 -0
  9. package/dist/client.mjs +107 -34
  10. package/dist/client.mjs.map +1 -0
  11. package/dist/dry-run.mjs +163 -0
  12. package/dist/dry-run.mjs.map +1 -0
  13. package/dist/format.mjs +24 -0
  14. package/dist/format.mjs.map +1 -0
  15. package/dist/index.d.mts +5 -9
  16. package/dist/index.mjs +4 -8
  17. package/dist/schemas.d.mts +642 -0
  18. package/dist/schemas.d.mts.map +1 -0
  19. package/dist/schemas.mjs +113 -0
  20. package/dist/schemas.mjs.map +1 -0
  21. package/dist/server.d.mts +21 -69
  22. package/dist/server.d.mts.map +1 -0
  23. package/dist/server.mjs +172 -411
  24. package/dist/server.mjs.map +1 -0
  25. package/dist/tools.d.mts +14 -155
  26. package/dist/tools.d.mts.map +1 -0
  27. package/dist/tools.mjs +297 -553
  28. package/dist/tools.mjs.map +1 -0
  29. package/dist/types.d.mts +109 -83
  30. package/dist/types.d.mts.map +1 -0
  31. package/package.json +83 -61
  32. package/dist/bin.cjs +0 -11
  33. package/dist/bin.d.cts +0 -1
  34. package/dist/client.cjs +0 -46
  35. package/dist/client.d.cts +0 -17
  36. package/dist/coin.cjs +0 -86
  37. package/dist/coin.d.cts +0 -35
  38. package/dist/coin.d.mts +0 -35
  39. package/dist/coin.mjs +0 -86
  40. package/dist/config.cjs +0 -183
  41. package/dist/config.d.cts +0 -15
  42. package/dist/config.d.mts +0 -15
  43. package/dist/config.mjs +0 -174
  44. package/dist/index.cjs +0 -53
  45. package/dist/index.d.cts +0 -10
  46. package/dist/mcp-support.cjs +0 -62
  47. package/dist/mcp-support.d.cts +0 -16
  48. package/dist/mcp-support.d.mts +0 -16
  49. package/dist/mcp-support.mjs +0 -60
  50. package/dist/metadata.cjs +0 -51
  51. package/dist/metadata.d.cts +0 -52
  52. package/dist/metadata.d.mts +0 -52
  53. package/dist/metadata.mjs +0 -47
  54. package/dist/server.cjs +0 -433
  55. package/dist/server.d.cts +0 -73
  56. package/dist/tools.cjs +0 -617
  57. package/dist/tools.d.cts +0 -158
  58. package/dist/transactions.cjs +0 -294
  59. package/dist/transactions.d.cts +0 -40
  60. package/dist/transactions.d.mts +0 -40
  61. package/dist/transactions.mjs +0 -286
  62. package/dist/types.d.cts +0 -111
  63. package/node_modules/@suigar/currency-registry/dist/index.cjs +0 -121
  64. package/node_modules/@suigar/currency-registry/dist/index.d.cts +0 -50
  65. package/node_modules/@suigar/currency-registry/dist/index.d.mts +0 -50
  66. package/node_modules/@suigar/currency-registry/dist/index.mjs +0 -110
  67. package/node_modules/@suigar/currency-registry/package.json +0 -31
  68. package/node_modules/@suigar/game-registry/dist/index.cjs +0 -310
  69. package/node_modules/@suigar/game-registry/dist/index.d.cts +0 -65
  70. package/node_modules/@suigar/game-registry/dist/index.d.mts +0 -65
  71. package/node_modules/@suigar/game-registry/dist/index.mjs +0 -292
  72. package/node_modules/@suigar/game-registry/package.json +0 -31
  73. package/node_modules/@suigar/sui-rpc-pool/dist/index.cjs +0 -45590
  74. package/node_modules/@suigar/sui-rpc-pool/dist/index.d.cts +0 -465
  75. package/node_modules/@suigar/sui-rpc-pool/dist/index.d.mts +0 -465
  76. package/node_modules/@suigar/sui-rpc-pool/dist/index.mjs +0 -45570
  77. package/node_modules/@suigar/sui-rpc-pool/package.json +0 -31
package/dist/tools.mjs CHANGED
@@ -1,608 +1,352 @@
1
- import { getRequiredConfigKeysForGame, inspectResolvedConfig, resolveGamePackageId, resolveSuigarConfig } from "./config.mjs";
2
- import { getCurrencyInfo, getGameMetadata, readConfigMetadata } from "./metadata.mjs";
3
- import { createReadOnlyClientBundle, dryRunTransaction, serializeTransactionToBase64 } from "./client.mjs";
4
- import { buildCoinflipTransaction, buildLimboTransaction, buildPlinkoTransaction, buildPvpCoinflipCancelTransaction, buildPvpCoinflipCreateTransaction, buildPvpCoinflipJoinTransaction, buildRangeTransaction, buildWheelTransaction, summarizeTransaction } from "./transactions.mjs";
5
- import { normalizeCoinType } from "@suigar/currency-registry";
6
- import { SuiGrpcClient } from "@mysten/sui/grpc";
7
- import { suigar } from "@suigar/sdk";
1
+ import { buildTransactionResult, createSuigarClient, resolveDefaultCoinType, resolveOwnerAddress } from "./client.mjs";
2
+ import { GAMES } from "@suigar/sdk/games";
8
3
  //#region src/tools.ts
9
- const asRecord = (value) => value && typeof value === "object" ? value : {};
10
- const asString = (value, fieldName, fallback) => {
4
+ const GAME_LABELS = {
5
+ coinflip: "Coinflip",
6
+ limbo: "Limbo",
7
+ plinko: "Plinko",
8
+ range: "Range",
9
+ wheel: "Wheel",
10
+ "pvp-coinflip": "PvP Coinflip"
11
+ };
12
+ const GAME_TO_PACKAGE_KEY = {
13
+ coinflip: "coinflip",
14
+ limbo: "limbo",
15
+ plinko: "plinko",
16
+ range: "range",
17
+ wheel: "wheel",
18
+ "pvp-coinflip": "pvpCoinflip"
19
+ };
20
+ const GAME_TO_TOOLS = {
21
+ coinflip: ["build_coinflip_transaction"],
22
+ limbo: ["build_limbo_transaction"],
23
+ plinko: ["build_plinko_transaction"],
24
+ range: ["build_range_transaction"],
25
+ wheel: ["build_wheel_transaction"],
26
+ "pvp-coinflip": [
27
+ "build_pvp_coinflip_create_transaction",
28
+ "build_pvp_coinflip_join_transaction",
29
+ "build_pvp_coinflip_cancel_transaction"
30
+ ]
31
+ };
32
+ const json = (value) => JSON.stringify(value, (_key, item) => typeof item === "bigint" ? item.toString() : item, 2);
33
+ const asTextResponse = (structuredContent) => ({
34
+ content: [{
35
+ type: "text",
36
+ text: json(structuredContent)
37
+ }],
38
+ structuredContent
39
+ });
40
+ const currencyAmountPattern = /^(?:\d+|\d+\.\d+|\.\d+)$/u;
41
+ const coinMetadataForAmount = (config, coinType) => {
42
+ const resolvedCoinType = resolveDefaultCoinType(config, coinType);
43
+ const coin = Object.values(config.sdk.coins).find((metadata) => resolveDefaultCoinType(config, metadata.coinType) === resolvedCoinType);
44
+ if (!coin) throw new RangeError(`Unable to resolve decimals for coin type ${resolvedCoinType}. Add the coin to config.coins before using currency-denominated amounts.`);
45
+ return {
46
+ coinType: resolvedCoinType,
47
+ decimals: coin.decimals
48
+ };
49
+ };
50
+ const toCurrencyAmountText = (value, fieldName) => {
51
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) return String(value);
52
+ if (typeof value === "string" && currencyAmountPattern.test(value.trim())) return value.trim();
53
+ throw new TypeError(`Missing or invalid ${fieldName}. Provide a non-negative currency amount such as 1, 2, or 1.5.`);
54
+ };
55
+ const toBaseUnits = (value, fieldName, decimals) => {
56
+ const [rawWhole, rawFraction = ""] = toCurrencyAmountText(value, fieldName).split(".");
57
+ const whole = rawWhole === "" ? "0" : rawWhole;
58
+ const overflow = rawFraction.slice(decimals);
59
+ if (/[^0]/u.test(overflow)) throw new RangeError(`${fieldName} has more fractional digits than the configured coin decimals (${decimals}).`);
60
+ const fraction = rawFraction.slice(0, decimals).padEnd(decimals, "0");
61
+ return BigInt(whole) * 10n ** BigInt(decimals) + BigInt(fraction || "0");
62
+ };
63
+ const toPositiveInteger = (value, fieldName) => {
64
+ if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) return value;
65
+ if (typeof value === "string" && /^[1-9]\d*$/u.test(value)) return BigInt(value);
66
+ throw new TypeError(`Missing or invalid ${fieldName}. Provide a positive integer.`);
67
+ };
68
+ const requireString = (value, fieldName) => {
11
69
  if (typeof value === "string" && value.trim()) return value.trim();
12
- if (fallback !== void 0) return fallback;
13
- throw new Error(`Missing required string field: ${fieldName}`);
70
+ throw new TypeError(`Missing required field: ${fieldName}.`);
71
+ };
72
+ const requireNumber = (value, fieldName) => {
73
+ if (typeof value === "number" && Number.isFinite(value)) return value;
74
+ throw new TypeError(`Missing or invalid numeric field: ${fieldName}.`);
14
75
  };
15
- const asOptionalString = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
16
- const asNumber = (value, fieldName) => {
17
- const numeric = typeof value === "bigint" ? Number(value) : Number(value);
18
- if (!Number.isFinite(numeric)) throw new Error(`Missing or invalid numeric field: ${fieldName}`);
19
- return numeric;
76
+ const getMode = (mode) => mode ?? "build";
77
+ const getConfigInput = (input) => ({
78
+ network: input.network,
79
+ providerUrl: input.providerUrl,
80
+ config: input.config,
81
+ partner: input.partner
82
+ });
83
+ const supportedGames = () => GAMES.map((id) => ({
84
+ id,
85
+ label: GAME_LABELS[id],
86
+ tools: [...GAME_TO_TOOLS[id]]
87
+ }));
88
+ const getPackageId = (config, game) => config.sdk.packageIds[GAME_TO_PACKAGE_KEY[game]];
89
+ const getTarget = (config, game, action) => {
90
+ const packageId = getPackageId(config, game);
91
+ if (game === "pvp-coinflip") return `${packageId}::pvp_coinflip::${action === "join" ? "join_game" : action === "cancel" ? "cancel_game" : "create_game"}`;
92
+ return `${packageId}::${game}::play`;
20
93
  };
21
- const asBoolean = (value, fallback = false) => typeof value === "boolean" ? value : fallback;
22
- const asStringArray = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
23
- const asMode = (value) => value === "dry-run" || value === "read-only" || value === "build" ? value : "build";
24
- const SDK_BUILD_NETWORKS = /* @__PURE__ */ new Set(["mainnet", "testnet"]);
25
- const SDK_UNSUPPORTED_CONFIG_OVERRIDE_KEYS = [
26
- "suigarPackageId",
27
- "coinflipPackageId",
28
- "pvpCoinflipPackageId",
29
- "plinkoPackageId",
30
- "limboPackageId",
31
- "rangePackageId",
32
- "wheelPackageId",
33
- "sweethouseId",
34
- "suiPythPriceInfoObjectId",
35
- "usdcPythPriceInfoObjectId",
36
- "pvpCoinflipRegistryId"
37
- ];
38
- const buildReadOnlyPlan = ({ game, config, coinType, targetSuffix, notes }) => {
39
- const resolvedConfig = resolveSuigarConfig(config);
40
- const packageId = resolveGamePackageId(game, resolvedConfig);
94
+ const readOnlyPlan = ({ input, game, action, requiredInputs, notes }) => {
95
+ const { config } = createSuigarClient(getConfigInput(input));
96
+ const coinType = resolveDefaultCoinType(config, input.coinType);
41
97
  return {
42
98
  mode: "read-only",
43
- network: resolvedConfig.network,
99
+ network: config.network,
44
100
  game,
45
- config: inspectResolvedConfig(resolvedConfig),
101
+ action,
102
+ config,
46
103
  plan: {
47
- target: packageId ? `${packageId}${targetSuffix}` : null,
48
- typeArguments: coinType ? [normalizeCoinType(coinType)] : [],
49
- requiredConfigKeys: getRequiredConfigKeysForGame(game),
104
+ target: getTarget(config, game, action),
105
+ typeArguments: [coinType],
106
+ requiredInputs,
50
107
  notes
51
108
  }
52
109
  };
53
110
  };
54
- const buildTransactionResult = async ({ mode, config, transaction, context }) => {
55
- const bundle = createReadOnlyClientBundle(config);
56
- const summary = summarizeTransaction(transaction, context);
57
- if (mode === "dry-run") return {
58
- mode,
59
- network: bundle.config.network,
60
- config: inspectResolvedConfig(bundle.config),
61
- summary,
62
- dryRun: await dryRunTransaction(transaction, bundle.config)
63
- };
111
+ const commonOptions = async (input, bundle) => {
64
112
  return {
65
- mode,
66
- network: bundle.config.network,
67
- config: inspectResolvedConfig(bundle.config),
68
- summary,
69
- transactionBytesBase64: await serializeTransactionToBase64(transaction, bundle.rawClient)
113
+ owner: await resolveOwnerAddress(requireString(input.owner, "owner"), bundle),
114
+ coinType: resolveDefaultCoinType(bundle.config, input.coinType),
115
+ metadata: input.metadata,
116
+ gasBudget: input.gasBudget,
117
+ useGasCoin: input.useGasCoin
70
118
  };
71
119
  };
72
- const extractConfigInput = (input) => {
73
- const nestedConfig = asRecord(input.config);
120
+ const stakeOptions = async (input, bundle) => {
121
+ const { decimals } = coinMetadataForAmount(bundle.config, input.coinType);
74
122
  return {
75
- ...nestedConfig,
76
- network: asOptionalString(input.network) ?? asOptionalString(nestedConfig.network),
77
- providerUrl: asOptionalString(input.providerUrl) ?? asOptionalString(nestedConfig.providerUrl),
78
- graphqlUrl: asOptionalString(input.graphqlUrl) ?? asOptionalString(nestedConfig.graphqlUrl)
79
- };
80
- };
81
- const extractPartner = (input) => asOptionalString(input.partner) ?? asOptionalString(asRecord(input.config).partner);
82
- const hasSdkUnsupportedConfigOverrides = (input) => {
83
- const mergedInput = {
84
- ...asRecord(input.config),
85
- ...input
123
+ ...await commonOptions(input, bundle),
124
+ stake: toBaseUnits(input.stake, "stake", decimals),
125
+ ...input.cashStake == null ? {} : { cashStake: toBaseUnits(input.cashStake, "cashStake", decimals) },
126
+ ...input.betCount == null ? {} : { betCount: toPositiveInteger(input.betCount, "betCount") }
86
127
  };
87
- return SDK_UNSUPPORTED_CONFIG_OVERRIDE_KEYS.some((key) => asOptionalString(mergedInput[key]));
88
128
  };
89
- const buildCommonSdkOptions = (record, coinType) => {
90
- const owner = asString(record.owner, "owner");
91
- return {
92
- owner,
93
- playerAddress: owner,
94
- coinType,
95
- metadata: asRecord(record.metadata),
96
- allowGasCoinShortcut: asBoolean(record.allowGasCoinShortcut, true),
97
- ...record.gasBudget == null ? {} : { gasBudget: asNumber(record.gasBudget, "gasBudget") }
98
- };
99
- };
100
- const buildStakeSdkOptions = (record, coinType) => ({
101
- ...buildCommonSdkOptions(record, coinType),
102
- stake: asNumber(record.stake, "stake"),
103
- ...record.cashStake == null ? {} : { cashStake: asNumber(record.cashStake, "cashStake") },
104
- ...record.betCount == null ? {} : { betCount: asNumber(record.betCount, "betCount") }
105
- });
106
- const tryBuildSdkTransaction = ({ record, config, coinType, game, pvpAction }) => {
107
- if (asStringArray(record.coinObjectIds).length > 0 || hasSdkUnsupportedConfigOverrides(record)) return null;
108
- const resolvedConfig = resolveSuigarConfig(config);
109
- if (!SDK_BUILD_NETWORKS.has(resolvedConfig.network)) return null;
110
- const partner = extractPartner(record);
111
- const client = new SuiGrpcClient({
112
- baseUrl: resolvedConfig.providerUrl,
113
- network: resolvedConfig.network
114
- }).$extend(suigar(partner ? { partner } : void 0));
115
- try {
116
- switch (game) {
117
- case "coinflip": return client.suigar.tx.createBetTransaction("coinflip", {
118
- ...buildStakeSdkOptions(record, coinType),
119
- side: asString(record.side, "side")
120
- });
121
- case "limbo": return client.suigar.tx.createBetTransaction("limbo", {
122
- ...buildStakeSdkOptions(record, coinType),
123
- targetMultiplier: asNumber(record.targetMultiplier, "targetMultiplier")
124
- });
125
- case "plinko": return client.suigar.tx.createBetTransaction("plinko", {
126
- ...buildStakeSdkOptions(record, coinType),
127
- configId: asNumber(record.configId, "configId")
128
- });
129
- case "wheel": return client.suigar.tx.createBetTransaction("wheel", {
130
- ...buildStakeSdkOptions(record, coinType),
131
- configId: asNumber(record.configId, "configId")
132
- });
133
- case "range": return client.suigar.tx.createBetTransaction("range", {
134
- ...buildStakeSdkOptions(record, coinType),
135
- leftPoint: asNumber(record.leftPoint, "leftPoint"),
136
- rightPoint: asNumber(record.rightPoint, "rightPoint"),
137
- outOfRange: asBoolean(record.outOfRange)
138
- });
139
- case "pvp-coinflip":
140
- if (pvpAction === "join" || pvpAction === "cancel") return client.suigar.tx.createPvPCoinflipTransaction(pvpAction, {
141
- ...buildCommonSdkOptions(record, coinType),
142
- config: client.suigar.getConfig(),
143
- gameId: asString(record.gameId, "gameId")
144
- });
145
- return client.suigar.tx.createPvPCoinflipTransaction("create", {
146
- ...buildCommonSdkOptions(record, coinType),
147
- config: client.suigar.getConfig(),
148
- stake: asNumber(record.stake, "stake"),
149
- side: asString(record.creatorSide, "creatorSide"),
150
- isPrivate: asBoolean(record.isPrivate)
151
- });
152
- }
153
- } catch {
154
- return null;
155
- }
156
- };
157
- const readConfigTool = async (input = {}) => {
158
- return readConfigMetadata(extractConfigInput(asRecord(input)));
159
- };
160
- const readGameMetadataTool = async (input = {}) => {
161
- const record = asRecord(input);
162
- const config = extractConfigInput(record);
163
- const game = asOptionalString(record.game);
164
- const coinType = asOptionalString(record.coinType);
165
- return {
166
- config: readConfigMetadata(config),
167
- game: game ? getGameMetadata(game, config) : null,
168
- currency: coinType ? getCurrencyInfo(coinType, config) : null
169
- };
170
- };
171
- const buildCoinflipTransactionTool = async (input = {}) => {
172
- const record = asRecord(input);
173
- const config = extractConfigInput(record);
174
- const mode = asMode(record.mode);
175
- const coinType = asString(record.coinType, "coinType", resolveSuigarConfig(config).suiCoinType);
176
- if (mode === "read-only") return buildReadOnlyPlan({
177
- game: "coinflip",
178
- config,
179
- coinType,
180
- targetSuffix: "::coinflip::play",
181
- notes: ["Uses the sweethouse shared object, a prepared bet coin, Pyth price info, clock, and random."]
182
- });
183
- const sdkTransaction = tryBuildSdkTransaction({
184
- record,
185
- config,
186
- coinType,
187
- game: "coinflip"
188
- });
189
- if (sdkTransaction) return buildTransactionResult({
190
- mode,
191
- config,
192
- transaction: sdkTransaction,
193
- context: {
194
- game: "coinflip",
195
- coinType,
196
- stake: asNumber(record.stake, "stake")
197
- }
198
- });
199
- const bundle = createReadOnlyClientBundle(config);
200
- const transaction = await buildCoinflipTransaction({
201
- client: bundle.client,
202
- config: bundle.config,
203
- owner: asString(record.owner, "owner"),
204
- coinType,
205
- stake: asNumber(record.stake, "stake"),
206
- cashStake: record.cashStake == null ? void 0 : asNumber(record.cashStake, "cashStake"),
207
- betCount: record.betCount == null ? void 0 : asNumber(record.betCount, "betCount"),
208
- side: asString(record.side, "side"),
209
- metadata: asRecord(record.metadata),
210
- partner: extractPartner(record),
211
- coinSource: asStringArray(record.coinObjectIds).length > 0 ? {
212
- kind: "object-ids",
213
- objectIds: asStringArray(record.coinObjectIds)
214
- } : void 0
215
- });
216
- return buildTransactionResult({
129
+ const executeTransactionTool = async ({ input, game, action, createTransaction, stake, stakeDisplay, gameInputs }) => {
130
+ const mode = getMode(input.mode);
131
+ if (mode === "read-only") throw new Error("read-only mode must be handled before transaction execution.");
132
+ const bundle = createSuigarClient(getConfigInput(input));
133
+ const coin = coinMetadataForAmount(bundle.config, input.coinType);
134
+ const baseStake = stake ?? (stakeDisplay == null ? void 0 : toBaseUnits(stakeDisplay, "stake", coin.decimals));
135
+ return asTextResponse(await buildTransactionResult({
217
136
  mode,
137
+ transaction: await createTransaction(bundle),
218
138
  config: bundle.config,
219
- transaction,
220
- context: {
221
- game: "coinflip",
222
- coinType,
223
- stake: asNumber(record.stake, "stake")
224
- }
225
- });
226
- };
227
- const buildLimboTransactionTool = async (input = {}) => {
228
- const record = asRecord(input);
229
- const config = extractConfigInput(record);
230
- const mode = asMode(record.mode);
231
- const coinType = asString(record.coinType, "coinType", resolveSuigarConfig(config).suiCoinType);
232
- if (mode === "read-only") return buildReadOnlyPlan({
233
- game: "limbo",
234
- config,
235
- coinType,
236
- targetSuffix: "::limbo::play",
237
- notes: ["Target multiplier is encoded as a fixed-point numerator and denominator."]
238
- });
239
- const sdkTransaction = tryBuildSdkTransaction({
240
- record,
241
- config,
242
- coinType,
243
- game: "limbo"
244
- });
245
- if (sdkTransaction) return buildTransactionResult({
246
- mode,
247
- config,
248
- transaction: sdkTransaction,
249
- context: {
250
- game: "limbo",
251
- coinType,
252
- stake: asNumber(record.stake, "stake")
253
- }
254
- });
255
- const bundle = createReadOnlyClientBundle(config);
256
- const transaction = await buildLimboTransaction({
257
139
  client: bundle.client,
258
- config: bundle.config,
259
- owner: asString(record.owner, "owner"),
260
- coinType,
261
- stake: asNumber(record.stake, "stake"),
262
- cashStake: record.cashStake == null ? void 0 : asNumber(record.cashStake, "cashStake"),
263
- betCount: record.betCount == null ? void 0 : asNumber(record.betCount, "betCount"),
264
- targetMultiplier: asNumber(record.targetMultiplier, "targetMultiplier"),
265
- metadata: asRecord(record.metadata),
266
- partner: extractPartner(record),
267
- coinSource: asStringArray(record.coinObjectIds).length > 0 ? {
268
- kind: "object-ids",
269
- objectIds: asStringArray(record.coinObjectIds)
270
- } : void 0
271
- });
272
- return buildTransactionResult({
273
- mode,
274
- config: bundle.config,
275
- transaction,
276
140
  context: {
277
- game: "limbo",
278
- coinType,
279
- stake: asNumber(record.stake, "stake")
141
+ game,
142
+ action,
143
+ coinType: coin.coinType,
144
+ stake: baseStake,
145
+ stakeDisplay,
146
+ coinDecimals: coin.decimals,
147
+ gameInputs
280
148
  }
281
- });
149
+ }));
282
150
  };
283
- const buildPlinkoTransactionTool = async (input = {}) => {
284
- const record = asRecord(input);
285
- const config = extractConfigInput(record);
286
- const mode = asMode(record.mode);
287
- const coinType = asString(record.coinType, "coinType", resolveSuigarConfig(config).suiCoinType);
288
- if (mode === "read-only") return buildReadOnlyPlan({
289
- game: "plinko",
290
- config,
291
- coinType,
292
- targetSuffix: "::plinko::play",
293
- notes: ["The config id is a u8 that selects the on-chain plinko board setup."]
294
- });
295
- const sdkTransaction = tryBuildSdkTransaction({
296
- record,
151
+ const readConfigTool = async (input = {}) => {
152
+ const { config } = createSuigarClient(getConfigInput(input));
153
+ return asTextResponse({
154
+ network: config.network,
297
155
  config,
298
- coinType,
299
- game: "plinko"
156
+ supportedGames: supportedGames()
300
157
  });
301
- if (sdkTransaction) return buildTransactionResult({
302
- mode,
158
+ };
159
+ const readGameMetadataTool = async (input = {}) => {
160
+ const { config } = createSuigarClient(getConfigInput(input));
161
+ const game = input.game ?? null;
162
+ const coinType = resolveDefaultCoinType(config, input.coinType);
163
+ return asTextResponse({
164
+ network: config.network,
303
165
  config,
304
- transaction: sdkTransaction,
305
- context: {
306
- game: "plinko",
307
- coinType,
308
- stake: asNumber(record.stake, "stake")
309
- }
310
- });
311
- const bundle = createReadOnlyClientBundle(config);
312
- const transaction = await buildPlinkoTransaction({
313
- client: bundle.client,
314
- config: bundle.config,
315
- owner: asString(record.owner, "owner"),
316
- coinType,
317
- stake: asNumber(record.stake, "stake"),
318
- cashStake: record.cashStake == null ? void 0 : asNumber(record.cashStake, "cashStake"),
319
- betCount: record.betCount == null ? void 0 : asNumber(record.betCount, "betCount"),
320
- configId: asNumber(record.configId, "configId"),
321
- metadata: asRecord(record.metadata),
322
- partner: extractPartner(record),
323
- coinSource: asStringArray(record.coinObjectIds).length > 0 ? {
324
- kind: "object-ids",
325
- objectIds: asStringArray(record.coinObjectIds)
326
- } : void 0
327
- });
328
- return buildTransactionResult({
329
- mode,
330
- config: bundle.config,
331
- transaction,
332
- context: {
333
- game: "plinko",
166
+ supportedGames: supportedGames(),
167
+ game: game ? {
168
+ id: game,
169
+ label: GAME_LABELS[game],
170
+ packageId: getPackageId(config, game),
334
171
  coinType,
335
- stake: asNumber(record.stake, "stake")
336
- }
172
+ notes: [game === "pvp-coinflip" ? "PvP coinflip uses dedicated create, join, and cancel transaction builders." : "Standard games use client.suigar.tx.createBetTransaction().", "Transactions are unsigned and are never executed by the MCP server."]
173
+ } : null
337
174
  });
338
175
  };
339
- const buildWheelTransactionTool = async (input = {}) => {
340
- const record = asRecord(input);
341
- const config = extractConfigInput(record);
342
- const mode = asMode(record.mode);
343
- const coinType = asString(record.coinType, "coinType", resolveSuigarConfig(config).suiCoinType);
344
- if (mode === "read-only") return buildReadOnlyPlan({
345
- game: "wheel",
346
- config,
347
- coinType,
348
- targetSuffix: "::wheel::play",
349
- notes: ["The config id is a u8 that selects the on-chain wheel table."]
350
- });
351
- const sdkTransaction = tryBuildSdkTransaction({
352
- record,
353
- config,
354
- coinType,
355
- game: "wheel"
356
- });
357
- if (sdkTransaction) return buildTransactionResult({
358
- mode,
359
- config,
360
- transaction: sdkTransaction,
361
- context: {
362
- game: "wheel",
363
- coinType,
364
- stake: asNumber(record.stake, "stake")
365
- }
176
+ const buildCoinflipTransactionTool = async (input = {}) => {
177
+ if (getMode(input.mode) === "read-only") return asTextResponse(readOnlyPlan({
178
+ input,
179
+ game: "coinflip",
180
+ requiredInputs: [
181
+ "owner",
182
+ "stake",
183
+ "side"
184
+ ],
185
+ notes: ["Uses the configured SweetHouse object, Pyth price info, clock, and randomness objects."]
186
+ }));
187
+ const side = requireString(input.side, "side");
188
+ return executeTransactionTool({
189
+ input,
190
+ game: "coinflip",
191
+ stakeDisplay: toCurrencyAmountText(input.stake, "stake"),
192
+ gameInputs: { side },
193
+ createTransaction: async (bundle) => bundle.client.suigar.tx.createBetTransaction("coinflip", {
194
+ ...await stakeOptions(input, bundle),
195
+ side
196
+ })
366
197
  });
367
- const bundle = createReadOnlyClientBundle(config);
368
- const transaction = await buildWheelTransaction({
369
- client: bundle.client,
370
- config: bundle.config,
371
- owner: asString(record.owner, "owner"),
372
- coinType,
373
- stake: asNumber(record.stake, "stake"),
374
- cashStake: record.cashStake == null ? void 0 : asNumber(record.cashStake, "cashStake"),
375
- betCount: record.betCount == null ? void 0 : asNumber(record.betCount, "betCount"),
376
- configId: asNumber(record.configId, "configId"),
377
- metadata: asRecord(record.metadata),
378
- partner: extractPartner(record),
379
- coinSource: asStringArray(record.coinObjectIds).length > 0 ? {
380
- kind: "object-ids",
381
- objectIds: asStringArray(record.coinObjectIds)
382
- } : void 0
198
+ };
199
+ const buildLimboTransactionTool = async (input = {}) => {
200
+ if (getMode(input.mode) === "read-only") return asTextResponse(readOnlyPlan({
201
+ input,
202
+ game: "limbo",
203
+ requiredInputs: [
204
+ "owner",
205
+ "stake",
206
+ "targetMultiplier"
207
+ ],
208
+ notes: ["Target multiplier is encoded by @suigar/sdk using the public fixed-point utility defaults."]
209
+ }));
210
+ const targetMultiplier = requireNumber(input.targetMultiplier, "targetMultiplier");
211
+ return executeTransactionTool({
212
+ input,
213
+ game: "limbo",
214
+ stakeDisplay: toCurrencyAmountText(input.stake, "stake"),
215
+ gameInputs: { targetMultiplier },
216
+ createTransaction: async (bundle) => bundle.client.suigar.tx.createBetTransaction("limbo", {
217
+ ...await stakeOptions(input, bundle),
218
+ targetMultiplier
219
+ })
383
220
  });
384
- return buildTransactionResult({
385
- mode,
386
- config: bundle.config,
387
- transaction,
388
- context: {
389
- game: "wheel",
390
- coinType,
391
- stake: asNumber(record.stake, "stake")
392
- }
221
+ };
222
+ const buildConfigIdTransactionTool = async (input, game) => {
223
+ if (getMode(input.mode) === "read-only") return asTextResponse(readOnlyPlan({
224
+ input,
225
+ game,
226
+ requiredInputs: [
227
+ "owner",
228
+ "stake",
229
+ "configId"
230
+ ],
231
+ notes: ["Config id selects the on-chain game configuration."]
232
+ }));
233
+ const configId = requireNumber(input.configId, "configId");
234
+ return executeTransactionTool({
235
+ input,
236
+ game,
237
+ stakeDisplay: toCurrencyAmountText(input.stake, "stake"),
238
+ gameInputs: { configId },
239
+ createTransaction: async (bundle) => bundle.client.suigar.tx.createBetTransaction(game, {
240
+ ...await stakeOptions(input, bundle),
241
+ configId
242
+ })
393
243
  });
394
244
  };
245
+ const buildPlinkoTransactionTool = (input = {}) => buildConfigIdTransactionTool(input, "plinko");
246
+ const buildWheelTransactionTool = (input = {}) => buildConfigIdTransactionTool(input, "wheel");
395
247
  const buildRangeTransactionTool = async (input = {}) => {
396
- const record = asRecord(input);
397
- const config = extractConfigInput(record);
398
- const mode = asMode(record.mode);
399
- const coinType = asString(record.coinType, "coinType", resolveSuigarConfig(config).suiCoinType);
400
- if (mode === "read-only") return buildReadOnlyPlan({
248
+ if (getMode(input.mode) === "read-only") return asTextResponse(readOnlyPlan({
249
+ input,
401
250
  game: "range",
402
- config,
403
- coinType,
404
- targetSuffix: "::range::play",
405
- notes: ["Range points are encoded as fixed-point integers with a 1e6 scale by default."]
406
- });
407
- const sdkTransaction = tryBuildSdkTransaction({
408
- record,
409
- config,
410
- coinType,
411
- game: "range"
412
- });
413
- if (sdkTransaction) return buildTransactionResult({
414
- mode,
415
- config,
416
- transaction: sdkTransaction,
417
- context: {
418
- game: "range",
419
- coinType,
420
- stake: asNumber(record.stake, "stake")
421
- }
422
- });
423
- const bundle = createReadOnlyClientBundle(config);
424
- const transaction = await buildRangeTransaction({
425
- client: bundle.client,
426
- config: bundle.config,
427
- owner: asString(record.owner, "owner"),
428
- coinType,
429
- stake: asNumber(record.stake, "stake"),
430
- cashStake: record.cashStake == null ? void 0 : asNumber(record.cashStake, "cashStake"),
431
- betCount: record.betCount == null ? void 0 : asNumber(record.betCount, "betCount"),
432
- leftPoint: asNumber(record.leftPoint, "leftPoint"),
433
- rightPoint: asNumber(record.rightPoint, "rightPoint"),
434
- outOfRange: asBoolean(record.outOfRange),
435
- metadata: asRecord(record.metadata),
436
- partner: extractPartner(record),
437
- coinSource: asStringArray(record.coinObjectIds).length > 0 ? {
438
- kind: "object-ids",
439
- objectIds: asStringArray(record.coinObjectIds)
440
- } : void 0
441
- });
442
- return buildTransactionResult({
443
- mode,
444
- config: bundle.config,
445
- transaction,
446
- context: {
447
- game: "range",
448
- coinType,
449
- stake: asNumber(record.stake, "stake")
450
- }
251
+ requiredInputs: [
252
+ "owner",
253
+ "stake",
254
+ "leftPoint",
255
+ "rightPoint"
256
+ ],
257
+ notes: ["Range points are normalized by @suigar/sdk before Move call construction."]
258
+ }));
259
+ const leftPoint = requireNumber(input.leftPoint, "leftPoint");
260
+ const rightPoint = requireNumber(input.rightPoint, "rightPoint");
261
+ const outOfRange = Boolean(input.outOfRange);
262
+ return executeTransactionTool({
263
+ input,
264
+ game: "range",
265
+ stakeDisplay: toCurrencyAmountText(input.stake, "stake"),
266
+ gameInputs: {
267
+ leftPoint,
268
+ rightPoint,
269
+ outOfRange
270
+ },
271
+ createTransaction: async (bundle) => bundle.client.suigar.tx.createBetTransaction("range", {
272
+ ...await stakeOptions(input, bundle),
273
+ leftPoint,
274
+ rightPoint,
275
+ outOfRange
276
+ })
451
277
  });
452
278
  };
453
279
  const buildPvpCoinflipCreateTransactionTool = async (input = {}) => {
454
- const record = asRecord(input);
455
- const config = extractConfigInput(record);
456
- const mode = asMode(record.mode);
457
- const coinType = asString(record.coinType, "coinType", resolveSuigarConfig(config).suiCoinType);
458
- if (mode === "read-only") return buildReadOnlyPlan({
280
+ if (getMode(input.mode) === "read-only") return asTextResponse(readOnlyPlan({
281
+ input,
459
282
  game: "pvp-coinflip",
460
- config,
461
- coinType,
462
- targetSuffix: "::pvp_coinflip::create_game",
463
- notes: ["Creates a public or private PvP lobby without needing a private key."]
464
- });
465
- const sdkTransaction = tryBuildSdkTransaction({
466
- record,
467
- config,
468
- coinType,
283
+ action: "create",
284
+ requiredInputs: [
285
+ "owner",
286
+ "stake",
287
+ "creatorSide"
288
+ ],
289
+ notes: ["Creates an unresolved PvP coinflip lobby without signing or executing the transaction."]
290
+ }));
291
+ const creatorSide = requireString(input.creatorSide, "creatorSide");
292
+ return executeTransactionTool({
293
+ input,
469
294
  game: "pvp-coinflip",
470
- pvpAction: "create"
471
- });
472
- if (sdkTransaction) return buildTransactionResult({
473
- mode,
474
- config,
475
- transaction: sdkTransaction,
476
- context: {
477
- game: "pvp-coinflip",
478
- coinType,
479
- stake: asNumber(record.stake, "stake")
480
- }
481
- });
482
- const bundle = createReadOnlyClientBundle(config);
483
- const transaction = await buildPvpCoinflipCreateTransaction({
484
- client: bundle.client,
485
- config: bundle.config,
486
- owner: asString(record.owner, "owner"),
487
- coinType,
488
- stake: asNumber(record.stake, "stake"),
489
- creatorSide: asString(record.creatorSide, "creatorSide"),
490
- isPrivate: asBoolean(record.isPrivate),
491
- metadata: asRecord(record.metadata),
492
- partner: extractPartner(record),
493
- coinSource: asStringArray(record.coinObjectIds).length > 0 ? {
494
- kind: "object-ids",
495
- objectIds: asStringArray(record.coinObjectIds)
496
- } : void 0
497
- });
498
- return buildTransactionResult({
499
- mode,
500
- config: bundle.config,
501
- transaction,
502
- context: {
503
- game: "pvp-coinflip",
504
- coinType,
505
- stake: asNumber(record.stake, "stake")
506
- }
295
+ action: "create",
296
+ stakeDisplay: toCurrencyAmountText(input.stake, "stake"),
297
+ gameInputs: {
298
+ creatorSide,
299
+ ...input.isPrivate == null ? {} : { isPrivate: input.isPrivate }
300
+ },
301
+ createTransaction: async (bundle) => bundle.client.suigar.tx.createPvPCoinflipTransaction("create", {
302
+ ...await commonOptions(input, bundle),
303
+ stake: toBaseUnits(input.stake, "stake", coinMetadataForAmount(bundle.config, input.coinType).decimals),
304
+ side: creatorSide,
305
+ isPrivate: input.isPrivate
306
+ })
507
307
  });
508
308
  };
509
309
  const buildPvpCoinflipJoinTransactionTool = async (input = {}) => {
510
- const record = asRecord(input);
511
- const config = extractConfigInput(record);
512
- const mode = asMode(record.mode);
513
- const coinType = asString(record.coinType, "coinType", resolveSuigarConfig(config).suiCoinType);
514
- if (mode === "read-only") return buildReadOnlyPlan({
310
+ if (getMode(input.mode) === "read-only") return asTextResponse(readOnlyPlan({
311
+ input,
515
312
  game: "pvp-coinflip",
516
- config,
517
- coinType,
518
- targetSuffix: "::pvp_coinflip::join_game",
519
- notes: ["Joining requires the game id, stake amount, and a price info object for the selected coin."]
520
- });
521
- const sdkTransaction = tryBuildSdkTransaction({
522
- record,
523
- config,
524
- coinType,
313
+ action: "join",
314
+ requiredInputs: ["owner", "gameId"],
315
+ notes: ["Join resolves the live game object during transaction build so the SDK can source the matching stake."]
316
+ }));
317
+ const gameId = requireString(input.gameId, "gameId");
318
+ return executeTransactionTool({
319
+ input,
525
320
  game: "pvp-coinflip",
526
- pvpAction: "join"
527
- });
528
- if (sdkTransaction) return buildTransactionResult({
529
- mode,
530
- config,
531
- transaction: sdkTransaction,
532
- context: {
533
- game: "pvp-coinflip",
534
- coinType,
535
- stake: asNumber(record.stake, "stake")
536
- }
537
- });
538
- const bundle = createReadOnlyClientBundle(config);
539
- const transaction = await buildPvpCoinflipJoinTransaction({
540
- client: bundle.client,
541
- config: bundle.config,
542
- owner: asString(record.owner, "owner"),
543
- gameId: asString(record.gameId, "gameId"),
544
- coinType,
545
- stake: asNumber(record.stake, "stake"),
546
- metadata: asRecord(record.metadata),
547
- partner: extractPartner(record),
548
- coinSource: asStringArray(record.coinObjectIds).length > 0 ? {
549
- kind: "object-ids",
550
- objectIds: asStringArray(record.coinObjectIds)
551
- } : void 0
552
- });
553
- return buildTransactionResult({
554
- mode,
555
- config: bundle.config,
556
- transaction,
557
- context: {
558
- game: "pvp-coinflip",
559
- coinType,
560
- stake: asNumber(record.stake, "stake")
561
- }
321
+ action: "join",
322
+ gameInputs: { gameId },
323
+ createTransaction: async (bundle) => bundle.client.suigar.tx.createPvPCoinflipTransaction("join", {
324
+ ...await commonOptions(input, bundle),
325
+ gameId
326
+ })
562
327
  });
563
328
  };
564
329
  const buildPvpCoinflipCancelTransactionTool = async (input = {}) => {
565
- const record = asRecord(input);
566
- const config = extractConfigInput(record);
567
- const mode = asMode(record.mode);
568
- const coinType = asString(record.coinType, "coinType", resolveSuigarConfig(config).suiCoinType);
569
- if (mode === "read-only") return buildReadOnlyPlan({
330
+ if (getMode(input.mode) === "read-only") return asTextResponse(readOnlyPlan({
331
+ input,
570
332
  game: "pvp-coinflip",
571
- config,
572
- coinType,
573
- targetSuffix: "::pvp_coinflip::cancel_game",
574
- notes: ["Cancel only needs the game id, sweethouse object, and coin type argument."]
575
- });
576
- const sdkTransaction = tryBuildSdkTransaction({
577
- record,
578
- config,
579
- coinType,
333
+ action: "cancel",
334
+ requiredInputs: ["owner", "gameId"],
335
+ notes: ["Cancel only prepares the unsigned cancellation transaction for the game creator."]
336
+ }));
337
+ const gameId = requireString(input.gameId, "gameId");
338
+ return executeTransactionTool({
339
+ input,
580
340
  game: "pvp-coinflip",
581
- pvpAction: "cancel"
582
- });
583
- if (sdkTransaction) return buildTransactionResult({
584
- mode,
585
- config,
586
- transaction: sdkTransaction,
587
- context: {
588
- game: "pvp-coinflip",
589
- coinType
590
- }
591
- });
592
- return buildTransactionResult({
593
- mode,
594
- config,
595
- transaction: buildPvpCoinflipCancelTransaction({
596
- config,
597
- owner: asString(record.owner, "owner"),
598
- gameId: asString(record.gameId, "gameId"),
599
- coinType
600
- }),
601
- context: {
602
- game: "pvp-coinflip",
603
- coinType
604
- }
341
+ action: "cancel",
342
+ gameInputs: { gameId },
343
+ createTransaction: async (bundle) => bundle.client.suigar.tx.createPvPCoinflipTransaction("cancel", {
344
+ ...await commonOptions(input, bundle),
345
+ gameId
346
+ })
605
347
  });
606
348
  };
607
349
  //#endregion
608
350
  export { buildCoinflipTransactionTool, buildLimboTransactionTool, buildPlinkoTransactionTool, buildPvpCoinflipCancelTransactionTool, buildPvpCoinflipCreateTransactionTool, buildPvpCoinflipJoinTransactionTool, buildRangeTransactionTool, buildWheelTransactionTool, readConfigTool, readGameMetadataTool };
351
+
352
+ //# sourceMappingURL=tools.mjs.map