@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
@@ -13,10 +13,12 @@ import { spawn } from 'node:child_process';
13
13
  import { equalBytes, fromBase64, toBase64 } from '../chain/bytes.js';
14
14
  import { checkCluster, notMainnet, rpcClient } from '../chain/rpc.js';
15
15
  import { addressBytes, encodeBase58 } from '../chain/base58.js';
16
+ import { slotHashes, tableAddress } from '../chain/lookup.js';
17
+ import { SYSVAR_SLOT_HASHES } from '../chain/pda.js';
16
18
  import { MINT_BYTES } from '../chain/token.js';
17
19
  import { compileMessage, readTransaction, wireTransaction } from '../chain/tx.js';
18
20
  import { accountKind, decode } from '../client/accounts.js';
19
- import { LOOT_PROGRAM } from '../client/pda.js';
21
+ import { LOOT_PROGRAM, pda } from '../client/pda.js';
20
22
  import { check, describe } from '../economy/check.mjs';
21
23
  import { cost } from '../economy/cost.mjs';
22
24
  import { LEGAL, legalFlags } from '../economy/legal.mjs';
@@ -47,25 +49,27 @@ class ApproveError extends Error {
47
49
  export function devnetChain(url = process.env.XSOLANA_RPC_URL || DEVNET) {
48
50
  const rpc = rpcClient(notMainnet(url));
49
51
  let cluster = null;
50
- const checked = () => (cluster ??= checkCluster(rpc).catch((e) => {
51
- cluster = null;
52
- throw e;
53
- }));
52
+ const checked = () =>
53
+ (cluster ??= checkCluster(rpc).catch(e => {
54
+ cluster = null;
55
+ throw e;
56
+ }));
54
57
  return {
55
58
  url,
56
59
  latestBlockhash: async () => {
57
60
  await checked();
58
61
  return rpc.latestBlockhash();
59
62
  },
60
- rent: async (bytes) => BigInt(await rpc.call('getMinimumBalanceForRentExemption', [bytes])),
61
- send: async (wire) => {
63
+ rent: async bytes => BigInt(await rpc.call('getMinimumBalanceForRentExemption', [bytes])),
64
+ send: async wire => {
62
65
  await checked();
63
66
  return rpc.send(wire);
64
67
  },
65
68
  confirm: (signature, options) => rpc.confirm(signature, options),
66
- account: (address) => rpc.account(address),
69
+ account: address => rpc.account(address),
67
70
  /** A sent transaction's status from the whole history: { err, confirmationStatus }, or null when unseen. */
68
- signatureStatus: async (signature) => (await rpc.call('getSignatureStatuses', [[signature], { searchTransactionHistory: true }])).value[0],
71
+ signatureStatus: async signature =>
72
+ (await rpc.call('getSignatureStatuses', [[signature], { searchTransactionHistory: true }])).value[0],
69
73
  blockHeight: () => rpc.blockHeight(),
70
74
  };
71
75
  }
@@ -77,7 +81,10 @@ export function devnetChain(url = process.env.XSOLANA_RPC_URL || DEVNET) {
77
81
  */
78
82
  export async function writeWhole(file, text) {
79
83
  const target = await realpath(file).catch(() => file);
80
- const mode = await stat(target).then((s) => s.mode & 0o777, () => 0o644);
84
+ const mode = await stat(target).then(
85
+ s => s.mode & 0o777,
86
+ () => 0o644,
87
+ );
81
88
  const temp = path.join(path.dirname(target), `.${path.basename(target)}.${randomBytes(6).toString('hex')}.tmp`);
82
89
  try {
83
90
  const handle = await openFile(temp, 'wx', mode);
@@ -101,7 +108,10 @@ function walletAddress(wallet) {
101
108
  try {
102
109
  addressBytes(text);
103
110
  } catch {
104
- throw new ApproveError(400, `${text ? JSON.stringify(text.slice(0, 60)) : 'No wallet'}: that is not a Solana address. Connect a wallet and try again.`);
111
+ throw new ApproveError(
112
+ 400,
113
+ `${text ? JSON.stringify(text.slice(0, 60)) : 'No wallet'}: that is not a Solana address. Connect a wallet and try again.`,
114
+ );
105
115
  }
106
116
  return text;
107
117
  }
@@ -110,18 +120,25 @@ function walletAddress(wallet) {
110
120
  * One approval session for the spec in `file`. `chain`: { latestBlockhash, rent(bytes), send(wire), confirm(sig) }.
111
121
  * `write(file, text)` writes the spec file (`writeWhole`). Exposed for tests; `approve` serves it.
112
122
  */
113
- export async function createApproval({ file, chain = devnetChain(), now = () => Math.floor(Date.now() / 1000), clock = Date.now, write = writeWhole }) {
123
+ export async function createApproval({
124
+ file,
125
+ chain = devnetChain(),
126
+ now = () => Math.floor(Date.now() / 1000),
127
+ clock = Date.now,
128
+ write = writeWhole,
129
+ }) {
114
130
  let spec = JSON.parse(await readFile(file, 'utf8'));
115
131
  if (spec.economy) spec = spec.economy;
116
132
  const verdict = check(spec, { now: now() });
117
- if (!verdict.ok) throw new ApproveError(400, `${file} does not check:\n${describe({ refusals: verdict.refusals, warnings: [] })}`);
133
+ if (!verdict.ok)
134
+ throw new ApproveError(400, `${file} does not check:\n${describe({ refusals: verdict.refusals, warnings: [] })}`);
118
135
  const keys = new Map();
119
136
  let prepared = null;
120
137
  let busy = false;
121
138
  // One wallet read, prepare or complete at a time: each settles and writes the live section, and a batch built while
122
139
  // another batch lands would send that batch's steps again.
123
140
  let queue = Promise.resolve();
124
- const serial = (fn) => {
141
+ const serial = fn => {
125
142
  const run = queue.then(fn, fn);
126
143
  queue = run.catch(() => {});
127
144
  return run;
@@ -148,13 +165,19 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
148
165
  await checkMints({ m: model(spec, { now: at }), chain });
149
166
  } catch (e) {
150
167
  if (!(e instanceof MintMismatch)) throw e;
151
- throw new ApproveError(409, `${e.message} Nothing was sent: correct ${path.basename(file)}, then run \`xsolana approve\` again.`);
168
+ throw new ApproveError(
169
+ 409,
170
+ `${e.message} Nothing was sent: correct ${path.basename(file)}, then run \`xsolana approve\` again.`,
171
+ );
152
172
  }
153
173
  try {
154
174
  return await checkLive({ live: live(), m: model(spec, { now: at }), wallet, chain });
155
175
  } catch (e) {
156
176
  if (!(e instanceof LiveMismatch)) throw e;
157
- throw new ApproveError(409, `${e.message} Nothing was sent. ${path.basename(file)}’s live section is not what the chain says: restore the file \`xsolana approve\` wrote, or remove its live section to start a new setup.`);
177
+ throw new ApproveError(
178
+ 409,
179
+ `${e.message} Nothing was sent. ${path.basename(file)}’s live section is not what the chain says: restore the file \`xsolana approve\` wrote, or remove its live section to start a new setup.`,
180
+ );
158
181
  }
159
182
  }
160
183
 
@@ -164,13 +187,30 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
164
187
  for (const name of p0.freshKeys) if (!keys.has(name)) keys.set(name, freshKeypair());
165
188
  // The accounts the owner makes outside loot: a mint, and the project's tree of compressed NFTs.
166
189
  const tree = compressedTree(model(spec, { now: at }));
167
- const rents = new Map(await Promise.all([MINT_BYTES, ...(tree ? [tree.bytes] : [])].map(async (bytes) => [bytes, boundedRent(bytes, await chain.rent(bytes))])));
168
- const rent = (bytes) => {
190
+ const rents = new Map(
191
+ await Promise.all(
192
+ [MINT_BYTES, ...(tree ? [tree.bytes] : [])].map(async bytes => [
193
+ bytes,
194
+ boundedRent(bytes, await chain.rent(bytes)),
195
+ ]),
196
+ ),
197
+ );
198
+ const rent = bytes => {
169
199
  if (!rents.has(bytes)) throw new Error(`No rent known for ${bytes} bytes.`);
170
200
  return rents.get(bytes);
171
201
  };
172
202
  const addresses = Object.fromEntries([...keys].map(([n, k]) => [n, k.address]));
173
- const p = prepare(spec, { owner, keys: addresses, live: live() ?? {}, now: at, rent, only });
203
+ // A recent slot, for the project's standing lookup table (made at go-live at a slot still in the slot hashes when it lands).
204
+ const hashes = slotHashes((await chain.account(SYSVAR_SLOT_HASHES).catch(() => null))?.data);
205
+ const p = prepare(spec, {
206
+ owner,
207
+ keys: addresses,
208
+ live: live() ?? {},
209
+ now: at,
210
+ rent,
211
+ only,
212
+ slot: hashes[0] ?? null,
213
+ });
174
214
  if (!p.ok) throw new ApproveError(400, describe({ refusals: p.refusals, warnings: [] }));
175
215
  return p;
176
216
  }
@@ -179,7 +219,10 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
179
219
  * What each step pays to whom and which keys it trusts, read back from its instructions, in full. A change says, for
180
220
  * a record that is live already, how each line differs from what is on chain (`readLive`).
181
221
  */
182
- const names = () => namesOf(model(spec, { now: at }), { treasury: madeSoFar(live())?.treasury ?? model(spec, { now: at }).treasuryWallet ?? null });
222
+ const names = () =>
223
+ namesOf(model(spec, { now: at }), {
224
+ treasury: madeSoFar(live())?.treasury ?? model(spec, { now: at }).treasuryWallet ?? null,
225
+ });
183
226
  /**
184
227
  * What is live, read from the chain once a wallet connects and the file's live section is checked: for each step,
185
228
  * the lines of the records it writes as they are on chain now ([] for a record not there yet), and the treasury.
@@ -197,7 +240,16 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
197
240
  if (lines) was.set(step.id, lines);
198
241
  }
199
242
  }
200
- wasLive = { steps: was, treasury: p.ctx.treasury };
243
+ // A grant published before and gone from the chain (closed) that the change makes again with a list: a list leaf
244
+ // names no grant or incarnation, so the same root would pay every listed round again.
245
+ const remade = [];
246
+ const published = new Set((spec.live.published.grants ?? []).map(g => g.key));
247
+ for (const g of model(spec, { now: at }).grants) {
248
+ if (!g.list?.length || !published.has(g.key) || !p.ctx.project) continue;
249
+ if (!(await chain.account(pda.grant(p.ctx.project, g.id)).catch(() => 'unread')))
250
+ remade.push(`“${g.name ?? g.key}”`);
251
+ }
252
+ wasLive = { steps: was, treasury: p.ctx.treasury, remade };
201
253
  }
202
254
  const detailsOf = (t, i, n) => {
203
255
  const lines = stepDetails(t.built[i], n);
@@ -205,12 +257,27 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
205
257
  return was ? compareDetails(lines, was) : lines;
206
258
  };
207
259
  /** The lines a change is held against for its outside-address warning: what was read from the chain, or nothing yet. */
208
- const wasLines = () => (spec.live?.published ? [...(wasLive ? [...wasLive.steps.values()].flat() : []), ...(wasLive?.treasury ? [`Treasury: ${wasLive.treasury}`] : [])] : null);
209
- const unread = () => (spec.live?.published && !wasLive ? 'What is live has not been read from the chain yet: connect your wallet, and each line is marked against what is on chain.' : null);
260
+ const wasLines = () =>
261
+ spec.live?.published
262
+ ? [
263
+ ...(wasLive ? [...wasLive.steps.values()].flat() : []),
264
+ ...(wasLive?.treasury ? [`Treasury: ${wasLive.treasury}`] : []),
265
+ ]
266
+ : null;
267
+ const remadeLists = () =>
268
+ wasLive?.remade?.length
269
+ ? `This change makes ${wasLive.remade.join(', ')} again after it was closed, with the same list: a list names no grant, so every wallet on it could claim every listed round again. Give the grant a fresh list (new rounds) before you approve.`
270
+ : null;
271
+ const unread = () =>
272
+ spec.live?.published && !wasLive
273
+ ? 'What is live has not been read from the chain yet: connect your wallet, and each line is marked against what is on chain.'
274
+ : null;
210
275
 
211
276
  // The project's own settings a change names are never published: the page says so.
212
277
  const changed = mode === 'change' ? changedParts(spec).settings : [];
213
- const settings = changed.length ? `This change names new project settings (${changed.join(', ')}) that publishing never writes: on chain they stay as they are.` : null;
278
+ const settings = changed.length
279
+ ? `This change names new project settings (${changed.join(', ')}) that publishing never writes: on chain they stay as they are.`
280
+ : null;
214
281
 
215
282
  const legal = legalFlags(verdict);
216
283
 
@@ -218,7 +285,17 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
218
285
  const said = new Map();
219
286
  function view(p, n = names(), extra = {}) {
220
287
  const done = new Set(live()?.landed ?? []);
221
- const steps = p.ok ? p.transactions.flatMap((t) => t.steps.map((s, i) => ({ id: s.id, says: s.says, details: detailsOf(t, i, n), signer: t.signer, done: done.has(s.id) }))) : [];
288
+ const steps = p.ok
289
+ ? p.transactions.flatMap(t =>
290
+ t.steps.map((s, i) => ({
291
+ id: s.id,
292
+ says: s.says,
293
+ details: detailsOf(t, i, n),
294
+ signer: t.signer,
295
+ done: done.has(s.id),
296
+ })),
297
+ )
298
+ : [];
222
299
  for (const s of steps) said.set(s.id, s);
223
300
  return {
224
301
  name: spec.name ?? path.basename(file),
@@ -227,13 +304,25 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
227
304
  program: LOOT_PROGRAM,
228
305
  cluster: 'devnet',
229
306
  setupSol: cost(spec, { now: at, live: live(), only }).setup?.totalSol ?? null,
230
- warnings: [...(extra.refused ? [extra.refused] : []), ...verdict.warnings.filter((w) => !Object.hasOwn(LEGAL, w.code)).map((w) => w.message), ...[settings, unread(), outsideAddresses(steps.flatMap((s) => s.details), { was: wasLines(), owner: n.owner })].filter(Boolean)],
307
+ warnings: [
308
+ ...(extra.refused ? [extra.refused] : []),
309
+ ...verdict.warnings.filter(w => !Object.hasOwn(LEGAL, w.code)).map(w => w.message),
310
+ ...[
311
+ settings,
312
+ unread(),
313
+ remadeLists(),
314
+ outsideAddresses(
315
+ steps.flatMap(s => s.details),
316
+ { was: wasLines(), owner: n.owner },
317
+ ),
318
+ ].filter(Boolean),
319
+ ],
231
320
  // The legal flags in plain words, shown in a block of their own whatever the agent said: Approve waits until the
232
321
  // owner ticks that they read them.
233
322
  legal,
234
323
  authority: model(spec).authority,
235
324
  steps,
236
- landed: [...done].map((id) => ({ id, says: said.get(id)?.says ?? id, details: said.get(id)?.details ?? [] })),
325
+ landed: [...done].map(id => ({ id, says: said.get(id)?.says ?? id, details: said.get(id)?.details ?? [] })),
237
326
  signatures: [...signatures],
238
327
  // The project the setup sends to, once a wallet is connected: always that wallet's own (or the one it is the
239
328
  // authority of).
@@ -295,17 +384,26 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
295
384
  const v = view(p0, namesOf(model(spec, { now: at }), { owner: wallet }), { refused: e.message });
296
385
  return { ...v, remaining: p0.ok ? p0.transactions.length : 0, done: false };
297
386
  }
298
- if (!p0.ok || !p0.transactions.length) return { ...view(p0, names(), { project: trusted.project }), remaining: 0, done: p0.ok };
387
+ if (!p0.ok || !p0.transactions.length)
388
+ return { ...view(p0, names(), { project: trusted.project }), remaining: 0, done: p0.ok };
299
389
  const p = await build(trusted.owner);
300
390
  await readLive(p);
301
- const v = view(p, namesOf(model(spec, { now: at }), { owner: wallet, treasury: p.ctx.treasury }), { project: p.ctx.project });
391
+ const v = view(p, namesOf(model(spec, { now: at }), { owner: wallet, treasury: p.ctx.treasury }), {
392
+ project: p.ctx.project,
393
+ });
302
394
  return { ...v, remaining: p.transactions.length, done: false };
303
395
  }
304
396
 
305
397
  async function prepareFor(wallet) {
306
398
  // A second ask while a batch is out for signing (a double click) gets that same batch, not a new one that would
307
399
  // make the first signature fail.
308
- if (prepared && prepared.wallet === wallet && prepared.generation === generation && prepared.expires > clock() + 15_000) return answer(prepared);
400
+ if (
401
+ prepared &&
402
+ prepared.wallet === wallet &&
403
+ prepared.generation === generation &&
404
+ prepared.expires > clock() + 15_000
405
+ )
406
+ return answer(prepared);
309
407
  await settlePending();
310
408
  const trusted = await trust(wallet);
311
409
  const p = await build(trusted.owner);
@@ -314,18 +412,24 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
314
412
  const authority = p.ctx.authority;
315
413
  const first = p.transactions[0];
316
414
  const signer = first.signer === 'authority' ? authority : p.ctx.owner;
317
- if (signer !== wallet) throw new ApproveError(403, first.signer === 'authority' ? `The next step is the authority’s: connect the wallet ${authority} and approve it there.` : `The next step is the project owner’s: connect the wallet ${p.ctx.owner} and approve it there.`);
415
+ if (signer !== wallet)
416
+ throw new ApproveError(
417
+ 403,
418
+ first.signer === 'authority'
419
+ ? `The next step is the authority’s: connect the wallet ${authority} and approve it there.`
420
+ : `The next step is the project owner’s: connect the wallet ${p.ctx.owner} and approve it there.`,
421
+ );
318
422
  const batch = [];
319
423
  for (const t of p.transactions) {
320
424
  if (t.signer !== first.signer || batch.length >= PER_APPROVAL) break;
321
425
  batch.push(t);
322
426
  }
323
427
  const { blockhash, lastValidBlockHeight } = await chain.latestBlockhash();
324
- const wires = batch.map((t) => {
428
+ const wires = batch.map(t => {
325
429
  const message = compileMessage({ payer: wallet, blockhash, instructions: t.instructions });
326
430
  const { signers } = readTransaction(wireTransaction(message));
327
- const sigs = signers.map((s) => {
328
- const fresh = [...keys.values()].find((k) => k.address === s);
431
+ const sigs = signers.map(s => {
432
+ const fresh = [...keys.values()].find(k => k.address === s);
329
433
  return fresh ? fresh.sign(message) : new Uint8Array(64);
330
434
  });
331
435
  return wireTransaction(message, sigs);
@@ -333,13 +437,27 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
333
437
  // What each transaction pays to whom, read back from the very instructions the wallet signs.
334
438
  const n = namesOf(model(spec, { now: at }), { owner: wallet, treasury: p.ctx.treasury });
335
439
  // Every line names the project it goes to first: always the wallet's own, or the one it is the authority of.
336
- const details = batch.map((t) => [`Project: ${p.ctx.project}${p.ctx.owner === wallet ? ' (your own)' : ` (owned by ${p.ctx.owner})`}`, ...t.steps.flatMap((_, i) => detailsOf(t, i, n))]);
337
- prepared = { id: randomBytes(12).toString('hex'), wallet, wires, batch, details, ctx: p.ctx, lastValidBlockHeight, generation, expires: clock() + PREPARE_TTL_MS };
440
+ const details = batch.map(t => [
441
+ `Project: ${p.ctx.project}${p.ctx.owner === wallet ? ' (your own)' : ` (owned by ${p.ctx.owner})`}`,
442
+ ...t.steps.flatMap((_, i) => detailsOf(t, i, n)),
443
+ ]);
444
+ prepared = {
445
+ id: randomBytes(12).toString('hex'),
446
+ wallet,
447
+ wires,
448
+ batch,
449
+ details,
450
+ ctx: p.ctx,
451
+ lastValidBlockHeight,
452
+ generation,
453
+ expires: clock() + PREPARE_TTL_MS,
454
+ };
338
455
  return answer(prepared);
339
456
  }
340
457
 
341
458
  async function complete(wallet, id, signed) {
342
- if (!prepared || !sameText(prepared.id, String(id ?? ''))) throw new ApproveError(409, 'That batch is not the one prepared: ask for the next one.');
459
+ if (!prepared || !sameText(prepared.id, String(id ?? '')))
460
+ throw new ApproveError(409, 'That batch is not the one prepared: ask for the next one.');
343
461
  if (prepared.expires < clock()) {
344
462
  prepared = null;
345
463
  throw new ApproveError(410, 'That batch expired (90 seconds). Ask for it again.');
@@ -347,7 +465,8 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
347
465
  if (prepared.wallet !== wallet) throw new ApproveError(403, 'That batch was prepared for another wallet.');
348
466
  const batch = prepared;
349
467
  prepared = null;
350
- if (batch.generation !== generation) throw new ApproveError(409, 'Something landed after that batch was built: ask for the next one.');
468
+ if (batch.generation !== generation)
469
+ throw new ApproveError(409, 'Something landed after that batch was built: ask for the next one.');
351
470
  const wires = checkSigned(batch.wires, signed, wallet);
352
471
  busy = true;
353
472
  try {
@@ -357,7 +476,12 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
357
476
  // sent twice (`settlePending`).
358
477
  const signature = encodeBase58(readTransaction(wire).signatures[0]);
359
478
  const sent = batch.batch[i];
360
- await remember({ signature, steps: sent.steps.map((s) => s.id), made: madeBy(sent, batch.ctx), lastValidBlockHeight: batch.lastValidBlockHeight });
479
+ await remember({
480
+ signature,
481
+ steps: sent.steps.map(s => s.id),
482
+ made: madeBy(sent, batch.ctx),
483
+ lastValidBlockHeight: batch.lastValidBlockHeight,
484
+ });
361
485
  try {
362
486
  await chain.send(wire);
363
487
  } catch (e) {
@@ -373,7 +497,11 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
373
497
  const status = chain.signatureStatus ? await chain.signatureStatus(signature).catch(() => null) : null;
374
498
  if (!landedStatus(status)) {
375
499
  if (status?.err || !chain.signatureStatus) await forget(signature);
376
- else throw new ApproveError(503, `${e.message} It may still land: it is looked up on chain before anything is sent again, so approving again never sends it twice. Wait a minute, then ask for the next batch.`);
500
+ else
501
+ throw new ApproveError(
502
+ 503,
503
+ `${e.message} It may still land: it is looked up on chain before anything is sent again, so approving again never sends it twice. Wait a minute, then ask for the next batch.`,
504
+ );
377
505
  throw e;
378
506
  }
379
507
  }
@@ -388,17 +516,26 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
388
516
 
389
517
  /** A prepared batch as the page gets it: the wires to sign, and what each does in words. */
390
518
  function answer(b) {
391
- return { id: b.id, project: b.ctx.project, transactions: b.wires.map(toBase64), says: b.batch.map((t) => t.steps.map((s) => s.says).join('; ')), details: b.details };
519
+ return {
520
+ id: b.id,
521
+ project: b.ctx.project,
522
+ transactions: b.wires.map(toBase64),
523
+ says: b.batch.map(t => t.steps.map(s => s.says).join('; ')),
524
+ details: b.details,
525
+ };
392
526
  }
393
527
 
394
528
  /** What a transaction's steps make, from the context it was built with: recorded when it lands. */
395
529
  function madeBy(transaction, ctx) {
396
- const ids = new Set(transaction.steps.map((s) => s.id));
397
- const mints = Object.fromEntries([...ids].filter((id) => id.startsWith('token:')).map((id) => [id.slice(6), ctx.mints[id.slice(6)]]));
530
+ const ids = new Set(transaction.steps.map(s => s.id));
531
+ const mints = Object.fromEntries(
532
+ [...ids].filter(id => id.startsWith('token:')).map(id => [id.slice(6), ctx.mints[id.slice(6)]]),
533
+ );
398
534
  return {
399
535
  ...(ids.has('project') ? { project: ctx.project, treasury: ctx.treasury, authority: ctx.authority } : {}),
400
536
  ...(ids.has('collection') && ctx.collection ? { collection: ctx.collection } : {}),
401
537
  ...(ids.has('tree') && ctx.tree ? { tree: ctx.tree } : {}),
538
+ ...(ids.has('table') && ctx.slot != null ? { tables: [tableAddress(ctx.owner, ctx.slot)[0]] } : {}),
402
539
  ...(Object.keys(mints).length ? { mints } : {}),
403
540
  };
404
541
  }
@@ -409,7 +546,16 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
409
546
  */
410
547
  async function remember(entry) {
411
548
  const was = spec.live ?? {};
412
- spec = { ...spec, live: { ...was, landed: was.landed ?? [], partial: true, ...(only ? { changing: [...only] } : {}), pending: [...(was.pending ?? []), entry].slice(-60) } };
549
+ spec = {
550
+ ...spec,
551
+ live: {
552
+ ...was,
553
+ landed: was.landed ?? [],
554
+ partial: true,
555
+ ...(only ? { changing: [...only] } : {}),
556
+ pending: [...(was.pending ?? []), entry].slice(-60),
557
+ },
558
+ };
413
559
  generation++;
414
560
  await write(file, `${JSON.stringify(spec, null, 2)}\n`);
415
561
  }
@@ -417,7 +563,7 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
417
563
  /** Takes a transaction out of the pending list: it landed (and is recorded), failed, or can no longer land. */
418
564
  async function forget(signature) {
419
565
  if (!spec.live?.pending) return;
420
- const pending = spec.live.pending.filter((p) => p.signature !== signature);
566
+ const pending = spec.live.pending.filter(p => p.signature !== signature);
421
567
  const { pending: _, ...rest } = spec.live;
422
568
  spec = { ...spec, live: pending.length ? { ...rest, pending } : rest };
423
569
  generation++;
@@ -428,7 +574,9 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
428
574
  async function projectOwner(project) {
429
575
  if (!project || !chain.account) return null;
430
576
  const account = await chain.account(project).catch(() => null);
431
- return account && account.owner === LOOT_PROGRAM && accountKind(account.data) === 'Project' ? decode('Project', account.data).owner : null;
577
+ return account && account.owner === LOOT_PROGRAM && accountKind(account.data) === 'Project'
578
+ ? decode('Project', account.data).owner
579
+ : null;
432
580
  }
433
581
 
434
582
  /**
@@ -445,7 +593,11 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
445
593
  const status = await chain.signatureStatus(entry.signature);
446
594
  if (landedStatus(status)) {
447
595
  if (!signatures.includes(entry.signature)) signatures.push(entry.signature);
448
- await landed({ steps: entry.steps.map((id) => ({ id })) }, { ...(entry.made ?? {}), owner: await projectOwner(entry.made?.project ?? spec.live?.project) }, entry.signature);
596
+ await landed(
597
+ { steps: entry.steps.map(id => ({ id })) },
598
+ { ...(entry.made ?? {}), owner: await projectOwner(entry.made?.project ?? spec.live?.project) },
599
+ entry.signature,
600
+ );
449
601
  continue;
450
602
  }
451
603
  if (status?.err) {
@@ -458,7 +610,10 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
458
610
  await forget(entry.signature);
459
611
  continue;
460
612
  }
461
- throw new ApproveError(409, `The transaction ${entry.signature}, sent earlier, has not landed yet and may still: wait a minute and try again, so nothing is sent twice.`);
613
+ throw new ApproveError(
614
+ 409,
615
+ `The transaction ${entry.signature}, sent earlier, has not landed yet and may still: wait a minute and try again, so nothing is sent twice.`,
616
+ );
462
617
  }
463
618
  }
464
619
 
@@ -469,23 +624,27 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
469
624
  */
470
625
  async function landed(transaction, ctx, signature) {
471
626
  const was = spec.live ?? {};
472
- const ids = transaction.steps.map((s) => s.id);
627
+ const ids = transaction.steps.map(s => s.id);
473
628
  const made = new Set(ids);
474
629
  const mints = { ...(was.mints ?? {}) };
475
- for (const id of ids) if (id.startsWith('token:') && ctx.mints?.[id.slice(6)]) mints[id.slice(6)] = ctx.mints[id.slice(6)];
630
+ for (const id of ids)
631
+ if (id.startsWith('token:') && ctx.mints?.[id.slice(6)]) mints[id.slice(6)] = ctx.mints[id.slice(6)];
476
632
  const progress = {
477
633
  ...was,
478
634
  program: LOOT_PROGRAM,
479
635
  ...(made.has('project') ? { project: ctx.project, treasury: ctx.treasury, authority: ctx.authority } : {}),
480
636
  ...(made.has('collection') && ctx.collection ? { collection: ctx.collection } : {}),
481
637
  ...(made.has('tree') && ctx.tree ? { tree: ctx.tree } : {}),
638
+ ...(made.has('table') && ctx.slot != null ? { tables: [tableAddress(ctx.owner, ctx.slot)[0]] } : {}),
482
639
  mints,
483
640
  landed: [...new Set([...(was.landed ?? []), ...ids])],
484
641
  partial: true,
485
642
  ...(only ? { changing: [...only] } : {}),
486
643
  // (each signature once: one found again by `settlePending` may have been recorded already)
487
- transactions: (was.transactions ?? []).some((t) => t.signature === signature) ? was.transactions : [...(was.transactions ?? []), { signature }].slice(-60),
488
- pending: (was.pending ?? []).filter((p) => p.signature !== signature),
644
+ transactions: (was.transactions ?? []).some(t => t.signature === signature)
645
+ ? was.transactions
646
+ : [...(was.transactions ?? []), { signature }].slice(-60),
647
+ pending: (was.pending ?? []).filter(p => p.signature !== signature),
489
648
  };
490
649
  if (!progress.pending.length) delete progress.pending;
491
650
  if (!Object.keys(progress.mints).length) delete progress.mints;
@@ -496,7 +655,13 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
496
655
  if (rest.ok && rest.transactions.length === 0) {
497
656
  const { live: _, ...bare } = spec;
498
657
  // Everything the setup made, whichever session or transaction made it.
499
- const all = { ...ctx, ...Object.fromEntries(['project', 'treasury', 'authority', 'collection', 'tree'].map((k) => [k, progress[k] ?? ctx[k]])), mints: { ...(ctx.mints ?? {}), ...(progress.mints ?? {}) } };
658
+ const all = {
659
+ ...ctx,
660
+ ...Object.fromEntries(
661
+ ['project', 'treasury', 'authority', 'collection', 'tree', 'tables'].map(k => [k, progress[k] ?? ctx[k]]),
662
+ ),
663
+ mints: { ...(ctx.mints ?? {}), ...(progress.mints ?? {}) },
664
+ };
500
665
  const final = recordLanded(bare, { ctx: all, now: at, at: new Date(clock()).toISOString() });
501
666
  final.live = { ...final.live, transactions: progress.transactions };
502
667
  spec = final;
@@ -514,12 +679,19 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
514
679
  function boundedRent(bytes, lamports) {
515
680
  const most = (BigInt(bytes) + ACCOUNT_OVERHEAD) * MOST_RENT_PER_BYTE;
516
681
  const given = BigInt(lamports);
517
- if (given > most) throw new ApproveError(502, `The RPC says an account of ${bytes} bytes needs ${given} lamports of rent; no cluster charges more than ${most}. Nothing was sent: use another RPC (XSOLANA_RPC_URL).`);
682
+ if (given > most)
683
+ throw new ApproveError(
684
+ 502,
685
+ `The RPC says an account of ${bytes} bytes needs ${given} lamports of rent; no cluster charges more than ${most}. Nothing was sent: use another RPC (XSOLANA_RPC_URL).`,
686
+ );
518
687
  return given;
519
688
  }
520
689
 
521
690
  /** Whether a signature's status says it landed: confirmed or finalized, and not failed. */
522
- const landedStatus = (status) => Boolean(status && !status.err && (status.confirmationStatus === 'confirmed' || status.confirmationStatus === 'finalized'));
691
+ const landedStatus = status =>
692
+ Boolean(
693
+ status && !status.err && (status.confirmationStatus === 'confirmed' || status.confirmationStatus === 'finalized'),
694
+ );
523
695
 
524
696
  const sameText = (a, b) => {
525
697
  const x = Buffer.from(String(a));
@@ -529,7 +701,8 @@ const sameText = (a, b) => {
529
701
 
530
702
  /** The wallet's signed copies: each the exact prepared message, each with the wallet's valid signature. */
531
703
  function checkSigned(prepared, signed, wallet) {
532
- if (!Array.isArray(signed) || signed.length !== prepared.length) throw new ApproveError(400, 'The wallet’s answer does not match the request.');
704
+ if (!Array.isArray(signed) || signed.length !== prepared.length)
705
+ throw new ApproveError(400, 'The wallet’s answer does not match the request.');
533
706
  return prepared.map((wire, i) => {
534
707
  let mine;
535
708
  let theirs;
@@ -539,11 +712,13 @@ function checkSigned(prepared, signed, wallet) {
539
712
  } catch {
540
713
  throw new ApproveError(400, 'The wallet returned something that is not a transaction.');
541
714
  }
542
- if (!equalBytes(mine.message, theirs.message)) throw new ApproveError(400, 'The wallet changed the transaction before signing it, so nothing was sent.');
715
+ if (!equalBytes(mine.message, theirs.message))
716
+ throw new ApproveError(400, 'The wallet changed the transaction before signing it, so nothing was sent.');
543
717
  const slot = mine.signers.indexOf(wallet);
544
718
  if (slot < 0) throw new ApproveError(400, 'This transaction was not prepared for that wallet.');
545
719
  const signature = theirs.signatures[slot];
546
- if (!verifySignature(wallet, mine.message, signature)) throw new ApproveError(400, 'The wallet’s signature is missing or does not match your wallet.');
720
+ if (!verifySignature(wallet, mine.message, signature))
721
+ throw new ApproveError(400, 'The wallet’s signature is missing or does not match your wallet.');
547
722
  const signatures = [...mine.signatures];
548
723
  signatures[slot] = signature;
549
724
  return wireTransaction(mine.message, signatures);
@@ -574,7 +749,7 @@ export function ownRequest(headers, port) {
574
749
  const hosts = [`127.0.0.1:${port}`, `localhost:${port}`];
575
750
  if (!hosts.includes(String(headers.host ?? '').toLowerCase())) return false;
576
751
  if (headers.origin === undefined) return true;
577
- return hosts.some((h) => String(headers.origin).toLowerCase() === `http://${h}`);
752
+ return hosts.some(h => String(headers.origin).toLowerCase() === `http://${h}`);
578
753
  }
579
754
 
580
755
  /** A request's body, as text: refused (413) past `limit` bytes, and the rest of it read and let go. */
@@ -582,7 +757,7 @@ function readBody(req, limit = BODY_LIMIT) {
582
757
  return new Promise((resolve, reject) => {
583
758
  const chunks = [];
584
759
  let size = 0;
585
- req.on('data', (chunk) => {
760
+ req.on('data', chunk => {
586
761
  size += chunk.length;
587
762
  if (size > limit) return reject(new ApproveError(413, 'That request is too large.'));
588
763
  chunks.push(chunk);
@@ -614,9 +789,18 @@ export async function openBrowser(url, { spawnImpl = spawn, dir = tmpdir() } = {
614
789
  try {
615
790
  await chmod(folder, 0o700);
616
791
  const file = path.join(folder, 'open.html');
617
- const href = url.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
618
- await writeFile(file, `<!doctype html><meta charset="utf-8"><meta name="referrer" content="no-referrer"><meta http-equiv="refresh" content="0;url=${href}"><title>xsolana approve</title><a href="${href}">Open the approval page</a>\n`, { mode: 0o600, flag: 'wx' });
619
- const [cmd, args] = process.platform === 'darwin' ? ['open', [file]] : process.platform === 'win32' ? ['cmd', ['/c', 'start', '', file]] : ['xdg-open', [file]];
792
+ const href = url.replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
793
+ await writeFile(
794
+ file,
795
+ `<!doctype html><meta charset="utf-8"><meta name="referrer" content="no-referrer"><meta http-equiv="refresh" content="0;url=${href}"><title>xsolana approve</title><a href="${href}">Open the approval page</a>\n`,
796
+ { mode: 0o600, flag: 'wx' },
797
+ );
798
+ const [cmd, args] =
799
+ process.platform === 'darwin'
800
+ ? ['open', [file]]
801
+ : process.platform === 'win32'
802
+ ? ['cmd', ['/c', 'start', '', file]]
803
+ : ['xdg-open', [file]];
620
804
  spawnImpl(cmd, args, { stdio: 'ignore', detached: true }).unref();
621
805
  } catch {
622
806
  /* the link is printed anyway */
@@ -631,16 +815,33 @@ export async function openBrowser(url, { spawnImpl = spawn, dir = tmpdir() } = {
631
815
  * after. The default `openLink` keeps even the code off the opener's command line (`openBrowser`); one that answers a
632
816
  * function has it called once the code is used.
633
817
  */
634
- export async function approve({ file = 'xsolana.json', port, open = true, chain = devnetChain(), log = (...a) => console.error(...a), openLink = openBrowser } = {}) {
818
+ export async function approve({
819
+ file = 'xsolana.json',
820
+ port,
821
+ open = true,
822
+ chain = devnetChain(),
823
+ log = (...a) => console.error(...a),
824
+ openLink = openBrowser,
825
+ } = {}) {
635
826
  const session = await createApproval({ file: path.resolve(file), chain });
636
827
  const token = randomBytes(18).toString('base64url');
637
828
  let code = randomBytes(18).toString('base64url');
638
829
  // (removes the file the browser was opened with, once: `openLink` answers how)
639
830
  let removeOpener = null;
640
- const forget = () => { const remove = removeOpener; removeOpener = null; remove?.(); };
831
+ const forget = () => {
832
+ const remove = removeOpener;
833
+ removeOpener = null;
834
+ remove?.();
835
+ };
641
836
  const page = await readFile(path.join(ROOT, 'approve/page.html'), 'utf8');
642
837
  const policy = pagePolicy(page);
643
- const headers = { 'cache-control': 'no-store', 'x-content-type-options': 'nosniff', 'referrer-policy': 'no-referrer', 'content-security-policy': policy, 'x-frame-options': 'DENY' };
838
+ const headers = {
839
+ 'cache-control': 'no-store',
840
+ 'x-content-type-options': 'nosniff',
841
+ 'referrer-policy': 'no-referrer',
842
+ 'content-security-policy': policy,
843
+ 'x-frame-options': 'DENY',
844
+ };
644
845
  const server = createServer(async (req, res) => {
645
846
  const url = new URL(req.url, 'http://127.0.0.1');
646
847
  const send = (status, body, type = 'application/json') => {
@@ -649,27 +850,41 @@ export async function approve({ file = 'xsolana.json', port, open = true, chain
649
850
  };
650
851
  try {
651
852
  // Only a request to this server by its own name (DNS rebinding: a site whose name now points at 127.0.0.1).
652
- if (!ownRequest(req.headers, server.address().port)) return send(403, 'Open the link `xsolana approve` printed.', 'text/plain');
853
+ if (!ownRequest(req.headers, server.address().port))
854
+ return send(403, 'Open the link `xsolana approve` printed.', 'text/plain');
653
855
  if (req.method === 'GET' && url.pathname === '/') {
654
856
  const once = url.searchParams.get('c');
655
857
  if (once !== null) {
656
- if (!code || !sameText(once, code)) return send(403, 'This link has been opened already: open the link `xsolana approve` printed.', 'text/plain');
858
+ if (!code || !sameText(once, code))
859
+ return send(
860
+ 403,
861
+ 'This link has been opened already: open the link `xsolana approve` printed.',
862
+ 'text/plain',
863
+ );
657
864
  code = null;
658
865
  forget();
659
866
  res.writeHead(303, { ...headers, location: `/?t=${token}` });
660
867
  return res.end();
661
868
  }
662
- if (!sameText(url.searchParams.get('t') ?? '', token)) return send(403, 'Open the link `xsolana approve` printed.', 'text/plain');
869
+ if (!sameText(url.searchParams.get('t') ?? '', token))
870
+ return send(403, 'Open the link `xsolana approve` printed.', 'text/plain');
663
871
  return send(200, page.replace('__TOKEN__', token), 'text/html; charset=utf-8');
664
872
  }
665
- if (req.method === 'GET' && BROWSER_FILES.test(url.pathname)) return send(200, await readFile(path.join(ROOT, url.pathname.slice(4)), 'utf8'), 'text/javascript; charset=utf-8');
666
- if (!sameText(req.headers['x-xsolana-token'] ?? '', token)) return send(403, { error: 'Not this session’s page.' });
873
+ if (req.method === 'GET' && BROWSER_FILES.test(url.pathname))
874
+ return send(
875
+ 200,
876
+ await readFile(path.join(ROOT, url.pathname.slice(4)), 'utf8'),
877
+ 'text/javascript; charset=utf-8',
878
+ );
879
+ if (!sameText(req.headers['x-xsolana-token'] ?? '', token))
880
+ return send(403, { error: 'Not this session’s page.' });
667
881
  if (req.method === 'GET' && url.pathname === '/api/state') {
668
882
  const wallet = url.searchParams.get('wallet');
669
883
  return send(200, wallet ? await session.stateFor(wallet) : session.state());
670
884
  }
671
885
  const body = req.method === 'POST' ? await readJson(req) : {};
672
- if (req.method === 'POST' && url.pathname === '/api/prepare') return send(200, await session.prepare(String(body.wallet ?? '')));
886
+ if (req.method === 'POST' && url.pathname === '/api/prepare')
887
+ return send(200, await session.prepare(String(body.wallet ?? '')));
673
888
  if (req.method === 'POST' && url.pathname === '/api/complete') {
674
889
  const state = await session.complete(String(body.wallet ?? ''), body.id, body.transactions);
675
890
  if (state.done) {
@@ -697,7 +912,9 @@ export async function approve({ file = 'xsolana.json', port, open = true, chain
697
912
  const link = `${origin}/?t=${token}`;
698
913
  const opened = `${origin}/?c=${code}`;
699
914
  log(`Approve going live in your wallet (devnet): ${link}`);
700
- log('Leave this running until the page says it is live. Ctrl+C stops it; running it again goes on from where it stopped.');
915
+ log(
916
+ 'Leave this running until the page says it is live. Ctrl+C stops it; running it again goes on from where it stopped.',
917
+ );
701
918
  if (open && !process.env.XSOLANA_NO_OPEN) {
702
919
  const remove = await openLink(opened);
703
920
  if (typeof remove === 'function') {