@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.
- package/README.md +10 -10
- package/examples/coin-flip-arena.json +2 -7
- package/examples/elements-set.json +2 -9
- package/examples/expedition-guild.json +1 -8
- package/examples/guild-missions.json +3 -10
- package/examples/halving-seasons.json +4 -9
- package/examples/hero-trials.json +1 -4
- package/examples/merge-2048.json +5 -5
- package/examples/paper-hands-farm.json +4 -16
- package/examples/partner-vault.json +1 -4
- package/examples/stakers-draw.json +1 -8
- package/examples/star-fleet.json +1 -8
- package/examples/starforce.json +6 -6
- package/js/approve/details.mjs +8 -7
- package/js/approve/server.mjs +20 -5
- package/js/chain/borsh.js +9 -0
- package/js/chain/core.js +14 -2
- package/js/chain/lookup.js +8 -2
- package/js/chain/rpc.js +4 -2
- package/js/client/accounts.js +448 -116
- package/js/client/grant.js +125 -66
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +2360 -2218
- package/js/client/index.js +1 -0
- package/js/client/ix.js +1204 -473
- package/js/client/pda.js +20 -16
- package/js/client/policy.js +44 -23
- package/js/client/random.js +70 -0
- package/js/client/sizes.js +255 -12
- package/js/economy/check.mjs +455 -210
- package/js/economy/cost.mjs +45 -30
- package/js/economy/examples.mjs +2 -2
- package/js/economy/explain.mjs +24 -20
- package/js/economy/guide.mjs +31 -30
- package/js/economy/live.mjs +30 -7
- package/js/economy/model.mjs +171 -56
- package/js/economy/plan.mjs +380 -112
- package/js/economy/schema.json +291 -143
- package/js/economy/simulate.mjs +154 -87
- package/js/mcp/tools.mjs +14 -8
- package/js/play/index.js +1121 -375
- package/js/play/keys.js +167 -0
- package/package.json +1 -1
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +15 -11
- package/skills/xsolana-economy/references/after-launch.md +37 -27
- package/skills/xsolana-economy/references/apps.md +55 -19
- package/skills/xsolana-economy/references/sentence-to-spec.md +30 -22
- package/skills/xsolana-economy/references/telling-the-person.md +52 -11
- package/types/chain/core.d.ts +3 -1
- package/types/chain/lookup.d.ts +5 -2
- package/types/chain/rpc.d.ts +3 -1
- package/types/client/accounts.d.ts +195 -55
- package/types/client/grant.d.ts +72 -27
- package/types/client/idl/loot.d.ts +590 -559
- package/types/client/index.d.ts +1 -0
- package/types/client/ix.d.ts +3862 -234
- package/types/client/pda.d.ts +19 -15
- package/types/client/policy.d.ts +39 -14
- package/types/client/random.d.ts +26 -0
- package/types/client/sizes.d.ts +93 -5
- package/types/economy/check.d.mts +5 -10
- package/types/economy/cost.d.mts +2 -1
- package/types/economy/live.d.mts +16 -1
- package/types/economy/model.d.mts +105 -37
- package/types/economy/plan.d.mts +193 -192
- package/types/economy/simulate.d.mts +21 -13
- package/types/mcp/tools.d.mts +21 -13
- package/types/play/index.d.ts +450 -39
- 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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
77
|
-
npx -y @xgamesfun/solana@0.
|
|
78
|
-
npx -y @xgamesfun/solana@0.
|
|
79
|
-
npx -y @xgamesfun/solana@0.
|
|
80
|
-
npx -y @xgamesfun/solana@0.
|
|
81
|
-
npx -y @xgamesfun/solana@0.
|
|
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": "
|
|
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": "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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": "
|
|
55
|
+
"does": "pay",
|
|
61
56
|
"from": "season",
|
|
62
|
-
"to": "
|
|
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": "
|
|
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
|
|
84
|
+
"rewards": "gem",
|
|
88
85
|
"rate": { "perDay": 0, "minted": true },
|
|
89
86
|
"tiers": [{ "lock": "1d", "multiplier": 1, "roll": "expedition-result" }]
|
|
90
87
|
}
|
package/examples/merge-2048.json
CHANGED
|
@@ -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
|
|
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
|
|
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": "
|
|
113
|
-
"
|
|
114
|
-
"
|
|
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.
|
|
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
|
-
"
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
48
|
+
"rewards": "gem",
|
|
56
49
|
"rate": {
|
|
57
50
|
"perDay": 0.001,
|
|
58
51
|
"minted": true
|
package/examples/star-fleet.json
CHANGED
|
@@ -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
|
|
114
|
+
"rewards": "ore",
|
|
122
115
|
"rate": {
|
|
123
116
|
"perDay": 100,
|
|
124
117
|
"minted": true
|
package/examples/starforce.json
CHANGED
|
@@ -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
|
|
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
|
|
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": "
|
|
96
|
+
"gives": "nothing",
|
|
97
97
|
"chance": 14.5
|
|
98
98
|
},
|
|
99
99
|
{
|
|
100
|
-
"gives": "
|
|
100
|
+
"gives": "pay",
|
|
101
101
|
"pool": "jackpot",
|
|
102
|
-
"
|
|
103
|
-
"
|
|
102
|
+
"chance": 0.5,
|
|
103
|
+
"amount": 0.2
|
|
104
104
|
}
|
|
105
105
|
]
|
|
106
106
|
}
|
package/js/approve/details.mjs
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
|
package/js/approve/server.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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). */
|