@xgamesfun/solana 0.20.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +10 -10
  2. package/examples/coin-flip-arena.json +2 -7
  3. package/examples/elements-set.json +2 -9
  4. package/examples/expedition-guild.json +1 -8
  5. package/examples/guild-missions.json +3 -10
  6. package/examples/halving-seasons.json +4 -9
  7. package/examples/hero-trials.json +1 -4
  8. package/examples/merge-2048.json +5 -5
  9. package/examples/paper-hands-farm.json +4 -16
  10. package/examples/partner-vault.json +1 -4
  11. package/examples/stakers-draw.json +1 -8
  12. package/examples/star-fleet.json +1 -8
  13. package/examples/starforce.json +6 -6
  14. package/js/approve/details.mjs +8 -7
  15. package/js/approve/server.mjs +20 -5
  16. package/js/chain/borsh.js +9 -0
  17. package/js/chain/core.js +14 -2
  18. package/js/chain/lookup.js +8 -2
  19. package/js/chain/rpc.js +4 -2
  20. package/js/client/accounts.js +448 -116
  21. package/js/client/grant.js +125 -66
  22. package/js/client/idl/loot.js +1 -1
  23. package/js/client/idl/loot.json +2360 -2218
  24. package/js/client/index.js +1 -0
  25. package/js/client/ix.js +1204 -473
  26. package/js/client/pda.js +20 -16
  27. package/js/client/policy.js +44 -23
  28. package/js/client/random.js +70 -0
  29. package/js/client/sizes.js +255 -12
  30. package/js/economy/check.mjs +455 -210
  31. package/js/economy/cost.mjs +45 -30
  32. package/js/economy/examples.mjs +2 -2
  33. package/js/economy/explain.mjs +24 -20
  34. package/js/economy/guide.mjs +31 -30
  35. package/js/economy/live.mjs +30 -7
  36. package/js/economy/model.mjs +171 -56
  37. package/js/economy/plan.mjs +380 -112
  38. package/js/economy/schema.json +291 -143
  39. package/js/economy/simulate.mjs +154 -87
  40. package/js/mcp/tools.mjs +14 -8
  41. package/js/play/index.js +1121 -375
  42. package/js/play/keys.js +167 -0
  43. package/package.json +1 -1
  44. package/release.json +5 -5
  45. package/skills/xsolana-economy/SKILL.md +15 -11
  46. package/skills/xsolana-economy/references/after-launch.md +37 -27
  47. package/skills/xsolana-economy/references/apps.md +55 -19
  48. package/skills/xsolana-economy/references/sentence-to-spec.md +30 -22
  49. package/skills/xsolana-economy/references/telling-the-person.md +52 -11
  50. package/types/chain/core.d.ts +3 -1
  51. package/types/chain/lookup.d.ts +5 -2
  52. package/types/chain/rpc.d.ts +3 -1
  53. package/types/client/accounts.d.ts +195 -55
  54. package/types/client/grant.d.ts +72 -27
  55. package/types/client/idl/loot.d.ts +590 -559
  56. package/types/client/index.d.ts +1 -0
  57. package/types/client/ix.d.ts +3862 -234
  58. package/types/client/pda.d.ts +19 -15
  59. package/types/client/policy.d.ts +39 -14
  60. package/types/client/random.d.ts +26 -0
  61. package/types/client/sizes.d.ts +93 -5
  62. package/types/economy/check.d.mts +5 -10
  63. package/types/economy/cost.d.mts +2 -1
  64. package/types/economy/live.d.mts +16 -1
  65. package/types/economy/model.d.mts +105 -37
  66. package/types/economy/plan.d.mts +193 -192
  67. package/types/economy/simulate.d.mts +21 -13
  68. package/types/mcp/tools.d.mts +21 -13
  69. package/types/play/index.d.ts +450 -39
  70. package/types/play/keys.d.ts +51 -0
package/README.md CHANGED
@@ -12,7 +12,7 @@ package `@xgamesfun/solana`:
12
12
 
13
13
  An economy is a JSON spec, not a new smart contract. Every project shares the one program, and none writes its own.
14
14
 
15
- > **Status:** loot 0.20.1, program ID `9cwqM1TXytPnFZsLfmUyv2dg4uW6Ch9C6UJVPns7NFQ`, **devnet only**. The program is
15
+ > **Status:** loot 0.24.0, program ID `9cwqM1TXytPnFZsLfmUyv2dg4uW6Ch9C6UJVPns7NFQ`, **devnet only**. The program is
16
16
  > not independently audited, and nothing runs on mainnet until it is. See [SECURITY.md](SECURITY.md).
17
17
 
18
18
  > **Disclaimer:** provided as is, with no warranty and no guarantee that the program or the tools are safe. Use them
@@ -27,7 +27,7 @@ An economy is a JSON spec, not a new smart contract. Every project shares the on
27
27
  - lotteries and raffles, including no-loss lotteries
28
28
  - prizes of any kind on every route, random or not: items, tokens, and NFTs the project holds in a stash
29
29
  - an NFT as the price on every route that takes money
30
- - staking farms with lock tiers, boosts, early exits and tolls
30
+ - staking farms with lock tiers, boosts, early exits, fixed or shared rates and terms
31
31
  - crafting recipes
32
32
  - holds and contests, with prizes by rank or by stake
33
33
  - auctions: the highest bid wins, the outbid refunded at once
@@ -59,13 +59,13 @@ codex plugin add xsolana@xgamesfun
59
59
  ```
60
60
 
61
61
  **Any agent with MCP** (Cursor, opencode, Cline…): run the server with
62
- `npx -y --package=@xgamesfun/solana@0.20.1 xsolana mcp`. Or let `npx -y @xgamesfun/solana@0.20.1 connect` register it, with
62
+ `npx -y --package=@xgamesfun/solana@0.24.0 xsolana mcp`. Or let `npx -y @xgamesfun/solana@0.24.0 connect` register it, with
63
63
  the skill, for Claude Code and Codex.
64
64
 
65
65
  Then ask for an economy, for example *"a fishing game where each cast costs 0.02 SOL, with a rare golden fish and a
66
66
  monthly draw"*.
67
67
 
68
- Going live is always yours: `npx -y @xgamesfun/solana@0.20.1 approve` opens a page on your computer where you connect a
68
+ Going live is always yours: `npx -y @xgamesfun/solana@0.24.0 approve` opens a page on your computer where you connect a
69
69
  devnet wallet and approve each transaction. The agent never signs and never asks for a key.
70
70
 
71
71
  ## Use it from a terminal
@@ -73,12 +73,12 @@ devnet wallet and approve each transaction. The agent never signs and never asks
73
73
  Every tool runs from the CLI too, on a spec file (`xsolana.json` by default):
74
74
 
75
75
  ```bash
76
- npx -y @xgamesfun/solana@0.20.1 examples shop with loot boxes # the nearest worked examples
77
- npx -y @xgamesfun/solana@0.20.1 check xsolana.json # refusals and warnings
78
- npx -y @xgamesfun/solana@0.20.1 simulate xsolana.json # where the money goes, day by day
79
- npx -y @xgamesfun/solana@0.20.1 cost xsolana.json # what going live and each action cost
80
- npx -y @xgamesfun/solana@0.20.1 explain xsolana.json # the economy in plain words
81
- npx -y @xgamesfun/solana@0.20.1 approve xsolana.json # go live, approved in your own wallet
76
+ npx -y @xgamesfun/solana@0.24.0 examples shop with loot boxes # the nearest worked examples
77
+ npx -y @xgamesfun/solana@0.24.0 check xsolana.json # refusals and warnings
78
+ npx -y @xgamesfun/solana@0.24.0 simulate xsolana.json # where the money goes, day by day
79
+ npx -y @xgamesfun/solana@0.24.0 cost xsolana.json # what going live and each action cost
80
+ npx -y @xgamesfun/solana@0.24.0 explain xsolana.json # the economy in plain words
81
+ npx -y @xgamesfun/solana@0.24.0 approve xsolana.json # go live, approved in your own wallet
82
82
  ```
83
83
 
84
84
  `xsolana help` lists every command. Every command names the exact version, so `npx` runs that release and never whatever
@@ -26,11 +26,6 @@
26
26
  "currency": "sol",
27
27
  "seed": 5,
28
28
  "about": "Pays the winning flips. 88% of every flip comes back into it."
29
- },
30
- {
31
- "key": "backers",
32
- "currency": "sol",
33
- "about": "The 2% edge, shared by everyone who stakes ARENA."
34
29
  }
35
30
  ],
36
31
  "boxes": [
@@ -48,7 +43,7 @@
48
43
  "percent": 10
49
44
  },
50
45
  {
51
- "to": "pool:backers",
46
+ "to": "farm:backers",
52
47
  "percent": 2
53
48
  }
54
49
  ],
@@ -96,7 +91,7 @@
96
91
  "key": "backers",
97
92
  "name": "Back the house",
98
93
  "stakes": "arena",
99
- "rewards": "backers",
94
+ "rewards": "sol",
100
95
  "tiers": [
101
96
  {
102
97
  "lock": "1d",
@@ -46,13 +46,6 @@
46
46
  "name": "Elemental Badge"
47
47
  }
48
48
  ],
49
- "pools": [
50
- {
51
- "key": "spark-rewards",
52
- "currency": "spark",
53
- "about": "Names the reward token: both farms mint SPARK at their rates, so nothing waits here."
54
- }
55
- ],
56
49
  "recipes": [
57
50
  {
58
51
  "key": "bind",
@@ -232,7 +225,7 @@
232
225
  "key": "altar",
233
226
  "name": "The altar",
234
227
  "stakes": "collectibles",
235
- "rewards": "spark-rewards",
228
+ "rewards": "spark",
236
229
  "rate": {
237
230
  "perDay": 10,
238
231
  "minted": true
@@ -249,7 +242,7 @@
249
242
  "name": "The sanctum",
250
243
  "stakes": "collectibles",
251
244
  "item": "badge",
252
- "rewards": "spark-rewards",
245
+ "rewards": "spark",
253
246
  "rate": {
254
247
  "perDay": 100,
255
248
  "minted": true
@@ -38,13 +38,6 @@
38
38
  "name": "Rare Relic"
39
39
  }
40
40
  ],
41
- "pools": [
42
- {
43
- "key": "dust-rewards",
44
- "currency": "dust",
45
- "about": "Names the guild's reward token: the guild mints DUST at its rate, so nothing waits here."
46
- }
47
- ],
48
41
  "boxes": [
49
42
  {
50
43
  "key": "chest",
@@ -110,7 +103,7 @@
110
103
  "name": "The guild",
111
104
  "stakes": "collectibles",
112
105
  "item": "hero",
113
- "rewards": "dust-rewards",
106
+ "rewards": "dust",
114
107
  "rate": {
115
108
  "perDay": 10,
116
109
  "minted": true
@@ -20,13 +20,6 @@
20
20
  "collection": {
21
21
  "name": "Guild Missions"
22
22
  },
23
- "pools": [
24
- {
25
- "key": "gem-rewards",
26
- "currency": "gem",
27
- "about": "Names the guild's token: every farm here mints it, so nothing waits in this pool."
28
- }
29
- ],
30
23
  "traits": [
31
24
  {
32
25
  "collection": "Cvb5ZbtpUMk8yNG9GzC5nQ7FkB2xPVUZzZaxXqgNbvxR",
@@ -43,7 +36,7 @@
43
36
  "gate": {
44
37
  "collection": "Cvb5ZbtpUMk8yNG9GzC5nQ7FkB2xPVUZzZaxXqgNbvxR"
45
38
  },
46
- "rewards": "gem-rewards",
39
+ "rewards": "gem",
47
40
  "rate": {
48
41
  "perDay": 5,
49
42
  "minted": true
@@ -77,7 +70,7 @@
77
70
  "attribute": "level",
78
71
  "atLeast": 3
79
72
  },
80
- "rewards": "gem-rewards",
73
+ "rewards": "gem",
81
74
  "rate": {
82
75
  "perDay": 0,
83
76
  "minted": true
@@ -110,7 +103,7 @@
110
103
  "atLeast": 6
111
104
  }
112
105
  ],
113
- "rewards": "gem-rewards",
106
+ "rewards": "gem",
114
107
  "rate": {
115
108
  "perDay": 0,
116
109
  "minted": true
@@ -2,7 +2,7 @@
2
2
  "version": 2,
3
3
  "network": "devnet",
4
4
  "name": "Halving Seasons",
5
- "prompt": "Rewards mint into the play pool on a schedule that halves every 30 days; a soulbound season pass claims 10 PLAY a day from it; 10% of the play pool drips into the vault's interest every day, shared by everyone who stakes PLAY.",
5
+ "prompt": "Rewards mint into the play pool on a schedule that halves every 30 days; a soulbound season pass claims 10 PLAY a day from it; 10% of the play pool drips into the vault every day, shared by everyone who stakes PLAY.",
6
6
  "currency": "sol",
7
7
  "tokens": [
8
8
  {
@@ -36,11 +36,6 @@
36
36
  "key": "season",
37
37
  "currency": "play",
38
38
  "about": "What the season mints, before it is claimed."
39
- },
40
- {
41
- "key": "interest",
42
- "currency": "play",
43
- "about": "The vault's interest."
44
39
  }
45
40
  ],
46
41
  "schedules": [
@@ -57,9 +52,9 @@
57
52
  {
58
53
  "key": "drip",
59
54
  "name": "Interest drip",
60
- "does": "move",
55
+ "does": "pay",
61
56
  "from": "season",
62
- "to": "interest",
57
+ "to": "farm:vault",
63
58
  "percent": 10,
64
59
  "every": "1d"
65
60
  }
@@ -100,7 +95,7 @@
100
95
  "key": "vault",
101
96
  "name": "The vault",
102
97
  "stakes": "play",
103
- "rewards": "interest",
98
+ "rewards": "play",
104
99
  "tiers": [
105
100
  {
106
101
  "lock": "1d",
@@ -13,9 +13,6 @@
13
13
  { "key": "hero", "name": "Hero", "price": 0.05 },
14
14
  { "key": "relic", "name": "Relic" }
15
15
  ],
16
- "pools": [
17
- { "key": "gem-rewards", "currency": "gem", "about": "Names the expedition's token: it pays nothing, its result is a roll." }
18
- ],
19
16
  "boxes": [
20
17
  {
21
18
  "key": "enchant",
@@ -84,7 +81,7 @@
84
81
  "stakes": "collectibles",
85
82
  "item": "hero",
86
83
  "custody": "freeze",
87
- "rewards": "gem-rewards",
84
+ "rewards": "gem",
88
85
  "rate": { "perDay": 0, "minted": true },
89
86
  "tiers": [{ "lock": "1d", "multiplier": 1, "roll": "expedition-result" }]
90
87
  }
@@ -2,7 +2,7 @@
2
2
  "version": 2,
3
3
  "network": "devnet",
4
4
  "name": "2048 Collection",
5
- "prompt": "Two of tier N merge into one of tier N+1. The single top piece also claims a huge pool prize, so the whole collection races to merge. Priced in a coin launched on pump.fun.",
5
+ "prompt": "Two of tier N merge into one of tier N+1. The single top piece also claims a fixed prize from a pool every sale fills, so the whole collection races to merge. Priced in a coin launched on pump.fun.",
6
6
  "currency": "tile",
7
7
  "tokens": [
8
8
  {
@@ -55,7 +55,7 @@
55
55
  {
56
56
  "key": "grand",
57
57
  "currency": "tile",
58
- "about": "20% of every tile sold; half of it goes to whoever makes the top tile."
58
+ "about": "20% of every tile sold; whoever makes the top tile takes 8, half of what its eight tiles paid in."
59
59
  }
60
60
  ],
61
61
  "recipes": [
@@ -109,9 +109,9 @@
109
109
  "item": "tier-4"
110
110
  },
111
111
  {
112
- "gives": "share",
113
- "pool": "grand",
114
- "percent": 50
112
+ "gives": "tokens",
113
+ "from": "pool:grand",
114
+ "amount": 8
115
115
  }
116
116
  ]
117
117
  }
@@ -2,7 +2,7 @@
2
2
  "version": 2,
3
3
  "network": "devnet",
4
4
  "name": "Paper Hands Farm",
5
- "prompt": "Stake GEM for 30 days to earn more GEM. Leave early and 10% of what you take out goes to the stakers who stay. Unstaking takes 7 days, earning nothing meanwhile. 2% of every harvest is burned. Holders can compound their rewards without restarting the lock.",
5
+ "prompt": "Stake GEM for 30 days to earn more GEM, 1,000 minted into the farm a day. Leave early and 10% of what you take out goes to the stakers who stay. Holders can compound their rewards without restarting the lock.",
6
6
  "currency": "gem",
7
7
  "tokens": [
8
8
  {
@@ -14,20 +14,13 @@
14
14
  }
15
15
  ],
16
16
  "collection": false,
17
- "pools": [
18
- {
19
- "key": "farm-rewards",
20
- "currency": "gem",
21
- "about": "What the farm shares: a daily emission, and early-exit fees."
22
- }
23
- ],
24
17
  "schedules": [
25
18
  {
26
19
  "key": "emission",
27
20
  "name": "Daily emission",
28
21
  "does": "issue",
29
22
  "currency": "gem",
30
- "pool": "farm-rewards",
23
+ "to": "farm:farm",
31
24
  "amount": 1000,
32
25
  "every": "1d"
33
26
  }
@@ -37,7 +30,7 @@
37
30
  "key": "farm",
38
31
  "name": "The farm",
39
32
  "stakes": "gem",
40
- "rewards": "farm-rewards",
33
+ "rewards": "gem",
41
34
  "tiers": [
42
35
  {
43
36
  "lock": "30d",
@@ -45,12 +38,7 @@
45
38
  "exitFee": 10
46
39
  }
47
40
  ],
48
- "exitFeeTo": "pool:farm-rewards",
49
- "unbond": "7d",
50
- "toll": {
51
- "percent": 2,
52
- "to": "burn"
53
- }
41
+ "exitFeeTo": "rewards"
54
42
  }
55
43
  ]
56
44
  }
@@ -13,9 +13,6 @@
13
13
  { "key": "pass", "name": "Expedition Pass", "price": 0.01 },
14
14
  { "key": "trinket", "name": "Trinket", "value": 0.01 }
15
15
  ],
16
- "pools": [
17
- { "key": "shard-rewards", "currency": "shards", "about": "Names the expedition's token: it pays nothing, its result is a chest." }
18
- ],
19
16
  "stashes": [
20
17
  {
21
18
  "key": "vault",
@@ -94,7 +91,7 @@
94
91
  "name": "Expedition",
95
92
  "stakes": "collectibles",
96
93
  "item": "pass",
97
- "rewards": "shard-rewards",
94
+ "rewards": "shards",
98
95
  "rate": { "perDay": 0, "minted": true },
99
96
  "tiers": [{ "lock": "1d", "multiplier": 1, "prize": { "gives": "roll", "box": "daily-chest" } }]
100
97
  }
@@ -40,19 +40,12 @@
40
40
  }
41
41
  }
42
42
  ],
43
- "pools": [
44
- {
45
- "key": "vault-rewards",
46
- "currency": "gem",
47
- "about": "Names the vault's token: its rate is minted, so nothing is paid from here."
48
- }
49
- ],
50
43
  "farms": [
51
44
  {
52
45
  "key": "vault",
53
46
  "name": "The vault",
54
47
  "stakes": "gem",
55
- "rewards": "vault-rewards",
48
+ "rewards": "gem",
56
49
  "rate": {
57
50
  "perDay": 0.001,
58
51
  "minted": true
@@ -34,13 +34,6 @@
34
34
  "price": 0.05
35
35
  }
36
36
  ],
37
- "pools": [
38
- {
39
- "key": "ore-rewards",
40
- "currency": "ore",
41
- "about": "Names the fleet's reward token: the mine mints ORE at its rate, so nothing waits here."
42
- }
43
- ],
44
37
  "recipes": [
45
38
  {
46
39
  "key": "refuel",
@@ -118,7 +111,7 @@
118
111
  "name": "The mine",
119
112
  "stakes": "collectibles",
120
113
  "item": "ship",
121
- "rewards": "ore-rewards",
114
+ "rewards": "ore",
122
115
  "rate": {
123
116
  "perDay": 100,
124
117
  "minted": true
@@ -2,7 +2,7 @@
2
2
  "version": 2,
3
3
  "network": "devnet",
4
4
  "name": "Starforce",
5
- "prompt": "Each try burns the sword and pays a fee; the roll goes up a level, stays (the same sword, minted again) or destroys it, with a tiny lucky chance at a tenth of the jackpot the fees fill.",
5
+ "prompt": "Each try burns the sword and pays a fee; the roll goes up a level, stays (the same sword, minted again) or destroys it, with a tiny lucky chance at a fixed prize from the pool the fees fill.",
6
6
  "currency": "sol",
7
7
  "tokens": [
8
8
  {
@@ -39,7 +39,7 @@
39
39
  "key": "jackpot",
40
40
  "currency": "sol",
41
41
  "seed": 2,
42
- "about": "Every try's fee; the lucky roll takes a tenth."
42
+ "about": "Every try's fee; the lucky roll takes 0.2 SOL."
43
43
  }
44
44
  ],
45
45
  "recipes": [
@@ -93,14 +93,14 @@
93
93
  "chance": 45
94
94
  },
95
95
  {
96
- "gives": "burn",
96
+ "gives": "nothing",
97
97
  "chance": 14.5
98
98
  },
99
99
  {
100
- "gives": "share",
100
+ "gives": "pay",
101
101
  "pool": "jackpot",
102
- "percent": 10,
103
- "chance": 0.5
102
+ "chance": 0.5,
103
+ "amount": 0.2
104
104
  }
105
105
  ]
106
106
  }
@@ -4,7 +4,7 @@
4
4
  // claim it. Addresses are written in full, so an owner sees a payee that an edit re-pointed before they sign.
5
5
  import { decodeBorsh } from '../chain/borsh.js';
6
6
  import { equalBytes, toHex } from '../chain/bytes.js';
7
- import { accountKind, decode, IDL, keyLeaf, merkleRoot } from '../client/index.js';
7
+ import { accountKind, decode, IDL, instructionOf, keyLeaf, merkleRoot } from '../client/index.js';
8
8
  import { LOOT_PROGRAM, pda } from '../client/pda.js';
9
9
  import { fromUnits } from '../economy/model.mjs';
10
10
  import { grantArgs } from '../economy/plan.mjs';
@@ -18,7 +18,7 @@ const leafNames = (list) => list.flatMap((a) => (a.accounts ? leafNames(a.accoun
18
18
  /** A loot instruction read back: { name, args, accounts: { name: address } }, or null for another program's. */
19
19
  function readLootInstruction(ix) {
20
20
  if (ix.programId !== LOOT_PROGRAM) return null;
21
- const def = IDL.instructions.find((i) => i.discriminator.length <= ix.data.length && i.discriminator.every((b, j) => ix.data[j] === b));
21
+ const def = instructionOf(ix.data);
22
22
  if (!def) return null;
23
23
  const args = {};
24
24
  let at = def.discriminator.length;
@@ -93,6 +93,7 @@ function readDetails(reads, names) {
93
93
  const owner = read.accounts.owner ?? read.accounts.seller;
94
94
  const project = read.accounts.project;
95
95
  const pots = new Map(names.m.lotteries.map((l) => [pda.pot(pda.lottery(project, l.id)), l.name ?? l.key]));
96
+ const vaults = new Map(names.m.farms.map((f) => [pda.rewards(pda.farm(project, f.id)), f.name ?? f.key]));
96
97
  // Before a wallet connects, the plan stands a made-up address in for it (one no spec may name): that one is not
97
98
  // shown. Once one is connected, only that wallet is called yours.
98
99
  const yours = (a) => (names.owner == null ? 'your wallet' : a === names.owner ? `your wallet (${a})` : `the project’s owner, ${a}`);
@@ -105,7 +106,7 @@ function readDetails(reads, names) {
105
106
  const v = to[kind];
106
107
  if (kind === 'Pool') return `the pool “${names.pools.get(v[0]) ?? v[0]}”`;
107
108
  if (kind === 'Wallet') return wallet(v[0]);
108
- if (kind === 'Account') return pots.has(v[0]) ? `the pot of “${pots.get(v[0])}”` : `the token account ${v[0]}`;
109
+ if (kind === 'Account') return pots.has(v[0]) ? `the pot of “${pots.get(v[0])}”` : vaults.has(v[0]) ? `the rewards of the farm “${vaults.get(v[0])}”` : `the token account ${v[0]}`;
109
110
  if (kind === 'Holder') return `whoever holds the NFT ${v[0]}`;
110
111
  if (kind === 'Chosen') return 'a wallet the buyer chooses';
111
112
  return JSON.stringify(to);
@@ -173,7 +174,7 @@ function prizeWords(gives, m) {
173
174
  const item = (id) => `the item “${named(m.items, id)?.name ?? named(m.items, id)?.key ?? id}”`;
174
175
  if (kind === 'Tokens') {
175
176
  const from = v.from.Pool ? `from the pool “${m.pools.find((p) => p.index === v.from.Pool[0])?.key ?? v.from.Pool[0]}”` : v.from.Mint ? `minted from ${v.from.Mint[0]}` : `from the vault ${v.from.Vest[0]}`;
176
- const amount = v.amount.Fixed ? tokens(v.amount.Fixed[0], prizeToken(gives, m)) : `${Number(v.amount.Bps[0]) / 100}% of what it holds`;
177
+ const amount = tokens(v.amount, prizeToken(gives, m));
177
178
  return `${amount} ${from}`;
178
179
  }
179
180
  if (kind === 'Item') return item(v[0]);
@@ -196,13 +197,13 @@ function grantTerms(args, names) {
196
197
  l.wallet?.per_window ? `each wallet ${times(l.wallet.per_window)} every ${l.wallet.window} seconds` : null,
197
198
  l.per_person ? `each person ${times(l.per_person)}` : null,
198
199
  l.per_key ? `each voucher key ${times(l.per_key)}` : null,
199
- l.per_day ? `${times(l.per_day)} a day` : null,
200
- l.total ? `${times(l.total)} in all` : null,
200
+ l.all?.per_window ? `${times(l.all.per_window)} ${Number(l.all.window) === 86_400 ? 'a day' : `every ${l.all.window} seconds`}` : null,
201
+ l.all?.total ? `${times(l.all.total)} in all` : null,
201
202
  BigInt(l.window?.opens_at ?? 0) ? `opens ${when(l.window.opens_at)}` : null,
202
203
  BigInt(l.window?.closes_at ?? 0) ? `closes ${when(l.window.closes_at)}` : null,
203
204
  ].filter(Boolean);
204
205
  // A voucher or a list entry may name an amount of its own, up to the grant's fixed amount (0 is the prize as it is).
205
- const upTo = !args.open && args.gives?.Tokens?.amount?.Fixed ? 'up to ' : '';
206
+ const upTo = !args.open && args.gives?.Tokens ? 'up to ' : '';
206
207
  return [`Each claim gets: ${upTo}${prizeWords(args.gives, names.m)}`, `Limits: ${limits.length ? limits.join(', ') : 'none'}`];
207
208
  }
208
209
 
@@ -13,10 +13,12 @@ import { spawn } from 'node:child_process';
13
13
  import { equalBytes, fromBase64, toBase64 } from '../chain/bytes.js';
14
14
  import { checkCluster, notMainnet, rpcClient } from '../chain/rpc.js';
15
15
  import { addressBytes, encodeBase58 } from '../chain/base58.js';
16
+ import { slotHashes, tableAddress } from '../chain/lookup.js';
17
+ import { SYSVAR_SLOT_HASHES } from '../chain/pda.js';
16
18
  import { MINT_BYTES } from '../chain/token.js';
17
19
  import { compileMessage, readTransaction, wireTransaction } from '../chain/tx.js';
18
20
  import { accountKind, decode } from '../client/accounts.js';
19
- import { LOOT_PROGRAM } from '../client/pda.js';
21
+ import { LOOT_PROGRAM, pda } from '../client/pda.js';
20
22
  import { check, describe } from '../economy/check.mjs';
21
23
  import { cost } from '../economy/cost.mjs';
22
24
  import { LEGAL, legalFlags } from '../economy/legal.mjs';
@@ -170,7 +172,9 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
170
172
  return rents.get(bytes);
171
173
  };
172
174
  const addresses = Object.fromEntries([...keys].map(([n, k]) => [n, k.address]));
173
- const p = prepare(spec, { owner, keys: addresses, live: live() ?? {}, now: at, rent, only });
175
+ // A recent slot, for the project's standing lookup table (made at go-live at a slot still in the slot hashes when it lands).
176
+ const hashes = slotHashes((await chain.account(SYSVAR_SLOT_HASHES).catch(() => null))?.data);
177
+ const p = prepare(spec, { owner, keys: addresses, live: live() ?? {}, now: at, rent, only, slot: hashes[0] ?? null });
174
178
  if (!p.ok) throw new ApproveError(400, describe({ refusals: p.refusals, warnings: [] }));
175
179
  return p;
176
180
  }
@@ -197,7 +201,15 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
197
201
  if (lines) was.set(step.id, lines);
198
202
  }
199
203
  }
200
- wasLive = { steps: was, treasury: p.ctx.treasury };
204
+ // A grant published before and gone from the chain (closed) that the change makes again with a list: a list leaf
205
+ // names no grant or incarnation, so the same root would pay every listed round again.
206
+ const remade = [];
207
+ const published = new Set((spec.live.published.grants ?? []).map((g) => g.key));
208
+ for (const g of model(spec, { now: at }).grants) {
209
+ if (!g.list?.length || !published.has(g.key) || !p.ctx.project) continue;
210
+ if (!(await chain.account(pda.grant(p.ctx.project, g.id)).catch(() => 'unread'))) remade.push(`“${g.name ?? g.key}”`);
211
+ }
212
+ wasLive = { steps: was, treasury: p.ctx.treasury, remade };
201
213
  }
202
214
  const detailsOf = (t, i, n) => {
203
215
  const lines = stepDetails(t.built[i], n);
@@ -206,6 +218,7 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
206
218
  };
207
219
  /** The lines a change is held against for its outside-address warning: what was read from the chain, or nothing yet. */
208
220
  const wasLines = () => (spec.live?.published ? [...(wasLive ? [...wasLive.steps.values()].flat() : []), ...(wasLive?.treasury ? [`Treasury: ${wasLive.treasury}`] : [])] : null);
221
+ const remadeLists = () => (wasLive?.remade?.length ? `This change makes ${wasLive.remade.join(', ')} again after it was closed, with the same list: a list names no grant, so every wallet on it could claim every listed round again. Give the grant a fresh list (new rounds) before you approve.` : null);
209
222
  const unread = () => (spec.live?.published && !wasLive ? 'What is live has not been read from the chain yet: connect your wallet, and each line is marked against what is on chain.' : null);
210
223
 
211
224
  // The project's own settings a change names are never published: the page says so.
@@ -227,7 +240,7 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
227
240
  program: LOOT_PROGRAM,
228
241
  cluster: 'devnet',
229
242
  setupSol: cost(spec, { now: at, live: live(), only }).setup?.totalSol ?? null,
230
- warnings: [...(extra.refused ? [extra.refused] : []), ...verdict.warnings.filter((w) => !Object.hasOwn(LEGAL, w.code)).map((w) => w.message), ...[settings, unread(), outsideAddresses(steps.flatMap((s) => s.details), { was: wasLines(), owner: n.owner })].filter(Boolean)],
243
+ warnings: [...(extra.refused ? [extra.refused] : []), ...verdict.warnings.filter((w) => !Object.hasOwn(LEGAL, w.code)).map((w) => w.message), ...[settings, unread(), remadeLists(), outsideAddresses(steps.flatMap((s) => s.details), { was: wasLines(), owner: n.owner })].filter(Boolean)],
231
244
  // The legal flags in plain words, shown in a block of their own whatever the agent said: Approve waits until the
232
245
  // owner ticks that they read them.
233
246
  legal,
@@ -399,6 +412,7 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
399
412
  ...(ids.has('project') ? { project: ctx.project, treasury: ctx.treasury, authority: ctx.authority } : {}),
400
413
  ...(ids.has('collection') && ctx.collection ? { collection: ctx.collection } : {}),
401
414
  ...(ids.has('tree') && ctx.tree ? { tree: ctx.tree } : {}),
415
+ ...(ids.has('table') && ctx.slot != null ? { tables: [tableAddress(ctx.owner, ctx.slot)[0]] } : {}),
402
416
  ...(Object.keys(mints).length ? { mints } : {}),
403
417
  };
404
418
  }
@@ -479,6 +493,7 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
479
493
  ...(made.has('project') ? { project: ctx.project, treasury: ctx.treasury, authority: ctx.authority } : {}),
480
494
  ...(made.has('collection') && ctx.collection ? { collection: ctx.collection } : {}),
481
495
  ...(made.has('tree') && ctx.tree ? { tree: ctx.tree } : {}),
496
+ ...(made.has('table') && ctx.slot != null ? { tables: [tableAddress(ctx.owner, ctx.slot)[0]] } : {}),
482
497
  mints,
483
498
  landed: [...new Set([...(was.landed ?? []), ...ids])],
484
499
  partial: true,
@@ -496,7 +511,7 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
496
511
  if (rest.ok && rest.transactions.length === 0) {
497
512
  const { live: _, ...bare } = spec;
498
513
  // Everything the setup made, whichever session or transaction made it.
499
- const all = { ...ctx, ...Object.fromEntries(['project', 'treasury', 'authority', 'collection', 'tree'].map((k) => [k, progress[k] ?? ctx[k]])), mints: { ...(ctx.mints ?? {}), ...(progress.mints ?? {}) } };
514
+ const all = { ...ctx, ...Object.fromEntries(['project', 'treasury', 'authority', 'collection', 'tree', 'tables'].map((k) => [k, progress[k] ?? ctx[k]])), mints: { ...(ctx.mints ?? {}), ...(progress.mints ?? {}) } };
500
515
  const final = recordLanded(bare, { ctx: all, now: at, at: new Date(clock()).toISOString() });
501
516
  final.live = { ...final.live, transactions: progress.transactions };
502
517
  spec = final;
package/js/chain/borsh.js CHANGED
@@ -35,6 +35,10 @@ function encodeScalar(type, value, out) {
35
35
  else if (type === 'string') {
36
36
  const bytes = utf8(String(value));
37
37
  out.push(uintLE(bytes.length, 4), bytes);
38
+ } else if (type === 'bytes') {
39
+ // A list of bytes (`Vec<u8>`): its length, then the bytes.
40
+ const bytes = Uint8Array.from(value);
41
+ out.push(uintLE(bytes.length, 4), bytes);
38
42
  } else throw new Error(`Unsupported IDL type ${type}`);
39
43
  }
40
44
 
@@ -146,6 +150,11 @@ function decodeScalar(type, data, at) {
146
150
  if (at + 4 + length > data.length) throw new RangeError('string runs past the end of the data');
147
151
  return [fromUtf8(data.subarray(at + 4, at + 4 + length)), at + 4 + length];
148
152
  }
153
+ if (type === 'bytes') {
154
+ const length = Number(readUintLE(data, at, 4));
155
+ if (at + 4 + length > data.length) throw new RangeError('bytes run past the end of the data');
156
+ return [data.slice(at + 4, at + 4 + length), at + 4 + length];
157
+ }
149
158
  throw new Error(`Unsupported IDL type ${type}`);
150
159
  }
151
160
 
package/js/chain/core.js CHANGED
@@ -150,14 +150,26 @@ export function parseCoreAsset(data) {
150
150
  return asset;
151
151
  }
152
152
 
153
- /** Decode the start of a Core CollectionV1: { updateAuthority, name, uri, numMinted, currentSize }. */
153
+ /** Decode the start of a Core CollectionV1: { updateAuthority, name, uri, numMinted, currentSize, bubblegum (whether it
154
+ * carries Bubblegum's plugin, BubblegumV2: compressed NFTs can be minted into it) }. */
154
155
  export function parseCoreCollection(data) {
155
156
  if (data[0] !== COLLECTION_V1) throw new Error('Not a Core collection');
156
157
  const r = reader(data, 1);
157
158
  const updateAuthority = r.address();
158
159
  const name = r.text();
159
160
  const uri = r.text();
160
- return { updateAuthority, name, uri, numMinted: r.u32(), currentSize: r.u32() };
161
+ const out = { updateAuthority, name, uri, numMinted: r.u32(), currentSize: r.u32(), bubblegum: false };
162
+ if (r.at >= data.length || data[r.at] !== PLUGIN_HEADER) return out;
163
+ r.skip(1);
164
+ const registry = reader(data, Number(r.u64()) + 1);
165
+ const plugins = registry.u32();
166
+ for (let i = 0; i < plugins; i++) {
167
+ const type = registry.byte();
168
+ if (registry.byte() === 3) registry.skip(32);
169
+ registry.u64();
170
+ if (type === PLUGIN.BubblegumV2) out.bubblegum = true;
171
+ }
172
+ return out;
161
173
  }
162
174
 
163
175
  /** getProgramAccounts filters for Core assets owned by `owner` in `collection` (no indexer needed). */