@zga/client 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/dist/addresses.d.ts +29 -0
  3. package/dist/addresses.js +43 -0
  4. package/dist/billing.d.ts +41 -0
  5. package/dist/billing.js +141 -0
  6. package/dist/bin/crank.d.ts +2 -0
  7. package/dist/bin/crank.js +34 -0
  8. package/dist/bin/relayer.d.ts +2 -0
  9. package/dist/bin/relayer.js +37 -0
  10. package/dist/chain.d.ts +14 -0
  11. package/dist/chain.js +48 -0
  12. package/dist/config.d.ts +11 -0
  13. package/dist/config.js +61 -0
  14. package/dist/crank/gate.d.ts +2 -0
  15. package/dist/crank/gate.js +26 -0
  16. package/dist/crank/lending.d.ts +16 -0
  17. package/dist/crank/lending.js +144 -0
  18. package/dist/crank/prices.d.ts +11 -0
  19. package/dist/crank/prices.js +177 -0
  20. package/dist/crank/runner.d.ts +15 -0
  21. package/dist/crank/runner.js +30 -0
  22. package/dist/crank/shieldFees.d.ts +15 -0
  23. package/dist/crank/shieldFees.js +161 -0
  24. package/dist/epoch-root.d.ts +23 -0
  25. package/dist/epoch-root.js +98 -0
  26. package/dist/explorer.d.ts +41 -0
  27. package/dist/explorer.js +222 -0
  28. package/dist/gossip.d.ts +4 -0
  29. package/dist/gossip.js +77 -0
  30. package/dist/idl.d.ts +2 -0
  31. package/dist/idl.js +52 -0
  32. package/dist/index.d.ts +31 -0
  33. package/dist/index.js +47 -0
  34. package/dist/lending.d.ts +82 -0
  35. package/dist/lending.js +164 -0
  36. package/dist/log.d.ts +27 -0
  37. package/dist/log.js +173 -0
  38. package/dist/lookupTable.d.ts +2 -0
  39. package/dist/lookupTable.js +47 -0
  40. package/dist/observer.d.ts +59 -0
  41. package/dist/observer.js +255 -0
  42. package/dist/receipt.d.ts +30 -0
  43. package/dist/receipt.js +27 -0
  44. package/dist/relayer/chain.d.ts +35 -0
  45. package/dist/relayer/chain.js +171 -0
  46. package/dist/relayer/errors.d.ts +8 -0
  47. package/dist/relayer/errors.js +29 -0
  48. package/dist/relayer/limits.d.ts +15 -0
  49. package/dist/relayer/limits.js +47 -0
  50. package/dist/relayer/relayer.d.ts +25 -0
  51. package/dist/relayer/relayer.js +54 -0
  52. package/dist/relayer/server.d.ts +8 -0
  53. package/dist/relayer/server.js +101 -0
  54. package/dist/relayer/settings.d.ts +23 -0
  55. package/dist/relayer/settings.js +69 -0
  56. package/dist/replay.d.ts +13 -0
  57. package/dist/replay.js +118 -0
  58. package/dist/session.d.ts +29 -0
  59. package/dist/session.js +31 -0
  60. package/dist/settlement.d.ts +17 -0
  61. package/dist/settlement.js +96 -0
  62. package/dist/sha256.d.ts +3 -0
  63. package/dist/sha256.js +83 -0
  64. package/dist/shieldLimits.d.ts +15 -0
  65. package/dist/shieldLimits.js +24 -0
  66. package/dist/signer.d.ts +5 -0
  67. package/dist/signer.js +43 -0
  68. package/dist/token.d.ts +6 -0
  69. package/dist/token.js +33 -0
  70. package/package.json +36 -4
  71. package/README.md +0 -3
@@ -0,0 +1,255 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.AvailabilityLog = void 0;
37
+ exports.ed25519Sign = ed25519Sign;
38
+ exports.ed25519Verify = ed25519Verify;
39
+ exports.envelopeHeaders = envelopeHeaders;
40
+ exports.availabilityAcceptable = availabilityAcceptable;
41
+ exports.probeGuild = probeGuild;
42
+ exports.fetchPendingReceipts = fetchPendingReceipts;
43
+ exports.postAttestation = postAttestation;
44
+ exports.checkUsageReceipt = checkUsageReceipt;
45
+ exports.attestPendingReceipts = attestPendingReceipts;
46
+ const crypto_1 = require("crypto");
47
+ const fs = __importStar(require("fs"));
48
+ const web3_js_1 = require("@solana/web3.js");
49
+ const anchor_1 = require("@coral-xyz/anchor");
50
+ const addresses_1 = require("./addresses");
51
+ const billing_1 = require("./billing");
52
+ const explorer_1 = require("./explorer");
53
+ const bs58 = anchor_1.utils.bytes.bs58;
54
+ const ED25519_PKCS8 = Buffer.from('302e020100300506032b657004220420', 'hex');
55
+ const ED25519_SPKI = Buffer.from('302a300506032b6570032100', 'hex');
56
+ function ed25519Sign(secretKey, message) {
57
+ const key = (0, crypto_1.createPrivateKey)({ key: Buffer.concat([ED25519_PKCS8, Buffer.from(secretKey.subarray(0, 32))]), format: 'der', type: 'pkcs8' });
58
+ return new Uint8Array((0, crypto_1.sign)(null, Buffer.from(message), key));
59
+ }
60
+ function ed25519Verify(publicKey, message, signature) {
61
+ try {
62
+ const key = (0, crypto_1.createPublicKey)({ key: Buffer.concat([ED25519_SPKI, Buffer.from(publicKey)]), format: 'der', type: 'spki' });
63
+ return (0, crypto_1.verify)(null, Buffer.from(message), key, Buffer.from(signature));
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
69
+ function envelopeHeaders(secretKey, publicKey, id, nowSeconds = Math.floor(Date.now() / 1000)) {
70
+ return {
71
+ 'x-zga-sender': publicKey.toBase58(),
72
+ 'x-zga-timestamp': String(nowSeconds),
73
+ 'x-zga-signature': bs58.encode(ed25519Sign(secretKey, Buffer.from(`${nowSeconds}:${id}`))),
74
+ };
75
+ }
76
+ class AvailabilityLog {
77
+ constructor(path = null, keepMs = 14 * 24 * 60 * 60 * 1000) {
78
+ this.path = path;
79
+ this.keepMs = keepMs;
80
+ this.samples = new Map();
81
+ if (path && fs.existsSync(path)) {
82
+ const raw = JSON.parse(fs.readFileSync(path, 'utf8'));
83
+ for (const [key, list] of Object.entries(raw))
84
+ this.samples.set(key, list);
85
+ }
86
+ }
87
+ record(key, up, atMs = Date.now()) {
88
+ const list = this.samples.get(key) ?? [];
89
+ list.push([atMs, up ? 1 : 0]);
90
+ const cutoff = atMs - this.keepMs;
91
+ while (list.length > 0 && list[0][0] < cutoff)
92
+ list.shift();
93
+ this.samples.set(key, list);
94
+ }
95
+ count(key) {
96
+ return this.samples.get(key)?.length ?? 0;
97
+ }
98
+ save() {
99
+ if (!this.path)
100
+ return;
101
+ const out = {};
102
+ for (const [key, list] of this.samples)
103
+ out[key] = list;
104
+ fs.writeFileSync(this.path, JSON.stringify(out));
105
+ }
106
+ coverage(key, fromMs, toMs, intervalMs) {
107
+ const inside = (this.samples.get(key) ?? []).filter(([at]) => at >= fromMs && at < toMs);
108
+ return { expected: Math.floor((toMs - fromMs) / intervalMs), samples: inside.length, up: inside.filter(([, ok]) => ok === 1).length };
109
+ }
110
+ }
111
+ exports.AvailabilityLog = AvailabilityLog;
112
+ function availabilityAcceptable(c, minCoverage = 0.9, minUp = 0.9) {
113
+ return c.expected > 0 && c.samples >= minCoverage * c.expected && c.up >= minUp * c.samples;
114
+ }
115
+ const anyUrl = async (url) => new URL(url);
116
+ async function guarded(guard, url, init = {}) {
117
+ const target = await guard(url);
118
+ return fetch(target, { redirect: 'manual', signal: AbortSignal.timeout(20000), ...init });
119
+ }
120
+ async function probeGuild(base, guard = anyUrl) {
121
+ try {
122
+ return (await guarded(guard, `${base}/v1/health`, { signal: AbortSignal.timeout(8000) })).ok;
123
+ }
124
+ catch {
125
+ return false;
126
+ }
127
+ }
128
+ async function fetchPendingReceipts(base, secretKey, publicKey, guard = anyUrl) {
129
+ const res = await guarded(guard, `${base}/v1/billing/pending`, { headers: envelopeHeaders(secretKey, publicKey, 'billing') });
130
+ if (!res.ok)
131
+ throw new Error(`the guild answered ${res.status}`);
132
+ return (await res.json());
133
+ }
134
+ async function postAttestation(base, secretKey, publicKey, hashHex, signature, guard = anyUrl) {
135
+ const res = await guarded(guard, `${base}/v1/billing/${hashHex}/attestation`, {
136
+ method: 'POST',
137
+ headers: { ...envelopeHeaders(secretKey, publicKey, hashHex), 'content-type': 'application/json' },
138
+ body: JSON.stringify({ signature: bs58.encode(signature) }),
139
+ });
140
+ return res.ok;
141
+ }
142
+ async function checkUsageReceipt(program, receipt, currentSlot, terms = billing_1.DEFAULT_BILLING_TERMS) {
143
+ const session = await program.account.session.fetch(receipt.session);
144
+ if (Object.keys(session.state)[0] !== 'active')
145
+ return 'the session is not active';
146
+ const guild = await program.account.guildAccount.fetch(session.guild);
147
+ if (Object.keys(guild.state)[0] !== 'active')
148
+ return 'the host guild is not active';
149
+ const addr = new addresses_1.Addresses(program.programId);
150
+ const manifest = await program.account.gameManifest.fetch(addr.gameManifest(Uint8Array.from(session.gameId)));
151
+ const hourlyRate = BigInt(manifest.hourlyRate.toString());
152
+ if (Object.keys(manifest.sessionDuration)[0] !== 'persistent' || hourlyRate === 0n)
153
+ return 'the game is not billed by the hour';
154
+ const subscriptionKey = (0, billing_1.subscriptionAddress)(program.programId, receipt.session, receipt.player);
155
+ let subscription;
156
+ try {
157
+ subscription = await program.account.subscription.fetch(subscriptionKey);
158
+ }
159
+ catch {
160
+ return 'the player has no subscription to the session';
161
+ }
162
+ if (BigInt(subscription.billedThroughSlot.toString()) !== receipt.fromSlot)
163
+ return 'the receipt does not start where billing stopped';
164
+ if (receipt.hours < 1 || receipt.hours > terms.maxHoursPerBill)
165
+ return 'the receipt bills too many hours';
166
+ if (receipt.toSlot <= receipt.fromSlot || receipt.toSlot > currentSlot)
167
+ return 'the receipt reaches a slot that is not yet past';
168
+ if (BigInt(receipt.hours) * terms.slotsPerHour > receipt.toSlot - receipt.fromSlot)
169
+ return 'the receipt bills more hours than its stretch holds';
170
+ const subscriptionRate = BigInt(subscription.rate.toString());
171
+ const rate = subscriptionRate < hourlyRate ? subscriptionRate : hourlyRate;
172
+ const vault = await program.provider.connection.getTokenAccountBalance((0, billing_1.subscriptionVaultAddress)(program.programId, subscriptionKey));
173
+ if (BigInt(vault.value.amount) < rate * BigInt(receipt.hours))
174
+ return 'the subscription does not hold the credit';
175
+ return null;
176
+ }
177
+ async function blockTimeNear(connection, slot) {
178
+ for (let s = slot; s < slot + 8; s++) {
179
+ try {
180
+ const time = await connection.getBlockTime(s);
181
+ if (time !== null)
182
+ return time;
183
+ }
184
+ catch {
185
+ }
186
+ }
187
+ return null;
188
+ }
189
+ const answered = new Set();
190
+ async function attestPendingReceipts(env, guildKey, base) {
191
+ const { program, connection } = env;
192
+ const terms = env.terms ?? billing_1.DEFAULT_BILLING_TERMS;
193
+ const out = { attested: [], declined: [] };
194
+ const pending = await fetchPendingReceipts(base, env.secretKey, env.publicKey, env.guard);
195
+ const guild = await program.account.guildAccount.fetch(guildKey);
196
+ const refusal = env.explorer ? await (0, explorer_1.explorerGate)(env.explorer, guildKey.toBase58(), base, env.guard) : null;
197
+ for (const p of pending) {
198
+ if (answered.has(p.hashHex))
199
+ continue;
200
+ const decline = (reason) => out.declined.push({ hashHex: p.hashHex, reason });
201
+ try {
202
+ const receipt = {
203
+ session: new web3_js_1.PublicKey(p.session), player: new web3_js_1.PublicKey(p.player), hours: p.hours, fromSlot: BigInt(p.fromSlot), toSlot: BigInt(p.toSlot),
204
+ };
205
+ const hash = (0, billing_1.usageReceiptHash)(receipt);
206
+ if (Buffer.from(hash).toString('hex') !== p.hashHex) {
207
+ decline('the hash is not the receipt\'s');
208
+ continue;
209
+ }
210
+ const session = await program.account.session.fetch(receipt.session);
211
+ if (!session.guild.equals(guildKey)) {
212
+ decline('the session is not hosted by this guild');
213
+ continue;
214
+ }
215
+ const observers = [session.assignedWitnesses[0], session.assignedWitnesses[1], session.observerNop];
216
+ if (!observers.some((o) => !o.equals(web3_js_1.PublicKey.default) && o.equals(env.publicKey)))
217
+ continue;
218
+ if (!ed25519Verify(Uint8Array.from(guild.enclavePubkey), hash, bs58.decode(p.guildSignature))) {
219
+ decline('the guild signature does not verify');
220
+ continue;
221
+ }
222
+ if (refusal) {
223
+ decline(refusal);
224
+ continue;
225
+ }
226
+ const reason = await checkUsageReceipt(program, receipt, BigInt(await connection.getSlot()), terms);
227
+ if (reason) {
228
+ decline(reason);
229
+ continue;
230
+ }
231
+ const fromTime = await blockTimeNear(connection, Number(receipt.fromSlot));
232
+ const toTime = await blockTimeNear(connection, Number(receipt.toSlot));
233
+ if (fromTime === null || toTime === null) {
234
+ decline('the times of the stretch are not known');
235
+ continue;
236
+ }
237
+ const coverage = env.log.coverage(guildKey.toBase58(), fromTime * 1000, toTime * 1000, env.probeIntervalMs ?? 300000);
238
+ if (!availabilityAcceptable(coverage, env.minCoverage, env.minUp)) {
239
+ decline(`the guild was seen up in ${coverage.up} of ${coverage.samples} probes, ${coverage.expected} expected`);
240
+ continue;
241
+ }
242
+ if (await postAttestation(base, env.secretKey, env.publicKey, p.hashHex, ed25519Sign(env.secretKey, hash), env.guard)) {
243
+ answered.add(p.hashHex);
244
+ out.attested.push(p.hashHex);
245
+ }
246
+ else {
247
+ decline('the guild did not take the attestation');
248
+ }
249
+ }
250
+ catch (e) {
251
+ decline(String(e?.message ?? e).slice(0, 120));
252
+ }
253
+ }
254
+ return out;
255
+ }
@@ -0,0 +1,30 @@
1
+ import { PublicKey } from '@solana/web3.js';
2
+ export interface Receipt {
3
+ sessionId: Uint8Array;
4
+ guild: PublicKey;
5
+ host: PublicKey;
6
+ entryFees: bigint;
7
+ prizePool: bigint;
8
+ guildFee: bigint;
9
+ hostRoyalty: bigint;
10
+ protocolTax: bigint;
11
+ timestamp: bigint;
12
+ winner: PublicKey | null;
13
+ second: PublicKey | null;
14
+ third: PublicKey | null;
15
+ logRoot: Uint8Array | null;
16
+ }
17
+ export declare function receiptHash(r: Receipt): Buffer;
18
+ export interface Terms {
19
+ protocolTax: bigint;
20
+ guildFee: bigint;
21
+ hostRoyalty: bigint;
22
+ prizePool: bigint;
23
+ }
24
+ export declare function settlementTerms(p: {
25
+ entryFees: bigint;
26
+ protocolTaxBps: bigint;
27
+ guildFeeBps: bigint;
28
+ royaltyBps: bigint;
29
+ draw: boolean;
30
+ }): Terms;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.receiptHash = receiptHash;
4
+ exports.settlementTerms = settlementTerms;
5
+ const crypto_1 = require("crypto");
6
+ const u64le = (v) => { const b = Buffer.alloc(8); b.writeBigUInt64LE(v); return b; };
7
+ const i64le = (v) => { const b = Buffer.alloc(8); b.writeBigInt64LE(v); return b; };
8
+ const key32 = (k) => (k ? Buffer.from(k.toBytes()) : Buffer.alloc(32));
9
+ function receiptHash(r) {
10
+ const base = (0, crypto_1.createHash)('sha256').update(Buffer.concat([
11
+ Buffer.from(r.sessionId), Buffer.from(r.guild.toBytes()), Buffer.from(r.host.toBytes()),
12
+ u64le(r.entryFees), u64le(r.prizePool), u64le(r.guildFee), u64le(r.hostRoyalty), u64le(r.protocolTax), i64le(r.timestamp),
13
+ key32(r.winner), key32(r.second), key32(r.third),
14
+ ])).digest();
15
+ if (!r.logRoot)
16
+ return base;
17
+ return (0, crypto_1.createHash)('sha256').update(Buffer.concat([base, Buffer.from(r.logRoot)])).digest();
18
+ }
19
+ function settlementTerms(p) {
20
+ const protocolTax = (p.entryFees * p.protocolTaxBps) / 10000n;
21
+ const net = p.entryFees - protocolTax;
22
+ if (p.draw)
23
+ return { protocolTax, guildFee: 0n, hostRoyalty: 0n, prizePool: net };
24
+ const guildFee = (net * p.guildFeeBps) / 10000n;
25
+ const hostRoyalty = (net * p.royaltyBps) / 10000n;
26
+ return { protocolTax, guildFee, hostRoyalty, prizePool: net - guildFee - hostRoyalty };
27
+ }
@@ -0,0 +1,35 @@
1
+ import { Connection, Keypair } from '@solana/web3.js';
2
+ import { RelayerInfo, RelayerUnshieldRequest } from '@zga/shielded-sdk/dist/relayer';
3
+ import { ShieldedRelayerChain } from './relayer';
4
+ import { RelayerSettings } from './settings';
5
+ export declare class SolanaShieldedRelayerChain implements ShieldedRelayerChain {
6
+ private readonly program;
7
+ private readonly connection;
8
+ private readonly relayer;
9
+ private readonly settings;
10
+ private readonly log;
11
+ private cached;
12
+ private tables;
13
+ private readonly atas;
14
+ private readonly operator;
15
+ constructor(program: any, connection: Connection, relayer: Keypair, settings: RelayerSettings, log?: (message: string) => void);
16
+ private get pid();
17
+ private seed;
18
+ private poolPda;
19
+ private vkPda;
20
+ private assetPda;
21
+ private vaultPda;
22
+ private nopPda;
23
+ private feesPda;
24
+ private nullifierPda;
25
+ private directoryPda;
26
+ private fetchAccount;
27
+ prepare(): Promise<RelayerInfo>;
28
+ info(): RelayerInfo;
29
+ currentPool(): Promise<number>;
30
+ anySpent(nullifiers: string[]): Promise<boolean>;
31
+ lowBalance(): Promise<boolean>;
32
+ submit(req: RelayerUnshieldRequest, currentPool: number): Promise<{
33
+ signature: string;
34
+ }>;
35
+ }
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SolanaShieldedRelayerChain = void 0;
4
+ const web3_js_1 = require("@solana/web3.js");
5
+ const anchor_1 = require("@coral-xyz/anchor");
6
+ const relayer_1 = require("@zga/shielded-sdk/dist/relayer");
7
+ const chain_1 = require("../chain");
8
+ const token_1 = require("../token");
9
+ const errors_1 = require("./errors");
10
+ const ASSOCIATED_TOKEN_PROGRAM_ID = new web3_js_1.PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL');
11
+ function createAssociatedTokenAccountIdempotent(payer, ata, owner, mint) {
12
+ return new web3_js_1.TransactionInstruction({
13
+ programId: ASSOCIATED_TOKEN_PROGRAM_ID,
14
+ keys: [
15
+ { pubkey: payer, isSigner: true, isWritable: true },
16
+ { pubkey: ata, isSigner: false, isWritable: true },
17
+ { pubkey: owner, isSigner: false, isWritable: false },
18
+ { pubkey: mint, isSigner: false, isWritable: false },
19
+ { pubkey: web3_js_1.SystemProgram.programId, isSigner: false, isWritable: false },
20
+ { pubkey: token_1.TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
21
+ ],
22
+ data: Buffer.from([1]),
23
+ });
24
+ }
25
+ class SolanaShieldedRelayerChain {
26
+ constructor(program, connection, relayer, settings, log = () => { }) {
27
+ this.program = program;
28
+ this.connection = connection;
29
+ this.relayer = relayer;
30
+ this.settings = settings;
31
+ this.log = log;
32
+ this.cached = null;
33
+ this.tables = [];
34
+ this.atas = new Map();
35
+ this.poolPda = (i) => (0, chain_1.pda)(this.pid, this.seed('shielded_pool'), Buffer.from([i]));
36
+ this.vkPda = (i) => (0, chain_1.pda)(this.pid, this.seed('shield_vk'), Buffer.from([i]));
37
+ this.assetPda = (id) => (0, chain_1.pda)(this.pid, this.seed('shield_asset'), Buffer.from([id]));
38
+ this.vaultPda = (id) => (0, chain_1.pda)(this.pid, this.seed('shield_vault'), Buffer.from([id]));
39
+ this.nopPda = () => (0, chain_1.pda)(this.pid, this.seed('nop'), this.operator.toBuffer());
40
+ this.feesPda = (id) => (0, chain_1.pda)(this.pid, this.seed('nop_shield_fees'), this.operator.toBuffer(), Buffer.from([id]));
41
+ this.nullifierPda = (n) => (0, chain_1.pda)(this.pid, this.seed('nullifier'), Buffer.from(n));
42
+ this.directoryPda = () => (0, chain_1.pda)(this.pid, this.seed('shield_directory'));
43
+ this.operator = settings.operator ? new web3_js_1.PublicKey(settings.operator) : relayer.publicKey;
44
+ }
45
+ get pid() {
46
+ return this.program.programId;
47
+ }
48
+ seed(s) {
49
+ return Buffer.from(s);
50
+ }
51
+ async fetchAccount(name, address) {
52
+ const info = await this.connection.getAccountInfo(address);
53
+ return info ? (0, chain_1.decodeAccount)(this.program, name, info.data) : null;
54
+ }
55
+ async prepare() {
56
+ const nop = await this.fetchAccount('NopAccount', this.nopPda());
57
+ if (!nop || Object.keys(nop.state)[0] !== 'active')
58
+ throw new Error(`${this.operator.toBase58()} is not an active NOP: only a NOP or a bonded agent may take a fee`);
59
+ if (!this.operator.equals(this.relayer.publicKey) && !new web3_js_1.PublicKey(nop.relayer).equals(this.relayer.publicKey)) {
60
+ throw new Error(`the NOP ${this.operator.toBase58()} has not named ${this.relayer.publicKey.toBase58()} as its relayer key: its operator names it with set_nop_relayer`);
61
+ }
62
+ this.tables = [];
63
+ for (const address of this.settings.lookupTables) {
64
+ const table = (await this.connection.getAddressLookupTable(new web3_js_1.PublicKey(address))).value;
65
+ if (!table)
66
+ throw new Error(`the lookup table ${address} does not exist on this cluster`);
67
+ this.tables.push(table);
68
+ }
69
+ const assets = [];
70
+ for (const s of this.settings.assets) {
71
+ const config = await this.fetchAccount('ShieldAssetConfig', this.assetPda(s.assetId));
72
+ if (!config)
73
+ throw new Error(`asset ${s.assetId} has no place in the shielded pool`);
74
+ const mint = config.mint;
75
+ const ata = (0, token_1.associatedTokenAddress)(mint, this.operator);
76
+ if (!(await this.connection.getAccountInfo(ata))) {
77
+ await (0, web3_js_1.sendAndConfirmTransaction)(this.connection, new web3_js_1.Transaction().add(createAssociatedTokenAccountIdempotent(this.relayer.publicKey, ata, this.operator, mint)), [this.relayer]);
78
+ this.log(`made the NOP's token account for asset ${s.assetId}`);
79
+ }
80
+ this.atas.set(s.assetId, ata);
81
+ if (!(await this.fetchAccount('NopShieldFees', this.feesPda(s.assetId)))) {
82
+ if (!this.operator.equals(this.relayer.publicKey))
83
+ throw new Error(`the NOP has no tax account for asset ${s.assetId}: its operator opens it once with init_nop_shield_fees`);
84
+ const open = await this.program.methods.initNopShieldFees(s.assetId).accounts({
85
+ fees: this.feesPda(s.assetId), nop: this.nopPda(), assetConfig: this.assetPda(s.assetId), operator: this.relayer.publicKey, systemProgram: web3_js_1.SystemProgram.programId,
86
+ }).instruction();
87
+ await (0, web3_js_1.sendAndConfirmTransaction)(this.connection, new web3_js_1.Transaction().add(open), [this.relayer]);
88
+ this.log(`opened the NOP tax account for asset ${s.assetId}`);
89
+ }
90
+ assets.push({ assetId: s.assetId, relayerTokenAccount: ata.toBase58(), minFee: s.minFee, feeBps: s.feeBps });
91
+ }
92
+ this.cached = {
93
+ version: 1,
94
+ relayer: this.relayer.publicKey.toBase58(),
95
+ programId: this.pid.toBase58(),
96
+ lookupTables: this.settings.lookupTables,
97
+ assets,
98
+ };
99
+ return this.cached;
100
+ }
101
+ info() {
102
+ if (!this.cached)
103
+ throw new Error('the relayer has not been prepared');
104
+ return this.cached;
105
+ }
106
+ async currentPool() {
107
+ return Number((await this.fetchAccount('ShieldDirectory', this.directoryPda())).current);
108
+ }
109
+ async anySpent(nullifiers) {
110
+ const infos = await this.connection.getMultipleAccountsInfo(nullifiers.map((n) => this.nullifierPda(Buffer.from(n, 'hex'))));
111
+ return infos.some((i) => i !== null);
112
+ }
113
+ async lowBalance() {
114
+ return (await this.connection.getBalance(this.relayer.publicKey)) < this.settings.minBalanceLamports;
115
+ }
116
+ async submit(req, currentPool) {
117
+ const a = (0, relayer_1.toUnshieldArgs)(req);
118
+ const args = {
119
+ proof: { a: Array.from(a.proof.a), b: Array.from(a.proof.b), c: Array.from(a.proof.c) },
120
+ root: Array.from(a.root),
121
+ withdrawAmount: new anchor_1.BN(a.withdrawAmount.toString()),
122
+ fee: new anchor_1.BN(a.fee.toString()),
123
+ nullifiers: a.nullifiers.map((n) => Array.from(n)),
124
+ commitments: a.commitments.map((c) => Array.from(c)),
125
+ encrypted: a.encrypted.map((e) => Buffer.from(e)),
126
+ };
127
+ const relayerAta = this.atas.get(req.assetId);
128
+ if (!relayerAta)
129
+ throw new errors_1.RelayRefused('asset', `this relayer does not take asset ${req.assetId}`);
130
+ const method = a.dropOutputs ? 'unshieldDrop' : 'unshield';
131
+ const ix = await this.program.methods[method](req.assetId, args).accountsPartial({
132
+ pool: this.poolPda(req.pool),
133
+ vk: this.vkPda(req.pool),
134
+ targetPool: a.dropOutputs || req.pool === currentPool ? null : this.poolPda(currentPool),
135
+ assetConfig: this.assetPda(req.assetId),
136
+ vault: this.vaultPda(req.assetId),
137
+ recipientTokenAccount: new web3_js_1.PublicKey(req.recipientTokenAccount),
138
+ relayerTokenAccount: relayerAta,
139
+ nullifier0: this.nullifierPda(a.nullifiers[0]),
140
+ nullifier1: this.nullifierPda(a.nullifiers[1]),
141
+ protocolConfig: (0, chain_1.pda)(this.pid, this.seed('config')),
142
+ relayer: this.relayer.publicKey,
143
+ relayerNop: this.nopPda(),
144
+ relayerAgentBond: null,
145
+ relayerFees: this.feesPda(req.assetId),
146
+ tokenProgram: token_1.TOKEN_PROGRAM_ID,
147
+ systemProgram: web3_js_1.SystemProgram.programId,
148
+ }).instruction();
149
+ const instructions = [web3_js_1.ComputeBudgetProgram.setComputeUnitLimit({ units: this.settings.computeUnitLimit })];
150
+ if (this.settings.priorityMicroLamports > 0)
151
+ instructions.push(web3_js_1.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: this.settings.priorityMicroLamports }));
152
+ instructions.push(ix);
153
+ const latest = await this.connection.getLatestBlockhash('confirmed');
154
+ const message = new web3_js_1.TransactionMessage({ payerKey: this.relayer.publicKey, recentBlockhash: latest.blockhash, instructions }).compileToV0Message(this.tables);
155
+ const tx = new web3_js_1.VersionedTransaction(message);
156
+ const sim = await this.connection.simulateTransaction(tx, { sigVerify: false, replaceRecentBlockhash: true, commitment: 'confirmed' });
157
+ if (sim.value.err) {
158
+ const f = (0, errors_1.explainRelayFailure)((sim.value.logs ?? []).join('\n') + JSON.stringify(sim.value.err));
159
+ throw new errors_1.RelayRefused(f.code, f.message);
160
+ }
161
+ tx.sign([this.relayer]);
162
+ const signature = await this.connection.sendRawTransaction(tx.serialize(), { skipPreflight: true, maxRetries: 3 });
163
+ const done = await this.connection.confirmTransaction({ signature, blockhash: latest.blockhash, lastValidBlockHeight: latest.lastValidBlockHeight }, 'confirmed');
164
+ if (done.value.err) {
165
+ const f = (0, errors_1.explainRelayFailure)(JSON.stringify(done.value.err));
166
+ throw new errors_1.RelayRefused(f.code === 'rejected' ? 'submission_failed' : f.code, f.message);
167
+ }
168
+ return { signature };
169
+ }
170
+ }
171
+ exports.SolanaShieldedRelayerChain = SolanaShieldedRelayerChain;
@@ -0,0 +1,8 @@
1
+ export declare class RelayRefused extends Error {
2
+ readonly code: string;
3
+ constructor(code: string, message: string);
4
+ }
5
+ export declare function explainRelayFailure(text: string): {
6
+ code: string;
7
+ message: string;
8
+ };
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RelayRefused = void 0;
4
+ exports.explainRelayFailure = explainRelayFailure;
5
+ class RelayRefused extends Error {
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.code = code;
9
+ this.name = 'RelayRefused';
10
+ }
11
+ }
12
+ exports.RelayRefused = RelayRefused;
13
+ const RULES = [
14
+ [/UnknownRoot/, 'unknown_root', 'the proof was made against a root the pool no longer holds: build it again from the current tree'],
15
+ [/InvalidProof/, 'invalid_proof', 'the pool did not accept the proof'],
16
+ [/already in use|AccountAlreadyInitialized/, 'already_spent', 'a note that this transaction spends has been spent'],
17
+ [/ShieldDailyCapExceeded/, 'daily_cap', 'the day\'s withdrawal cap for this asset is reached: try again later'],
18
+ [/ShieldAssetNotLive/, 'asset_not_live', 'the shielded pool is not live for this asset'],
19
+ [/ShieldInsufficientNotes/, 'insufficient_notes', 'the pool does not hold that much of the asset'],
20
+ [/NotAnAuthorizedRelayer/, 'relayer_not_authorized', 'this relayer cannot take a fee'],
21
+ [/ShieldPoolNotCurrent|ShieldPoolKeyMismatch/, 'pool', 'the notes are in a pool whose new notes cannot go to the current pool'],
22
+ [/Paused/, 'paused', 'the protocol is paused'],
23
+ ];
24
+ function explainRelayFailure(text) {
25
+ for (const [pattern, code, message] of RULES)
26
+ if (pattern.test(text))
27
+ return { code, message };
28
+ return { code: 'rejected', message: 'the pool refused the transaction' };
29
+ }
@@ -0,0 +1,15 @@
1
+ export declare class RelayerRateLimiter {
2
+ private readonly perMinute;
3
+ private readonly now;
4
+ private readonly buckets;
5
+ constructor(perMinute: number, now?: () => number);
6
+ allow(key: string): boolean;
7
+ private prune;
8
+ }
9
+ export declare class RelayerInFlight {
10
+ readonly max: number;
11
+ private readonly active;
12
+ constructor(max: number);
13
+ tryEnter(key: string): 'ok' | 'busy' | 'duplicate';
14
+ leave(key: string): void;
15
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RelayerInFlight = exports.RelayerRateLimiter = void 0;
4
+ class RelayerRateLimiter {
5
+ constructor(perMinute, now = Date.now) {
6
+ this.perMinute = perMinute;
7
+ this.now = now;
8
+ this.buckets = new Map();
9
+ }
10
+ allow(key) {
11
+ const t = this.now();
12
+ const b = this.buckets.get(key) ?? { tokens: this.perMinute, at: t };
13
+ b.tokens = Math.min(this.perMinute, b.tokens + ((t - b.at) * this.perMinute) / 60000);
14
+ b.at = t;
15
+ const ok = b.tokens >= 1;
16
+ if (ok)
17
+ b.tokens -= 1;
18
+ this.buckets.set(key, b);
19
+ if (this.buckets.size > 10000)
20
+ this.prune(t);
21
+ return ok;
22
+ }
23
+ prune(t) {
24
+ for (const [k, b] of this.buckets)
25
+ if (b.tokens + ((t - b.at) * this.perMinute) / 60000 >= this.perMinute)
26
+ this.buckets.delete(k);
27
+ }
28
+ }
29
+ exports.RelayerRateLimiter = RelayerRateLimiter;
30
+ class RelayerInFlight {
31
+ constructor(max) {
32
+ this.max = max;
33
+ this.active = new Set();
34
+ }
35
+ tryEnter(key) {
36
+ if (this.active.has(key))
37
+ return 'duplicate';
38
+ if (this.active.size >= this.max)
39
+ return 'busy';
40
+ this.active.add(key);
41
+ return 'ok';
42
+ }
43
+ leave(key) {
44
+ this.active.delete(key);
45
+ }
46
+ }
47
+ exports.RelayerInFlight = RelayerInFlight;
@@ -0,0 +1,25 @@
1
+ import { RelayerInfo, RelayerUnshieldRequest } from '@zga/shielded-sdk/dist/relayer';
2
+ import { RelayerInFlight, RelayerRateLimiter } from './limits';
3
+ export interface Reply {
4
+ status: number;
5
+ body: any;
6
+ }
7
+ export declare const refuse: (status: number, code: string, message: string) => Reply;
8
+ export interface ShieldedRelayerChain {
9
+ info(): RelayerInfo;
10
+ currentPool(): Promise<number>;
11
+ anySpent(nullifiers: string[]): Promise<boolean>;
12
+ lowBalance(): Promise<boolean>;
13
+ submit(req: RelayerUnshieldRequest, currentPool: number): Promise<{
14
+ signature: string;
15
+ }>;
16
+ }
17
+ export declare class ShieldedRelayer {
18
+ private readonly chain;
19
+ private readonly rate;
20
+ private readonly inflight;
21
+ private readonly log;
22
+ constructor(chain: ShieldedRelayerChain, rate: RelayerRateLimiter, inflight: RelayerInFlight, log?: (message: string) => void);
23
+ info(): Reply;
24
+ unshield(raw: unknown, clientKey: string): Promise<Reply>;
25
+ }