@xgamesfun/solana 0.20.1 → 0.25.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 (112) hide show
  1. package/README.md +95 -52
  2. package/bin/xsolana.mjs +23 -8
  3. package/examples/blind-forge.json +2 -4
  4. package/examples/coin-flip-arena.json +4 -12
  5. package/examples/duel-arena.json +1 -4
  6. package/examples/elements-set.json +2 -9
  7. package/examples/expedition-guild.json +1 -8
  8. package/examples/fair-presale.json +1 -3
  9. package/examples/guild-missions.json +3 -10
  10. package/examples/halving-seasons.json +4 -9
  11. package/examples/hero-trials.json +15 -6
  12. package/examples/holder-den.json +38 -0
  13. package/examples/holders-drop.json +105 -0
  14. package/examples/ideas.json +788 -788
  15. package/examples/match-predictions.json +24 -0
  16. package/examples/merge-2048.json +5 -5
  17. package/examples/nft-raffle.json +1 -0
  18. package/examples/pack-league.json +108 -0
  19. package/examples/paper-hands-farm.json +4 -16
  20. package/examples/partner-vault.json +20 -7
  21. package/examples/sacrifice-raffle.json +2 -10
  22. package/examples/shelter-fair.json +1 -0
  23. package/examples/stakers-draw.json +4 -15
  24. package/examples/star-fleet.json +1 -8
  25. package/examples/starforce.json +6 -6
  26. package/examples/stride-sneakers.json +164 -0
  27. package/examples/trade-in-shop.json +2 -3
  28. package/js/approve/details.mjs +122 -52
  29. package/js/approve/ed25519.mjs +5 -1
  30. package/js/approve/live-check.mjs +83 -26
  31. package/js/approve/page.html +217 -78
  32. package/js/approve/page.js +43 -15
  33. package/js/approve/server.mjs +290 -73
  34. package/js/chain/base58.js +7 -1
  35. package/js/chain/borsh.js +24 -11
  36. package/js/chain/bytes.js +11 -8
  37. package/js/chain/core.js +35 -7
  38. package/js/chain/curve.js +1 -1
  39. package/js/chain/lookup.js +124 -36
  40. package/js/chain/mint.js +6 -2
  41. package/js/chain/pda.js +2 -2
  42. package/js/chain/programs.js +6 -6
  43. package/js/chain/rpc.js +44 -16
  44. package/js/chain/token.js +13 -2
  45. package/js/chain/token2022.js +55 -16
  46. package/js/chain/tx.js +32 -22
  47. package/js/client/accounts.js +653 -131
  48. package/js/client/compressed.js +4 -3
  49. package/js/client/grant.js +267 -83
  50. package/js/client/idl/loot.js +1 -1
  51. package/js/client/idl/loot.json +2406 -2176
  52. package/js/client/index.js +1 -0
  53. package/js/client/ix.js +3059 -664
  54. package/js/client/pda.js +39 -33
  55. package/js/client/policy.js +52 -24
  56. package/js/client/random.js +70 -0
  57. package/js/client/sizes.js +414 -16
  58. package/js/client/traits.js +14 -5
  59. package/js/connect/connect.mjs +59 -17
  60. package/js/economy/check.mjs +4153 -725
  61. package/js/economy/cost.mjs +403 -64
  62. package/js/economy/examples.mjs +176 -35
  63. package/js/economy/explain.mjs +466 -111
  64. package/js/economy/guide.mjs +296 -72
  65. package/js/economy/legal.mjs +12 -6
  66. package/js/economy/live.mjs +83 -19
  67. package/js/economy/model.mjs +462 -124
  68. package/js/economy/plan.mjs +1306 -219
  69. package/js/economy/schema.json +390 -484
  70. package/js/economy/secrets.mjs +17 -7
  71. package/js/economy/simulate.mjs +883 -223
  72. package/js/economy/txsize.mjs +15 -3
  73. package/js/economy/validate.mjs +61 -24
  74. package/js/mcp/server.mjs +42 -14
  75. package/js/mcp/tools.mjs +307 -88
  76. package/js/play/index.js +2864 -619
  77. package/js/play/keys.js +193 -0
  78. package/js/results/index.js +223 -30
  79. package/js/results/server.js +230 -0
  80. package/js/results/wire.js +64 -16
  81. package/package.json +6 -4
  82. package/release.json +5 -5
  83. package/skills/xsolana-economy/SKILL.md +39 -21
  84. package/skills/xsolana-economy/references/after-launch.md +58 -38
  85. package/skills/xsolana-economy/references/apps.md +97 -50
  86. package/skills/xsolana-economy/references/sentence-to-spec.md +148 -121
  87. package/skills/xsolana-economy/references/telling-the-person.md +61 -17
  88. package/types/chain/core.d.ts +4 -2
  89. package/types/chain/lookup.d.ts +5 -2
  90. package/types/chain/rpc.d.ts +3 -1
  91. package/types/client/accounts.d.ts +217 -56
  92. package/types/client/grant.d.ts +72 -27
  93. package/types/client/idl/loot.d.ts +602 -549
  94. package/types/client/index.d.ts +1 -0
  95. package/types/client/ix.d.ts +3884 -247
  96. package/types/client/pda.d.ts +22 -16
  97. package/types/client/policy.d.ts +39 -14
  98. package/types/client/random.d.ts +26 -0
  99. package/types/client/sizes.d.ts +93 -5
  100. package/types/economy/check.d.mts +5 -10
  101. package/types/economy/cost.d.mts +2 -1
  102. package/types/economy/guide.d.mts +5 -0
  103. package/types/economy/live.d.mts +16 -1
  104. package/types/economy/model.d.mts +151 -38
  105. package/types/economy/plan.d.mts +237 -194
  106. package/types/economy/simulate.d.mts +21 -13
  107. package/types/mcp/tools.d.mts +21 -13
  108. package/types/play/index.d.ts +453 -53
  109. package/types/play/keys.d.ts +51 -0
  110. package/types/results/index.d.ts +7 -6
  111. package/types/results/server.d.ts +126 -0
  112. package/types/results/wire.d.ts +1 -1
@@ -0,0 +1,193 @@
1
+ // The throwaway keys js/play settles with (the key job rule, AGENTS.md), made so a page that closes in the middle of a
2
+ // job can find them again and finish it, or send their SOL back.
3
+ //
4
+ // A key is named by the account its job is about (an order, a hold). With a wallet that signs messages, every key comes
5
+ // from one signature of the wallet's over a fixed text naming the project and the wallet, asked once a session: the
6
+ // key's seed is sha256("loot settle key" | that signature | the name). Ed25519 signatures are deterministic, so after a
7
+ // reload the same signature gives the same keys. A wallet that signs no messages, or whose signature of the text
8
+ // changes from one session to the next (some MPC wallets), gets a random key instead, kept in the page's storage
9
+ // encrypted with a device key the page can use but never read out (WebCrypto, IndexedDB); with no such storage, in
10
+ // memory only, as before.
11
+ import { addressBytes, encodeBase58 } from '../chain/base58.js';
12
+ import { concat, fromBase64, randomBytes, toBase64, toHex, utf8 } from '../chain/bytes.js';
13
+ import { sha256 } from '../chain/sha256.js';
14
+
15
+ /** The PKCS #8 header of a raw 32-byte Ed25519 seed (RFC 8410). */
16
+ const PKCS8_ED25519 = Uint8Array.from([
17
+ 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
18
+ ]);
19
+
20
+ const DERIVED = 'derived';
21
+ const fromBase64Url = text =>
22
+ fromBase64(
23
+ text
24
+ .replace(/-/g, '+')
25
+ .replace(/_/g, '/')
26
+ .padEnd(Math.ceil(text.length / 4) * 4, '='),
27
+ );
28
+
29
+ /** An Ed25519 key from its 32-byte seed: { address, sign(message) }. */
30
+ export async function keyFromSeed(seed, name = null) {
31
+ const subtle = globalThis.crypto.subtle;
32
+ const pkcs8 = concat(PKCS8_ED25519, seed);
33
+ const jwk = await subtle.exportKey(
34
+ 'jwk',
35
+ await subtle.importKey('pkcs8', pkcs8, { name: 'Ed25519' }, true, ['sign']),
36
+ );
37
+ const signer = await subtle.importKey('pkcs8', pkcs8, { name: 'Ed25519' }, false, ['sign']);
38
+ return {
39
+ address: encodeBase58(fromBase64Url(jwk.x)),
40
+ name,
41
+ sign: async message => new Uint8Array(await subtle.sign('Ed25519', signer, message)),
42
+ };
43
+ }
44
+
45
+ /** The text a wallet signs once a session for its settle keys. It moves nothing and says so. */
46
+ export const settleKeysText = (project, wallet) =>
47
+ `loot: the keys that settle your orders\nproject ${project}\nwallet ${wallet}\n\nSigning this moves nothing. A page that closes before it settles makes the same key again from it, to finish or send your SOL back.`;
48
+
49
+ /**
50
+ * The page's storage of keys that can't be made again (`localStorage`), and a device key that encrypts them, kept in
51
+ * IndexedDB as a non-extractable AES-GCM key. Null where either is missing (then keys live in memory only).
52
+ */
53
+ export function browserStore() {
54
+ const ls = globalThis.localStorage;
55
+ if (!ls || !globalThis.indexedDB) return null;
56
+ let device = null;
57
+ return {
58
+ get: k => ls.getItem(k),
59
+ set: (k, v) => ls.setItem(k, v),
60
+ remove: k => ls.removeItem(k),
61
+ keys: () => Array.from({ length: ls.length }, (_, i) => ls.key(i)).filter(Boolean),
62
+ cryptoKey: () => (device ??= deviceKey().catch(() => null)),
63
+ };
64
+ }
65
+
66
+ async function deviceKey() {
67
+ const db = await new Promise((resolve, reject) => {
68
+ const r = globalThis.indexedDB.open('loot-keys', 1);
69
+ r.onupgradeneeded = () => r.result.createObjectStore('keys');
70
+ r.onsuccess = () => resolve(r.result);
71
+ r.onerror = () => reject(r.error);
72
+ });
73
+ const found = await new Promise((resolve, reject) => {
74
+ const r = db.transaction('keys').objectStore('keys').get('device');
75
+ r.onsuccess = () => resolve(r.result ?? null);
76
+ r.onerror = () => reject(r.error);
77
+ });
78
+ if (found) return found;
79
+ const key = await globalThis.crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, false, [
80
+ 'encrypt',
81
+ 'decrypt',
82
+ ]);
83
+ await new Promise((resolve, reject) => {
84
+ const tx = db.transaction('keys', 'readwrite');
85
+ tx.objectStore('keys').put(key, 'device');
86
+ tx.oncomplete = () => resolve();
87
+ tx.onerror = () => reject(tx.error);
88
+ });
89
+ return key;
90
+ }
91
+
92
+ /**
93
+ * The keys of `wallet` ({ address, signMessage? }) in `project`. `store`: `browserStore()`'s shape ({ get, set, remove,
94
+ * keys, cryptoKey }), or null. `key(name)` makes the key named by an address (asking the wallet for its signature the
95
+ * first time in a session); `again(name)` makes it again for a job left unfinished, or finds it stored, or null;
96
+ * `stored()` lists the names of keys stored; `forget(name)` drops a stored key once its job is done and it is empty.
97
+ * `how()` says how keys are made: 'derived', 'stored' or 'memory'.
98
+ */
99
+ export function keyring({ wallet, project, store = null }) {
100
+ let root = null;
101
+ const prefix = () => `loot-key:${project}:${wallet.address}:`;
102
+ const checkName = () => `loot-key-check:${project}:${wallet.address}`;
103
+ const safe = (f, fallback = null) => {
104
+ try {
105
+ return f();
106
+ } catch {
107
+ return fallback;
108
+ }
109
+ };
110
+
111
+ /** The wallet's signature of `settleKeysText`, once a session; null when it signs no messages, refuses, or signs the
112
+ * text differently from an earlier session (then nothing it signed could be made again). */
113
+ function signature() {
114
+ if (!wallet?.signMessage) return Promise.resolve(null);
115
+ root ??= (async () => {
116
+ const sig = new Uint8Array(await wallet.signMessage(utf8(settleKeysText(project, wallet.address))));
117
+ if (sig.length !== 64) return null;
118
+ const mark = toHex(sha256(sha256(sig))).slice(0, 32);
119
+ const had = store ? safe(() => store.get(checkName())) : null;
120
+ if (had && had !== mark) return null;
121
+ if (!had && store) safe(() => store.set(checkName(), mark));
122
+ return sig;
123
+ })().catch(() => null);
124
+ return root;
125
+ }
126
+
127
+ const derived = (sig, name) => keyFromSeed(sha256(concat(utf8('loot settle key'), sig, addressBytes(name))), name);
128
+
129
+ async function seal(seed) {
130
+ const device = await store?.cryptoKey?.();
131
+ if (!device) return null;
132
+ const iv = randomBytes(12);
133
+ const sealed = new Uint8Array(await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv }, device, seed));
134
+ return toBase64(concat(iv, sealed));
135
+ }
136
+
137
+ async function open(text) {
138
+ const device = await store?.cryptoKey?.();
139
+ if (!device || !text) return null;
140
+ const bytes = fromBase64(text);
141
+ return new Uint8Array(
142
+ await globalThis.crypto.subtle.decrypt(
143
+ { name: 'AES-GCM', iv: bytes.subarray(0, 12) },
144
+ device,
145
+ bytes.subarray(12),
146
+ ),
147
+ );
148
+ }
149
+
150
+ return {
151
+ async how() {
152
+ if (await signature()) return 'derived';
153
+ return store && (await store.cryptoKey?.()) ? 'stored' : 'memory';
154
+ },
155
+
156
+ /** The key named `name` (an address) for a new job. */
157
+ async key(name) {
158
+ const sig = await signature();
159
+ if (sig) {
160
+ // Its name is kept, so `recover` finds it once its job's account is gone.
161
+ if (store) safe(() => store.set(prefix() + name, DERIVED));
162
+ return derived(sig, name);
163
+ }
164
+ const seed = randomBytes(32);
165
+ const sealed = store ? await seal(seed).catch(() => null) : null;
166
+ if (sealed) safe(() => store.set(prefix() + name, sealed));
167
+ return keyFromSeed(seed, name);
168
+ },
169
+
170
+ /** The key named `name` again, for a job left unfinished: made again from the wallet's signature, or read from
171
+ * storage; null when neither can. `ask`: false to make it only from a signature already given this session. */
172
+ async again(name, { ask = true } = {}) {
173
+ const sig = ask || root ? await signature() : null;
174
+ if (sig) return derived(sig, name);
175
+ const text = store ? safe(() => store.get(prefix() + name)) : null;
176
+ const seed = text && text !== DERIVED ? await open(text).catch(() => null) : null;
177
+ return seed ? keyFromSeed(seed, name) : null;
178
+ },
179
+
180
+ /** The names of the keys this page stored for the wallet (none when keys are made from its signature). */
181
+ stored() {
182
+ if (!store) return [];
183
+ const p = prefix();
184
+ return safe(() => store.keys(), [])
185
+ .filter(k => k.startsWith(p))
186
+ .map(k => k.slice(p.length));
187
+ },
188
+
189
+ forget(name) {
190
+ if (store && name) safe(() => store.remove(prefix() + name));
191
+ },
192
+ };
193
+ }
@@ -23,21 +23,37 @@ import { toHex } from '../chain/bytes.js';
23
23
  import { pda, voucherBytes } from '../client/index.js';
24
24
  import { canonicalJson, drawText, entryText, lockText, resultBytes, rosterHash } from './wire.js';
25
25
 
26
- export { canonicalJson, CERT_KINDS, CLUSTERS, drawText, entryText, giveawayHostText, hostCertText, lockText, NO_GRANT, RESULT_DOMAIN, resultBytes, revokeGiveawayText, revokeText, rosterHash } from './wire.js';
26
+ export {
27
+ canonicalJson,
28
+ CERT_KINDS,
29
+ CLUSTERS,
30
+ drawText,
31
+ entryText,
32
+ giveawayHostText,
33
+ hostCertText,
34
+ lockText,
35
+ NO_GRANT,
36
+ RESULT_DOMAIN,
37
+ resultBytes,
38
+ revokeGiveawayText,
39
+ revokeText,
40
+ rosterHash,
41
+ } from './wire.js';
27
42
 
28
43
  /** games.fun's results server, on its play host (devnet until the program's audit). */
29
44
  export const RESULTS_SERVER = 'https://play.xgames.fun/xsolana';
30
45
 
31
46
  const subtle = () => globalThis.crypto.subtle;
32
47
  /** A value as a query's value: encoded, so a `&`, `#` or `/` in it is part of the value. */
33
- const q = (value) => encodeURIComponent(String(value));
48
+ const q = value => encodeURIComponent(String(value));
34
49
  /**
35
50
  * A value as one segment of a URL's path: encoded, so a `/` or `?` in it stays inside the segment. One that is empty,
36
51
  * `.` or `..` (which a URL reads as a step up the path, encoded or not) is refused.
37
52
  */
38
- const seg = (value) => {
53
+ const seg = value => {
39
54
  const text = String(value);
40
- if (text === '' || text === '.' || text === '..') throw new RangeError(`${JSON.stringify(text)} can't be a part of a path.`);
55
+ if (text === '' || text === '.' || text === '..')
56
+ throw new RangeError(`${JSON.stringify(text)} can't be a part of a path.`);
41
57
  return encodeURIComponent(text);
42
58
  };
43
59
 
@@ -58,7 +74,25 @@ export async function newKey() {
58
74
  export async function keyFromSecret(secret) {
59
75
  const seed = decodeBase58(secret);
60
76
  if (seed.length !== 32) throw new Error('A host key secret is 32 bytes in base58.');
61
- const pkcs8 = new Uint8Array([0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20, ...seed]);
77
+ const pkcs8 = new Uint8Array([
78
+ 0x30,
79
+ 0x2e,
80
+ 0x02,
81
+ 0x01,
82
+ 0x00,
83
+ 0x30,
84
+ 0x05,
85
+ 0x06,
86
+ 0x03,
87
+ 0x2b,
88
+ 0x65,
89
+ 0x70,
90
+ 0x04,
91
+ 0x22,
92
+ 0x04,
93
+ 0x20,
94
+ ...seed,
95
+ ]);
62
96
  const privateKey = await subtle().importKey('pkcs8', pkcs8, { name: 'Ed25519' }, true, ['sign']);
63
97
  const jwk = await subtle().exportKey('jwk', privateKey);
64
98
  return keyOf(privateKey, base64url(jwk.x), jwk.d);
@@ -68,13 +102,13 @@ function keyOf(privateKey, publicBytes, d) {
68
102
  return {
69
103
  address: encodeBase58(publicBytes),
70
104
  secret: encodeBase58(base64url(d)),
71
- sign: async (message) => new Uint8Array(await subtle().sign({ name: 'Ed25519' }, privateKey, message)),
105
+ sign: async message => new Uint8Array(await subtle().sign({ name: 'Ed25519' }, privateKey, message)),
72
106
  };
73
107
  }
74
108
 
75
109
  function base64url(text) {
76
110
  const bin = atob(text.replace(/-/g, '+').replace(/_/g, '/'));
77
- return Uint8Array.from(bin, (c) => c.charCodeAt(0));
111
+ return Uint8Array.from(bin, c => c.charCodeAt(0));
78
112
  }
79
113
 
80
114
  /** One signer's signature of a result: { key, signature }. `signer` is a key from newKey, or anything with sign(). */
@@ -87,7 +121,17 @@ export async function signResult(signer, result, cluster = 'devnet') {
87
121
  * with the same result is safe: it is the same answer. Returns the server's answer, { result, vouchers }, or throws its
88
122
  * { code, why }.
89
123
  */
90
- export async function report({ server, cert, signer, game, round, endedAt, winners, cluster = 'devnet', fetchImpl = globalThis.fetch }) {
124
+ export async function report({
125
+ server,
126
+ cert,
127
+ signer,
128
+ game,
129
+ round,
130
+ endedAt,
131
+ winners,
132
+ cluster = 'devnet',
133
+ fetchImpl = globalThis.fetch,
134
+ }) {
91
135
  const result = { game, round, endedAt, winners };
92
136
  const all = [await signResult(signer, result, cluster)];
93
137
  const response = await fetchImpl(`${server.replace(/\/$/, '')}/v1/results`, {
@@ -104,14 +148,26 @@ export async function report({ server, cert, signer, game, round, endedAt, winne
104
148
  */
105
149
  async function answerOf(response) {
106
150
  const answer = await response.json().catch(() => null);
107
- if (!response.ok) throw Object.assign(new Error(answer?.error?.why ?? `The results server answered ${response.status}${response.statusText ? ` ${response.statusText}` : ''}.`), { code: answer?.error?.code, status: response.status });
108
- if (answer === null || typeof answer !== 'object') throw Object.assign(new Error(`The results server answered ${response.status} with no JSON.`), { status: response.status });
151
+ if (!response.ok)
152
+ throw Object.assign(
153
+ new Error(
154
+ answer?.error?.why ??
155
+ `The results server answered ${response.status}${response.statusText ? ` ${response.statusText}` : ''}.`,
156
+ ),
157
+ { code: answer?.error?.code, status: response.status },
158
+ );
159
+ if (answer === null || typeof answer !== 'object')
160
+ throw Object.assign(new Error(`The results server answered ${response.status} with no JSON.`), {
161
+ status: response.status,
162
+ });
109
163
  return answer;
110
164
  }
111
165
 
112
166
  /** A wallet's vouchers for a round it won: [{ grant, publicKey, signature, round, expires, … }]. */
113
167
  export async function fetchVouchers({ server, game, round, wallet, fetchImpl = globalThis.fetch }) {
114
- const response = await fetchImpl(`${server.replace(/\/$/, '')}/v1/vouchers?game=${q(game)}&round=${q(round)}&wallet=${q(wallet)}`);
168
+ const response = await fetchImpl(
169
+ `${server.replace(/\/$/, '')}/v1/vouchers?game=${q(game)}&round=${q(round)}&wallet=${q(wallet)}`,
170
+ );
115
171
  return (await answerOf(response)).vouchers;
116
172
  }
117
173
 
@@ -126,19 +182,56 @@ export async function fetchVouchers({ server, game, round, wallet, fetchImpl = g
126
182
  * Returns { wallet, grant, incarnation, publicKey, signature, round, expires, personHash, amount } for js/play claimGrant,
127
183
  * plain JSON (`incarnation` and `amount` as decimal text), so a server can send it as it is.
128
184
  */
129
- export async function signVoucher({ signer, game, grant, incarnation, wallet, round, now, seconds = 7 * 86_400, personHash = null, amount = 0n }) {
130
- if (!(Number(seconds) > 0)) throw new RangeError('A voucher lives some seconds from now (`seconds` above 0): one that has run out already pays nothing.');
185
+ export async function signVoucher({
186
+ signer,
187
+ game,
188
+ grant,
189
+ incarnation,
190
+ wallet,
191
+ round,
192
+ now,
193
+ seconds = 7 * 86_400,
194
+ personHash = null,
195
+ amount = 0n,
196
+ }) {
197
+ if (!(Number(seconds) > 0))
198
+ throw new RangeError(
199
+ 'A voucher lives some seconds from now (`seconds` above 0): one that has run out already pays nothing.',
200
+ );
131
201
  if (Number(seconds) > 30 * 86_400) throw new Error('A voucher lives 30 days at most.');
132
202
  // The cluster's clock comes as a number or a bigint (an RPC's, a decoded Clock's); `expires` is a number either way.
133
203
  const expires = Math.floor(typeof now === 'bigint' ? Number(now) : now) + Number(seconds);
134
- if (!Number.isSafeInteger(expires)) throw new RangeError('A voucher needs the cluster\'s clock (`now`, unix seconds).');
135
- const message = voucherBytes({ grant: pda.grant(game, grant), incarnation, wallet, personHash, round, expires, amount: BigInt(amount) });
136
- return { wallet, grant, incarnation: String(BigInt(incarnation)), publicKey: signer.address, signature: encodeBase58(await signer.sign(message)), round, expires, personHash: personHash ?? '0'.repeat(64), amount: String(BigInt(amount)) };
204
+ if (!Number.isSafeInteger(expires))
205
+ throw new RangeError("A voucher needs the cluster's clock (`now`, unix seconds).");
206
+ const message = voucherBytes({
207
+ grant: pda.grant(game, grant),
208
+ incarnation,
209
+ wallet,
210
+ personHash,
211
+ round,
212
+ expires,
213
+ amount: BigInt(amount),
214
+ });
215
+ return {
216
+ wallet,
217
+ grant,
218
+ incarnation: String(BigInt(incarnation)),
219
+ publicKey: signer.address,
220
+ signature: encodeBase58(await signer.sign(message)),
221
+ round,
222
+ expires,
223
+ personHash: personHash ?? '0'.repeat(64),
224
+ amount: String(BigInt(amount)),
225
+ };
137
226
  }
138
227
 
139
228
  /** Posts JSON to the server and answers its body, or throws its { code, why }. */
140
229
  async function post(server, path, body, fetchImpl) {
141
- const response = await fetchImpl(`${server.replace(/\/$/, '')}${path}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
230
+ const response = await fetchImpl(`${server.replace(/\/$/, '')}${path}`, {
231
+ method: 'POST',
232
+ headers: { 'content-type': 'application/json' },
233
+ body: JSON.stringify(body),
234
+ });
142
235
  return answerOf(response);
143
236
  }
144
237
 
@@ -151,34 +244,134 @@ const fetchJson = async (server, path, fetchImpl) => answerOf(await fetchImpl(`$
151
244
  */
152
245
  export const draws = {
153
246
  open: async ({ server, signMessage, by, cert = null, rates = null, fetchImpl = globalThis.fetch, ...spec }) => {
154
- const ratesHash = rates ? Array.from(new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(canonicalJson(rates)))), (b) => b.toString(16).padStart(2, '0')).join('') : 'default';
247
+ const ratesHash = rates
248
+ ? Array.from(
249
+ new Uint8Array(
250
+ await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(canonicalJson(rates))),
251
+ ),
252
+ b => b.toString(16).padStart(2, '0'),
253
+ ).join('')
254
+ : 'default';
155
255
  const text = drawText({ ...spec, rates: ratesHash });
156
- return post(server, '/v1/draws', { text, signature: encodeBase58(await signMessage(new TextEncoder().encode(text))), by, ...(cert ? { cert } : {}), ...(rates ? { rates } : {}) }, fetchImpl);
256
+ return post(
257
+ server,
258
+ '/v1/draws',
259
+ {
260
+ text,
261
+ signature: encodeBase58(await signMessage(new TextEncoder().encode(text))),
262
+ by,
263
+ ...(cert ? { cert } : {}),
264
+ ...(rates ? { rates } : {}),
265
+ },
266
+ fetchImpl,
267
+ );
157
268
  },
158
269
  /** A player enters with their play-host token, or with their wallet's signature (`wallet`, `signMessage`). */
159
- enter: async ({ server, game, draw, token = null, wallet = null, signMessage = null, cluster = 'devnet', fetchImpl = globalThis.fetch }) => {
270
+ enter: async ({
271
+ server,
272
+ game,
273
+ draw,
274
+ token = null,
275
+ wallet = null,
276
+ signMessage = null,
277
+ cluster = 'devnet',
278
+ fetchImpl = globalThis.fetch,
279
+ }) => {
160
280
  if (token) return post(server, `/v1/draws/${seg(game)}/${seg(draw)}/entries`, { token }, fetchImpl);
161
281
  const text = entryText({ cluster, game, draw });
162
- return post(server, `/v1/draws/${seg(game)}/${seg(draw)}/entries`, { wallet, text, signature: encodeBase58(await signMessage(new TextEncoder().encode(text))) }, fetchImpl);
282
+ return post(
283
+ server,
284
+ `/v1/draws/${seg(game)}/${seg(draw)}/entries`,
285
+ { wallet, text, signature: encodeBase58(await signMessage(new TextEncoder().encode(text))) },
286
+ fetchImpl,
287
+ );
163
288
  },
164
289
  /** Locks the players the server holds now to `beacon`, a drand round still to come. Checks their hash itself. */
165
- lock: async ({ server, game, draw, beacon, signMessage, by, cert = null, cluster = 'devnet', net = 'quicknet', fetchImpl = globalThis.fetch }) => {
290
+ lock: async ({
291
+ server,
292
+ game,
293
+ draw,
294
+ beacon,
295
+ signMessage,
296
+ by,
297
+ cert = null,
298
+ cluster = 'devnet',
299
+ net = 'quicknet',
300
+ fetchImpl = globalThis.fetch,
301
+ }) => {
166
302
  const { draw: view } = await fetchJson(server, `/v1/draws/${seg(game)}/${seg(draw)}`, fetchImpl);
167
- const text = lockText({ cluster, game, draw, roster: await rosterHash(view.entries), players: view.entries.length, beacon, net });
168
- return post(server, `/v1/draws/${seg(game)}/${seg(draw)}/lock`, { text, signature: encodeBase58(await signMessage(new TextEncoder().encode(text))), by, ...(cert ? { cert } : {}) }, fetchImpl);
303
+ const text = lockText({
304
+ cluster,
305
+ game,
306
+ draw,
307
+ roster: await rosterHash(view.entries),
308
+ players: view.entries.length,
309
+ beacon,
310
+ net,
311
+ });
312
+ return post(
313
+ server,
314
+ `/v1/draws/${seg(game)}/${seg(draw)}/lock`,
315
+ {
316
+ text,
317
+ signature: encodeBase58(await signMessage(new TextEncoder().encode(text))),
318
+ by,
319
+ ...(cert ? { cert } : {}),
320
+ },
321
+ fetchImpl,
322
+ );
169
323
  },
170
324
  /** The draw as anyone may check it; settled, with its result, once its beacon round is out. */
171
- get: async ({ server, game, draw, fetchImpl = globalThis.fetch }) => (await fetchJson(server, `/v1/draws/${seg(game)}/${seg(draw)}`, fetchImpl)).draw,
325
+ get: async ({ server, game, draw, fetchImpl = globalThis.fetch }) =>
326
+ (await fetchJson(server, `/v1/draws/${seg(game)}/${seg(draw)}`, fetchImpl)).draw,
172
327
  };
173
328
 
174
329
  /** A giveaway, from its appointed host's page. */
175
330
  export const giveaways = {
176
331
  /** Opens one: `cert` the owner's appointment, `token` the host's play-host sign-in, `session` a key from newKey(). */
177
- open: async ({ server, game, cert, token, giveaway, winners, endsBy, session, fetchImpl = globalThis.fetch }) => post(server, `/v1/giveaways/${seg(game)}`, { cert, token, giveaway, winners, endsBy, session: session.address }, fetchImpl),
332
+ open: async ({ server, game, cert, token, giveaway, winners, endsBy, session, fetchImpl = globalThis.fetch }) =>
333
+ post(
334
+ server,
335
+ `/v1/giveaways/${seg(game)}`,
336
+ { cert, token, giveaway, winners, endsBy, session: session.address },
337
+ fetchImpl,
338
+ ),
178
339
  /** Reports its result, signed by its session key. `grant`: the host's grant (each winner is paid from it). */
179
- report: async ({ server, game, giveaway, session, endedAt, winners, grant, cluster = 'devnet', fetchImpl = globalThis.fetch }) => {
180
- const signed = winners.map((w) => ({ wallet: w.wallet, place: w.place, grant }));
181
- const signature = encodeBase58(await session.sign(resultBytes({ cluster, game, round: giveaway, endedAt, winners: signed })));
182
- return post(server, `/v1/giveaways/${seg(game)}/${seg(giveaway)}/result`, { endedAt, winners: winners.map(({ wallet, place }) => ({ wallet, place })), signature }, fetchImpl);
340
+ report: async ({
341
+ server,
342
+ game,
343
+ giveaway,
344
+ session,
345
+ endedAt,
346
+ winners,
347
+ grant,
348
+ cluster = 'devnet',
349
+ fetchImpl = globalThis.fetch,
350
+ }) => {
351
+ const signed = winners.map(w => ({ wallet: w.wallet, place: w.place, grant }));
352
+ const signature = encodeBase58(
353
+ await session.sign(resultBytes({ cluster, game, round: giveaway, endedAt, winners: signed })),
354
+ );
355
+ return post(
356
+ server,
357
+ `/v1/giveaways/${seg(game)}/${seg(giveaway)}/result`,
358
+ { endedAt, winners: winners.map(({ wallet, place }) => ({ wallet, place })), signature },
359
+ fetchImpl,
360
+ );
183
361
  },
184
362
  };
363
+
364
+ // What a game's server checks: a wallet's collectibles, a wallet's signed proof, promo codes (server.js).
365
+ export {
366
+ codeAddresses,
367
+ codeKey,
368
+ codeVoucher,
369
+ collectiblesOf,
370
+ holds,
371
+ makeCodes,
372
+ normalizeCode,
373
+ proofRequest,
374
+ proofText,
375
+ readProof,
376
+ verifyProof,
377
+ } from './server.js';