@xgamesfun/solana 0.0.0-stage → 0.20.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/DISCLAIMER.md +41 -0
- package/LICENSE +202 -0
- package/NOTICE +13 -0
- package/README.md +172 -2
- package/bin/xsolana.mjs +189 -0
- package/examples/artwork-auction.json +51 -0
- package/examples/blind-forge.json +128 -0
- package/examples/coin-flip-arena.json +108 -0
- package/examples/duel-arena.json +137 -0
- package/examples/elements-set.json +281 -0
- package/examples/event-badges.json +67 -0
- package/examples/expedition-guild.json +161 -0
- package/examples/fair-presale.json +48 -0
- package/examples/falling-drop.json +90 -0
- package/examples/guild-missions.json +144 -0
- package/examples/halving-seasons.json +112 -0
- package/examples/hero-trials.json +96 -0
- package/examples/ideas.json +789 -0
- package/examples/level-ladder.json +65 -0
- package/examples/merge-2048.json +128 -0
- package/examples/nft-raffle.json +33 -0
- package/examples/paper-hands-farm.json +56 -0
- package/examples/partner-vault.json +115 -0
- package/examples/sacrifice-raffle.json +118 -0
- package/examples/scorched-treasury.json +84 -0
- package/examples/shelter-fair.json +164 -0
- package/examples/spark-pity.json +153 -0
- package/examples/stakers-draw.json +125 -0
- package/examples/star-fleet.json +157 -0
- package/examples/starforce.json +116 -0
- package/examples/stud-farm.json +134 -0
- package/examples/team-vesting.json +70 -0
- package/examples/trade-in-shop.json +168 -0
- package/js/approve/details.mjs +262 -0
- package/js/approve/ed25519.mjs +36 -0
- package/js/approve/live-check.mjs +144 -0
- package/js/approve/page.html +86 -0
- package/js/approve/page.js +203 -0
- package/js/approve/server.mjs +711 -0
- package/js/chain/base58.js +73 -0
- package/js/chain/borsh.js +261 -0
- package/js/chain/bytes.js +141 -0
- package/js/chain/core.js +169 -0
- package/js/chain/curve.js +52 -0
- package/js/chain/lookup.js +273 -0
- package/js/chain/mint.js +96 -0
- package/js/chain/pda.js +66 -0
- package/js/chain/programs.js +102 -0
- package/js/chain/rpc.js +217 -0
- package/js/chain/sha256.js +68 -0
- package/js/chain/token.js +85 -0
- package/js/chain/token2022.js +247 -0
- package/js/chain/tx.js +224 -0
- package/js/client/accounts.js +347 -0
- package/js/client/compressed.js +46 -0
- package/js/client/grant.js +240 -0
- package/js/client/idl/loot.js +2 -0
- package/js/client/idl/loot.json +17064 -0
- package/js/client/index.js +9 -0
- package/js/client/ix.js +2156 -0
- package/js/client/pda.js +110 -0
- package/js/client/policy.js +48 -0
- package/js/client/sizes.js +54 -0
- package/js/client/traits.js +44 -0
- package/js/connect/connect.mjs +182 -0
- package/js/economy/check.mjs +1991 -0
- package/js/economy/cost.mjs +170 -0
- package/js/economy/examples.mjs +114 -0
- package/js/economy/explain.mjs +235 -0
- package/js/economy/guide.mjs +231 -0
- package/js/economy/legal.mjs +15 -0
- package/js/economy/live.mjs +107 -0
- package/js/economy/model.mjs +583 -0
- package/js/economy/plan.mjs +887 -0
- package/js/economy/schema.json +2923 -0
- package/js/economy/secrets.mjs +60 -0
- package/js/economy/simulate.mjs +952 -0
- package/js/economy/txsize.mjs +45 -0
- package/js/economy/validate.mjs +85 -0
- package/js/index.js +10 -0
- package/js/mcp/server.mjs +113 -0
- package/js/mcp/tools.mjs +479 -0
- package/js/play/index.js +2109 -0
- package/js/results/index.js +184 -0
- package/js/results/wire.js +90 -0
- package/package.json +100 -4
- package/release.json +8 -0
- package/skills/xsolana-economy/SKILL.md +105 -0
- package/skills/xsolana-economy/references/after-launch.md +114 -0
- package/skills/xsolana-economy/references/apps.md +157 -0
- package/skills/xsolana-economy/references/sentence-to-spec.md +172 -0
- package/skills/xsolana-economy/references/telling-the-person.md +93 -0
- package/types/chain/base58.d.ts +10 -0
- package/types/chain/borsh.d.ts +23 -0
- package/types/chain/bytes.d.ts +45 -0
- package/types/chain/core.d.ts +50 -0
- package/types/chain/curve.d.ts +1 -0
- package/types/chain/lookup.d.ts +191 -0
- package/types/chain/mint.d.ts +25 -0
- package/types/chain/pda.d.ts +25 -0
- package/types/chain/programs.d.ts +65 -0
- package/types/chain/rpc.d.ts +66 -0
- package/types/chain/sha256.d.ts +1 -0
- package/types/chain/token.d.ts +141 -0
- package/types/chain/token2022.d.ts +53 -0
- package/types/chain/tx.d.ts +60 -0
- package/types/client/accounts.d.ts +178 -0
- package/types/client/compressed.d.ts +29 -0
- package/types/client/grant.d.ts +125 -0
- package/types/client/idl/loot.d.ts +4467 -0
- package/types/client/index.d.ts +8 -0
- package/types/client/ix.d.ts +1687 -0
- package/types/client/pda.d.ts +91 -0
- package/types/client/policy.d.ts +23 -0
- package/types/client/sizes.d.ts +13 -0
- package/types/client/traits.d.ts +12 -0
- package/types/economy/check.d.mts +105 -0
- package/types/economy/cost.d.mts +68 -0
- package/types/economy/examples.d.mts +27 -0
- package/types/economy/explain.d.mts +19 -0
- package/types/economy/guide.d.mts +140 -0
- package/types/economy/legal.d.mts +12 -0
- package/types/economy/live.d.mts +109 -0
- package/types/economy/model.d.mts +363 -0
- package/types/economy/plan.d.mts +1013 -0
- package/types/economy/secrets.d.mts +4 -0
- package/types/economy/simulate.d.mts +156 -0
- package/types/economy/txsize.d.mts +17 -0
- package/types/economy/validate.d.mts +3 -0
- package/types/index.d.ts +9 -0
- package/types/mcp/tools.d.mts +627 -0
- package/types/play/index.d.ts +469 -0
- package/types/results/index.d.ts +155 -0
- package/types/results/wire.d.ts +122 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 2,
|
|
3
|
+
"network": "devnet",
|
|
4
|
+
"name": "Stud Farm",
|
|
5
|
+
"prompt": "Studs earn a cover fee for their holder, foals are ready 7 days after breeding, mares have 6 foals in their life, hay rots in 3 days, and a mare can be rested to breed again.",
|
|
6
|
+
"currency": "usdc",
|
|
7
|
+
"tokens": [
|
|
8
|
+
{
|
|
9
|
+
"key": "usdc",
|
|
10
|
+
"source": "existing",
|
|
11
|
+
"name": "USDC (devnet)",
|
|
12
|
+
"symbol": "USDC",
|
|
13
|
+
"decimals": 6,
|
|
14
|
+
"mint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
15
|
+
"venue": "other",
|
|
16
|
+
"powers": {
|
|
17
|
+
"mintAuthority": "someone else",
|
|
18
|
+
"freeze": true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"collection": {
|
|
23
|
+
"name": "Stud Farm"
|
|
24
|
+
},
|
|
25
|
+
"items": [
|
|
26
|
+
{
|
|
27
|
+
"key": "mare",
|
|
28
|
+
"name": "Mare",
|
|
29
|
+
"price": 50,
|
|
30
|
+
"life": {
|
|
31
|
+
"uses": 6,
|
|
32
|
+
"cooldown": "30d"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"key": "stud",
|
|
37
|
+
"name": "Stud",
|
|
38
|
+
"price": 500,
|
|
39
|
+
"supply": 1,
|
|
40
|
+
"rarity": "Legendary"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"key": "hay",
|
|
44
|
+
"name": "Hay bale",
|
|
45
|
+
"price": 1,
|
|
46
|
+
"life": {
|
|
47
|
+
"lasts": "3d"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"key": "foal",
|
|
52
|
+
"name": "Foal",
|
|
53
|
+
"life": {
|
|
54
|
+
"readyAfter": "7d"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"recipes": [
|
|
59
|
+
{
|
|
60
|
+
"key": "breed",
|
|
61
|
+
"name": "Breed",
|
|
62
|
+
"in": [
|
|
63
|
+
{
|
|
64
|
+
"takes": "use",
|
|
65
|
+
"item": "mare"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"takes": "burn",
|
|
69
|
+
"item": "hay",
|
|
70
|
+
"count": 1
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"takes": "pay",
|
|
74
|
+
"amount": 5,
|
|
75
|
+
"currency": "usdc",
|
|
76
|
+
"to": "holder:3rzrHLNCpMjZ1eNTKr5p6cg3byL3UwRzBePEVDJ1RQ4R"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"takes": "pay",
|
|
80
|
+
"amount": 1,
|
|
81
|
+
"currency": "usdc",
|
|
82
|
+
"to": "burn"
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
"out": [
|
|
86
|
+
{
|
|
87
|
+
"gives": "item",
|
|
88
|
+
"item": "foal"
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
"needs": [
|
|
92
|
+
{
|
|
93
|
+
"place": 0,
|
|
94
|
+
"key": "age",
|
|
95
|
+
"atLeast": 2
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
"keeps": [
|
|
99
|
+
{
|
|
100
|
+
"key": "coat",
|
|
101
|
+
"from": 0,
|
|
102
|
+
"to": 0
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"key": "rest",
|
|
108
|
+
"name": "Rest a mare",
|
|
109
|
+
"in": [
|
|
110
|
+
{
|
|
111
|
+
"takes": "renew",
|
|
112
|
+
"item": "mare"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"takes": "pay",
|
|
116
|
+
"amount": 10,
|
|
117
|
+
"currency": "usdc",
|
|
118
|
+
"to": "burn"
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
"out": []
|
|
122
|
+
}
|
|
123
|
+
],
|
|
124
|
+
"scenario": {
|
|
125
|
+
"days": 30,
|
|
126
|
+
"perDay": {
|
|
127
|
+
"item:mare": 10,
|
|
128
|
+
"item:stud": 0,
|
|
129
|
+
"item:hay": 40,
|
|
130
|
+
"recipe:breed": 20,
|
|
131
|
+
"recipe:rest": 2
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 2,
|
|
3
|
+
"network": "devnet",
|
|
4
|
+
"name": "Team Vesting",
|
|
5
|
+
"prompt": "Vest 1,200,000 SHARE to our lead developer: nothing for three months, then 100,000 a month for a year. Nobody, me included, should be able to take it back or stop it. Also pay our two advisors their monthly allocations from a list.",
|
|
6
|
+
"currency": "share",
|
|
7
|
+
"tokens": [
|
|
8
|
+
{
|
|
9
|
+
"key": "share",
|
|
10
|
+
"source": "game",
|
|
11
|
+
"name": "Share",
|
|
12
|
+
"symbol": "SHARE",
|
|
13
|
+
"decimals": 6
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"collection": false,
|
|
17
|
+
"schedules": [
|
|
18
|
+
{
|
|
19
|
+
"key": "lead",
|
|
20
|
+
"name": "Lead developer",
|
|
21
|
+
"does": "pay",
|
|
22
|
+
"sealed": true,
|
|
23
|
+
"currency": "share",
|
|
24
|
+
"to": "wallet:5DckzByxgf3nMQ1m1SGfpsnRXozawiFncTBZDtZ9Nj7T",
|
|
25
|
+
"amount": 100000,
|
|
26
|
+
"start": "publish+90d",
|
|
27
|
+
"every": "30d",
|
|
28
|
+
"maxRuns": 12,
|
|
29
|
+
"fund": 1200000,
|
|
30
|
+
"fundFrom": "mint"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"grants": [
|
|
34
|
+
{
|
|
35
|
+
"key": "advisors",
|
|
36
|
+
"name": "Advisor allocations",
|
|
37
|
+
"prize": {
|
|
38
|
+
"gives": "tokens",
|
|
39
|
+
"amount": 20000,
|
|
40
|
+
"from": "mint",
|
|
41
|
+
"currency": "share"
|
|
42
|
+
},
|
|
43
|
+
"list": [
|
|
44
|
+
{
|
|
45
|
+
"wallet": "Adv1111111111111111111111111111111111111111",
|
|
46
|
+
"amount": 20000,
|
|
47
|
+
"round": 1
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"wallet": "Adv1111111111111111111111111111111111111111",
|
|
51
|
+
"amount": 20000,
|
|
52
|
+
"round": 2
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"wallet": "Adv2222222222222222222222222222222222222222",
|
|
56
|
+
"amount": 10000,
|
|
57
|
+
"round": 1
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"wallet": "Adv2222222222222222222222222222222222222222",
|
|
61
|
+
"amount": 10000,
|
|
62
|
+
"round": 2
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"limits": {
|
|
66
|
+
"total": 4
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 2,
|
|
3
|
+
"network": "devnet",
|
|
4
|
+
"name": "Trade-in Shop",
|
|
5
|
+
"prompt": "A starter pack with a sword, a shield and some SOL back, bought in one go; a founder badge only for the wallets on our list; trade any of our NFTs back for a little SOL, and sell what comes back as mystery gear; and a raffle whose tickets cost a shield.",
|
|
6
|
+
"currency": "sol",
|
|
7
|
+
"tokens": [
|
|
8
|
+
{
|
|
9
|
+
"key": "sol",
|
|
10
|
+
"source": "sol"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"collection": {
|
|
14
|
+
"name": "Trade-in Shop"
|
|
15
|
+
},
|
|
16
|
+
"items": [
|
|
17
|
+
{
|
|
18
|
+
"key": "sword",
|
|
19
|
+
"name": "Iron Sword",
|
|
20
|
+
"uri": "https://example.invalid/sword/{serial}.json"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"key": "shield",
|
|
24
|
+
"name": "Oak Shield"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"key": "badge",
|
|
28
|
+
"name": "Founder Badge",
|
|
29
|
+
"soulbound": true
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"pools": [
|
|
33
|
+
{
|
|
34
|
+
"key": "house",
|
|
35
|
+
"currency": "sol",
|
|
36
|
+
"seed": 0.5
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"stashes": [
|
|
40
|
+
{
|
|
41
|
+
"key": "returns",
|
|
42
|
+
"name": "Returned gear",
|
|
43
|
+
"nfts": []
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"boxes": [
|
|
47
|
+
{
|
|
48
|
+
"key": "starter-pack",
|
|
49
|
+
"name": "Starter Pack",
|
|
50
|
+
"price": 0.05,
|
|
51
|
+
"outcomes": [
|
|
52
|
+
{
|
|
53
|
+
"gives": "bundle",
|
|
54
|
+
"chance": 100,
|
|
55
|
+
"prizes": [
|
|
56
|
+
{
|
|
57
|
+
"gives": "item",
|
|
58
|
+
"item": "sword"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"gives": "item",
|
|
62
|
+
"item": "shield"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"gives": "pay",
|
|
66
|
+
"pool": "house",
|
|
67
|
+
"amount": 0.01
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
"split": [
|
|
73
|
+
{
|
|
74
|
+
"to": "pool:house",
|
|
75
|
+
"percent": 40
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"key": "founder-badge",
|
|
81
|
+
"name": "Founder Badge",
|
|
82
|
+
"price": 0.01,
|
|
83
|
+
"perWallet": 1,
|
|
84
|
+
"gate": {
|
|
85
|
+
"list": [
|
|
86
|
+
"UbsgpGnLM9y9mFqZcbmAgPYQ599XPnDaWnhZA4WdrT5",
|
|
87
|
+
"3yboyxWADP9MK7Wchffkw4wmRwvWbTAnRYqYMMyAoABz",
|
|
88
|
+
"6UbkH6jY6R8jUTmPqijkhSxFn815xeKd792vYUxkzr3P",
|
|
89
|
+
"8ybgaExuyT87dp2AykzhZmfX8YYdud82fbqLVMnwJCYJ",
|
|
90
|
+
"BUbcsPCHrV7Vo95CQQEobnkWVr9Bnz1aJ2byFpkNALud"
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
"outcomes": [
|
|
94
|
+
{
|
|
95
|
+
"gives": "item",
|
|
96
|
+
"item": "badge",
|
|
97
|
+
"chance": 100
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"key": "mystery-gear",
|
|
103
|
+
"name": "Mystery Gear",
|
|
104
|
+
"price": 0.02,
|
|
105
|
+
"outcomes": [
|
|
106
|
+
{
|
|
107
|
+
"gives": "stash",
|
|
108
|
+
"stash": "returns",
|
|
109
|
+
"chance": 60
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"gives": "pay",
|
|
113
|
+
"pool": "house",
|
|
114
|
+
"amount": 0.01,
|
|
115
|
+
"chance": 40
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
"recipes": [
|
|
121
|
+
{
|
|
122
|
+
"key": "trade-in",
|
|
123
|
+
"name": "Trade in gear",
|
|
124
|
+
"in": [
|
|
125
|
+
{
|
|
126
|
+
"takes": "trade",
|
|
127
|
+
"then": {
|
|
128
|
+
"stash": "returns"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
"out": [
|
|
133
|
+
{
|
|
134
|
+
"gives": "tokens",
|
|
135
|
+
"amount": 0.002,
|
|
136
|
+
"from": "pool:house"
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
"lotteries": [
|
|
142
|
+
{
|
|
143
|
+
"key": "shield-raffle",
|
|
144
|
+
"name": "Shield raffle",
|
|
145
|
+
"ticketNft": {
|
|
146
|
+
"rule": {
|
|
147
|
+
"item": "shield"
|
|
148
|
+
},
|
|
149
|
+
"then": "burn"
|
|
150
|
+
},
|
|
151
|
+
"seed": 0.1,
|
|
152
|
+
"places": [
|
|
153
|
+
100
|
|
154
|
+
],
|
|
155
|
+
"every": "7d"
|
|
156
|
+
}
|
|
157
|
+
],
|
|
158
|
+
"scenario": {
|
|
159
|
+
"days": 28,
|
|
160
|
+
"perDay": {
|
|
161
|
+
"box:starter-pack": 40,
|
|
162
|
+
"box:founder-badge": 1,
|
|
163
|
+
"recipe:trade-in": 20,
|
|
164
|
+
"lottery:shield-raffle": 15,
|
|
165
|
+
"box:mystery-gear": 10
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// What a setup step does with money and keys, read back from the instructions it sends (not from the spec's words):
|
|
2
|
+
// every payee of a split, where a schedule or a recipe sends tokens, who is given an NFT, the project's treasury and
|
|
3
|
+
// authority, a grant's prize, limits, voucher keys and list (each wallet with its amount), and whether any wallet may
|
|
4
|
+
// claim it. Addresses are written in full, so an owner sees a payee that an edit re-pointed before they sign.
|
|
5
|
+
import { decodeBorsh } from '../chain/borsh.js';
|
|
6
|
+
import { equalBytes, toHex } from '../chain/bytes.js';
|
|
7
|
+
import { accountKind, decode, IDL, keyLeaf, merkleRoot } from '../client/index.js';
|
|
8
|
+
import { LOOT_PROGRAM, pda } from '../client/pda.js';
|
|
9
|
+
import { fromUnits } from '../economy/model.mjs';
|
|
10
|
+
import { grantArgs } from '../economy/plan.mjs';
|
|
11
|
+
|
|
12
|
+
const NOBODY = '11111111111111111111111111111111';
|
|
13
|
+
const ZERO = new Uint8Array(32);
|
|
14
|
+
|
|
15
|
+
/** An instruction's accounts by name, nested groups flattened, in the order the program takes them. */
|
|
16
|
+
const leafNames = (list) => list.flatMap((a) => (a.accounts ? leafNames(a.accounts) : [a.name]));
|
|
17
|
+
|
|
18
|
+
/** A loot instruction read back: { name, args, accounts: { name: address } }, or null for another program's. */
|
|
19
|
+
function readLootInstruction(ix) {
|
|
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));
|
|
22
|
+
if (!def) return null;
|
|
23
|
+
const args = {};
|
|
24
|
+
let at = def.discriminator.length;
|
|
25
|
+
for (const a of def.args) [args[a.name], at] = decodeBorsh(IDL, a.type, ix.data, at);
|
|
26
|
+
const accounts = Object.fromEntries(leafNames(def.accounts).map((name, i) => [name, ix.keys[i]?.pubkey]));
|
|
27
|
+
return { name: def.name, args, accounts };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The names an owner knows the spec's parts by. `m`: the spec's model; `owner`: the wallet that approves, once it is
|
|
32
|
+
* connected; `treasury`: the treasury's address when it is known (else "your wallet" stands for it, as the setup
|
|
33
|
+
* makes it).
|
|
34
|
+
*/
|
|
35
|
+
export function namesOf(m, { owner = null, treasury = null } = {}) {
|
|
36
|
+
return {
|
|
37
|
+
m,
|
|
38
|
+
owner,
|
|
39
|
+
treasury,
|
|
40
|
+
pools: new Map(m.pools.map((p) => [p.index, p.key])),
|
|
41
|
+
stashes: new Map(m.stashes.map((s) => [s.id, s.name ?? s.key])),
|
|
42
|
+
grants: new Map(m.grants.map((g) => [g.id, g])),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Plain-word lines for one step's instructions: what it pays to whom, and which keys it trusts. */
|
|
47
|
+
export function stepDetails(instructions, names) {
|
|
48
|
+
const reads = [];
|
|
49
|
+
for (const ix of instructions) {
|
|
50
|
+
try {
|
|
51
|
+
reads.push(readLootInstruction(ix));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
reads.push({ unreadable: e.message });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return readDetails(reads, names);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The account each record an `upsert` writes is kept in on chain. */
|
|
60
|
+
const RECORD_ACCOUNT = { Item: 'Item', Box: 'LootBox', Schedule: 'Schedule', Lottery: 'Lottery', Recipe: 'Recipe', Grant: 'Grant' };
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* What is live for one step, read from the chain (`chain.account`): the lines of the records its `upsert`s write, as
|
|
64
|
+
* they are on chain now (an account's fields are named as the upsert's). [] when none of them is on chain yet (all of
|
|
65
|
+
* it is new); null when the step writes no record, so there is nothing on chain to hold it against.
|
|
66
|
+
*/
|
|
67
|
+
export async function liveStepDetails(instructions, names, chain) {
|
|
68
|
+
const reads = [];
|
|
69
|
+
for (const ix of instructions) {
|
|
70
|
+
const read = readLootInstruction(ix);
|
|
71
|
+
const [kind] = Object.keys(read?.name === 'upsert' ? read.args.def ?? {} : {});
|
|
72
|
+
if (!RECORD_ACCOUNT[kind]) continue;
|
|
73
|
+
const account = await chain.account(read.accounts.target);
|
|
74
|
+
if (!account) {
|
|
75
|
+
reads.push(null);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (account.owner !== LOOT_PROGRAM || accountKind(account.data) !== RECORD_ACCOUNT[kind]) throw new Error(`${read.accounts.target} is not the ${kind.toLowerCase()} it should be.`);
|
|
79
|
+
reads.push({ ...read, args: { def: { [kind]: { id: read.args.def[kind].id, args: decode(RECORD_ACCOUNT[kind], account.data) } } } });
|
|
80
|
+
}
|
|
81
|
+
return reads.length ? readDetails(reads.filter(Boolean), names) : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The lines for instructions read back (readLootInstruction's answers; null for another program's). */
|
|
85
|
+
function readDetails(reads, names) {
|
|
86
|
+
const lines = [];
|
|
87
|
+
for (const read of reads) {
|
|
88
|
+
if (read?.unreadable) {
|
|
89
|
+
lines.push(`This step could not be read back (${read.unreadable}): check what it pays in your wallet before approving`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (!read) continue;
|
|
93
|
+
const owner = read.accounts.owner ?? read.accounts.seller;
|
|
94
|
+
const project = read.accounts.project;
|
|
95
|
+
const pots = new Map(names.m.lotteries.map((l) => [pda.pot(pda.lottery(project, l.id)), l.name ?? l.key]));
|
|
96
|
+
// Before a wallet connects, the plan stands a made-up address in for it (one no spec may name): that one is not
|
|
97
|
+
// shown. Once one is connected, only that wallet is called yours.
|
|
98
|
+
const yours = (a) => (names.owner == null ? 'your wallet' : a === names.owner ? `your wallet (${a})` : `the project’s owner, ${a}`);
|
|
99
|
+
const address = (a) => (a === owner ? yours(a) : a);
|
|
100
|
+
const wallet = (a) => (a === owner ? yours(a) : `the wallet ${a}`);
|
|
101
|
+
const treasury = () => `the treasury (${names.treasury ? address(names.treasury) : 'your wallet'})`;
|
|
102
|
+
const where = (to) => {
|
|
103
|
+
if (to === 'Treasury') return treasury();
|
|
104
|
+
const [kind] = Object.keys(to);
|
|
105
|
+
const v = to[kind];
|
|
106
|
+
if (kind === 'Pool') return `the pool “${names.pools.get(v[0]) ?? v[0]}”`;
|
|
107
|
+
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 === 'Holder') return `whoever holds the NFT ${v[0]}`;
|
|
110
|
+
if (kind === 'Chosen') return 'a wallet the buyer chooses';
|
|
111
|
+
return JSON.stringify(to);
|
|
112
|
+
};
|
|
113
|
+
if (read.name === 'create_project') {
|
|
114
|
+
lines.push(`Treasury: ${address(read.args.treasury)}`);
|
|
115
|
+
lines.push(`Authority (signs the project’s decisions): ${address(read.args.authority)}`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// Where what a split leaves goes: a lottery's pot, an auction's seller (or the treasury for a new item), else the treasury.
|
|
119
|
+
const def = read.args.def ?? {};
|
|
120
|
+
const restTo = def.Lottery ? 'its pot' : read.name === 'open_auction' ? (read.args.args?.lot?.Asset ? `the seller, ${address(read.accounts.seller)}` : treasury()) : treasury();
|
|
121
|
+
const to = (v) => (v === 'Burn' ? 'burned' : `to ${where(v)}`);
|
|
122
|
+
const isTo = (v) => v === 'Burn' || v === 'Treasury' || (v && typeof v === 'object' && ['Pool', 'Account', 'Holder', 'Wallet', 'Chosen'].some((k) => k in v) && Object.keys(v).length === 1);
|
|
123
|
+
const grantId = read.args.def?.Grant?.id;
|
|
124
|
+
if (read.args.def?.Grant) lines.push(...grantTerms(read.args.def.Grant.args, names));
|
|
125
|
+
const walk = (value) => {
|
|
126
|
+
if (Array.isArray(value)) return value.forEach(walk);
|
|
127
|
+
if (!value || typeof value !== 'object' || value instanceof Uint8Array) return;
|
|
128
|
+
for (const [k, v] of Object.entries(value)) {
|
|
129
|
+
if (k === 'shares' && Array.isArray(v)) {
|
|
130
|
+
const rest = 10_000 - v.reduce((n, s) => n + Number(s.bps), 0);
|
|
131
|
+
const parts = v.map((s) => `${Number(s.bps) / 100}% ${to(s.to)}`);
|
|
132
|
+
if (rest > 0) parts.push(v.length ? `the rest (${rest / 100}%) to ${restTo}` : `all of it to ${restTo}`);
|
|
133
|
+
lines.push(`Pays: ${parts.join(', ')}`);
|
|
134
|
+
} else if (k === 'to' && isTo(v)) lines.push(v === 'Burn' ? 'Burns the tokens it takes' : `Sends tokens to ${where(v)}`);
|
|
135
|
+
else if (k === 'Give' && Array.isArray(v) && typeof v[0] === 'string') lines.push(`Gives the NFT to ${address(v[0])}`);
|
|
136
|
+
else if (k === 'vouchers' && Array.isArray(v)) {
|
|
137
|
+
const keys = v.filter((a) => a !== NOBODY);
|
|
138
|
+
if (keys.length) lines.push(`Voucher keys (each decides who gets it): ${keys.join(', ')}`);
|
|
139
|
+
} else if (k === 'key_root' && v instanceof Uint8Array) {
|
|
140
|
+
if (!equalBytes(v, ZERO)) {
|
|
141
|
+
const listed = names.grants.get(grantId)?.keys ?? [];
|
|
142
|
+
const ours = listed.length && equalBytes(merkleRoot(listed.map((key) => keyLeaf(key))), v);
|
|
143
|
+
lines.push(ours ? `Voucher keys from its list (each decides who gets it): ${listed.join(', ')}` : `Voucher keys: a list with the root ${toHex(v)}, which is not the spec’s list`);
|
|
144
|
+
}
|
|
145
|
+
} else if (k === 'list_root' && v instanceof Uint8Array) {
|
|
146
|
+
if (!equalBytes(v, ZERO)) lines.push(...listLines(v, names.grants.get(grantId), read.args.def.Grant.args.gives, names));
|
|
147
|
+
} else if (k === 'open' && v === true) lines.push('Open: any wallet may claim it, with no voucher and no list, within its limits');
|
|
148
|
+
else walk(v);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
walk(read.args);
|
|
152
|
+
}
|
|
153
|
+
return lines;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The token a prize pays in, from its bytes: { symbol, decimals }, or null when it is not one the spec names. */
|
|
157
|
+
function prizeToken(gives, m) {
|
|
158
|
+
const from = gives?.Tokens?.from;
|
|
159
|
+
if (!from) return null;
|
|
160
|
+
const key = from.Pool ? (m.pools.find((p) => p.index === from.Pool[0])?.currency ?? m.currencyKey) : [...m.tokens.values()].find((t) => t.mint === (from.Mint ?? from.Vest)?.[0])?.key;
|
|
161
|
+
return m.tokens.get(key) ?? null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Base units of `token` in its own words ("100 COIN"), or as base units when the token is not known. */
|
|
165
|
+
const tokens = (units, token) => (token ? `${fromUnits(BigInt(units), token.decimals ?? 0)} ${token.symbol ?? token.key}` : `${units} base units`);
|
|
166
|
+
|
|
167
|
+
/** A prize in words, from its bytes. Named by the spec's parts; an NFT or a tree is not a payee, so its address is left out. */
|
|
168
|
+
function prizeWords(gives, m) {
|
|
169
|
+
const [kind] = Object.keys(gives ?? {});
|
|
170
|
+
const v = gives?.[kind];
|
|
171
|
+
const named = (list, id, field = 'id') => list.find((x) => x[field] === id);
|
|
172
|
+
const stash = (id) => `the stash “${named(m.stashes, id)?.name ?? named(m.stashes, id)?.key ?? id}”`;
|
|
173
|
+
const item = (id) => `the item “${named(m.items, id)?.name ?? named(m.items, id)?.key ?? id}”`;
|
|
174
|
+
if (kind === 'Tokens') {
|
|
175
|
+
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
|
+
return `${amount} ${from}`;
|
|
178
|
+
}
|
|
179
|
+
if (kind === 'Item') return item(v[0]);
|
|
180
|
+
if (kind === 'Nft') return `an NFT of ${stash(v.stash)}`;
|
|
181
|
+
if (kind === 'Stash') return `an NFT of ${stash(v[0])} at random`;
|
|
182
|
+
if (kind === 'Roll') return `a roll of the box “${m.allBoxes.find((b) => b.id === v[0])?.name ?? m.allBoxes.find((b) => b.id === v[0])?.key ?? v[0]}”`;
|
|
183
|
+
if (kind === 'Compressed') return `a compressed NFT of ${item(v.item)}`;
|
|
184
|
+
if (kind === 'Held') return 'an NFT the lottery holds';
|
|
185
|
+
return 'nothing';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const times = (n) => `${n} claim${Number(n) === 1 ? '' : 's'}`;
|
|
189
|
+
const when = (seconds) => new Date(Number(seconds) * 1000).toISOString().replace('.000Z', 'Z');
|
|
190
|
+
|
|
191
|
+
/** A grant's prize and its limits: what each claim takes from the project, and how many claims there can be. */
|
|
192
|
+
function grantTerms(args, names) {
|
|
193
|
+
const l = args.limits ?? {};
|
|
194
|
+
const limits = [
|
|
195
|
+
l.wallet?.total ? `each wallet ${times(l.wallet.total)} in all` : null,
|
|
196
|
+
l.wallet?.per_window ? `each wallet ${times(l.wallet.per_window)} every ${l.wallet.window} seconds` : null,
|
|
197
|
+
l.per_person ? `each person ${times(l.per_person)}` : null,
|
|
198
|
+
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,
|
|
201
|
+
BigInt(l.window?.opens_at ?? 0) ? `opens ${when(l.window.opens_at)}` : null,
|
|
202
|
+
BigInt(l.window?.closes_at ?? 0) ? `closes ${when(l.window.closes_at)}` : null,
|
|
203
|
+
].filter(Boolean);
|
|
204
|
+
// 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
|
+
return [`Each claim gets: ${upTo}${prizeWords(args.gives, names.m)}`, `Limits: ${limits.length ? limits.join(', ') : 'none'}`];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* A grant's list, read against the root it writes: every wallet on it with what it gets and in which round, when the
|
|
211
|
+
* root is the spec's list; else that the root is not the spec's.
|
|
212
|
+
*/
|
|
213
|
+
function listLines(root, g, gives, names) {
|
|
214
|
+
const list = g?.list ?? [];
|
|
215
|
+
if (!list.length || !equalBytes(grantArgs(g, names.m).list_root, root)) return [`Its list: a list with the root ${toHex(root)}, which is not the spec’s list`];
|
|
216
|
+
const token = prizeToken(gives, names.m);
|
|
217
|
+
return [
|
|
218
|
+
`Only wallets on its list may claim it (${list.length} wallet${list.length === 1 ? '' : 's'}, root ${toHex(root)})`,
|
|
219
|
+
...list.map((e) => {
|
|
220
|
+
const entry = typeof e === 'string' ? { wallet: e } : e;
|
|
221
|
+
const gets = entry.amount !== undefined ? `gets ${entry.amount} ${token?.symbol ?? token?.key ?? 'tokens'}` : 'gets the prize above';
|
|
222
|
+
return `On its list, ${entry.wallet}: ${gets}${entry.round ? `, in round ${entry.round}` : ''}`;
|
|
223
|
+
}),
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* A change's lines against what is live: a line of `now` that differs from the one `was` had about the same thing
|
|
229
|
+
* (the words before the colon) says what it was, a line about something new says so, and what `was` had that `now`
|
|
230
|
+
* lacks is listed as no longer so.
|
|
231
|
+
*/
|
|
232
|
+
export function compareDetails(now, was) {
|
|
233
|
+
const topic = (line) => line.split(':')[0];
|
|
234
|
+
const left = [...was];
|
|
235
|
+
const out = now.map((line) => {
|
|
236
|
+
const same = left.indexOf(line);
|
|
237
|
+
if (same >= 0) return left.splice(same, 1)[0];
|
|
238
|
+
const i = left.findIndex((old) => topic(old) === topic(line));
|
|
239
|
+
if (i < 0) return `${line} (new)`;
|
|
240
|
+
const [old] = left.splice(i, 1);
|
|
241
|
+
return `${line} (changed: it was ${old.slice(topic(old).length + 1).trim() || old})`;
|
|
242
|
+
});
|
|
243
|
+
return [...out, ...left.map((old) => `No longer: ${old}`)];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const ADDRESS = /\b[1-9A-HJ-NP-Za-km-z]{32,44}\b/g;
|
|
247
|
+
|
|
248
|
+
/** Every address the lines name, in the order they name them. */
|
|
249
|
+
const addressesIn = (lines) => [...new Set(lines.flatMap((l) => l.match(ADDRESS) ?? []))];
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* A warning for the page when the approval pays or trusts an address: on a change, those the live economy did not
|
|
253
|
+
* (`was`: the lines of what is live); on a new setup, every one but the owner's wallet (`owner`, when known).
|
|
254
|
+
*/
|
|
255
|
+
export function outsideAddresses(lines, { was = null, owner = null } = {}) {
|
|
256
|
+
const known = new Set([...(was ? addressesIn(was) : []), ...(owner ? [owner] : [])]);
|
|
257
|
+
const outside = addressesIn(lines).filter((a) => !known.has(a));
|
|
258
|
+
if (!outside.length) return null;
|
|
259
|
+
return was
|
|
260
|
+
? `This change pays or trusts ${outside.length === 1 ? 'an address' : 'addresses'} the live economy does not: ${outside.join(', ')}. Approve only if you meant ${outside.length === 1 ? 'it' : 'each one'} (the lines below say what each gets).`
|
|
261
|
+
: `This setup pays or trusts ${outside.length === 1 ? 'an address' : 'addresses'} other than your wallet: ${outside.join(', ')}. Approve only if you meant ${outside.length === 1 ? 'it' : 'each one'} (the lines below say what each gets).`;
|
|
262
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Ed25519 with node:crypto: the fresh keys a setup needs (a new collection, a project's own mint), and checking
|
|
2
|
+
// a wallet's signature. loot never holds a user's key; these fresh keys sign their part of one transaction, live
|
|
3
|
+
// in memory only, and are dropped when the approval ends (the economy is live, or the server closes).
|
|
4
|
+
import { createPublicKey, generateKeyPairSync, sign, verify } from 'node:crypto';
|
|
5
|
+
import { addressBytes, encodeBase58 } from '../chain/base58.js';
|
|
6
|
+
|
|
7
|
+
const SPKI_ED25519 = Buffer.from('302a300506032b6570032100', 'hex');
|
|
8
|
+
|
|
9
|
+
/** A fresh keypair: { address, sign(message) → 64-byte signature }. The private key never leaves this object. */
|
|
10
|
+
export function freshKeypair() {
|
|
11
|
+
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
|
|
12
|
+
const raw = Buffer.from(publicKey.export({ format: 'jwk' }).x, 'base64url');
|
|
13
|
+
const address = encodeBase58(new Uint8Array(raw));
|
|
14
|
+
let key = privateKey;
|
|
15
|
+
return {
|
|
16
|
+
address,
|
|
17
|
+
sign(message) {
|
|
18
|
+
if (!key) throw new Error('This key was dropped.');
|
|
19
|
+
return new Uint8Array(sign(null, Buffer.from(message), key));
|
|
20
|
+
},
|
|
21
|
+
drop() {
|
|
22
|
+
key = null;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Whether `signature` is `address`'s ed25519 signature of `message`. */
|
|
28
|
+
export function verifySignature(address, message, signature) {
|
|
29
|
+
if (!signature || signature.length !== 64) return false;
|
|
30
|
+
try {
|
|
31
|
+
const key = createPublicKey({ key: Buffer.concat([SPKI_ED25519, Buffer.from(addressBytes(address))]), format: 'der', type: 'spki' });
|
|
32
|
+
return verify(null, Buffer.from(message), key, Buffer.from(signature));
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|