@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.
- package/README.md +95 -52
- package/bin/xsolana.mjs +23 -8
- package/examples/blind-forge.json +2 -4
- package/examples/coin-flip-arena.json +4 -12
- package/examples/duel-arena.json +1 -4
- package/examples/elements-set.json +2 -9
- package/examples/expedition-guild.json +1 -8
- package/examples/fair-presale.json +1 -3
- package/examples/guild-missions.json +3 -10
- package/examples/halving-seasons.json +4 -9
- package/examples/hero-trials.json +15 -6
- package/examples/holder-den.json +38 -0
- package/examples/holders-drop.json +105 -0
- package/examples/ideas.json +788 -788
- package/examples/match-predictions.json +24 -0
- package/examples/merge-2048.json +5 -5
- package/examples/nft-raffle.json +1 -0
- package/examples/pack-league.json +108 -0
- package/examples/paper-hands-farm.json +4 -16
- package/examples/partner-vault.json +20 -7
- package/examples/sacrifice-raffle.json +2 -10
- package/examples/shelter-fair.json +1 -0
- package/examples/stakers-draw.json +4 -15
- package/examples/star-fleet.json +1 -8
- package/examples/starforce.json +6 -6
- package/examples/stride-sneakers.json +164 -0
- package/examples/trade-in-shop.json +2 -3
- package/js/approve/details.mjs +122 -52
- package/js/approve/ed25519.mjs +5 -1
- package/js/approve/live-check.mjs +83 -26
- package/js/approve/page.html +217 -78
- package/js/approve/page.js +43 -15
- package/js/approve/server.mjs +290 -73
- package/js/chain/base58.js +7 -1
- package/js/chain/borsh.js +24 -11
- package/js/chain/bytes.js +11 -8
- package/js/chain/core.js +35 -7
- package/js/chain/curve.js +1 -1
- package/js/chain/lookup.js +124 -36
- package/js/chain/mint.js +6 -2
- package/js/chain/pda.js +2 -2
- package/js/chain/programs.js +6 -6
- package/js/chain/rpc.js +44 -16
- package/js/chain/token.js +13 -2
- package/js/chain/token2022.js +55 -16
- package/js/chain/tx.js +32 -22
- package/js/client/accounts.js +653 -131
- package/js/client/compressed.js +4 -3
- package/js/client/grant.js +267 -83
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +2406 -2176
- package/js/client/index.js +1 -0
- package/js/client/ix.js +3059 -664
- package/js/client/pda.js +39 -33
- package/js/client/policy.js +52 -24
- package/js/client/random.js +70 -0
- package/js/client/sizes.js +414 -16
- package/js/client/traits.js +14 -5
- package/js/connect/connect.mjs +59 -17
- package/js/economy/check.mjs +4153 -725
- package/js/economy/cost.mjs +403 -64
- package/js/economy/examples.mjs +176 -35
- package/js/economy/explain.mjs +466 -111
- package/js/economy/guide.mjs +296 -72
- package/js/economy/legal.mjs +12 -6
- package/js/economy/live.mjs +83 -19
- package/js/economy/model.mjs +462 -124
- package/js/economy/plan.mjs +1306 -219
- package/js/economy/schema.json +390 -484
- package/js/economy/secrets.mjs +17 -7
- package/js/economy/simulate.mjs +883 -223
- package/js/economy/txsize.mjs +15 -3
- package/js/economy/validate.mjs +61 -24
- package/js/mcp/server.mjs +42 -14
- package/js/mcp/tools.mjs +307 -88
- package/js/play/index.js +2864 -619
- package/js/play/keys.js +193 -0
- package/js/results/index.js +223 -30
- package/js/results/server.js +230 -0
- package/js/results/wire.js +64 -16
- package/package.json +6 -4
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +39 -21
- package/skills/xsolana-economy/references/after-launch.md +58 -38
- package/skills/xsolana-economy/references/apps.md +97 -50
- package/skills/xsolana-economy/references/sentence-to-spec.md +148 -121
- package/skills/xsolana-economy/references/telling-the-person.md +61 -17
- package/types/chain/core.d.ts +4 -2
- package/types/chain/lookup.d.ts +5 -2
- package/types/chain/rpc.d.ts +3 -1
- package/types/client/accounts.d.ts +217 -56
- package/types/client/grant.d.ts +72 -27
- package/types/client/idl/loot.d.ts +602 -549
- package/types/client/index.d.ts +1 -0
- package/types/client/ix.d.ts +3884 -247
- package/types/client/pda.d.ts +22 -16
- 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/guide.d.mts +5 -0
- package/types/economy/live.d.mts +16 -1
- package/types/economy/model.d.mts +151 -38
- package/types/economy/plan.d.mts +237 -194
- package/types/economy/simulate.d.mts +21 -13
- package/types/mcp/tools.d.mts +21 -13
- package/types/play/index.d.ts +453 -53
- package/types/play/keys.d.ts +51 -0
- package/types/results/index.d.ts +7 -6
- package/types/results/server.d.ts +126 -0
- package/types/results/wire.d.ts +1 -1
package/js/play/keys.js
ADDED
|
@@ -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
|
+
}
|
package/js/results/index.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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 =
|
|
53
|
+
const seg = value => {
|
|
39
54
|
const text = String(value);
|
|
40
|
-
if (text === '' || text === '.' || text === '..')
|
|
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([
|
|
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
|
|
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,
|
|
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({
|
|
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)
|
|
108
|
-
|
|
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(
|
|
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({
|
|
130
|
-
|
|
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))
|
|
135
|
-
|
|
136
|
-
|
|
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}`, {
|
|
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
|
|
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(
|
|
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 ({
|
|
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(
|
|
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 ({
|
|
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({
|
|
168
|
-
|
|
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 }) =>
|
|
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 }) =>
|
|
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 ({
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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';
|