@suigar/mcp 0.1.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 (59) hide show
  1. package/README.md +164 -0
  2. package/dist/bin.cjs +11 -0
  3. package/dist/bin.d.cts +1 -0
  4. package/dist/bin.d.mts +1 -0
  5. package/dist/bin.mjs +12 -0
  6. package/dist/client.cjs +46 -0
  7. package/dist/client.d.cts +17 -0
  8. package/dist/client.d.mts +17 -0
  9. package/dist/client.mjs +43 -0
  10. package/dist/coin.cjs +86 -0
  11. package/dist/coin.d.cts +35 -0
  12. package/dist/coin.d.mts +35 -0
  13. package/dist/coin.mjs +86 -0
  14. package/dist/config.cjs +183 -0
  15. package/dist/config.d.cts +15 -0
  16. package/dist/config.d.mts +15 -0
  17. package/dist/config.mjs +174 -0
  18. package/dist/index.cjs +53 -0
  19. package/dist/index.d.cts +10 -0
  20. package/dist/index.d.mts +10 -0
  21. package/dist/index.mjs +9 -0
  22. package/dist/mcp-support.cjs +62 -0
  23. package/dist/mcp-support.d.cts +16 -0
  24. package/dist/mcp-support.d.mts +16 -0
  25. package/dist/mcp-support.mjs +60 -0
  26. package/dist/metadata.cjs +51 -0
  27. package/dist/metadata.d.cts +52 -0
  28. package/dist/metadata.d.mts +52 -0
  29. package/dist/metadata.mjs +47 -0
  30. package/dist/server.cjs +433 -0
  31. package/dist/server.d.cts +73 -0
  32. package/dist/server.d.mts +73 -0
  33. package/dist/server.mjs +431 -0
  34. package/dist/tools.cjs +617 -0
  35. package/dist/tools.d.cts +158 -0
  36. package/dist/tools.d.mts +158 -0
  37. package/dist/tools.mjs +608 -0
  38. package/dist/transactions.cjs +294 -0
  39. package/dist/transactions.d.cts +40 -0
  40. package/dist/transactions.d.mts +40 -0
  41. package/dist/transactions.mjs +286 -0
  42. package/dist/types.d.cts +111 -0
  43. package/dist/types.d.mts +111 -0
  44. package/node_modules/@suigar/currency-registry/dist/index.cjs +121 -0
  45. package/node_modules/@suigar/currency-registry/dist/index.d.cts +50 -0
  46. package/node_modules/@suigar/currency-registry/dist/index.d.mts +50 -0
  47. package/node_modules/@suigar/currency-registry/dist/index.mjs +110 -0
  48. package/node_modules/@suigar/currency-registry/package.json +31 -0
  49. package/node_modules/@suigar/game-registry/dist/index.cjs +310 -0
  50. package/node_modules/@suigar/game-registry/dist/index.d.cts +65 -0
  51. package/node_modules/@suigar/game-registry/dist/index.d.mts +65 -0
  52. package/node_modules/@suigar/game-registry/dist/index.mjs +292 -0
  53. package/node_modules/@suigar/game-registry/package.json +31 -0
  54. package/node_modules/@suigar/sui-rpc-pool/dist/index.cjs +45590 -0
  55. package/node_modules/@suigar/sui-rpc-pool/dist/index.d.cts +465 -0
  56. package/node_modules/@suigar/sui-rpc-pool/dist/index.d.mts +465 -0
  57. package/node_modules/@suigar/sui-rpc-pool/dist/index.mjs +45570 -0
  58. package/node_modules/@suigar/sui-rpc-pool/package.json +31 -0
  59. package/package.json +62 -0
package/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # `@suigar/mcp`
2
+
3
+ Lightweight reusable Suigar helpers for Sui casino flows.
4
+
5
+ It includes:
6
+
7
+ - read helpers for Suigar config, game metadata, and configured currencies
8
+ - SDK-backed transaction builders for `coinflip`, `limbo`, `plinko`, `wheel`, `range`, and `pvp-coinflip`
9
+ - shared support metadata that marks available MCP tools and currently unsupported games
10
+ - beta-aligned partner attribution for supported on-chain builders
11
+ - an MCP stdio server entrypoint for tool-based usage
12
+ - build and dry-run support without private keys
13
+
14
+ ## Defaults
15
+
16
+ - network defaults to `testnet`
17
+ - deployed package ids, supported coins, registries, and price info defaults come from `@suigar/sdk`
18
+ - config resolution still accepts existing `SUIGAR_*` and frontend `VITE_*` keys as overrides for repo/test workflows
19
+ - transaction building never signs or executes transactions
20
+ - `partner` follows the beta SDK model: pass it once per build input and do not set `metadata.partner` or `metadata.referrer` manually
21
+
22
+ ## Supported env keys
23
+
24
+ The package reads either `SUIGAR_*` or `VITE_*` values.
25
+
26
+ - `SUIGAR_NETWORK` / `VITE_NETWORK`
27
+ - `SUIGAR_PACKAGE_ID` / `VITE_SUIGAR_PACKAGE_ID`
28
+ - `COINFLIP_PACKAGE_ID` / `VITE_COINFLIP_PACKAGE_ID`
29
+ - `PVP_COINFLIP_PACKAGE_ID` / `VITE_PVP_COINFLIP_PACKAGE_ID`
30
+ - `PLINKO_PACKAGE_ID` / `VITE_PLINKO_PACKAGE_ID`
31
+ - `LIMBO_PACKAGE_ID` / `VITE_LIMBO_PACKAGE_ID`
32
+ - `RANGE_PACKAGE_ID` / `VITE_RANGE_PACKAGE_ID`
33
+ - `WHEEL_PACKAGE_ID` / `VITE_WHEEL_PACKAGE_ID`
34
+ - `SWEETHOUSE_ID` / `VITE_SWEETHOUSE_ID`
35
+ - `SUI_COIN_TYPE` / `VITE_SUI_COIN_TYPE`
36
+ - `USDC_COIN_TYPE` / `VITE_USDC_COIN_TYPE`
37
+ - `SUI_PYTH_PRICE_INFO_OBJECT_ID` / `VITE_SUI_PYTH_PRICE_INFO_OBJECT_ID`
38
+ - `USDC_PYTH_PRICE_INFO_OBJECT_ID` / `VITE_USDC_PYTH_PRICE_INFO_OBJECT_ID`
39
+
40
+ ## Library usage
41
+
42
+ ```ts
43
+ import { buildCoinflipTransaction, createReadOnlyClientBundle, resolveSuigarConfig, serializeTransactionToBase64 } from '@suigar/mcp';
44
+
45
+ const bundle = createReadOnlyClientBundle({ network: 'testnet' });
46
+
47
+ const tx = await buildCoinflipTransaction({
48
+ client: bundle.client,
49
+ config: resolveSuigarConfig({ network: 'testnet' }),
50
+ owner: '0xabc...',
51
+ coinType: '0x2::sui::SUI',
52
+ stake: 1_000_000_000,
53
+ side: 'heads',
54
+ partner: '0xpartner_wallet_address',
55
+ });
56
+
57
+ const bytes = await serializeTransactionToBase64(tx, bundle.rawClient);
58
+ ```
59
+
60
+ ## Partner attribution
61
+
62
+ The beta SDK moved partner attribution into extension-level configuration instead of per-call metadata patching.
63
+
64
+ `@suigar/mcp` follows the same rule:
65
+
66
+ - pass `partner` as a top-level builder input when you need attribution onchain
67
+ - do not pass `metadata.partner` or `metadata.referrer`
68
+ - reserved attribution keys are ignored from manual metadata input
69
+
70
+ This applies to both the exported transaction builders and the MCP tools.
71
+
72
+ ## MCP server
73
+
74
+ For a packaged install, point your MCP client at the package bin:
75
+
76
+ ```json
77
+ {
78
+ "mcpServers": {
79
+ "suigar": {
80
+ "command": "npx",
81
+ "args": ["-y", "@suigar/mcp"]
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ Because the package carries SDK-backed testnet/mainnet defaults, a normal install does not need to source this repository's `.env.testnet.shared`.
88
+
89
+ When packed, the MCP package bundles its private internal runtime packages, so consumers do not need repo-relative `file:` dependencies.
90
+
91
+ Run the workspace stdio server while developing this package:
92
+
93
+ ```bash
94
+ npm run -w packages/mcp build
95
+ node packages/mcp/dist/bin.mjs
96
+ ```
97
+
98
+ Available tools:
99
+
100
+ - `read_config`
101
+ - `read_game_metadata`
102
+ - `build_coinflip_transaction`
103
+ - `build_limbo_transaction`
104
+ - `build_plinko_transaction`
105
+ - `build_wheel_transaction`
106
+ - `build_range_transaction`
107
+ - `build_pvp_coinflip_create_transaction`
108
+ - `build_pvp_coinflip_join_transaction`
109
+ - `build_pvp_coinflip_cancel_transaction`
110
+
111
+ Tool modes:
112
+
113
+ - `build`: returns a serialized transaction plus a summary
114
+ - `dry-run`: simulates the transaction on-chain with a read-only client
115
+ - `read-only`: returns resolved config and a transaction plan without building
116
+
117
+ For supported on-chain builders, you can also pass:
118
+
119
+ - `partner`: partner wallet address for beta-style attribution injection
120
+ - `metadata`: custom metadata, excluding reserved attribution keys
121
+
122
+ `read_config` and `read_game_metadata` include MCP support fields so agents can distinguish:
123
+
124
+ - `executionSurface: "onchain" | "backend"`
125
+ - `toolSupported: boolean`
126
+ - `primaryToolName: string | null`
127
+
128
+ Slots are intentionally disabled in the MCP package for now. They may still appear in game metadata as backend-driven, but `toolSupported` is `false` and no slots tool is exposed.
129
+
130
+ ## Testnet smoke
131
+
132
+ Use the repo-level smoke command to validate the MCP through the MCP protocol with managed wallets:
133
+
134
+ ```bash
135
+ npm run smoke:mcp:testnet
136
+ ```
137
+
138
+ This smoke path loads `.env.testnet.shared`, starts the MCP stdio server, validates read tools, runs dry-runs, and executes testnet transactions for the on-chain builders.
139
+
140
+ ## Use With Codex
141
+
142
+ The repo includes a local skill scaffold at `.codex/skills/mcp/`.
143
+
144
+ Install it into Codex by copying or symlinking it into `$CODEX_HOME/skills`:
145
+
146
+ ```bash
147
+ mkdir -p "${CODEX_HOME:-$HOME/.codex}/skills"
148
+ ln -s "$(pwd)/.codex/skills/mcp" "${CODEX_HOME:-$HOME/.codex}/skills/mcp"
149
+ ```
150
+
151
+ If you prefer a copy instead of a symlink:
152
+
153
+ ```bash
154
+ mkdir -p "${CODEX_HOME:-$HOME/.codex}/skills"
155
+ cp -R .codex/skills/mcp "${CODEX_HOME:-$HOME/.codex}/skills/mcp"
156
+ ```
157
+
158
+ ## Notes
159
+
160
+ - The gas shortcut only applies to native `0x2::sui::SUI`. Configured gameplay coins such as testnet `TEST_SUI` must come from owned coin objects.
161
+ - For normal packaged use, SDK transaction builders use Sui transaction coin selection.
162
+ - For explicit object-id coin sourcing, MCP falls back to its local compatibility builders because the public SDK builder API does not expose `coinObjectIds`.
163
+ - For fully pure local builds, pass `coinSource: { kind: 'object-ids', objectIds: [...] }`.
164
+ - No private key is required for building or dry-running.
package/dist/bin.cjs ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ const require_server = require("./server.cjs");
3
+ //#region src/bin.ts
4
+ const main = async () => {
5
+ await require_server.startSuigarMcpServer();
6
+ };
7
+ main().catch((error) => {
8
+ console.error(error);
9
+ process.exitCode = 1;
10
+ });
11
+ //#endregion
package/dist/bin.d.cts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/bin.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/bin.mjs ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { startSuigarMcpServer } from "./server.mjs";
3
+ //#region src/bin.ts
4
+ const main = async () => {
5
+ await startSuigarMcpServer();
6
+ };
7
+ main().catch((error) => {
8
+ console.error(error);
9
+ process.exitCode = 1;
10
+ });
11
+ //#endregion
12
+ export {};
@@ -0,0 +1,46 @@
1
+ const require_config = require("./config.cjs");
2
+ let _suigar_sui_rpc_pool = require("@suigar/sui-rpc-pool");
3
+ //#region src/client.ts
4
+ const createReadOnlyClientBundle = (configInput = {}) => {
5
+ const config = require_config.resolveSuigarConfig(configInput);
6
+ const runtime = (0, _suigar_sui_rpc_pool.getOrCreateResilientSuiClientRuntime)({
7
+ network: config.network,
8
+ providerUrl: config.providerUrl
9
+ });
10
+ return {
11
+ config,
12
+ runtime,
13
+ client: runtime.createCompatClient(),
14
+ rawClient: runtime.getGrpcClient()
15
+ };
16
+ };
17
+ const serializeTransactionToBase64 = async (transaction, client) => {
18
+ const bytes = await transaction.build({ client });
19
+ return Buffer.from(bytes).toString("base64");
20
+ };
21
+ const sanitizeForJson = (value) => {
22
+ if (typeof value === "bigint") return value.toString();
23
+ if (value instanceof Uint8Array) return Buffer.from(value).toString("base64");
24
+ if (Array.isArray(value)) return value.map((entry) => sanitizeForJson(entry));
25
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, sanitizeForJson(entry)]));
26
+ return value;
27
+ };
28
+ const dryRunTransaction = async (transaction, configInput = {}) => {
29
+ const { rawClient } = createReadOnlyClientBundle(configInput);
30
+ const bytes = await transaction.build({ client: rawClient });
31
+ return sanitizeForJson(await rawClient.simulateTransaction({
32
+ transaction: bytes,
33
+ include: {
34
+ effects: true,
35
+ events: true,
36
+ balanceChanges: true,
37
+ objectChanges: true,
38
+ input: true
39
+ }
40
+ }));
41
+ };
42
+ //#endregion
43
+ exports.createReadOnlyClientBundle = createReadOnlyClientBundle;
44
+ exports.dryRunTransaction = dryRunTransaction;
45
+ exports.sanitizeForJson = sanitizeForJson;
46
+ exports.serializeTransactionToBase64 = serializeTransactionToBase64;
@@ -0,0 +1,17 @@
1
+ import { SuigarConfig, SuigarConfigInput } from "./types.cjs";
2
+ import { SuiCompatClient } from "@suigar/sui-rpc-pool";
3
+ import { Transaction } from "@mysten/sui/transactions";
4
+ import { SuiGrpcClient } from "@mysten/sui/grpc";
5
+
6
+ //#region src/client.d.ts
7
+ declare const createReadOnlyClientBundle: (configInput?: SuigarConfigInput) => {
8
+ config: SuigarConfig;
9
+ runtime: import("@suigar/sui-rpc-pool").ResilientSuiClientRuntime;
10
+ client: SuiCompatClient;
11
+ rawClient: SuiGrpcClient;
12
+ };
13
+ declare const serializeTransactionToBase64: (transaction: Transaction, client: SuiGrpcClient) => Promise<string>;
14
+ declare const sanitizeForJson: (value: unknown) => unknown;
15
+ declare const dryRunTransaction: (transaction: Transaction, configInput?: SuigarConfigInput) => Promise<unknown>;
16
+ //#endregion
17
+ export { createReadOnlyClientBundle, dryRunTransaction, sanitizeForJson, serializeTransactionToBase64 };
@@ -0,0 +1,17 @@
1
+ import { SuigarConfig, SuigarConfigInput } from "./types.mjs";
2
+ import { SuiGrpcClient } from "@mysten/sui/grpc";
3
+ import { SuiCompatClient } from "@suigar/sui-rpc-pool";
4
+ import { Transaction } from "@mysten/sui/transactions";
5
+
6
+ //#region src/client.d.ts
7
+ declare const createReadOnlyClientBundle: (configInput?: SuigarConfigInput) => {
8
+ config: SuigarConfig;
9
+ runtime: import("@suigar/sui-rpc-pool").ResilientSuiClientRuntime;
10
+ client: SuiCompatClient;
11
+ rawClient: SuiGrpcClient;
12
+ };
13
+ declare const serializeTransactionToBase64: (transaction: Transaction, client: SuiGrpcClient) => Promise<string>;
14
+ declare const sanitizeForJson: (value: unknown) => unknown;
15
+ declare const dryRunTransaction: (transaction: Transaction, configInput?: SuigarConfigInput) => Promise<unknown>;
16
+ //#endregion
17
+ export { createReadOnlyClientBundle, dryRunTransaction, sanitizeForJson, serializeTransactionToBase64 };
@@ -0,0 +1,43 @@
1
+ import { resolveSuigarConfig } from "./config.mjs";
2
+ import { getOrCreateResilientSuiClientRuntime } from "@suigar/sui-rpc-pool";
3
+ //#region src/client.ts
4
+ const createReadOnlyClientBundle = (configInput = {}) => {
5
+ const config = resolveSuigarConfig(configInput);
6
+ const runtime = getOrCreateResilientSuiClientRuntime({
7
+ network: config.network,
8
+ providerUrl: config.providerUrl
9
+ });
10
+ return {
11
+ config,
12
+ runtime,
13
+ client: runtime.createCompatClient(),
14
+ rawClient: runtime.getGrpcClient()
15
+ };
16
+ };
17
+ const serializeTransactionToBase64 = async (transaction, client) => {
18
+ const bytes = await transaction.build({ client });
19
+ return Buffer.from(bytes).toString("base64");
20
+ };
21
+ const sanitizeForJson = (value) => {
22
+ if (typeof value === "bigint") return value.toString();
23
+ if (value instanceof Uint8Array) return Buffer.from(value).toString("base64");
24
+ if (Array.isArray(value)) return value.map((entry) => sanitizeForJson(entry));
25
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, sanitizeForJson(entry)]));
26
+ return value;
27
+ };
28
+ const dryRunTransaction = async (transaction, configInput = {}) => {
29
+ const { rawClient } = createReadOnlyClientBundle(configInput);
30
+ const bytes = await transaction.build({ client: rawClient });
31
+ return sanitizeForJson(await rawClient.simulateTransaction({
32
+ transaction: bytes,
33
+ include: {
34
+ effects: true,
35
+ events: true,
36
+ balanceChanges: true,
37
+ objectChanges: true,
38
+ input: true
39
+ }
40
+ }));
41
+ };
42
+ //#endregion
43
+ export { createReadOnlyClientBundle, dryRunTransaction, sanitizeForJson, serializeTransactionToBase64 };
package/dist/coin.cjs ADDED
@@ -0,0 +1,86 @@
1
+ let _suigar_currency_registry = require("@suigar/currency-registry");
2
+ let _mysten_sui_utils = require("@mysten/sui/utils");
3
+ let _suigar_sdk_utils = require("@suigar/sdk/utils");
4
+ //#region src/coin.ts
5
+ const DEFAULT_PAGE_LIMIT = 50;
6
+ const NETWORK_GAS_COIN_TYPE = (0, _suigar_currency_registry.normalizeCoinType)(_mysten_sui_utils.SUI_TYPE_ARG);
7
+ const mergeCoinObjects = (tx, coinIds) => {
8
+ if (coinIds.length === 0) throw new Error("No coin objects available");
9
+ const primaryCoin = tx.object(coinIds[0]);
10
+ if (coinIds.length > 1) tx.mergeCoins(primaryCoin, coinIds.slice(1).map((coinId) => tx.object(coinId)));
11
+ return primaryCoin;
12
+ };
13
+ const selectCoinsForAmount = async ({ client, owner, coinType, requiredAmount, pageLimit = DEFAULT_PAGE_LIMIT }) => {
14
+ if (requiredAmount <= 0n) return [];
15
+ let cursor = null;
16
+ let total = 0n;
17
+ const coins = [];
18
+ const normalizedCoinType = (0, _suigar_currency_registry.normalizeCoinType)(coinType);
19
+ do {
20
+ const response = await client.getCoins({
21
+ owner,
22
+ coinType: normalizedCoinType,
23
+ cursor,
24
+ limit: pageLimit
25
+ });
26
+ for (const coin of response.data ?? []) {
27
+ try {
28
+ const balance = BigInt(coin.balance);
29
+ coins.push({
30
+ coinObjectId: coin.coinObjectId,
31
+ balance
32
+ });
33
+ total += balance;
34
+ } catch {}
35
+ if (total >= requiredAmount) break;
36
+ }
37
+ cursor = response.hasNextPage && total < requiredAmount ? response.nextCursor : null;
38
+ } while (cursor);
39
+ if (coins.length === 0) throw new Error(`No usable gameplay coin objects were found for ${normalizedCoinType} at owner ${owner}`);
40
+ if (total < requiredAmount) throw new Error("Insufficient balance for this bet");
41
+ coins.sort((left, right) => {
42
+ if (left.balance === right.balance) return 0;
43
+ return left.balance > right.balance ? -1 : 1;
44
+ });
45
+ const selectedCoinIds = [];
46
+ let selectedTotal = 0n;
47
+ for (const coin of coins) {
48
+ selectedCoinIds.push(coin.coinObjectId);
49
+ selectedTotal += coin.balance;
50
+ if (selectedTotal >= requiredAmount) break;
51
+ }
52
+ return selectedCoinIds;
53
+ };
54
+ const resolveBetCoin = async ({ tx, coinType, amount, suiCoinType: _suiCoinType, coinSource, client, owner, allowGasCoinShortcut = true }) => {
55
+ const normalizedCoinType = (0, _suigar_currency_registry.normalizeCoinType)(coinType);
56
+ const resolvedAmount = (0, _suigar_sdk_utils.toBigInt)(amount);
57
+ if (resolvedAmount < 0n) throw new Error("Amount must be non-negative");
58
+ if (resolvedAmount === 0n) return tx.moveCall({
59
+ target: "0x2::coin::zero",
60
+ typeArguments: [normalizedCoinType],
61
+ arguments: []
62
+ });
63
+ if (coinSource?.kind === "object-ids") {
64
+ const primaryCoin = mergeCoinObjects(tx, coinSource.objectIds);
65
+ if (coinSource.split === false) return primaryCoin;
66
+ const [coin] = tx.splitCoins(primaryCoin, [tx.pure.u64(resolvedAmount)]);
67
+ return coin;
68
+ }
69
+ if (coinSource?.kind === "gas" && normalizedCoinType !== NETWORK_GAS_COIN_TYPE) throw new Error(`Gas coin sourcing is only supported for the native SUI gas coin (${NETWORK_GAS_COIN_TYPE})`);
70
+ if ((coinSource?.kind === "gas" || allowGasCoinShortcut) && normalizedCoinType === NETWORK_GAS_COIN_TYPE) {
71
+ if (coinSource?.kind === "gas" && coinSource.split === false) return tx.gas;
72
+ const [coin] = tx.splitCoins(tx.gas, [tx.pure.u64(resolvedAmount)]);
73
+ return coin;
74
+ }
75
+ if (!client || !owner) throw new Error(`A read-only client and owner address are required to source non-native coin ${normalizedCoinType}`);
76
+ const primaryCoin = mergeCoinObjects(tx, await selectCoinsForAmount({
77
+ client,
78
+ owner,
79
+ coinType: normalizedCoinType,
80
+ requiredAmount: resolvedAmount
81
+ }));
82
+ const [coin] = tx.splitCoins(primaryCoin, [tx.pure.u64(resolvedAmount)]);
83
+ return coin;
84
+ };
85
+ //#endregion
86
+ exports.resolveBetCoin = resolveBetCoin;
@@ -0,0 +1,35 @@
1
+ import { BetCoinSource, CoinReadClient } from "./types.cjs";
2
+ import { Transaction } from "@mysten/sui/transactions";
3
+
4
+ //#region src/coin.d.ts
5
+ declare const resolveBetCoin: ({
6
+ tx,
7
+ coinType,
8
+ amount,
9
+ suiCoinType: _suiCoinType,
10
+ coinSource,
11
+ client,
12
+ owner,
13
+ allowGasCoinShortcut
14
+ }: {
15
+ tx: Transaction;
16
+ coinType: string;
17
+ amount: number | bigint;
18
+ suiCoinType?: string;
19
+ coinSource?: BetCoinSource;
20
+ client?: CoinReadClient;
21
+ owner?: string;
22
+ allowGasCoinShortcut?: boolean;
23
+ }) => Promise<{
24
+ NestedResult: [number, number];
25
+ $kind: "NestedResult";
26
+ } | import("@mysten/sui/transactions").TransactionResult | {
27
+ $kind: "Input";
28
+ Input: number;
29
+ type?: "object";
30
+ } | {
31
+ $kind: "GasCoin";
32
+ GasCoin: true;
33
+ }>;
34
+ //#endregion
35
+ export { resolveBetCoin };
@@ -0,0 +1,35 @@
1
+ import { BetCoinSource, CoinReadClient } from "./types.mjs";
2
+ import { Transaction } from "@mysten/sui/transactions";
3
+
4
+ //#region src/coin.d.ts
5
+ declare const resolveBetCoin: ({
6
+ tx,
7
+ coinType,
8
+ amount,
9
+ suiCoinType: _suiCoinType,
10
+ coinSource,
11
+ client,
12
+ owner,
13
+ allowGasCoinShortcut
14
+ }: {
15
+ tx: Transaction;
16
+ coinType: string;
17
+ amount: number | bigint;
18
+ suiCoinType?: string;
19
+ coinSource?: BetCoinSource;
20
+ client?: CoinReadClient;
21
+ owner?: string;
22
+ allowGasCoinShortcut?: boolean;
23
+ }) => Promise<{
24
+ NestedResult: [number, number];
25
+ $kind: "NestedResult";
26
+ } | import("@mysten/sui/transactions").TransactionResult | {
27
+ $kind: "Input";
28
+ Input: number;
29
+ type?: "object";
30
+ } | {
31
+ $kind: "GasCoin";
32
+ GasCoin: true;
33
+ }>;
34
+ //#endregion
35
+ export { resolveBetCoin };
package/dist/coin.mjs ADDED
@@ -0,0 +1,86 @@
1
+ import { normalizeCoinType } from "@suigar/currency-registry";
2
+ import { SUI_TYPE_ARG } from "@mysten/sui/utils";
3
+ import { toBigInt } from "@suigar/sdk/utils";
4
+ //#region src/coin.ts
5
+ const DEFAULT_PAGE_LIMIT = 50;
6
+ const NETWORK_GAS_COIN_TYPE = normalizeCoinType(SUI_TYPE_ARG);
7
+ const mergeCoinObjects = (tx, coinIds) => {
8
+ if (coinIds.length === 0) throw new Error("No coin objects available");
9
+ const primaryCoin = tx.object(coinIds[0]);
10
+ if (coinIds.length > 1) tx.mergeCoins(primaryCoin, coinIds.slice(1).map((coinId) => tx.object(coinId)));
11
+ return primaryCoin;
12
+ };
13
+ const selectCoinsForAmount = async ({ client, owner, coinType, requiredAmount, pageLimit = DEFAULT_PAGE_LIMIT }) => {
14
+ if (requiredAmount <= 0n) return [];
15
+ let cursor = null;
16
+ let total = 0n;
17
+ const coins = [];
18
+ const normalizedCoinType = normalizeCoinType(coinType);
19
+ do {
20
+ const response = await client.getCoins({
21
+ owner,
22
+ coinType: normalizedCoinType,
23
+ cursor,
24
+ limit: pageLimit
25
+ });
26
+ for (const coin of response.data ?? []) {
27
+ try {
28
+ const balance = BigInt(coin.balance);
29
+ coins.push({
30
+ coinObjectId: coin.coinObjectId,
31
+ balance
32
+ });
33
+ total += balance;
34
+ } catch {}
35
+ if (total >= requiredAmount) break;
36
+ }
37
+ cursor = response.hasNextPage && total < requiredAmount ? response.nextCursor : null;
38
+ } while (cursor);
39
+ if (coins.length === 0) throw new Error(`No usable gameplay coin objects were found for ${normalizedCoinType} at owner ${owner}`);
40
+ if (total < requiredAmount) throw new Error("Insufficient balance for this bet");
41
+ coins.sort((left, right) => {
42
+ if (left.balance === right.balance) return 0;
43
+ return left.balance > right.balance ? -1 : 1;
44
+ });
45
+ const selectedCoinIds = [];
46
+ let selectedTotal = 0n;
47
+ for (const coin of coins) {
48
+ selectedCoinIds.push(coin.coinObjectId);
49
+ selectedTotal += coin.balance;
50
+ if (selectedTotal >= requiredAmount) break;
51
+ }
52
+ return selectedCoinIds;
53
+ };
54
+ const resolveBetCoin = async ({ tx, coinType, amount, suiCoinType: _suiCoinType, coinSource, client, owner, allowGasCoinShortcut = true }) => {
55
+ const normalizedCoinType = normalizeCoinType(coinType);
56
+ const resolvedAmount = toBigInt(amount);
57
+ if (resolvedAmount < 0n) throw new Error("Amount must be non-negative");
58
+ if (resolvedAmount === 0n) return tx.moveCall({
59
+ target: "0x2::coin::zero",
60
+ typeArguments: [normalizedCoinType],
61
+ arguments: []
62
+ });
63
+ if (coinSource?.kind === "object-ids") {
64
+ const primaryCoin = mergeCoinObjects(tx, coinSource.objectIds);
65
+ if (coinSource.split === false) return primaryCoin;
66
+ const [coin] = tx.splitCoins(primaryCoin, [tx.pure.u64(resolvedAmount)]);
67
+ return coin;
68
+ }
69
+ if (coinSource?.kind === "gas" && normalizedCoinType !== NETWORK_GAS_COIN_TYPE) throw new Error(`Gas coin sourcing is only supported for the native SUI gas coin (${NETWORK_GAS_COIN_TYPE})`);
70
+ if ((coinSource?.kind === "gas" || allowGasCoinShortcut) && normalizedCoinType === NETWORK_GAS_COIN_TYPE) {
71
+ if (coinSource?.kind === "gas" && coinSource.split === false) return tx.gas;
72
+ const [coin] = tx.splitCoins(tx.gas, [tx.pure.u64(resolvedAmount)]);
73
+ return coin;
74
+ }
75
+ if (!client || !owner) throw new Error(`A read-only client and owner address are required to source non-native coin ${normalizedCoinType}`);
76
+ const primaryCoin = mergeCoinObjects(tx, await selectCoinsForAmount({
77
+ client,
78
+ owner,
79
+ coinType: normalizedCoinType,
80
+ requiredAmount: resolvedAmount
81
+ }));
82
+ const [coin] = tx.splitCoins(primaryCoin, [tx.pure.u64(resolvedAmount)]);
83
+ return coin;
84
+ };
85
+ //#endregion
86
+ export { resolveBetCoin };