@xgamesfun/solana 0.25.4 → 0.26.1
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/README.md +8 -8
- package/js/approve/details.mjs +52 -2
- package/js/approve/live-check.mjs +0 -15
- package/js/approve/server.mjs +5 -4
- package/js/chain/token2022.js +3 -2
- package/js/client/accounts.js +67 -25
- package/js/client/grant.js +7 -2
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +716 -183
- package/js/client/ix.js +617 -113
- package/js/client/sizes.js +210 -64
- package/js/economy/check.mjs +691 -261
- package/js/economy/cost.mjs +2 -4
- package/js/economy/explain.mjs +16 -2
- package/js/economy/guide.mjs +12 -12
- package/js/economy/model.mjs +133 -14
- package/js/economy/plan.mjs +68 -19
- package/js/economy/review.mjs +50 -8
- package/js/economy/schema.json +73 -29
- package/js/economy/simulate.mjs +116 -13
- package/js/mcp/tools.mjs +2 -2
- package/js/play/index.js +855 -296
- package/package.json +1 -1
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +12 -4
- package/skills/xsolana-economy/references/after-launch.md +1 -1
- package/skills/xsolana-economy/references/apps.md +11 -2
- package/skills/xsolana-economy/references/sentence-to-spec.md +3 -3
- package/skills/xsolana-economy/references/telling-the-person.md +22 -4
- package/skills/xsolana-economy/references/trust-and-cheating.md +15 -5
- package/types/chain/token2022.d.ts +3 -2
- package/types/client/accounts.d.ts +34 -11
- package/types/client/idl/loot.d.ts +25 -4
- package/types/client/ix.d.ts +217 -45
- package/types/client/sizes.d.ts +42 -8
- package/types/economy/check.d.mts +11 -2
- package/types/economy/cost.d.mts +1 -0
- package/types/economy/live.d.mts +2 -0
- package/types/economy/model.d.mts +75 -12
- package/types/economy/plan.d.mts +31 -2
- package/types/economy/simulate.d.mts +12 -1
- package/types/mcp/server.d.mts +12 -1
- package/types/mcp/tools.d.mts +12 -1
- package/types/play/index.d.ts +210 -144
package/js/economy/check.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { gateList } from '../client/grant.js';
|
|
|
9
9
|
import { MAX_BID_NFTS, MAX_SHARES } from '../client/ix.js';
|
|
10
10
|
import { MAX_POTS } from '../client/accounts.js';
|
|
11
11
|
import { vault } from '../client/pda.js';
|
|
12
|
-
import { boxBytes, buyFits, drawsFit, MAX_ORDER_BYTES, tableBudget } from '../client/sizes.js';
|
|
12
|
+
import { ASSUMED_TRIES, boxBytes, buyFits, drawsFit, MAX_ORDER_BYTES, tableBudget } from '../client/sizes.js';
|
|
13
13
|
import { TOKEN_2022_PROGRAM, TOKEN_PROGRAM } from '../chain/pda.js';
|
|
14
14
|
import { addressBytes, encodeBase58 } from '../chain/base58.js';
|
|
15
15
|
import { isOnCurve } from '../chain/curve.js';
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
FEES,
|
|
20
20
|
rent as rentOf,
|
|
21
21
|
LIMITS,
|
|
22
|
+
ANSWER_HOLDS,
|
|
22
23
|
RESERVED_ATTRIBUTES,
|
|
23
24
|
destination,
|
|
24
25
|
fromUnits,
|
|
@@ -45,6 +46,7 @@ import {
|
|
|
45
46
|
releaseWords,
|
|
46
47
|
farmEndWords,
|
|
47
48
|
stashNfts,
|
|
49
|
+
stopsSales,
|
|
48
50
|
poolTakers,
|
|
49
51
|
sharesOf,
|
|
50
52
|
toBps,
|
|
@@ -57,7 +59,7 @@ import {
|
|
|
57
59
|
weights,
|
|
58
60
|
} from './model.mjs';
|
|
59
61
|
// plan.mjs imports check() in turn: each module only calls the other's functions, never at load time.
|
|
60
|
-
import { boxArgs, boxOrderBytes, placeholderContext, recipeArgs, setupSteps,
|
|
62
|
+
import { boxArgs, boxOrderBytes, placeholderContext, recipeArgs, setupSteps, stepAloneBytes } from './plan.mjs';
|
|
61
63
|
import { findSecrets } from './secrets.mjs';
|
|
62
64
|
import { simulate } from './simulate.mjs';
|
|
63
65
|
import { BUDGET, MAX_TRANSACTION_BYTES, transactionLocks, transactionSize, WALLET_ROOM } from './txsize.mjs';
|
|
@@ -669,20 +671,30 @@ export function buySize(b, m, { detail = false, count = 1 } = {}) {
|
|
|
669
671
|
* holds its key, and the approval page calls it "your wallet" until a wallet connects: no spec may name it. */
|
|
670
672
|
export const STAND_IN_OWNER = encodeBase58(sha256(utf8('loot-plan:owner')));
|
|
671
673
|
|
|
672
|
-
/** The longest a contest's result or a lottery's draw may be off, or an auction's end (contest.rs, lottery.rs, auction.rs). */
|
|
673
|
-
const MAX_CONTEST_SECONDS = Number(IDL.constants.find(c => c.name === 'MAX_CONTEST_SECONDS')?.value ?? 157_680_000);
|
|
674
|
-
/** A sealed schedule's last run comes within this of going live (schedule.rs). */
|
|
675
|
-
const MAX_SCHEDULE_SECONDS = Number(IDL.constants.find(c => c.name === 'MAX_SCHEDULE_SECONDS')?.value ?? 315_360_000);
|
|
676
|
-
/** The most runs a sealed schedule makes (schedule.rs). */
|
|
677
674
|
/** The most places a run of a lottery's places given one by one holds (lottery.rs). */
|
|
678
|
-
const MAX_RUN_PLACES = Number(IDL.constants.find(c => c.name === 'MAX_RUN_PLACES')
|
|
679
|
-
|
|
675
|
+
const MAX_RUN_PLACES = Number(IDL.constants.find(c => c.name === 'MAX_RUN_PLACES').value);
|
|
676
|
+
|
|
677
|
+
/** How far off a time may land before the checker asks whether its unit is right: three years. The program
|
|
678
|
+
* takes any time a date holds; a time written in milliseconds lands thousands of years out and locks what waits on it. */
|
|
679
|
+
export const FAR_SECONDS = 3 * 365 * 86_400;
|
|
680
|
+
/** A Unix time in words, in English: "14 March 2031". Past what a date can show, the year alone. */
|
|
681
|
+
export function dateWords(seconds) {
|
|
682
|
+
const d = new Date(Number(seconds) * 1000);
|
|
683
|
+
if (Number.isNaN(d.getTime())) return `the year ${Math.floor(Number(seconds) / 31_557_600) + 1970}`;
|
|
684
|
+
return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC' });
|
|
685
|
+
}
|
|
680
686
|
|
|
681
687
|
/**
|
|
682
|
-
* Checks an economy spec. `now` (Unix seconds) dates "publish".
|
|
688
|
+
* Checks an economy spec. `now` (Unix seconds) dates "publish". `project`: the project's address, once a wallet is named
|
|
689
|
+
* (the plan for a real setup passes it; a live economy's file names it): a box's write searches for each of its pools'
|
|
690
|
+
* and stashes' addresses, and the checker counts each search exactly with it, else at an assumed 2 tries an address
|
|
691
|
+
* (`ASSUMED_TRIES`).
|
|
683
692
|
* Returns { ok, refusals: [{ code, where, message, fix }], warnings: [{ code, where, message, fix? }] }.
|
|
684
693
|
*/
|
|
685
|
-
export function check(
|
|
694
|
+
export function check(
|
|
695
|
+
economy,
|
|
696
|
+
{ network = 'devnet', now = Math.floor(Date.now() / 1000), project = economy?.live?.project ?? null } = {},
|
|
697
|
+
) {
|
|
686
698
|
const refusals = [];
|
|
687
699
|
const warnings = [];
|
|
688
700
|
const refuse = (code, where, message, fix) => refusals.push({ code, where, message, fix });
|
|
@@ -852,6 +864,25 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
852
864
|
);
|
|
853
865
|
return s;
|
|
854
866
|
};
|
|
867
|
+
/**
|
|
868
|
+
* A time that lands far off: the program takes it, and what waits on it (bids, tickets, NFTs, a vault) is
|
|
869
|
+
* locked until then. `at` is when it lands (Unix seconds), `written` what the spec says, `locks` what waits. Warned
|
|
870
|
+
* with the date in words, and as milliseconds when the number written reads as a time in milliseconds.
|
|
871
|
+
*/
|
|
872
|
+
const farTime = (at, where, what, written, locks) => {
|
|
873
|
+
if (!(at - now > FAR_SECONDS)) return;
|
|
874
|
+
// A plain number this far off is most likely milliseconds: a thousandth of it is what was meant.
|
|
875
|
+
const millis = typeof written === 'number' || /^\d+$/.test(String(written ?? ''));
|
|
876
|
+
const meant = Math.round(Number(written) / 1000);
|
|
877
|
+
warn(
|
|
878
|
+
'W-FAR-TIME',
|
|
879
|
+
where,
|
|
880
|
+
`${what} lands on ${dateWords(at)}, ${Math.floor((at - now) / 31_536_000).toLocaleString('en-US')} years after go-live${millis ? `: ${written} reads like milliseconds, and the program counts seconds (${meant.toLocaleString('en-US')} seconds is ${meant >= 86_400 ? `${Math.round(meant / 86_400).toLocaleString('en-US')} days` : `${Math.round(meant / 3_600).toLocaleString('en-US')} hours`})` : ''}. The program takes any time, so ${locks} until then.`,
|
|
881
|
+
millis
|
|
882
|
+
? 'Write the time in seconds, or as a duration like "7d".'
|
|
883
|
+
: 'Check the unit and the date before you sign: a duration like "30d" or a date like "2027-01-01" is safest.',
|
|
884
|
+
);
|
|
885
|
+
};
|
|
855
886
|
/** Refuses a list over one of the program's caps (`LIMITS`), which it would refuse when the owner signs. */
|
|
856
887
|
const tooMany = (count, most, code, where, what, error, fix) => {
|
|
857
888
|
if (count > most) refuse(code, where, `${what}; the program takes at most ${most} (${error}).`, fix);
|
|
@@ -1012,22 +1043,29 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
1012
1043
|
}
|
|
1013
1044
|
const p = t.powers ?? {};
|
|
1014
1045
|
percent(p.transferFeePercent, `${where}.powers.transferFeePercent`);
|
|
1046
|
+
// A token that can't be transferred moves only by a mint or a burn: nothing can be paid in it, staked, lent or
|
|
1047
|
+
// sealed in a vault, and a prize of it given from a pool never arrives. The program takes it; the project hears it.
|
|
1015
1048
|
if (p.nonTransferable)
|
|
1016
|
-
|
|
1017
|
-
'
|
|
1049
|
+
warn(
|
|
1050
|
+
'W-MINT-POWERS',
|
|
1018
1051
|
`${where}.powers`,
|
|
1019
|
-
`“${t.key}” cannot be transferred
|
|
1020
|
-
'Use another token.',
|
|
1052
|
+
`“${t.key}” cannot be transferred: nothing can be paid in it, staked in a farm, lent, bid or sealed in a vault, and a prize of it can only be minted to its winner. A share of it, a refund or a payout that has to move it never arrives, and a split prize of it minted to a contest's winners must be given up by each one.`,
|
|
1053
|
+
'Use it only as points the project mints to players, or use another token.',
|
|
1021
1054
|
);
|
|
1055
|
+
// What each power lets its holder do to a farm's stakes, a vest's vault, a box's payments or a prize.
|
|
1022
1056
|
const powers = [
|
|
1023
1057
|
p.freeze &&
|
|
1024
|
-
'freeze any account holding it, including the game’s pools and the vault that holds open orders and bids (a frozen vault holds them until it thaws)',
|
|
1025
|
-
p.permanentDelegate &&
|
|
1058
|
+
'freeze any account holding it, including the game’s pools and the vault that holds open orders and bids (a frozen vault holds them until it thaws), and so block a repayment or a refund',
|
|
1059
|
+
p.permanentDelegate &&
|
|
1060
|
+
'move or burn it out of any account, the game’s pools, a farm’s stakes and a sealed vest’s vault included',
|
|
1061
|
+
p.closeAuthority && 'close the mint and make it again with other powers',
|
|
1026
1062
|
p.transferHook &&
|
|
1027
|
-
'run its own program on every transfer, which can refuse one (a settle that pays a price or its shares in it waits until the hook allows it; a box that rolls, or that a recipe, a mission or a prize rolls, can’t give it as a prize)',
|
|
1063
|
+
'run its own program on every transfer, which can refuse one and so block a repayment or a refund (a settle that pays a price or its shares in it waits until the hook allows it; a box that rolls, or that a recipe, a mission or a prize rolls, can’t give it as a prize)',
|
|
1028
1064
|
p.transferFeePercent > 0 &&
|
|
1029
1065
|
`keep ${p.transferFeePercent}% of every transfer (pools and prizes receive that much less)`,
|
|
1030
|
-
p.
|
|
1066
|
+
p.feeChangeable &&
|
|
1067
|
+
'raise its transfer fee, up to 100%, and so take every stake, payment or prize as it leaves a vault',
|
|
1068
|
+
p.pausable && 'pause every transfer of it, and so block a repayment, a refund or an unstake',
|
|
1031
1069
|
p.defaultFrozen && 'start every new account frozen',
|
|
1032
1070
|
!p.defaultFrozen && p.defaultState && p.freeze && 'switch every new account to start frozen',
|
|
1033
1071
|
p.mintAuthority === 'someone else' && 'mint more of it whenever its owner wants',
|
|
@@ -1036,8 +1074,8 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
1036
1074
|
warn(
|
|
1037
1075
|
'W-MINT-POWERS',
|
|
1038
1076
|
`${where}.powers`,
|
|
1039
|
-
`Whoever controls the mint of “${t.key}” can ${powers.join('; ')}. The program
|
|
1040
|
-
'Use a mint without those powers, or say so where players buy.',
|
|
1077
|
+
`Whoever controls the mint of “${t.key}” can ${powers.join('; ')}. The program takes any token: a farm, a vest, a box or a prize in it is only as safe as the token, and players should know.`,
|
|
1078
|
+
'Use a mint without those powers, or say so where players buy, stake or are paid.',
|
|
1041
1079
|
);
|
|
1042
1080
|
if (source === 'existing' && !t.powers)
|
|
1043
1081
|
warn(
|
|
@@ -1209,7 +1247,15 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
1209
1247
|
);
|
|
1210
1248
|
const p = poolKey ? pool(poolKey, `${at}.from`) : null;
|
|
1211
1249
|
const t = minted ? token(line.currency, `${at}.currency`) : p ? m.tokens.get(p.currency) : null;
|
|
1212
|
-
|
|
1250
|
+
// A lottery's place passes over a prize it can't mint: taken, and said. Elsewhere the claim or craft fails.
|
|
1251
|
+
if (minted && t && !t.gameMints && code === 'E-LOTTERY')
|
|
1252
|
+
warn(
|
|
1253
|
+
'W-MINT-NOT-OURS',
|
|
1254
|
+
`${at}.currency`,
|
|
1255
|
+
`${what} mints ${t.symbol}, which the project cannot mint: until the project is the mint’s authority, a pull by anyone but the place’s winner or the project’s keys fails, and the winner or the keys can pass it over for nothing.`,
|
|
1256
|
+
'Make the project the mint’s authority before the draw, pay it from a pool ("from": "pool:<key>"), or use a token with source "project".',
|
|
1257
|
+
);
|
|
1258
|
+
else if (minted && t && !t.gameMints)
|
|
1213
1259
|
refuse(
|
|
1214
1260
|
'E-MINT-NOT-OURS',
|
|
1215
1261
|
`${at}.currency`,
|
|
@@ -1223,8 +1269,14 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
1223
1269
|
`${what} gives ${m.tokens.get(line.currency)?.symbol ?? line.currency} from “${p.key}”, which holds ${m.tokens.get(p.currency)?.symbol}.`,
|
|
1224
1270
|
`Leave "currency" out: a pool’s prize is in what it holds.`,
|
|
1225
1271
|
);
|
|
1272
|
+
// A prize of 0 tokens is given with nothing moved, on every route.
|
|
1226
1273
|
if (t && units(line.amount, t, `${at}.amount`, `What ${what} gives`) === 0n)
|
|
1227
|
-
|
|
1274
|
+
warn(
|
|
1275
|
+
'W-ZERO',
|
|
1276
|
+
`${at}.amount`,
|
|
1277
|
+
`${what} gives 0 ${t.symbol ?? 'tokens'}: the prize is given, and its winner gets nothing.`,
|
|
1278
|
+
'Give it an amount above 0, or leave the prize out.',
|
|
1279
|
+
);
|
|
1228
1280
|
} else if (line.gives === 'nft' && line.held) {
|
|
1229
1281
|
// An NFT a lottery's place holds in the lottery's own stash: a lottery's place only (prize.rs Prize::Held).
|
|
1230
1282
|
if (code !== 'E-LOTTERY')
|
|
@@ -1297,31 +1349,34 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
1297
1349
|
return line;
|
|
1298
1350
|
};
|
|
1299
1351
|
/**
|
|
1300
|
-
* A
|
|
1301
|
-
*
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
if (t?.powers?.confidentialTransfers)
|
|
1305
|
-
refuse(
|
|
1306
|
-
code,
|
|
1307
|
-
where,
|
|
1308
|
-
`${what} is in ${t.symbol}, whose mint allows confidential transfers: any holder could turn plain transfers away, so a payout or a bid sent back might never land (the program refuses it).`,
|
|
1309
|
-
'Use a mint without confidential transfers.',
|
|
1310
|
-
);
|
|
1311
|
-
};
|
|
1312
|
-
/**
|
|
1313
|
-
* A mint whose new accounts start frozen, or can be made to (a default state and a freeze authority, which can switch
|
|
1314
|
-
* it to frozen): a lottery, a contest, an auction and a sealed schedule refuse it as they open, since each reads the
|
|
1315
|
-
* setting once, and a payout or a refund opened into a frozen account could never land. Only when the spec says so.
|
|
1352
|
+
* A token a player can hold up a payment in: its new accounts start frozen or can be switched to, its
|
|
1353
|
+
* holders can turn plain transfers away (confidential transfers), or it can't be transferred at all. The program
|
|
1354
|
+
* takes it in an auction, an offer, a loan, a lottery and a contest; this says what a player can then do, by `kind`.
|
|
1355
|
+
* Only when the spec says so (`powers`).
|
|
1316
1356
|
*/
|
|
1317
|
-
const
|
|
1357
|
+
const holdsUp = (t, where, what, kind) => {
|
|
1318
1358
|
const p = t?.powers ?? {};
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1359
|
+
const frozen = p.defaultFrozen || (p.defaultState && p.freeze);
|
|
1360
|
+
if (!frozen && !p.confidentialTransfers && !p.nonTransferable) return;
|
|
1361
|
+
const how = [
|
|
1362
|
+
p.defaultFrozen && 'its new accounts start frozen',
|
|
1363
|
+
!p.defaultFrozen && frozen && 'its new accounts can be switched to start frozen',
|
|
1364
|
+
p.confidentialTransfers && 'its holders can turn plain transfers away (confidential transfers)',
|
|
1365
|
+
p.nonTransferable && 'it cannot be transferred at all',
|
|
1366
|
+
].filter(Boolean);
|
|
1367
|
+
const does = {
|
|
1368
|
+
auction:
|
|
1369
|
+
'the highest bidder can close their own account of it so that every higher bid fails (a topped bid is sent back to that account), and win at their price. Tell the seller and every bidder.',
|
|
1370
|
+
lottery:
|
|
1371
|
+
'a player who turns plain transfers into their own account off (or closes it) holds up only their own part, but the round cannot close meanwhile and its rent waits: keep spare SOL in the lottery ("roundRent") so no buyer fronts it. A pot or pool in a token that could not reach the round gives that round nothing.',
|
|
1372
|
+
contest:
|
|
1373
|
+
'a winner who closes their own account of it holds up their own claim and the contest’s close; a split prize minted in a token that cannot be transferred must be given up by each winner.',
|
|
1374
|
+
}[kind];
|
|
1375
|
+
warn(
|
|
1376
|
+
'W-HOLD-UP',
|
|
1322
1377
|
where,
|
|
1323
|
-
`${what} is in ${t.symbol},
|
|
1324
|
-
'Use a
|
|
1378
|
+
`${what} is in ${t.symbol ?? t.key}, and ${how.join('; ')}: ${p.nonTransferable ? 'nothing can be paid in, so nobody can take part. ' : ''}${does}`,
|
|
1379
|
+
'Use a token with none of those powers, or tell players before they take part.',
|
|
1325
1380
|
);
|
|
1326
1381
|
};
|
|
1327
1382
|
/** Whether `address` is the game's own vault for `t` (where open orders, prizes set aside and bids wait), when both are known. */
|
|
@@ -1363,6 +1418,8 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
1363
1418
|
};
|
|
1364
1419
|
/** Whether the words on a Token Metadata print were said (once a spec). */
|
|
1365
1420
|
let printsSaid = false;
|
|
1421
|
+
/** Whether the words on NFTs of a collection the project doesn't control were said (once a spec). */
|
|
1422
|
+
let outsideSaid = false;
|
|
1366
1423
|
/** A rule on what an NFT says (rules.rs): an item of the game's must be a collectible; or a collection is named. */
|
|
1367
1424
|
const rule = (r, where, what) => {
|
|
1368
1425
|
if (!r) return;
|
|
@@ -1377,6 +1434,16 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
1377
1434
|
'Nothing to change: tell holders of prints that only the original NFTs count.',
|
|
1378
1435
|
);
|
|
1379
1436
|
}
|
|
1437
|
+
// An NFT whose creator kept a permanent delegate can be pulled back out of what the project holds.
|
|
1438
|
+
if (r.collection && isAddress(r.collection) && !m.collections.includes(r.collection) && !outsideSaid) {
|
|
1439
|
+
outsideSaid = true;
|
|
1440
|
+
warn(
|
|
1441
|
+
'W-OUTSIDE-NFT',
|
|
1442
|
+
`${where}.collection`,
|
|
1443
|
+
`${what} takes NFTs of ${r.collection}, a collection the project does not control. The creator of an NFT with a permanent delegate can pull it back out of a hold, a stake, a loan or a listing, and a record it leaves behind can later take the next owner’s deposit of the same NFT. This holds for every rule on such a collection in the spec.`,
|
|
1444
|
+
'Before a player deposits one, the app looks for this project’s records that name the NFT (a listing, a hold) and warns when one is left from an earlier owner; prefer collections without a permanent delegate.',
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1380
1447
|
// A gate may list wallets instead (rules.rs Gate::Wallets): a duel's two players, a tournament's entrants.
|
|
1381
1448
|
// Or a list of any size (rules.rs Gate::List): its root on chain, each player's proof passed as accounts.
|
|
1382
1449
|
if (r.list !== undefined) {
|
|
@@ -1492,13 +1559,8 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
1492
1559
|
`${what} names "items" and an item, a collection or an attribute: "items" alone means any of those items.`,
|
|
1493
1560
|
'Keep "items", or the other form.',
|
|
1494
1561
|
);
|
|
1495
|
-
if (!Array.isArray(r.items) || !r.items.length
|
|
1496
|
-
refuse(
|
|
1497
|
-
'E-RULE',
|
|
1498
|
-
`${where}.items`,
|
|
1499
|
-
`${what} names ${r.items?.length ?? 0} items: 1 to 16.`,
|
|
1500
|
-
'Name 1 to 16 items.',
|
|
1501
|
-
);
|
|
1562
|
+
if (!Array.isArray(r.items) || !r.items.length)
|
|
1563
|
+
refuse('E-RULE', `${where}.items`, `${what} names no items.`, 'Name at least one item.');
|
|
1502
1564
|
for (const [i, k] of (r.items ?? []).entries()) {
|
|
1503
1565
|
const it = item(k, `${where}.items[${i}]`);
|
|
1504
1566
|
if (it && !isCollectible(it))
|
|
@@ -1511,13 +1573,10 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
1511
1573
|
}
|
|
1512
1574
|
return;
|
|
1513
1575
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
`${what} lists ${r.oneOf?.length ?? 0} texts in "oneOf": 1 to 16.`,
|
|
1519
|
-
'List 1 to 16 texts.',
|
|
1520
|
-
);
|
|
1576
|
+
// "One of" takes as many texts as the record that holds the rule fits (rules.rs): bounded where it is read, a
|
|
1577
|
+
// lottery's NFT ticket and a box's gate and odds.
|
|
1578
|
+
if (r.oneOf !== undefined && (!Array.isArray(r.oneOf) || !r.oneOf.length))
|
|
1579
|
+
refuse('E-RULE', `${where}.oneOf`, `${what} lists no texts in "oneOf".`, 'List at least one text.');
|
|
1521
1580
|
if (r.oneOf !== undefined && (r.equals !== undefined || r.atLeast !== undefined || r.time !== undefined))
|
|
1522
1581
|
refuse(
|
|
1523
1582
|
'E-RULE',
|
|
@@ -1651,6 +1710,7 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
1651
1710
|
* treasury wallet's account of `tok`, whatever token it is. `chosen`: the buyer may name one share ("chosen",
|
|
1652
1711
|
* "chosen:<item>"): a box's or a lottery's split; 'wallet': only "chosen", a wallet (a payment's split).
|
|
1653
1712
|
*/
|
|
1713
|
+
let holderSaid = false;
|
|
1654
1714
|
const split = (list, tok, where, { what, chosen = false, single = false }) => {
|
|
1655
1715
|
// Where share `i` is: one destination given alone (`single`) is the field itself.
|
|
1656
1716
|
const to = i => (single ? where : `${where}[${i}].to`);
|
|
@@ -1718,6 +1778,15 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
1718
1778
|
`Send it to a farm whose rewards are ${tok.symbol}.`,
|
|
1719
1779
|
);
|
|
1720
1780
|
}
|
|
1781
|
+
// A holder's share names the holder when it is paid.
|
|
1782
|
+
if ((s.kind === 'holder' || (s.kind === 'chosen' && s.key)) && !holderSaid) {
|
|
1783
|
+
holderSaid = true;
|
|
1784
|
+
warn(
|
|
1785
|
+
'W-HOLDER-SHARE',
|
|
1786
|
+
to(i),
|
|
1787
|
+
`${what} pays a share to whoever holds a collectible: the settle names the holder at that moment, and if the holder moves the collectible first, the settle fails and is sent again with the holder read afresh.`,
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1721
1790
|
if (['account', 'holder', 'wallet'].includes(s.kind) && !isAddress(s.key))
|
|
1722
1791
|
refuse(
|
|
1723
1792
|
'E-REF',
|
|
@@ -1850,7 +1919,16 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
1850
1919
|
warn(
|
|
1851
1920
|
'W-COLLECTIONS',
|
|
1852
1921
|
'economy.collections',
|
|
1853
|
-
`The project takes ${m.collections.length} collection${m.collections.length === 1 ? '' : 's'} you already have: going live passes ${m.collections.length === 1 ? 'it' : 'them'} to the program, which
|
|
1922
|
+
`The project takes ${m.collections.length} collection${m.collections.length === 1 ? '' : 's'} you already have: going live passes ${m.collections.length === 1 ? 'it' : 'them'} to the program, which takes one whose update authority is your wallet, or a Core collection where the project is an update delegate (BadCollection otherwise). An item minted into one needs the project as an update delegate of it, which you add to the collection yourself.`,
|
|
1923
|
+
);
|
|
1924
|
+
// A Core collection where the project is only an update delegate: its owner and every other delegate can
|
|
1925
|
+
// write to it, and what they make reads as the project's own.
|
|
1926
|
+
if (m.collections.length)
|
|
1927
|
+
warn(
|
|
1928
|
+
'W-COLLECTION-PARTNER',
|
|
1929
|
+
'economy.collections',
|
|
1930
|
+
'If the project is only an update delegate of a collection it names (someone else owns it), you trust that partner: every item they mint into it counts as your own, and they can take your access away at any time. Anyone who can write to that collection, its owner and every other update delegate they named, can make an NFT that reads as any of the project’s items, in any of its collections, by minting one there with that "item_id": it passes the project’s gates, boosts and odds for that item and takes its missions’ prizes. Two projects that name one shared collection read each other’s items of the same number as their own. Once the access is taken away, purchases of items in it are refused, a won item can only be settled by giving it up, a staked NFT in a farm that writes on it leaves only by forfeit, and a box that holds a player’s NFT to change its numbers cannot settle until the access is back.',
|
|
1931
|
+
'Name only a collection whose every writer you trust, and never one another loot project also names.',
|
|
1854
1932
|
);
|
|
1855
1933
|
const given = new Set();
|
|
1856
1934
|
for (const b of m.allBoxes)
|
|
@@ -1912,6 +1990,15 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
1912
1990
|
'Shorten it.',
|
|
1913
1991
|
);
|
|
1914
1992
|
});
|
|
1993
|
+
// A row of weight 0 or stock 0 is never drawn; a table with none that can be is drawn from as an empty one.
|
|
1994
|
+
const dead = (t.rows ?? []).map((r, i) => [r, i]).filter(([r]) => r.weight === 0 || r.stock === 0);
|
|
1995
|
+
if (dead.length)
|
|
1996
|
+
warn(
|
|
1997
|
+
'W-ZERO',
|
|
1998
|
+
`${where}.rows`,
|
|
1999
|
+
`${dead.length === (t.rows ?? []).length ? 'Every row' : `Row${dead.length === 1 ? '' : 's'} ${dead.map(([, i]) => i).join(', ')}`} of the table “${t.key}” ${dead.length === 1 ? 'has' : 'have'} a weight or a stock of 0: never drawn${dead.length === (t.rows ?? []).length ? ', so a draw from it finds nothing' : ''}.`,
|
|
2000
|
+
'Give each row a weight and a stock above 0, or leave the row out.',
|
|
2001
|
+
);
|
|
1915
2002
|
if (!m.items.some(it => (it.drawn ?? []).some(d => d.table === t.key)))
|
|
1916
2003
|
warn(
|
|
1917
2004
|
'W-TABLE',
|
|
@@ -2141,6 +2228,15 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
2141
2228
|
}
|
|
2142
2229
|
const users = stashUsers.get(s.key);
|
|
2143
2230
|
const named = directs.filter(p => p.prize.stash === s.key);
|
|
2231
|
+
// NFTs of weight 0: never drawn, given only by name.
|
|
2232
|
+
const light = stashNfts(s).filter(n => n.weight === 0);
|
|
2233
|
+
if (light.length)
|
|
2234
|
+
warn(
|
|
2235
|
+
'W-STASH',
|
|
2236
|
+
`${where}.nfts`,
|
|
2237
|
+
`${label(s)} holds ${light.length} NFT${light.length === 1 ? '' : 's'} of weight 0: never drawn, and given only by name. A raffle counts them among the stash’s NFTs, so a raffle with more places than NFTs that weigh something leaves the extra places with nothing${light.length === stashNfts(s).length ? '; with none that weighs, a draw from it finds nothing, and a box that stops selling when an outcome is out is not sold' : '; a box that stops selling when out may land on the stash and find nothing to draw'}.`,
|
|
2238
|
+
'Give the NFTs to draw a weight above 0; keep weight 0 for NFTs a prize names.',
|
|
2239
|
+
);
|
|
2144
2240
|
// A known limit of the program: a named NFT is found by reading the stash's list from its start, about 52
|
|
2145
2241
|
// compute units for each NFT ahead of it, and a box's roll looks for every NFT it names, then takes the one won. A
|
|
2146
2242
|
// stash that players fill (a box's fate, a recipe's trade-in, a lottery's NFT ticket) can grow past what fits.
|
|
@@ -2162,7 +2258,7 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
2162
2258
|
}
|
|
2163
2259
|
if (!(s.nfts ?? []).length && !intoStash.has(s.key)) {
|
|
2164
2260
|
// A box that stops selling when an outcome runs out (the default) isn't sold at all while it is empty.
|
|
2165
|
-
const stopped = users.filter(b => b.active !== false && b
|
|
2261
|
+
const stopped = users.filter(b => b.active !== false && stopsSales(b) && !boxBuys(b, m));
|
|
2166
2262
|
warn(
|
|
2167
2263
|
'W-STASH',
|
|
2168
2264
|
`${where}.nfts`,
|
|
@@ -2222,6 +2318,82 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
2222
2318
|
}
|
|
2223
2319
|
|
|
2224
2320
|
// ---- boxes (catalog.rs, order.rs)
|
|
2321
|
+
/**
|
|
2322
|
+
* What one answer of the oracle's holds: the program refuses a box when it is written (PackTooLarge) when one
|
|
2323
|
+
* answer can't hold a draw of it, a box of one draw included. Each measured number (`ANSWER_HOLDS`) is the most of
|
|
2324
|
+
* one thing in a box otherwise small: past one, the box is refused here; a box that mixes several near their numbers
|
|
2325
|
+
* is warned of, since together they fit less.
|
|
2326
|
+
*/
|
|
2327
|
+
const answerHolds = (b, where) => {
|
|
2328
|
+
const outs = b.outcomes ?? [];
|
|
2329
|
+
const texts = r => (r?.oneOf ?? []).length + (r?.items ?? []).length;
|
|
2330
|
+
const parts = outs.map(outcomeParts);
|
|
2331
|
+
const tokenOutcomes = parts.filter(p => p.some(x => x.gives === 'pay' || x.gives === 'mint')).length;
|
|
2332
|
+
const itemOutcomes = parts.filter(p => p.some(x => x.gives === 'item')).length;
|
|
2333
|
+
const gateTexts = texts(b.gate);
|
|
2334
|
+
const oddsTexts = (b.odds ?? []).reduce((n, o) => n + texts(o.when), 0);
|
|
2335
|
+
const over = [
|
|
2336
|
+
[outs.length, ANSWER_HOLDS.entriesOfNothing, `${outs.length} outcomes`, `${where}.outcomes`],
|
|
2337
|
+
[(b.slots ?? []).length, ANSWER_HOLDS.slots, `${(b.slots ?? []).length} slots`, `${where}.slots`],
|
|
2338
|
+
[(b.odds ?? []).length, ANSWER_HOLDS.odds, `${(b.odds ?? []).length} odds rules`, `${where}.odds`],
|
|
2339
|
+
[gateTexts, ANSWER_HOLDS.gateTexts, `${gateTexts} texts in its gate`, `${where}.gate`],
|
|
2340
|
+
...parts.flatMap((p, i) => [
|
|
2341
|
+
[
|
|
2342
|
+
p.filter(x => x.gives === 'pay' || x.gives === 'mint').length,
|
|
2343
|
+
ANSWER_HOLDS.entryTokens,
|
|
2344
|
+
`${p.filter(x => x.gives === 'pay' || x.gives === 'mint').length} token prizes in outcome ${i}`,
|
|
2345
|
+
`${where}.outcomes[${i}]`,
|
|
2346
|
+
],
|
|
2347
|
+
[
|
|
2348
|
+
p.filter(x => x.gives === 'item').length,
|
|
2349
|
+
ANSWER_HOLDS.entryCollectibles,
|
|
2350
|
+
`${p.filter(x => x.gives === 'item').length} items in outcome ${i}`,
|
|
2351
|
+
`${where}.outcomes[${i}]`,
|
|
2352
|
+
],
|
|
2353
|
+
]),
|
|
2354
|
+
].filter(([n, most]) => n > most);
|
|
2355
|
+
for (const [, most, what, at] of over)
|
|
2356
|
+
refuse(
|
|
2357
|
+
'E-ODDS',
|
|
2358
|
+
at,
|
|
2359
|
+
`${label(b)} has ${what}: one answer of the oracle’s holds at most ${most} of that, as measured, and the program refuses a box when it is written that one answer can’t draw (PackTooLarge).`,
|
|
2360
|
+
'Put a long weighted list in a table (an item that draws a trait from it) or a stash, drawn by one outcome; give fewer prizes in one outcome; keep a gate’s "oneOf" short.',
|
|
2361
|
+
);
|
|
2362
|
+
if (over.length) return;
|
|
2363
|
+
// How much of one answer the box takes, each part at its measured number: a guide, not the program's count.
|
|
2364
|
+
const used =
|
|
2365
|
+
(outs.length - tokenOutcomes - itemOutcomes) / ANSWER_HOLDS.entriesOfNothing +
|
|
2366
|
+
tokenOutcomes / ANSWER_HOLDS.entriesOfTokens +
|
|
2367
|
+
itemOutcomes / ANSWER_HOLDS.entriesOfItems +
|
|
2368
|
+
(b.slots ?? []).length / ANSWER_HOLDS.slots +
|
|
2369
|
+
(b.odds ?? []).length / ANSWER_HOLDS.odds +
|
|
2370
|
+
gateTexts / ANSWER_HOLDS.gateTexts;
|
|
2371
|
+
if (used > 0.75)
|
|
2372
|
+
warn(
|
|
2373
|
+
'W-ANSWER',
|
|
2374
|
+
where,
|
|
2375
|
+
`${label(b)} is long: by the measured numbers it takes about ${Math.round(used * 100)}% of what one answer of the oracle’s holds. One answer holds 128 outcomes that give nothing, 29 of token amounts or 18 each of its own item; 41 slots; 72 odds rules; 16 token prizes or 18 collectibles in one outcome; 340 texts in a box’s gate. A box that mixes them fits less, and one that does not fit is refused when it is written (PackTooLarge). A box written from a wallet carries about 960 bytes; a longer one needs a program call (a multisig).`,
|
|
2376
|
+
'Put a long weighted list in a table or a stash, drawn by one outcome.',
|
|
2377
|
+
);
|
|
2378
|
+
// An outcome of 15 to 18 collectibles fits an answer, but not a sale in one step.
|
|
2379
|
+
const many = parts.findIndex(p => p.filter(x => x.gives === 'item').length >= 15);
|
|
2380
|
+
if (many >= 0 && outs.length === 1 && boxBuys(b, m))
|
|
2381
|
+
warn(
|
|
2382
|
+
'W-ANSWER',
|
|
2383
|
+
`${where}.outcomes[${many}]`,
|
|
2384
|
+
`${label(b)} gives ${parts[many].filter(x => x.gives === 'item').length} collectibles in one outcome: a box of one outcome with 15 to 18 collectibles is too wide for a sale in one step and sells only through the oracle (an open, then a settle).`,
|
|
2385
|
+
'Fine as it is; or split the bundle in two boxes to sell each in one step.',
|
|
2386
|
+
);
|
|
2387
|
+
if (oddsTexts > 200)
|
|
2388
|
+
warn(
|
|
2389
|
+
'W-ANSWER',
|
|
2390
|
+
`${where}.odds`,
|
|
2391
|
+
`${label(b)}’s odds rules list ${oddsTexts} "one of" texts in all: the oracle’s answer reads them all, and past a few hundred it cannot land, so every purchase is refunded after 10 minutes.`,
|
|
2392
|
+
'Keep "one of" lists in odds rules short, or raise the odds by a number (an attribute "atLeast").',
|
|
2393
|
+
);
|
|
2394
|
+
};
|
|
2395
|
+
/** Every prize a route gives (a grant, a place, a mission, a recipe...), for what a box shares with them. */
|
|
2396
|
+
const routePrizes = prizes(m);
|
|
2225
2397
|
let chance = false;
|
|
2226
2398
|
let randomItems = false;
|
|
2227
2399
|
/** Per pool, the largest fixed prize a box pays from it, and every box that pays one: warned of once a pool, below. */
|
|
@@ -2232,43 +2404,9 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
2232
2404
|
// rolls, or one outcome of items, with no outcome that burns the payment and no NFT to roll on (BadEntries).
|
|
2233
2405
|
const slots = b.slots ?? [];
|
|
2234
2406
|
const outs = b.outcomes ?? [];
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
'E-ODDS',
|
|
2239
|
-
`${where}.outcomes`,
|
|
2240
|
-
`${label(b)} has ${outs.length} outcomes`,
|
|
2241
|
-
'BadEntries',
|
|
2242
|
-
'Use fewer outcomes, or roll on a table of rows ("drawn").',
|
|
2243
|
-
);
|
|
2244
|
-
tooMany(
|
|
2245
|
-
slots.length,
|
|
2246
|
-
LIMITS.boxSlots,
|
|
2247
|
-
'E-ODDS',
|
|
2248
|
-
`${where}.slots`,
|
|
2249
|
-
`${label(b)} has ${slots.length} slots`,
|
|
2250
|
-
'BadEntries',
|
|
2251
|
-
'Put alike draws into one slot with "count".',
|
|
2252
|
-
);
|
|
2253
|
-
tooMany(
|
|
2254
|
-
(b.odds ?? []).length,
|
|
2255
|
-
LIMITS.boxOdds,
|
|
2256
|
-
'E-ODDS',
|
|
2257
|
-
`${where}.odds`,
|
|
2258
|
-
`${label(b)} has ${(b.odds ?? []).length} odds that read an NFT`,
|
|
2259
|
-
'BadEntries',
|
|
2260
|
-
'Keep the ones that matter.',
|
|
2261
|
-
);
|
|
2262
|
-
const fated = outs.filter(o => (o.shifts ?? []).length || (o.then ?? b.then ?? 'back') !== 'back').length;
|
|
2263
|
-
tooMany(
|
|
2264
|
-
fated,
|
|
2265
|
-
LIMITS.boxFates,
|
|
2266
|
-
'E-ROLL',
|
|
2267
|
-
`${where}.outcomes`,
|
|
2268
|
-
`${fated} outcomes of ${label(b)} act on the NFT it rolls on`,
|
|
2269
|
-
'BadEntries',
|
|
2270
|
-
'Have fewer outcomes act on it.',
|
|
2271
|
-
);
|
|
2407
|
+
const heldBefore = refusals.length;
|
|
2408
|
+
answerHolds(b, where);
|
|
2409
|
+
const answerRefused = refusals.length > heldBefore;
|
|
2272
2410
|
for (const [i, o] of outs.entries()) {
|
|
2273
2411
|
const given = outcomeParts(o);
|
|
2274
2412
|
tooMany(
|
|
@@ -2280,6 +2418,14 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
2280
2418
|
'BadEntries',
|
|
2281
2419
|
'Give fewer, or make it two outcomes.',
|
|
2282
2420
|
);
|
|
2421
|
+
// What a roll changes on the NFT it is on is written by the settle, with the NFT's own attributes.
|
|
2422
|
+
if ((o.shifts ?? []).length > 16)
|
|
2423
|
+
warn(
|
|
2424
|
+
'W-ANSWER',
|
|
2425
|
+
`${where}.outcomes[${i}].shifts`,
|
|
2426
|
+
`An outcome of ${label(b)} changes ${(o.shifts ?? []).length} numbers on the NFT it rolls on. The settle writes them with the NFT’s own attributes, and together they must stay under about 60, or the settle cannot land and a held NFT waits with the project.`,
|
|
2427
|
+
'Change fewer numbers in one outcome, and keep the NFTs it rolls on to few attributes.',
|
|
2428
|
+
);
|
|
2283
2429
|
// An outcome gives all it names or isn't given (order.rs): what it names is refused at the write when it can clash.
|
|
2284
2430
|
for (const [j, x] of given.entries()) {
|
|
2285
2431
|
const clash = given
|
|
@@ -2302,14 +2448,14 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
2302
2448
|
warn(
|
|
2303
2449
|
'W-OUT-OF-STOCK',
|
|
2304
2450
|
`${where}.outcomes[${i}]`,
|
|
2305
|
-
`An outcome of ${label(b)} gives ${count} of ${label(m.item.get(key))}, which has a supply of ${supply}: it needs them all left, so it can never be given${b
|
|
2451
|
+
`An outcome of ${label(b)} gives ${count} of ${label(m.item.get(key))}, which has a supply of ${supply}: it needs them all left, so it can never be given${stopsSales(b) && b.active !== false && boxRolls(b, m) ? ', and a box that stops selling while an outcome is out ("whenOut": "stopSales") is never sold' : ', and a draw that lands on it gives back that draw’s worth'}.`,
|
|
2306
2452
|
'Give fewer, or raise its supply.',
|
|
2307
2453
|
);
|
|
2308
2454
|
}
|
|
2309
2455
|
}
|
|
2310
2456
|
// A box that stops selling once an outcome is out covers each purchase's draws, and a random draw of a stash could
|
|
2311
2457
|
// take one of its NFTs another outcome names: it can't list both (catalog.rs, BadEntries).
|
|
2312
|
-
if (b
|
|
2458
|
+
if (stopsSales(b)) {
|
|
2313
2459
|
// One outcome naming both is refused above already.
|
|
2314
2460
|
const parts = outs.map(outcomeParts);
|
|
2315
2461
|
const random = parts.flatMap((p, i) => p.filter(x => x.gives === 'stash').map(x => [i, x.stash]));
|
|
@@ -2325,7 +2471,7 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
2325
2471
|
'E-ODDS',
|
|
2326
2472
|
`${where}.outcomes`,
|
|
2327
2473
|
`${label(b)} gives an NFT of the stash “${key}” at random and, in another outcome, one of its NFTs by name, and it stops selling once an outcome is out ("whenOut": "stopSales", the default): a random draw could take the named one, so nothing could cover it, and the program refuses the box (BadEntries).`,
|
|
2328
|
-
`Make it "whenOut": "drawLeft", or give the named NFT from a stash of its own.`,
|
|
2474
|
+
`Make it "whenOut": "drawLeft" or "giveNothing", or give the named NFT from a stash of its own.`,
|
|
2329
2475
|
);
|
|
2330
2476
|
}
|
|
2331
2477
|
// A fresh outcome's tables come with the answer while they fit what the box's own accounts leave, in outcome order
|
|
@@ -2417,8 +2563,10 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
2417
2563
|
// Every draw of one open fits one answer of the oracle's (PackTooLarge): a pack past it could only draw its first
|
|
2418
2564
|
// cards, so the program refuses the box. A box that doesn't roll sells by `buy`, the whole purchase in one
|
|
2419
2565
|
// transaction, or through the oracle when one `buy` can't carry it (`buyFits`): then the answer's heap counts, and it
|
|
2420
|
-
// can't give a roll, which only `buy` gives (catalog.rs). The heap counts the box's own bytes too (`boxBytes`)
|
|
2421
|
-
|
|
2566
|
+
// can't give a roll, which only `buy` gives (catalog.rs). The heap counts the box's own bytes too (`boxBytes`), and
|
|
2567
|
+
// the compute finding each pool's and stash's address its prizes name (exact with the project, else assumed).
|
|
2568
|
+
// A box of one draw is held to the same count: past it the program refuses the box when it is written.
|
|
2569
|
+
if (outs.length && (cards > 1 || !answerRefused)) {
|
|
2422
2570
|
const args = (() => {
|
|
2423
2571
|
try {
|
|
2424
2572
|
return boxArgs(b, m);
|
|
@@ -2428,38 +2576,45 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
2428
2576
|
})();
|
|
2429
2577
|
const entries = args?.entries;
|
|
2430
2578
|
if (entries) {
|
|
2431
|
-
const byBuy = boxRolls(b, m) || Boolean(b.rollsOn) || buyFits(args, cards);
|
|
2579
|
+
const byBuy = boxRolls(b, m) || Boolean(b.rollsOn) || buyFits(args, Math.max(1, cards));
|
|
2432
2580
|
const extras = {
|
|
2433
2581
|
odds: args.odds.length,
|
|
2434
2582
|
shares: args.shares.length,
|
|
2435
2583
|
fates: args.fates.length,
|
|
2436
2584
|
bytes: boxBytes(args),
|
|
2437
2585
|
oracle: !byBuy,
|
|
2586
|
+
project,
|
|
2438
2587
|
};
|
|
2588
|
+
const searches = entries.some(e => e.prizes.some(p => p.Tokens?.from?.Pool || p.Stash != null || p.Nft));
|
|
2589
|
+
const searched = searches
|
|
2590
|
+
? project
|
|
2591
|
+
? ' (finding the address of each pool and stash it names counted for this project)'
|
|
2592
|
+
: ` (finding the address of each pool and stash it names counted at ${ASSUMED_TRIES} tries each, as no wallet is named yet: the check with your wallet counts it exactly)`
|
|
2593
|
+
: '';
|
|
2594
|
+
const drawn = slots.length ? slots.map(s => ({ count: s.count, only: s.from ?? [] })) : 1;
|
|
2439
2595
|
if (!byBuy && entries.some(e => e.prizes.some(p => p.Roll != null)))
|
|
2440
2596
|
refuse(
|
|
2441
2597
|
'E-ODDS',
|
|
2442
2598
|
`${where}.slots`,
|
|
2443
|
-
`${label(b)} gives a roll, which only a sale in one step gives, but ${cards} of what it gives don’t fit one transaction (its accounts, its instructions or its compute), so the program refuses the box (PackTooLarge).`,
|
|
2599
|
+
`${label(b)} gives a roll, which only a sale in one step gives, but ${Math.max(1, cards)} of what it gives don’t fit one transaction (its accounts, its instructions or its compute), so the program refuses the box (PackTooLarge).`,
|
|
2444
2600
|
'Fewer cards an open, fewer prizes in its outcome, or fewer shares in its split.',
|
|
2445
2601
|
);
|
|
2446
|
-
else if (
|
|
2447
|
-
!drawsFit(
|
|
2448
|
-
entries,
|
|
2449
|
-
slots.map(s => ({ count: s.count, only: s.from ?? [] })),
|
|
2450
|
-
1,
|
|
2451
|
-
extras,
|
|
2452
|
-
)
|
|
2453
|
-
) {
|
|
2602
|
+
else if (cards > 1 && !drawsFit(entries, drawn, 1, extras)) {
|
|
2454
2603
|
let most = cards - 1;
|
|
2455
2604
|
while (most > 1 && !drawsFit(entries, most, 1, extras)) most -= 1;
|
|
2456
2605
|
refuse(
|
|
2457
2606
|
'E-ODDS',
|
|
2458
2607
|
`${where}.slots`,
|
|
2459
|
-
`${label(b)} draws ${cards} cards an open among ${outs.length} outcome${outs.length === 1 ? '' : 's'}: more than one answer of the oracle’s can draw (about ${most} here), so the program refuses the box (PackTooLarge).`,
|
|
2608
|
+
`${label(b)} draws ${cards} cards an open among ${outs.length} outcome${outs.length === 1 ? '' : 's'}: more than one answer of the oracle’s can draw (about ${most} here${searched}), so the program refuses the box (PackTooLarge).`,
|
|
2460
2609
|
'Fewer cards an open (sell two packs instead of one), fewer outcomes, fewer prizes in an outcome, or a smaller box (fewer odds, shares, slots or outcomes that act on the NFT).',
|
|
2461
2610
|
);
|
|
2462
|
-
}
|
|
2611
|
+
} else if (cards <= 1 && !drawsFit(entries, drawn, 1, extras))
|
|
2612
|
+
refuse(
|
|
2613
|
+
'E-ODDS',
|
|
2614
|
+
`${where}.outcomes`,
|
|
2615
|
+
`${label(b)} is too long for one answer of the oracle’s to draw it once${searched}, so the program refuses the box when it is written (PackTooLarge).`,
|
|
2616
|
+
'Fewer outcomes, fewer prizes in an outcome, fewer pools or stashes named, or a smaller box (fewer odds, shares or slots). Put a long weighted list in a table or a stash, drawn by one outcome.',
|
|
2617
|
+
);
|
|
2463
2618
|
}
|
|
2464
2619
|
}
|
|
2465
2620
|
if (cards > 1 && b.rollsOn)
|
|
@@ -2799,11 +2954,12 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
2799
2954
|
: refuse('E-ODDS', at, `An outcome of ${label(b)} pays from a pool but does not say which.`, 'Add "pool".');
|
|
2800
2955
|
if (p) {
|
|
2801
2956
|
const amount = units(o.amount, m.tokens.get(p.currency), `${at}.amount`, `${label(b)}’s prize`);
|
|
2957
|
+
// A prize of 0 tokens is given with nothing moved.
|
|
2802
2958
|
if (amount === 0n)
|
|
2803
|
-
|
|
2804
|
-
'
|
|
2959
|
+
warn(
|
|
2960
|
+
'W-ZERO',
|
|
2805
2961
|
`${at}.amount`,
|
|
2806
|
-
`An outcome of ${label(b)} pays nothing
|
|
2962
|
+
`An outcome of ${label(b)} pays 0: it is drawn and given, and its winner gets nothing.`,
|
|
2807
2963
|
'Give it an amount above 0, or make it "nothing".',
|
|
2808
2964
|
);
|
|
2809
2965
|
// In base units, exact (null when it is not an amount, refused above).
|
|
@@ -2909,6 +3065,54 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
2909
3065
|
`${label(b)} sells in one step, all of it or nothing, so "whenOut" changes nothing.`,
|
|
2910
3066
|
'Leave "whenOut" out.',
|
|
2911
3067
|
);
|
|
3068
|
+
// What a draw on an outcome that is out does, by the setting the project chose, and who can make it happen by
|
|
3069
|
+
// emptying a source this box shares with another route.
|
|
3070
|
+
const runsDry = o =>
|
|
3071
|
+
outcomeParts(o).some(
|
|
3072
|
+
x =>
|
|
3073
|
+
x.gives === 'pay' ||
|
|
3074
|
+
x.gives === 'stash' ||
|
|
3075
|
+
x.gives === 'nft' ||
|
|
3076
|
+
(x.gives === 'item' && Number(m.item.get(x.item)?.supply ?? 0) > 0),
|
|
3077
|
+
);
|
|
3078
|
+
const rolledHere = !boxBuys(b, m) || rolls.some(r => r.box === b.key);
|
|
3079
|
+
if (b.whenOut === 'giveNothing' && rolledHere)
|
|
3080
|
+
warn(
|
|
3081
|
+
'W-WHEN-OUT',
|
|
3082
|
+
`${where}.whenOut`,
|
|
3083
|
+
`${label(b)} gives nothing for a draw on an outcome that is out ("whenOut": "giveNothing"): a paid draw is not refunded, a free roll is over, and the box keeps selling. Anyone who empties what an outcome gives (a pool, an item’s units, a stash), by this box or another route that takes from it, makes its rolls miss.`,
|
|
3084
|
+
'Tell players a draw can miss, keep each outcome in stock, or keep "stopSales" (a paid miss refunded) or "drawLeft" (a draw among what is left).',
|
|
3085
|
+
);
|
|
3086
|
+
const sharedSources = outs
|
|
3087
|
+
.flatMap(outcomeParts)
|
|
3088
|
+
.map(x =>
|
|
3089
|
+
x.gives === 'pay'
|
|
3090
|
+
? ['pool', x.pool]
|
|
3091
|
+
: x.gives === 'stash' || x.gives === 'nft'
|
|
3092
|
+
? ['stash', x.stash]
|
|
3093
|
+
: x.gives === 'item'
|
|
3094
|
+
? ['item', x.item]
|
|
3095
|
+
: null,
|
|
3096
|
+
)
|
|
3097
|
+
.filter(Boolean)
|
|
3098
|
+
.filter(([kind, key]) =>
|
|
3099
|
+
kind === 'pool'
|
|
3100
|
+
? takers.some(t => t.pool === key && t.by !== `box:${b.key}`)
|
|
3101
|
+
: kind === 'stash'
|
|
3102
|
+
? routePrizes.some(p => p.prize.stash === key) ||
|
|
3103
|
+
m.allBoxes.some(x => x !== b && (x.outcomes ?? []).flatMap(outcomeParts).some(y => y.stash === key))
|
|
3104
|
+
: Number(m.item.get(key)?.supply ?? 0) > 0 &&
|
|
3105
|
+
(Number(m.item.get(key)?.price ?? 0) > 0 ||
|
|
3106
|
+
routePrizes.some(p => p.prize.item === key) ||
|
|
3107
|
+
m.allBoxes.some(x => x !== b && (x.outcomes ?? []).flatMap(outcomeParts).some(y => y.item === key))),
|
|
3108
|
+
);
|
|
3109
|
+
if (sharedSources.length && rolledHere && outs.some(runsDry) && b.active !== false)
|
|
3110
|
+
warn(
|
|
3111
|
+
'W-SHARED-SOURCE',
|
|
3112
|
+
`${where}.outcomes`,
|
|
3113
|
+
`${label(b)} draws from ${[...new Set(sharedSources.map(([kind, key]) => `the ${kind} “${key}”`))].join(', ')}, which another route also takes from. ${stopsSales(b) ? 'A buyer can open the box, empty that source by the other route before the answers land, and be refunded every miss' : b.whenOut === 'giveNothing' ? 'Anyone emptying that source makes this box’s rolls miss, and a miss gives nothing' : 'Anyone emptying that source moves this box’s odds to what is left'}.`,
|
|
3114
|
+
'Give the box sources of its own; cap a fresh item’s supply at what its table holds.',
|
|
3115
|
+
);
|
|
2912
3116
|
if (b.whenOut === 'drawLeft' && boxRolls(b, m)) {
|
|
2913
3117
|
// The rarest outcome that gives something, against the likeliest outcome that can run out.
|
|
2914
3118
|
const w = weights(b);
|
|
@@ -2950,7 +3154,7 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
2950
3154
|
const others = [...boxes].filter(x => x !== box).map(label);
|
|
2951
3155
|
// A box that stops selling when an outcome is out (the default) needs the prize for this purchase's draws and every
|
|
2952
3156
|
// draw still waiting for the oracle before it sells at all.
|
|
2953
|
-
const stopped = [...boxes].filter(x => x.active !== false && x
|
|
3157
|
+
const stopped = [...boxes].filter(x => x.active !== false && stopsSales(x) && boxRolls(x, m));
|
|
2954
3158
|
const stops = stopped.length
|
|
2955
3159
|
? ` ${stopped.map(label).join(', ')} stop${stopped.length === 1 ? 's' : ''} selling while an outcome is out ("whenOut": "stopSales"): ${stopped.length === 1 ? 'it isn’t' : 'they aren’t'} sold at all while the pool holds less than the prize for that purchase’s draws and every draw still waiting for the oracle.`
|
|
2956
3160
|
: '';
|
|
@@ -3017,22 +3221,32 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
3017
3221
|
};
|
|
3018
3222
|
}
|
|
3019
3223
|
})();
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
['
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3224
|
+
// A recipe takes any number of lines; what one craft can carry is measured: past it, the recipe is written
|
|
3225
|
+
// and never crafts.
|
|
3226
|
+
{
|
|
3227
|
+
const pays = ins.filter(l => l.takes === 'pay');
|
|
3228
|
+
const coins = new Set(pays.map(l => l.currency ?? m.currencyKey)).size;
|
|
3229
|
+
const pooled = (r.out ?? []).filter(l => l.gives === 'tokens' && prizePool(l)).length;
|
|
3230
|
+
const raised = lines.raises.length + lines.needs.length;
|
|
3231
|
+
const past = [
|
|
3232
|
+
coins > 1 &&
|
|
3233
|
+
pays.length > ANSWER_HOLDS.recipeCoins &&
|
|
3234
|
+
`${pays.length} payments in ${coins} coins (at most ${ANSWER_HOLDS.recipeCoins})`,
|
|
3235
|
+
coins <= 1 &&
|
|
3236
|
+
pays.length > ANSWER_HOLDS.recipePayments &&
|
|
3237
|
+
`${pays.length} payments (at most ${ANSWER_HOLDS.recipePayments} in one coin)`,
|
|
3238
|
+
pooled > ANSWER_HOLDS.recipePoolOutputs &&
|
|
3239
|
+
`${pooled} outputs from a pool (at most ${ANSWER_HOLDS.recipePoolOutputs})`,
|
|
3240
|
+
raised > ANSWER_HOLDS.recipeRaises && `${raised} raises and needs (at most ${ANSWER_HOLDS.recipeRaises})`,
|
|
3241
|
+
].filter(Boolean);
|
|
3242
|
+
if (past.length)
|
|
3243
|
+
warn(
|
|
3244
|
+
'W-ANSWER',
|
|
3245
|
+
where,
|
|
3246
|
+
`${label(r)} has ${past.join(', ')}: the program writes it, but one craft can’t carry that much, so it never crafts.`,
|
|
3247
|
+
'Split it into two recipes.',
|
|
3248
|
+
);
|
|
3249
|
+
}
|
|
3036
3250
|
const outs = r.out ?? [];
|
|
3037
3251
|
if (!ins.length && !outs.length)
|
|
3038
3252
|
warn(
|
|
@@ -3226,13 +3440,28 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
3226
3440
|
const places = ins.flatMap(l =>
|
|
3227
3441
|
l.takes === 'burn' ? Array(l.count ?? 1).fill('burn') : l.takes === 'pay' ? [] : [l.takes],
|
|
3228
3442
|
);
|
|
3443
|
+
// Each roll is its own order, at the craft's nonce plus its count (craft.rs); its receipts must fit the log.
|
|
3229
3444
|
const rolls = outs.filter(l => l.gives === 'roll');
|
|
3230
|
-
if (rolls.length >
|
|
3231
|
-
|
|
3232
|
-
'
|
|
3445
|
+
if (rolls.length > ANSWER_HOLDS.recipeRolls)
|
|
3446
|
+
warn(
|
|
3447
|
+
'W-ANSWER',
|
|
3448
|
+
`${where}.out`,
|
|
3449
|
+
`${label(r)} gives ${rolls.length} rolls: past ${ANSWER_HOLDS.recipeRolls}, a craft’s receipts run past what a transaction’s log holds (10,000 bytes), and its Crafted receipt is cut, so an indexer or a results page can’t read what it made.`,
|
|
3450
|
+
`Give at most ${ANSWER_HOLDS.recipeRolls} rolls a craft, or roll a box that gives a pack.`,
|
|
3451
|
+
);
|
|
3452
|
+
// Two rolls on one NFT whose box holds it while it rolls: the craft fails whole.
|
|
3453
|
+
const held = rolls.filter(
|
|
3454
|
+
l =>
|
|
3455
|
+
l.on !== undefined &&
|
|
3456
|
+
(m.box.get(l.box)?.outcomes ?? []).some(o => (o.shifts ?? []).length || (o.then && o.then !== 'back')),
|
|
3457
|
+
);
|
|
3458
|
+
const twice = held.find((l, i) => held.slice(0, i).some(x => x.on === l.on));
|
|
3459
|
+
if (twice)
|
|
3460
|
+
warn(
|
|
3461
|
+
'W-RECIPE',
|
|
3233
3462
|
`${where}.out`,
|
|
3234
|
-
`${label(r)} rolls ${
|
|
3235
|
-
'
|
|
3463
|
+
`${label(r)} gives two rolls on collectible ${twice.on}, and a box that holds the NFT while it rolls: the second can’t take the NFT the first holds, so every craft of it fails whole.`,
|
|
3464
|
+
'Give one roll on each collectible.',
|
|
3236
3465
|
);
|
|
3237
3466
|
for (const [i, line] of outs.entries()) {
|
|
3238
3467
|
if (line.gives !== 'roll') continue;
|
|
@@ -3394,7 +3623,15 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
3394
3623
|
if (fromNowSeconds(s.every, `${where}.every`) <= 0)
|
|
3395
3624
|
refuse('E-SCHEDULE', `${where}.every`, `${label(s)} never comes round (every 0).`, 'Give it a time like "1d".');
|
|
3396
3625
|
try {
|
|
3397
|
-
toTime(s.start, now);
|
|
3626
|
+
farTime(toTime(s.start, now), `${where}.start`, `${label(s)}’s start`, s.start, 'it runs nothing');
|
|
3627
|
+
if (!s.sealed)
|
|
3628
|
+
farTime(
|
|
3629
|
+
now + seconds(s.every, `${where}.every`),
|
|
3630
|
+
`${where}.every`,
|
|
3631
|
+
`${label(s)}’s next run`,
|
|
3632
|
+
s.every,
|
|
3633
|
+
'it runs nothing',
|
|
3634
|
+
);
|
|
3398
3635
|
} catch (err) {
|
|
3399
3636
|
refuse(err.code, `${where}.start`, err.message, err.fix);
|
|
3400
3637
|
}
|
|
@@ -3481,16 +3718,17 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
3481
3718
|
'Leave "active" out.',
|
|
3482
3719
|
);
|
|
3483
3720
|
const d = destination(s.to ?? '');
|
|
3484
|
-
// A
|
|
3485
|
-
//
|
|
3721
|
+
// A payee nobody signs for (an address off the curve): a multisig's vault is one, and so is an address of this
|
|
3722
|
+
// program, where a record made later would make every run pass it over. The program refuses only an
|
|
3723
|
+
// account this program already owns (BadMove), which it checks when the schedule is written.
|
|
3486
3724
|
if (d.kind === 'wallet' && isAddress(d.key) && !isOnCurve(addressBytes(d.key)))
|
|
3487
|
-
|
|
3488
|
-
'
|
|
3725
|
+
warn(
|
|
3726
|
+
'W-SCHEDULE-PAYEE',
|
|
3489
3727
|
`${where}.to`,
|
|
3490
|
-
`${label(s)} is sealed and pays ${d.key},
|
|
3491
|
-
'
|
|
3728
|
+
`${label(s)} is sealed and pays ${d.key}, an address nobody holds a key of: a program's address. A multisig's vault is fine; an address of the loot program is not: a record made there later would make every run pass it over, and the vault would never empty. Nothing can change the payee once it is sealed.`,
|
|
3729
|
+
'Check the address is a wallet or a multisig vault you can spend from, before you sign.',
|
|
3492
3730
|
);
|
|
3493
|
-
// Its last run
|
|
3731
|
+
// Its last run: far off is taken, and warned of with its date.
|
|
3494
3732
|
if (s.maxRuns > 0) {
|
|
3495
3733
|
let first = now + seconds(s.every, `${where}.every`);
|
|
3496
3734
|
try {
|
|
@@ -3498,21 +3736,23 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
3498
3736
|
} catch {
|
|
3499
3737
|
// A start that doesn't read is refused where the schedule's times are checked.
|
|
3500
3738
|
}
|
|
3501
|
-
|
|
3739
|
+
const last = first + seconds(s.every, `${where}.every`) * (s.maxRuns - 1);
|
|
3740
|
+
if (last > Number.MAX_SAFE_INTEGER / 1000)
|
|
3502
3741
|
refuse(
|
|
3503
3742
|
'E-SCHEDULE',
|
|
3504
3743
|
`${where}.maxRuns`,
|
|
3505
|
-
`${label(s)} is sealed and its last run would come
|
|
3744
|
+
`${label(s)} is sealed and its last run would come further off than a date holds (BadSchedule).`,
|
|
3506
3745
|
'Fewer runs, a shorter "every", or an earlier "start".',
|
|
3507
3746
|
);
|
|
3747
|
+
else
|
|
3748
|
+
farTime(
|
|
3749
|
+
last,
|
|
3750
|
+
`${where}.maxRuns`,
|
|
3751
|
+
`${label(s)}’s last run`,
|
|
3752
|
+
s.every,
|
|
3753
|
+
'the vault keeps what is left, and nobody can take it out early',
|
|
3754
|
+
);
|
|
3508
3755
|
}
|
|
3509
|
-
if (s.maxRuns > MAX_SEALED_RUNS)
|
|
3510
|
-
refuse(
|
|
3511
|
-
'E-SCHEDULE',
|
|
3512
|
-
`${where}.maxRuns`,
|
|
3513
|
-
`${label(s)} is sealed with ${s.maxRuns} runs; a sealed schedule runs at most ${MAX_SEALED_RUNS} times (BadSchedule).`,
|
|
3514
|
-
'Fewer runs, each larger.',
|
|
3515
|
-
);
|
|
3516
3756
|
// A wallet, the treasury or a burn: a stream into a pool or an account would be only as safe as where it lands (the
|
|
3517
3757
|
// authority moves a pool; an account's holder may close it).
|
|
3518
3758
|
if (!['wallet', 'treasury', 'burn'].includes(d.kind) || !s.to)
|
|
@@ -3529,14 +3769,34 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
3529
3769
|
`${label(s)} burns SOL, and wrapped SOL cannot be burned.`,
|
|
3530
3770
|
'Pay it somewhere instead.',
|
|
3531
3771
|
);
|
|
3532
|
-
|
|
3533
|
-
|
|
3772
|
+
// A vault that pays a wallet or the treasury moves its tokens by a transfer: a token that can't be transferred
|
|
3773
|
+
// would sit in it for good. The program takes it (only a burn of a token whose burn needs its issuer is refused).
|
|
3774
|
+
if (tok?.powers?.nonTransferable && d.kind !== 'burn')
|
|
3534
3775
|
refuse(
|
|
3535
3776
|
'E-SCHEDULE',
|
|
3536
3777
|
`${where}.currency`,
|
|
3537
|
-
`${label(s)} is sealed in ${tok.symbol},
|
|
3778
|
+
`${label(s)} is sealed in ${tok.symbol}, which cannot be transferred: no run could ever pay ${d.kind === 'treasury' ? 'the treasury' : 'the wallet'}, and the vault, which nobody can open, would keep it all for good.`,
|
|
3538
3779
|
'Seal it in another token.',
|
|
3539
3780
|
);
|
|
3781
|
+
// A vest is only as safe as its token.
|
|
3782
|
+
const p = tok?.powers ?? {};
|
|
3783
|
+
const powers = [
|
|
3784
|
+
p.permanentDelegate && 'a permanent delegate can take the tokens out of the vault',
|
|
3785
|
+
p.feeChangeable && 'its issuer can raise the transfer fee to 100% and take each run as it leaves',
|
|
3786
|
+
p.closeAuthority && 'its mint can be closed and made again with other powers',
|
|
3787
|
+
(p.freeze || p.defaultFrozen || (p.defaultState && p.freeze)) &&
|
|
3788
|
+
'a freeze can hold the vault or the payee’s account',
|
|
3789
|
+
p.pausable && 'a pause can hold every run',
|
|
3790
|
+
p.transferHook && 'its transfer hook can refuse a run',
|
|
3791
|
+
p.confidentialTransfers && 'a payee can turn plain transfers away',
|
|
3792
|
+
].filter(Boolean);
|
|
3793
|
+
if (powers.length)
|
|
3794
|
+
warn(
|
|
3795
|
+
'W-SCHEDULE',
|
|
3796
|
+
`${where}.currency`,
|
|
3797
|
+
`${label(s)} is sealed in ${tok.symbol}: a vest is only as safe as its token, and ${powers.join('; ')}.`,
|
|
3798
|
+
'Seal it in a token without those powers, or tell whoever it pays.',
|
|
3799
|
+
);
|
|
3540
3800
|
if (byShare && !(percent(s.percent, `${where}.percent`) > 0))
|
|
3541
3801
|
refuse('E-SCHEDULE', `${where}.percent`, `${label(s)} pays 0%.`, 'Give it a percent above 0.');
|
|
3542
3802
|
else if (!byShare && units(s.amount, tok, `${where}.amount`, `What ${label(s)} pays`) === 0n)
|
|
@@ -3667,11 +3927,8 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
3667
3927
|
const where = `economy.lotteries.${l.key}`;
|
|
3668
3928
|
const potToken = token(l.currency, `${where}.currency`);
|
|
3669
3929
|
const ticket = l.ticketCurrency ? token(l.ticketCurrency, `${where}.ticketCurrency`) : potToken;
|
|
3670
|
-
|
|
3671
|
-
if (ticket !== potToken)
|
|
3672
|
-
startsFrozen(potToken, `${where}.currency`, label(l), 'E-LOTTERY', 'BadLottery');
|
|
3673
|
-
if (ticket !== potToken)
|
|
3674
|
-
startsFrozen(ticket, `${where}.ticketCurrency`, `${label(l)}’s ticket`, 'E-LOTTERY', 'BadLottery');
|
|
3930
|
+
holdsUp(potToken, `${where}.currency`, label(l), 'lottery');
|
|
3931
|
+
if (ticket !== potToken) holdsUp(ticket, `${where}.ticketCurrency`, `${label(l)}’s ticket`, 'lottery');
|
|
3675
3932
|
if (l.ticketCurrency && ticket?.wsol)
|
|
3676
3933
|
refuse(
|
|
3677
3934
|
'E-LOTTERY',
|
|
@@ -3717,12 +3974,14 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
3717
3974
|
single: true,
|
|
3718
3975
|
});
|
|
3719
3976
|
}
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3977
|
+
// A no-loss lottery that pays from its own money: every ticket goes back, so its places take no money and give
|
|
3978
|
+
// only their prizes.
|
|
3979
|
+
if (l.noLoss && (l.places ?? [100]).some(p => placeOf(p).percent > 0))
|
|
3980
|
+
warn(
|
|
3981
|
+
'W-LOTTERY',
|
|
3982
|
+
`${where}.places`,
|
|
3983
|
+
`${label(l)} gives every ticket back and pays each round from that round's own ticket money ("rest"): there is no money left for its places, so a place's percent wins nothing from it, and only its prize is won.`,
|
|
3984
|
+
'Give the places prizes, or keep a pot ("pot": 0) that the owner or other sales fill.',
|
|
3726
3985
|
);
|
|
3727
3986
|
if (Number(l.seed ?? 0) > 0)
|
|
3728
3987
|
refuse(
|
|
@@ -3798,26 +4057,58 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
3798
4057
|
`${label(l)} draws once (no "every"), and the draw is due at go-live.`,
|
|
3799
4058
|
'Give it a "firstDraw" (like "publish+1d"), or an "every" to draw again and again.',
|
|
3800
4059
|
);
|
|
3801
|
-
// A
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
'E-LOTTERY',
|
|
3805
|
-
`${where}.every`,
|
|
3806
|
-
`${label(l)} draws every ${l.every}: a draw more than 5 years off sells no ticket (SalesClosed).`,
|
|
3807
|
-
'Draw within 5 years.',
|
|
3808
|
-
);
|
|
4060
|
+
// A draw far off is taken, and warned of with its date: the tickets wait until then.
|
|
4061
|
+
const every = seconds(l.every, `${where}.every`);
|
|
4062
|
+
let firstDraw = now + every;
|
|
3809
4063
|
try {
|
|
3810
|
-
if (l.firstDraw
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
4064
|
+
if (l.firstDraw) firstDraw = toTime(l.firstDraw, now);
|
|
4065
|
+
farTime(
|
|
4066
|
+
firstDraw,
|
|
4067
|
+
l.firstDraw ? `${where}.firstDraw` : `${where}.every`,
|
|
4068
|
+
`${label(l)}’s first draw`,
|
|
4069
|
+
l.firstDraw ?? l.every,
|
|
4070
|
+
'its tickets and their money wait in it',
|
|
4071
|
+
);
|
|
3818
4072
|
} catch (err) {
|
|
3819
4073
|
refuse(err.code, `${where}.firstDraw`, err.message, err.fix);
|
|
3820
4074
|
}
|
|
4075
|
+
// Sales close this long before each draw (lottery.rs `closes_before`): never a whole rhythm or more before it.
|
|
4076
|
+
const closesBefore = seconds(l.closesBefore, `${where}.closesBefore`);
|
|
4077
|
+
if (every > 0 && closesBefore >= every)
|
|
4078
|
+
refuse(
|
|
4079
|
+
'E-LOTTERY',
|
|
4080
|
+
`${where}.closesBefore`,
|
|
4081
|
+
`${label(l)} draws every ${l.every} and closes its sales ${l.closesBefore} before each draw: sales would never open (BadLottery).`,
|
|
4082
|
+
`Close sales less than ${l.every} before the draw.`,
|
|
4083
|
+
);
|
|
4084
|
+
else if (closesBefore > 0 && firstDraw - closesBefore <= now)
|
|
4085
|
+
warn(
|
|
4086
|
+
'W-LOTTERY',
|
|
4087
|
+
`${where}.closesBefore`,
|
|
4088
|
+
`${label(l)} closes its sales ${l.closesBefore} before its first draw, which is sooner than that after go-live: its first round sells no ticket (SalesClosed).`,
|
|
4089
|
+
'Draw later, or close sales closer to the draw.',
|
|
4090
|
+
);
|
|
4091
|
+
// Tickets weighed by the time they are held (lottery.rs `time_weighted`): a no-loss lottery only (BadLottery).
|
|
4092
|
+
if (l.timeWeighted && !l.noLoss)
|
|
4093
|
+
refuse(
|
|
4094
|
+
'E-LOTTERY',
|
|
4095
|
+
`${where}.timeWeighted`,
|
|
4096
|
+
`${label(l)} weighs its tickets by the time they are held, but keeps its ticket money: only a lottery that gives every ticket back may weigh them by time (BadLottery).`,
|
|
4097
|
+
'Make it "noLoss": true, or leave "timeWeighted" out.',
|
|
4098
|
+
);
|
|
4099
|
+
else if (l.timeWeighted)
|
|
4100
|
+
warn(
|
|
4101
|
+
'W-LOTTERY-WEIGHT',
|
|
4102
|
+
`${where}.timeWeighted`,
|
|
4103
|
+
`${label(l)} weighs a purchase by its tickets times the seconds from it to the close of sales${closesBefore ? ` (${l.closesBefore} before each draw)` : ' (at the draw: set "closesBefore" to the close you mean)'}: a ticket’s chance grows with the time it is held. Tell every buyer, on the buy screen, what their purchase weighs now and that the same tickets bought later weigh less. ${Number(l.price ?? 0) > 0 || l.ticketNft ? '' : 'Tickets are free: give it a "perWallet" limit or a gate, as any free lottery wants. '}"winners", "minSold" and "perWallet" still count tickets.`,
|
|
4104
|
+
);
|
|
4105
|
+
else if (l.noLoss && !closesBefore)
|
|
4106
|
+
warn(
|
|
4107
|
+
'W-LOTTERY',
|
|
4108
|
+
`${where}.closesBefore`,
|
|
4109
|
+
`${label(l)} gives every ticket back and sells until the draw: tickets bought seconds before it have the same chance at the pot as tickets held all period, and their price comes back.`,
|
|
4110
|
+
'Set "closesBefore" (sales close that long before the draw), or "timeWeighted": true so a ticket counts for the time it is held.',
|
|
4111
|
+
);
|
|
3821
4112
|
// A place takes a share of the pot, a roll of a box for its winner, or both (lottery.rs Place).
|
|
3822
4113
|
const placed = (l.places ?? [100]).map(placeOf);
|
|
3823
4114
|
tooMany(
|
|
@@ -3843,13 +4134,21 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
3843
4134
|
);
|
|
3844
4135
|
if (typeof raw === 'object' && raw?.prize)
|
|
3845
4136
|
prizeCheck(raw.prize, `${where}.places[${i}].prize`, `${label(l)}’s place ${i + 1}`, 'E-LOTTERY');
|
|
3846
|
-
// A run holds places alike: a named NFT
|
|
3847
|
-
if (p.times > 1 &&
|
|
4137
|
+
// A run holds places alike: a named NFT is one place's (a roll may repeat: each place its own roll).
|
|
4138
|
+
if (p.times > 1 && p.prize?.gives === 'nft')
|
|
3848
4139
|
refuse(
|
|
3849
4140
|
'E-LOTTERY',
|
|
3850
4141
|
`${where}.places[${i}].times`,
|
|
3851
|
-
`${label(l)}’s place ${i + 1} repeats ${p.times} times a
|
|
3852
|
-
'Give it to one place, or make it a prize places can share alike (tokens, an item, an NFT drawn from the lottery’s stash).',
|
|
4142
|
+
`${label(l)}’s place ${i + 1} repeats ${p.times} times a named NFT, which one place gives (BadLottery).`,
|
|
4143
|
+
'Give it to one place, or make it a prize places can share alike (tokens, an item, a roll, an NFT drawn from the lottery’s stash).',
|
|
4144
|
+
);
|
|
4145
|
+
// What a place of tokens from a pool gets when the pool is short (lottery.rs `WhenShort`).
|
|
4146
|
+
if (raw?.whenShort !== undefined && !(p.prize?.gives === 'tokens' && prizePool(p.prize)))
|
|
4147
|
+
warn(
|
|
4148
|
+
'W-LOTTERY',
|
|
4149
|
+
`${where}.places[${i}].whenShort`,
|
|
4150
|
+
`${label(l)}’s place ${i + 1} says what it gets when its pool is short, but it is not paid in tokens from a pool: "whenShort" changes nothing there.`,
|
|
4151
|
+
'Leave "whenShort" out.',
|
|
3853
4152
|
);
|
|
3854
4153
|
// A run of places given one by one (anything but tokens: a collectible, a compressed NFT, a raffled NFT) holds at
|
|
3855
4154
|
// most MAX_RUN_PLACES places: a pull walks the run from its first place (BadLottery).
|
|
@@ -3916,15 +4215,62 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
3916
4215
|
`${label(l)} gives the same NFT on two places: the second could never be given (BadLottery).`,
|
|
3917
4216
|
'Name each NFT once.',
|
|
3918
4217
|
);
|
|
3919
|
-
//
|
|
3920
|
-
|
|
3921
|
-
const draw = 6_000 + 250 * Math.ceil(rows / 4_096);
|
|
3922
|
-
if (raffled * draw + held.length * 15 * rows > 1_300_000)
|
|
4218
|
+
// The places from its own stash, raffled or named, are what its draw's answer loads (lottery.rs `MOST_OWN`).
|
|
4219
|
+
if (raffled + held.length > LIMITS.ownStashPlaces)
|
|
3923
4220
|
refuse(
|
|
3924
4221
|
'E-LOTTERY',
|
|
3925
4222
|
`${where}.places`,
|
|
3926
|
-
`${label(l)}
|
|
3927
|
-
|
|
4223
|
+
`${label(l)} has ${raffled + held.length} places from its own stash: a raffle has at most ${LIMITS.ownStashPlaces}, the most the oracle’s answer loads on the chain’s default memory (BadLottery).`,
|
|
4224
|
+
`Raffle at most ${LIMITS.ownStashPlaces} NFTs a lottery: run a second lottery for the rest.`,
|
|
4225
|
+
);
|
|
4226
|
+
// Every run paying from one pool says the same about a short pool (lottery.rs, BadLottery), and the project hears
|
|
4227
|
+
// what its setting does.
|
|
4228
|
+
const byPool = new Map();
|
|
4229
|
+
for (const [i, p] of placed.entries()) {
|
|
4230
|
+
const key = p.prize?.gives === 'tokens' ? prizePool(p.prize) : null;
|
|
4231
|
+
if (!key) continue;
|
|
4232
|
+
byPool.set(key, [...(byPool.get(key) ?? []), [i, p.whenShort ?? 'inOrder']]);
|
|
4233
|
+
}
|
|
4234
|
+
for (const [key, runs] of byPool) {
|
|
4235
|
+
const said = [...new Set(runs.map(([, w]) => w))];
|
|
4236
|
+
if (said.length > 1)
|
|
4237
|
+
refuse(
|
|
4238
|
+
'E-LOTTERY',
|
|
4239
|
+
`${where}.places`,
|
|
4240
|
+
`${label(l)}’s places paid from the pool “${key}” say different things about a short pool (${said.join(', ')}): every run paying from one pool must say the same (BadLottery).`,
|
|
4241
|
+
`Give each place paid from “${key}” the same "whenShort".`,
|
|
4242
|
+
);
|
|
4243
|
+
else
|
|
4244
|
+
warn(
|
|
4245
|
+
'W-LOTTERY-POOL',
|
|
4246
|
+
`${where}.places`,
|
|
4247
|
+
`${label(l)}’s places paid from the pool “${key}” read it once, when the draw is asked, and the project can spend the pool until then. When it holds less than the places with a winner win, each place gets what "whenShort" says, here ${said[0] === 'inProportion' ? '"inProportion": each place a share in proportion to its prize' : said[0] === 'nothing' ? '"nothing": no place of that pool gets anything' : '"inOrder" (the default): the first places in full, the rest what is left'}. Prizes of 1,000, 500 and 500 from a pool holding 1,700 give 1,000, 500 and 0 in order, 850, 425 and 425 in proportion, or nothing.`,
|
|
4248
|
+
'Keep the pool stocked for every place before the draw, or pay from the lottery’s own pot.',
|
|
4249
|
+
);
|
|
4250
|
+
}
|
|
4251
|
+
// A named NFT of a stash the project holds: the first winner whose prize is pulled gets it.
|
|
4252
|
+
const named = placed.filter(p => p.prize?.gives === 'nft' && !p.prize.held);
|
|
4253
|
+
const nftNames = named.map(p => p.prize.nft);
|
|
4254
|
+
if (named.length && (every > 0 || new Set(nftNames).size < nftNames.length))
|
|
4255
|
+
warn(
|
|
4256
|
+
'W-LOTTERY',
|
|
4257
|
+
`${where}.places`,
|
|
4258
|
+
`${label(l)} ${every > 0 ? 'draws again and again and names' : 'names'} ${new Set(nftNames).size < nftNames.length ? 'one NFT of a stash for several places' : 'NFTs of a stash as prizes'}: the first winner whose prize is pulled gets the NFT, and later winners get nothing.`,
|
|
4259
|
+
'Raffle NFTs from the lottery’s own stash ("raffled", or "raffle"), or give each place its own NFT in a lottery that draws once.',
|
|
4260
|
+
);
|
|
4261
|
+
if (l.ticketNft?.rule?.oneOf && l.ticketNft.rule.oneOf.length > LIMITS.ticketOneOf)
|
|
4262
|
+
refuse(
|
|
4263
|
+
'E-LOTTERY',
|
|
4264
|
+
`${where}.ticketNft.rule.oneOf`,
|
|
4265
|
+
`${label(l)}’s NFT ticket rule lists ${l.ticketNft.rule.oneOf.length} "one of" texts: the oracle’s answer reads the rule, and holds at most ${LIMITS.ticketOneOf} (BadLottery).`,
|
|
4266
|
+
`List at most ${LIMITS.ticketOneOf}, or test a number ("atLeast") instead.`,
|
|
4267
|
+
);
|
|
4268
|
+
if (l.ticketNft?.rule?.items && l.ticketNft.rule.items.length > LIMITS.ticketOneOf)
|
|
4269
|
+
refuse(
|
|
4270
|
+
'E-LOTTERY',
|
|
4271
|
+
`${where}.ticketNft.rule.items`,
|
|
4272
|
+
`${label(l)}’s NFT ticket rule names ${l.ticketNft.rule.items.length} items: the oracle’s answer reads the rule, and holds at most ${LIMITS.ticketOneOf} (BadLottery).`,
|
|
4273
|
+
`Name at most ${LIMITS.ticketOneOf}.`,
|
|
3928
4274
|
);
|
|
3929
4275
|
if (held.length)
|
|
3930
4276
|
warn(
|
|
@@ -3946,7 +4292,7 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
3946
4292
|
`${label(l)}’s places add up to ${total / 100}% of the pot, more than all of it.`,
|
|
3947
4293
|
'Make them add up to 100% or less.',
|
|
3948
4294
|
);
|
|
3949
|
-
else if (total < 10_000)
|
|
4295
|
+
else if (total < 10_000 && !(l.noLoss && !keepsPot(l)))
|
|
3950
4296
|
warn(
|
|
3951
4297
|
'W-LOTTERY',
|
|
3952
4298
|
`${where}.places`,
|
|
@@ -3997,8 +4343,10 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
3997
4343
|
warn(
|
|
3998
4344
|
'W-LOTTERY',
|
|
3999
4345
|
where,
|
|
4000
|
-
`${label(l)} loses nobody their tickets: every ticket goes back to its buyer once the round is drawn (anyone can finish each purchase), and the winners take only the pot, what is paid into it (the seed, a sponsor, a share of other sales). Ticket money waits in the lottery’s own vault until then. Tickets are not weighted by how long they were held: someone may buy most of a round’s tickets right before the draw and risk only the fees
|
|
4001
|
-
|
|
4346
|
+
`${label(l)} loses nobody their tickets: every ticket goes back to its buyer once the round is drawn (anyone can finish each purchase), and the winners take only the pot, what is paid into it (the seed, a sponsor, a share of other sales). Ticket money waits in the lottery’s own vault until then.${l.timeWeighted ? '' : ` Tickets are not weighted by how long they were held: someone may buy most of a round’s tickets ${closesBefore ? `just before sales close (${l.closesBefore} before the draw)` : 'right before the draw'} and risk only the fees.`}`,
|
|
4347
|
+
l.timeWeighted
|
|
4348
|
+
? undefined
|
|
4349
|
+
: 'Weigh tickets by the time they are held ("timeWeighted": true), close sales a while before the draw ("closesBefore"), keep the pot small next to what a late buyer would put in, or gate it (a stake, a list).',
|
|
4002
4350
|
);
|
|
4003
4351
|
}
|
|
4004
4352
|
rule(l.gate, `${where}.gate`, label(l));
|
|
@@ -4225,13 +4573,13 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
4225
4573
|
);
|
|
4226
4574
|
for (const [i, t] of tiers.entries()) {
|
|
4227
4575
|
fourByteSeconds(t.lock, `${where}.tiers[${i}].lock`);
|
|
4228
|
-
// A
|
|
4229
|
-
if (t.prize && !(toSeconds(t.lock ?? 0) > 0))
|
|
4230
|
-
|
|
4231
|
-
'
|
|
4576
|
+
// A mission with no lock is the project's choice: one NFT can come and go and take it each time.
|
|
4577
|
+
if ((t.prize || t.roll) && !(toSeconds(t.lock ?? 0) > 0))
|
|
4578
|
+
warn(
|
|
4579
|
+
'W-FARM-MISSION',
|
|
4232
4580
|
`${where}.tiers[${i}].lock`,
|
|
4233
|
-
|
|
4234
|
-
'Give the tier a "lock" above 0.',
|
|
4581
|
+
`No lock on ${label(f)}’s mission tier ${i}: one player can stake and leave one NFT again and again and ${t.prize ? 'take the prize' : 'roll the box'} each time.`,
|
|
4582
|
+
'Give the tier a "lock" above 0, such as "1d".',
|
|
4235
4583
|
);
|
|
4236
4584
|
if (t.earnsFor !== undefined) fourByteSeconds(t.earnsFor, `${where}.tiers[${i}].earnsFor`);
|
|
4237
4585
|
const bps = Math.round((t.multiplier ?? 1) * 10_000);
|
|
@@ -4239,8 +4587,16 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
4239
4587
|
refuse(
|
|
4240
4588
|
'E-FARM',
|
|
4241
4589
|
`${where}.tiers[${i}].multiplier`,
|
|
4242
|
-
`${label(f)}’s tier ${i} multiplies by ${t.multiplier}: a tier is above 0 and at most
|
|
4243
|
-
|
|
4590
|
+
`${label(f)}’s tier ${i} multiplies by ${t.multiplier}: a tier is above 0 and at most x${LIMITS.maxTierBps / 10_000} (BadFarm).`,
|
|
4591
|
+
`Use a multiplier above 0, up to ${LIMITS.maxTierBps / 10_000}.`,
|
|
4592
|
+
);
|
|
4593
|
+
// A heavy tier a seat can leave early for a fee: the weight is bought for the fee.
|
|
4594
|
+
else if (bps >= 30_000 && Number(t.exitFee ?? 0) > 0 && !ofCollectibles)
|
|
4595
|
+
warn(
|
|
4596
|
+
'W-FARM',
|
|
4597
|
+
`${where}.tiers[${i}].exitFee`,
|
|
4598
|
+
`${label(f)}’s tier ${i} weighs x${t.multiplier} and lets a seat leave early for ${t.exitFee}%: a player can take the large weight, earn at it, and leave for the fee.`,
|
|
4599
|
+
'Check the fee is worth what the tier pays over its lock, or drop the early exit on heavy tiers.',
|
|
4244
4600
|
);
|
|
4245
4601
|
else if (bps < 10_000)
|
|
4246
4602
|
warn(
|
|
@@ -4252,6 +4608,15 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
4252
4608
|
for (const [i, b] of (f.boosts ?? []).entries()) {
|
|
4253
4609
|
rule(b.when, `${where}.boosts[${i}].when`, `${label(f)}’s boost`);
|
|
4254
4610
|
effect(b.add, `${where}.boosts[${i}].add`, `${label(f)}’s boost`);
|
|
4611
|
+
// A boost per point adds its percent times its most points, at most +100,000% (rules.rs `Boost::check`).
|
|
4612
|
+
const most = Number(b.add?.perPointPercent ?? 0) * Number(b.add?.upTo ?? 0);
|
|
4613
|
+
if (b.add?.perPointPercent !== undefined && Math.round(most * 100) > LIMITS.maxBoostBps)
|
|
4614
|
+
refuse(
|
|
4615
|
+
'E-FARM',
|
|
4616
|
+
`${where}.boosts[${i}].add`,
|
|
4617
|
+
`${label(f)}’s boost adds ${b.add.perPointPercent}% a point up to ${b.add.upTo} points: +${most.toLocaleString('en-US')}% at most, past the +${(LIMITS.maxBoostBps / 100).toLocaleString('en-US')}% a farm’s boost may add (BadFarm).`,
|
|
4618
|
+
'Add less a point, or count fewer points ("upTo").',
|
|
4619
|
+
);
|
|
4255
4620
|
}
|
|
4256
4621
|
boostPercent(f.maxBoostPercent, `${where}.maxBoostPercent`);
|
|
4257
4622
|
if ((f.boosts ?? []).length && !(f.maxBoostPercent > 0))
|
|
@@ -4287,12 +4652,20 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
4287
4652
|
`${label(f)}’s rate of ${f.rate.perDay} a day is less than one base unit for each 1,000,000 base units staked.`,
|
|
4288
4653
|
'Raise "perDay".',
|
|
4289
4654
|
);
|
|
4655
|
+
// A farm that mints while the project is not the mint's authority credits nothing, and what it would have paid is
|
|
4656
|
+
// not owed later; the program takes it, so the mint can be handed over after.
|
|
4290
4657
|
if (f.rate.minted && rewardsToken && !rewardsToken.gameMints)
|
|
4291
|
-
|
|
4292
|
-
'
|
|
4658
|
+
warn(
|
|
4659
|
+
'W-FARM-AUTHORITY',
|
|
4660
|
+
`${where}.rate.minted`,
|
|
4661
|
+
`${label(f)} mints its rewards, but the project is not the authority of ${rewardsToken.symbol}’s mint. It pays nothing until you set the mint’s authority to the project’s address; rewards for that time are lost, not paid later. When you hand the mint over, send \`walk_farm\` in the same transaction, just before it: then nothing from before the hand-over is ever paid; without it, the first move after the hand-over credits all the time since the farm’s last move.`,
|
|
4662
|
+
'Reward in a token the project makes (source "project"), pay the rate from its rewards vault ("minted": false), or hand the mint to the project before the farm opens.',
|
|
4663
|
+
);
|
|
4664
|
+
if (f.rate.minted && rewardsToken)
|
|
4665
|
+
warn(
|
|
4666
|
+
'W-FARM',
|
|
4293
4667
|
`${where}.rate.minted`,
|
|
4294
|
-
`${label(f)} mints
|
|
4295
|
-
'Pay the rate from its rewards vault ("minted": false), or reward in a token with source "project".',
|
|
4668
|
+
`${label(f)} mints ${rewardsToken.symbol}: if the project ever moves the mint’s authority away, stakers cannot harvest what they are owed and can only leave by giving it up.`,
|
|
4296
4669
|
);
|
|
4297
4670
|
if (!f.rate.minted)
|
|
4298
4671
|
warn(
|
|
@@ -4349,6 +4722,14 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
4349
4722
|
if (!ofCollectibles && !(f.boosts ?? []).length)
|
|
4350
4723
|
warn('W-FARM', at, `${label(f)} stakes a token: only a collectible put on a seat as its booster grows.`);
|
|
4351
4724
|
}
|
|
4725
|
+
// A farm whose seats stop earning at times of their own keeps one entry a different second.
|
|
4726
|
+
if (tiers.some(t => t.earnsFor !== undefined) || f.upkeepAttribute !== undefined)
|
|
4727
|
+
warn(
|
|
4728
|
+
'W-FARM-ENDS',
|
|
4729
|
+
`${where}.tiers`,
|
|
4730
|
+
`${label(f)} keeps one entry for each different second at which a seat stops earning, 238,312 at most; the first staker on a new second pays about 0.0003 SOL for it. When it is full, a stake at a new end time fails until older seats are touched. A stake in a large farm needs more than the default compute (an insert took 145,279 units at the full list).`,
|
|
4731
|
+
'Expect it with many stakers: the app asks for more compute on a stake, and anyone may touch old seats for free.',
|
|
4732
|
+
);
|
|
4352
4733
|
if (f.runs !== undefined) {
|
|
4353
4734
|
const runs = fromNowSeconds(f.runs, `${where}.runs`);
|
|
4354
4735
|
for (const [i, t] of tiers.entries())
|
|
@@ -4392,13 +4773,6 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
4392
4773
|
`${label(f)} stakes a token: a mission’s prize is for a collectible that stays its tier’s lock.`,
|
|
4393
4774
|
'Make it a farm of collectibles, or remove the prizes.',
|
|
4394
4775
|
);
|
|
4395
|
-
if (toSeconds(t.lock ?? 0) === 0)
|
|
4396
|
-
refuse(
|
|
4397
|
-
'E-FARM',
|
|
4398
|
-
at,
|
|
4399
|
-
`${label(f)}’s tier ${i} gives a prize with no lock: staking and leaving at once would give it again and again.`,
|
|
4400
|
-
'Give the mission a lock, such as "1d".',
|
|
4401
|
-
);
|
|
4402
4776
|
}
|
|
4403
4777
|
for (const [i, t] of tiers.entries()) {
|
|
4404
4778
|
if (!t.roll) continue;
|
|
@@ -4410,13 +4784,6 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
4410
4784
|
`${label(f)} stakes a token: a mission’s roll is for a collectible leaving its seat.`,
|
|
4411
4785
|
'Make it a farm of collectibles, or remove the roll.',
|
|
4412
4786
|
);
|
|
4413
|
-
if (toSeconds(t.lock ?? 0) === 0)
|
|
4414
|
-
refuse(
|
|
4415
|
-
'E-FARM',
|
|
4416
|
-
at,
|
|
4417
|
-
`${label(f)}’s tier ${i} rolls with no lock: staking and leaving at once would roll it again and again.`,
|
|
4418
|
-
'Give the mission a lock, such as "1d".',
|
|
4419
|
-
);
|
|
4420
4787
|
if (!m.box.get(t.roll))
|
|
4421
4788
|
refuse('E-FARM', at, `${label(f)}’s tier ${i} rolls “${t.roll}”, which is no box.`, 'Name a box of this spec.');
|
|
4422
4789
|
else if (!m.box.get(t.roll).rollsOn)
|
|
@@ -4539,8 +4906,7 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
4539
4906
|
'Name fewer deciders.',
|
|
4540
4907
|
);
|
|
4541
4908
|
const tok = token(c.currency, `${where}.currency`);
|
|
4542
|
-
|
|
4543
|
-
startsFrozen(tok, `${where}.currency`, label(c), 'E-CONTEST', 'BadContest');
|
|
4909
|
+
holdsUp(tok, `${where}.currency`, label(c), 'contest');
|
|
4544
4910
|
// Entries close no later than the result is due, and with deciders before it: a decider's verdict is taken only
|
|
4545
4911
|
// once entries close (contest.rs, BadContest). With deciders, a side left out is an hour from the other (plan.mjs
|
|
4546
4912
|
// `contestArgs`).
|
|
@@ -4553,13 +4919,6 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
4553
4919
|
`${label(c)}’s entries close after its result is due (BadContest).`,
|
|
4554
4920
|
'Close entries before the result is due.',
|
|
4555
4921
|
);
|
|
4556
|
-
else if (Math.max(closes, due) > MAX_CONTEST_SECONDS)
|
|
4557
|
-
refuse(
|
|
4558
|
-
'E-CONTEST',
|
|
4559
|
-
where,
|
|
4560
|
-
`${label(c)}’s result is due more than 5 years after it opens: the program refuses to open it (BadContest).`,
|
|
4561
|
-
'Have its result due within 5 years.',
|
|
4562
|
-
);
|
|
4563
4922
|
else if ((c.deciders ?? []).length && c.entriesClose !== undefined && c.resultDue !== undefined && closes >= due)
|
|
4564
4923
|
refuse(
|
|
4565
4924
|
'E-CONTEST',
|
|
@@ -4567,6 +4926,36 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
4567
4926
|
`${label(c)} has deciders, whose verdict is taken only once entries close, and its entries close when its result is due: the program refuses to open it (BadContest).`,
|
|
4568
4927
|
'Close entries before the result is due, or leave one of "entriesClose" and "resultDue" out (an hour apart).',
|
|
4569
4928
|
);
|
|
4929
|
+
// A result far off is taken, and warned of with its date: the stakes wait until then.
|
|
4930
|
+
farTime(
|
|
4931
|
+
now + Math.max(closes, due),
|
|
4932
|
+
c.resultDue !== undefined ? `${where}.resultDue` : `${where}.entriesClose`,
|
|
4933
|
+
`${label(c)}’s result`,
|
|
4934
|
+
c.resultDue ?? c.entriesClose,
|
|
4935
|
+
'every entry’s stake waits in its pot',
|
|
4936
|
+
);
|
|
4937
|
+
// Whoever names the winner, and how the pot is paid.
|
|
4938
|
+
warn(
|
|
4939
|
+
'W-CONTEST',
|
|
4940
|
+
where,
|
|
4941
|
+
`${label(c)}’s winner is named by ${(c.deciders ?? []).length ? 'its deciders’ verdict' : 'the project’s keys'}, who can also enter it: players trust them with the result. The pot is the contest’s own account, paid only by each entry’s claim.`,
|
|
4942
|
+
);
|
|
4943
|
+
// One prize a winning entry, and nothing that costs an entry anything: one person's many wallets take one each.
|
|
4944
|
+
const stakeUnits = c.stakeNft ? 1n : quietUnits(c.stake, tok);
|
|
4945
|
+
if (c.prize && !c.prize.split && !c.gate && !c.stakeNft && !(stakeUnits > 0n))
|
|
4946
|
+
warn(
|
|
4947
|
+
'W-CONTEST-PRIZE',
|
|
4948
|
+
`${where}.prize`,
|
|
4949
|
+
`${label(c)} gives a prize to each winning entry, with no fixed stake and no gate: one person with many wallets enters from each and collects one prize a wallet.`,
|
|
4950
|
+
'Set a "stake" that makes an entry cost something, gate it, or split one prize by stake ("split": true).',
|
|
4951
|
+
);
|
|
4952
|
+
if (c.stakeNft && c.losersEach === 0)
|
|
4953
|
+
warn(
|
|
4954
|
+
'W-ZERO',
|
|
4955
|
+
`${where}.losersEach`,
|
|
4956
|
+
`${label(c)} takes 0 losers’ NFTs a winner: every loser’s NFT goes to whoever opened it, and the winners get only their own back.`,
|
|
4957
|
+
'Give "losersEach" a number above 0, or leave it out for no cap.',
|
|
4958
|
+
);
|
|
4570
4959
|
if (c.sideNames && c.sideNames.length !== c.sides)
|
|
4571
4960
|
warn(
|
|
4572
4961
|
'W-CONTEST',
|
|
@@ -4577,6 +4966,15 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
4577
4966
|
split(c.split, tok, `${where}.split`, { what: label(c) });
|
|
4578
4967
|
rule(c.gate, `${where}.gate`, label(c));
|
|
4579
4968
|
const cp = c.prize ? prizeCheck(c.prize, `${where}.prize`, label(c), 'E-CONTEST') : null;
|
|
4969
|
+
// A prize split among the winners is set aside when the winner is named, and the program sets aside only an amount
|
|
4970
|
+
// above 0 (contest.rs `set_aside`: PrizeNotCovered). The contest opens, but no winner can ever be named.
|
|
4971
|
+
if (cp && c.prize.split && cp.gives === 'tokens' && Number(cp.amount) === 0)
|
|
4972
|
+
refuse(
|
|
4973
|
+
'E-CONTEST',
|
|
4974
|
+
`${where}.prize.amount`,
|
|
4975
|
+
`${label(c)} splits a prize of 0 among its winners: the program opens the contest but can never name its winner (PrizeNotCovered), so it runs past its result and is called off, every entry refunded.`,
|
|
4976
|
+
'Give the prize an amount above 0, or leave the prize out.',
|
|
4977
|
+
);
|
|
4580
4978
|
// One NFT, the last units of an item, a pool's prize: the winning entries share it in the order they are claimed.
|
|
4581
4979
|
const scarce =
|
|
4582
4980
|
cp &&
|
|
@@ -4664,8 +5062,7 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
4664
5062
|
for (const a of m.auctions) {
|
|
4665
5063
|
const where = `economy.auctions.${a.key}`;
|
|
4666
5064
|
const tok = token(a.currency, `${where}.currency`);
|
|
4667
|
-
|
|
4668
|
-
startsFrozen(tok, `${where}.currency`, label(a), 'E-AUCTION', 'BadAuction');
|
|
5065
|
+
holdsUp(tok, `${where}.currency`, label(a), 'auction');
|
|
4669
5066
|
const reserve = a.bidNfts
|
|
4670
5067
|
? BigInt(Number.isInteger(a.reserve) ? a.reserve : 1)
|
|
4671
5068
|
: units(a.reserve, tok, `${where}.reserve`, `${label(a)}’s reserve`);
|
|
@@ -4716,31 +5113,35 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
4716
5113
|
`${label(a)} would be over before it goes live (BadAuction).`,
|
|
4717
5114
|
'Start it at "publish", or later.',
|
|
4718
5115
|
);
|
|
4719
|
-
// Nothing but a higher bid hands a high bid back before the end: an end, or a push of it,
|
|
4720
|
-
//
|
|
4721
|
-
if (runs > 0
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
`${where}.${start - now
|
|
4725
|
-
`${label(a)}
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
5116
|
+
// Nothing but a higher bid hands a high bid back before the end: an end, or a push of it, far off holds the bid and
|
|
5117
|
+
// the lot that long. The program takes it; the date is said in words.
|
|
5118
|
+
if (runs > 0)
|
|
5119
|
+
farTime(
|
|
5120
|
+
start + runs,
|
|
5121
|
+
`${where}.${start - now > FAR_SECONDS ? 'start' : 'runs'}`,
|
|
5122
|
+
`${label(a)}’s end`,
|
|
5123
|
+
start - now > FAR_SECONDS ? a.start : a.runs,
|
|
5124
|
+
'the high bid and the lot are held',
|
|
5125
|
+
);
|
|
5126
|
+
farTime(
|
|
5127
|
+
now + extend,
|
|
5128
|
+
`${where}.extendBy`,
|
|
5129
|
+
`${label(a)}’s end, pushed by one bid,`,
|
|
5130
|
+
a.extendBy,
|
|
5131
|
+
'that bid and the lot are held',
|
|
5132
|
+
);
|
|
5133
|
+
// No extension or no least raise: two wallets topping each other by a unit in the last seconds keep every other bid
|
|
5134
|
+
// out.
|
|
5135
|
+
const raise = Number(a.minRaise ?? 5);
|
|
5136
|
+
if (!a.bidNfts && (extend < 60 || raise < 1))
|
|
5137
|
+
warn(
|
|
5138
|
+
'W-AUCTION',
|
|
5139
|
+
`${where}.${extend < 60 ? 'extendBy' : 'minRaise'}`,
|
|
5140
|
+
`${label(a)} ${extend < 60 ? `moves its end by ${extend ? `only ${extend} seconds` : 'nothing'} after a late bid` : `takes a bid that tops the last by ${raise ? `only ${raise}%` : 'a single unit'}`}: ${extend === 0 && raise === 0 ? 'with both at zero, ' : ''}two wallets topping each other in the last seconds keep every other bid out.`,
|
|
5141
|
+
'Give it an extension of 60 seconds or more ("extendBy": "1m") and a least raise of 1% or more ("minRaise": 1).',
|
|
4734
5142
|
);
|
|
4735
5143
|
if (a.lot?.item) {
|
|
4736
5144
|
const it = item(a.lot.item, `${where}.lot.item`);
|
|
4737
|
-
if (it && it.active === false)
|
|
4738
|
-
refuse(
|
|
4739
|
-
'E-AUCTION',
|
|
4740
|
-
`${where}.lot.item`,
|
|
4741
|
-
`${label(a)} sells ${label(it)}, which is switched off (LotRefused).`,
|
|
4742
|
-
`Switch ${label(it)} on.`,
|
|
4743
|
-
);
|
|
4744
5145
|
if (it?.kind === 'normal')
|
|
4745
5146
|
warn(
|
|
4746
5147
|
'W-AUCTION',
|
|
@@ -4758,6 +5159,15 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
4758
5159
|
);
|
|
4759
5160
|
randomItems = true;
|
|
4760
5161
|
} else if (a.lot?.asset) {
|
|
5162
|
+
// An NFT outside the project's collections: whoever controls its collection can stop it moving or take it
|
|
5163
|
+
// back while the project holds it.
|
|
5164
|
+
if (a.lot.collection && !m.collections.includes(a.lot.collection))
|
|
5165
|
+
warn(
|
|
5166
|
+
'W-AUCTION-OUTSIDE',
|
|
5167
|
+
`${where}.lot.collection`,
|
|
5168
|
+
`${label(a)} sells an NFT of ${a.lot.collection}, outside the project’s collections: whoever controls that collection can stop the NFT moving or take it back while the project holds it. The seller is paid only when the lot reaches the winner; while it cannot move, the winning bid waits in the project’s vault until it can or until the project’s keys call the sale off, and a lot taken back or burned sends the bid back. A bidder’s money can be tied up, not lost. Tell every bidder before a bid.`,
|
|
5169
|
+
'Check the NFT and its collection for a permanent freeze, transfer or burn delegate (Core), a rule set (a programmable Token Metadata NFT), or such delegates on a compressed NFT’s collection, before it opens.',
|
|
5170
|
+
);
|
|
4761
5171
|
if (!a.lot.collection && !m.live.project)
|
|
4762
5172
|
refuse(
|
|
4763
5173
|
'E-AUCTION',
|
|
@@ -4810,7 +5220,19 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
4810
5220
|
`${label(h)} holds ${label(it)}, which is ${it.soulbound ? 'soulbound' : 'not a collectible'}: only a collectible that can move can be held.`,
|
|
4811
5221
|
'Hold a collectible that is not soulbound.',
|
|
4812
5222
|
);
|
|
4813
|
-
|
|
5223
|
+
// While a hold runs, the project's authority alone decides where the NFT goes; a far end is taken.
|
|
5224
|
+
farTime(
|
|
5225
|
+
now + seconds(h.for, `${where}.for`),
|
|
5226
|
+
`${where}.for`,
|
|
5227
|
+
`${label(h)}’s holds`,
|
|
5228
|
+
h.for,
|
|
5229
|
+
'each NFT waits with the project',
|
|
5230
|
+
);
|
|
5231
|
+
warn(
|
|
5232
|
+
'W-HOLD',
|
|
5233
|
+
where,
|
|
5234
|
+
`${label(h)}: while a hold runs, the project’s authority alone decides where its NFT goes, and it runs as long as the project sets. Players trust the project with the NFT until then.`,
|
|
5235
|
+
);
|
|
4814
5236
|
if (h.until !== undefined) {
|
|
4815
5237
|
try {
|
|
4816
5238
|
if (toTime(h.until, now) < now)
|
|
@@ -4850,6 +5272,14 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
4850
5272
|
const keys = g.keys ?? [];
|
|
4851
5273
|
const l = g.limits ?? {};
|
|
4852
5274
|
const open = !vouchers.length && !keys.length && !(g.list ?? []).length;
|
|
5275
|
+
// A grant written again with the same list lets its wallets claim again.
|
|
5276
|
+
if ((g.list ?? []).length)
|
|
5277
|
+
warn(
|
|
5278
|
+
'W-GRANT',
|
|
5279
|
+
`${where}.list`,
|
|
5280
|
+
`${label(g)} pays the wallets on its list: if it is ever made again with the same list (closed and opened anew), they can claim again.`,
|
|
5281
|
+
'For a second round, change the list or the amounts; leave it as it is to pay each wallet once.',
|
|
5282
|
+
);
|
|
4853
5283
|
// List entries that name their own amount: tokens of a fixed amount only, each at most the grant's (OverPrize).
|
|
4854
5284
|
for (const [i, e] of (g.list ?? []).entries()) {
|
|
4855
5285
|
if (typeof e !== 'object' || e.amount === undefined) continue;
|
|
@@ -5287,7 +5717,7 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
5287
5717
|
for (const step of setup?.steps ?? []) {
|
|
5288
5718
|
let bytes;
|
|
5289
5719
|
try {
|
|
5290
|
-
bytes =
|
|
5720
|
+
bytes = stepAloneBytes(step, ctx);
|
|
5291
5721
|
} catch {
|
|
5292
5722
|
continue;
|
|
5293
5723
|
}
|