@1upmonster/duel 0.2.2 → 0.2.3

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/src/player.ts CHANGED
@@ -1,13 +1,16 @@
1
1
  import {
2
2
  createSolanaRpc,
3
+ AccountRole,
3
4
  type Address,
4
5
  type TransactionSigner,
5
6
  type Rpc,
6
7
  type SolanaRpcApi,
8
+ type Instruction,
7
9
  } from "@solana/kit";
8
10
  import {
9
11
  getCreateTicketInstructionAsync,
10
12
  getDelegateTicketInstructionAsync,
13
+ getSetupTicketPermissionInstructionAsync,
11
14
  getJoinQueueInstructionAsync,
12
15
  getCancelTicketInstructionAsync,
13
16
  getCloseTicketInstructionAsync,
@@ -15,7 +18,6 @@ import {
15
18
  accountType,
16
19
  } from "./generated/duel/index.js";
17
20
  import { sendInstruction } from "./transaction.js";
18
- import { waitUntilPermissionActive } from "./tee.js";
19
21
  import * as utils from "./utils.js";
20
22
 
21
23
  const DUEL_PROGRAM_ID = "EdZzUwKd1X2ZWjxLPpz1cpEzMF7RUZC43Pq64v1VcK5X" as Address;
@@ -130,7 +132,8 @@ export class MatchmakingPlayer {
130
132
 
131
133
  /**
132
134
  * High-level: full matchmaking TEE entry flow.
133
- * Creates ticket on L1, delegates it to TEE, waits for activation, then joins the queue.
135
+ * Creates ticket on L1, sets up permission PDA (so only player + queue authority can read it),
136
+ * delegates the permission PDA and ticket to TEE, then joins the queue.
134
137
  * Use individual methods (createTicket, delegateTicket, joinQueue) as escape hatches if needed.
135
138
  */
136
139
  async enterQueue(
@@ -145,10 +148,45 @@ export class MatchmakingPlayer {
145
148
  const player = this.signer.address as Address;
146
149
  const ticketPda = await this.getTicketPda(player, tenant);
147
150
 
151
+ // 1. Create ticket on L1
148
152
  await this.createTicket(tenant);
153
+
154
+ // 2. Create Permission PDA for the ticket (CPI from duel program with invoke_signed)
155
+ const permissionPda = await utils.derivePermissionPda(ticketPda);
156
+ const setupIx = await getSetupTicketPermissionInstructionAsync({
157
+ player: this.signer,
158
+ tenant,
159
+ permission: permissionPda,
160
+ permissionProgram: utils.PERMISSION_PROGRAM,
161
+ }, { programAddress: this.programId });
162
+ await sendInstruction(this.rpc, setupIx, this.signer);
163
+
164
+ // 3. Delegate Permission PDA to TEE (called on permission program, player signs)
165
+ const { delegationBuffer, delegationRecord, delegationMetadata } =
166
+ await utils.derivePermissionDelegationPdas(permissionPda);
167
+ const delegatePermIx: Instruction = {
168
+ programAddress: utils.PERMISSION_PROGRAM,
169
+ data: new Uint8Array([3, 0, 0, 0, 0, 0, 0, 0]),
170
+ accounts: [
171
+ { address: this.signer.address, role: AccountRole.WRITABLE_SIGNER },
172
+ { address: this.signer.address, role: AccountRole.READONLY_SIGNER },
173
+ { address: ticketPda, role: AccountRole.READONLY },
174
+ { address: permissionPda, role: AccountRole.WRITABLE },
175
+ { address: "11111111111111111111111111111111" as Address, role: AccountRole.READONLY },
176
+ { address: utils.PERMISSION_PROGRAM, role: AccountRole.READONLY },
177
+ { address: delegationBuffer, role: AccountRole.WRITABLE },
178
+ { address: delegationRecord, role: AccountRole.WRITABLE },
179
+ { address: delegationMetadata, role: AccountRole.WRITABLE },
180
+ { address: utils.DELEGATION_PROGRAM, role: AccountRole.READONLY },
181
+ ...(validator ? [{ address: validator, role: AccountRole.READONLY }] : []),
182
+ ],
183
+ };
184
+ await sendInstruction(this.rpc, delegatePermIx, this.signer);
185
+
186
+ // 4. Delegate ticket to TEE
149
187
  await this.delegateTicket(player, tenant, validator);
150
- await waitUntilPermissionActive(teeUrlWithToken, ticketPda);
151
188
 
189
+ // 5. Join the queue on TEE
152
190
  const teeClient = new MatchmakingPlayer(teeRpc, this.signer, this.programId);
153
191
  await teeClient.joinQueue(queue, tenant, playerData, callbackProgram);
154
192
 
package/src/tee.ts CHANGED
@@ -45,35 +45,31 @@ export async function getAuthToken(
45
45
  }
46
46
 
47
47
  /**
48
- * Poll the TEE /permission endpoint until the given PDA has authorized users,
49
- * indicating delegation is active. Returns false on timeout (does not throw).
48
+ * Poll the TEE until the Permission PDA for the given account is active (authorizedUsers non-empty).
49
+ * This confirms the TEE has picked up the delegated Permission PDA and is enforcing access control.
50
+ * Returns true if active before timeout, false otherwise.
50
51
  */
51
- export async function waitUntilPermissionActive(
52
+ export async function waitForPermission(
52
53
  teeUrlWithToken: string,
53
- pda: Address,
54
- timeoutMs = 30000,
54
+ accountAddress: Address,
55
+ timeoutMs = 10000,
55
56
  ): Promise<boolean> {
56
- // Parse URL: "https://host/path?token=xxx" -> baseUrl="https://host/path", tokenParam="token=xxx"
57
- const [baseUrl, tokenParam] = teeUrlWithToken.replace("/?", "?").split("?");
58
- let permissionUrl: string;
59
- if (tokenParam) {
60
- permissionUrl = `${baseUrl}/permission?${tokenParam}&pubkey=${pda}`;
61
- } else {
62
- permissionUrl = `${baseUrl}/permission?pubkey=${pda}`;
63
- }
57
+ const [baseUrl, token] = teeUrlWithToken.replace("/?", "?").split("?");
58
+ const permUrl = token
59
+ ? `${baseUrl}/permission?${token}&pubkey=${accountAddress}`
60
+ : `${baseUrl}/permission?pubkey=${accountAddress}`;
64
61
 
65
- const start = Date.now();
66
- while (Date.now() - start < timeoutMs) {
62
+ const deadline = Date.now() + timeoutMs;
63
+ while (Date.now() < deadline) {
67
64
  try {
68
- const res = await fetch(permissionUrl);
69
- if (res.ok) {
70
- const { authorizedUsers } = (await res.json()) as { authorizedUsers?: unknown[] };
71
- if (authorizedUsers && authorizedUsers.length > 0) return true;
72
- }
65
+ const res = await fetch(permUrl);
66
+ const json = (await res.json()) as { authorizedUsers?: unknown[] | null };
67
+ if (json.authorizedUsers && json.authorizedUsers.length > 0) return true;
73
68
  } catch {
74
- // ignore transient errors, keep polling
69
+ // ignore transient errors
75
70
  }
76
- await new Promise((r) => setTimeout(r, 400));
71
+ await new Promise((r) => setTimeout(r, 500));
77
72
  }
78
73
  return false;
79
74
  }
75
+
@@ -15,8 +15,34 @@ import {
15
15
 
16
16
  type SolanaRpc = Rpc<SolanaRpcApi>;
17
17
 
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ const bigIntReplacer = (_: string, v: unknown) => typeof v === 'bigint' ? v.toString() : v;
20
+
21
+ /**
22
+ * Poll for transaction confirmation and throw if it failed.
23
+ * This ensures callers always know immediately when a transaction is rejected.
24
+ */
25
+ async function confirmTransaction(rpc: SolanaRpc, sig: string): Promise<void> {
26
+ for (let i = 0; i < 8; i++) {
27
+ await new Promise(r => setTimeout(r, 1000));
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ const statuses = await (rpc as any).getSignatureStatuses([sig], { searchTransactionHistory: false }).send().catch(() => null);
30
+ const status = statuses?.value?.[0];
31
+ if (status) {
32
+ if (status.err) {
33
+ throw new Error(`[TX] ${sig.slice(0, 16)}... FAILED: ${JSON.stringify(status.err, bigIntReplacer)}`);
34
+ }
35
+ console.log(`[TX] ${sig.slice(0, 16)}... ${status.confirmationStatus}`);
36
+ return;
37
+ }
38
+ }
39
+ // No status after 8s — treat as timeout rather than silently succeeding
40
+ throw new Error(`[TX] ${sig.slice(0, 16)}... confirmation timeout (no status after 8s)`);
41
+ }
42
+
18
43
  /**
19
44
  * Build, sign with a Kit keypair signer, and send a single instruction.
45
+ * Throws if the transaction is rejected or times out.
20
46
  */
21
47
  export async function sendInstruction(
22
48
  rpc: SolanaRpc,
@@ -42,27 +68,13 @@ export async function sendInstruction(
42
68
  skipPreflight: true,
43
69
  }).send() as Promise<string>);
44
70
 
45
- // Poll for status to detect runtime errors
46
- for (let i = 0; i < 8; i++) {
47
- await new Promise(r => setTimeout(r, 1000));
48
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
- const statuses = await (rpc as any).getSignatureStatuses([sig], { searchTransactionHistory: false }).send().catch(() => null);
50
- const status = statuses?.value?.[0];
51
- if (status) {
52
- if (status.err) {
53
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
- console.error(`[TX] ${sig.slice(0, 16)}... FAILED:`, JSON.stringify(status.err, (_, v) => typeof v === 'bigint' ? v.toString() : v));
55
- } else if (status.confirmationStatus) {
56
- console.log(`[TX] ${sig.slice(0, 16)}... ${status.confirmationStatus}`);
57
- }
58
- break;
59
- }
60
- }
71
+ await confirmTransaction(rpc, sig);
61
72
  return sig;
62
73
  }
63
74
 
64
75
  /**
65
76
  * Build, sign, and send multiple instructions in a single transaction.
77
+ * Throws if the transaction is rejected or times out.
66
78
  */
67
79
  export async function sendInstructions(
68
80
  rpc: SolanaRpc,
@@ -83,8 +95,11 @@ export async function sendInstructions(
83
95
  const encoded = getBase64EncodedWireTransaction(signedTx);
84
96
 
85
97
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
- return rpc.sendTransaction(encoded as any, {
98
+ const sig = await (rpc.sendTransaction(encoded as any, {
87
99
  encoding: "base64",
88
100
  skipPreflight: true,
89
- }).send() as Promise<string>;
101
+ }).send() as Promise<string>);
102
+
103
+ await confirmTransaction(rpc, sig);
104
+ return sig;
90
105
  }
package/src/utils.ts CHANGED
@@ -8,6 +8,40 @@ import {
8
8
  const addressEncoder = getAddressEncoder();
9
9
  const utf8Encoder = getUtf8Encoder();
10
10
 
11
+ export const PERMISSION_PROGRAM = "ACLseoPoyC3cBqoUtkbjZ4aDrkurZW86v19pXz2XQnp1" as Address;
12
+ export const DELEGATION_PROGRAM = "DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh" as Address;
13
+
14
+ /** Derives the Permission PDA for a given permissioned account. */
15
+ export async function derivePermissionPda(accountAddress: Address): Promise<Address> {
16
+ const [pda] = await getProgramDerivedAddress({
17
+ programAddress: PERMISSION_PROGRAM,
18
+ seeds: [utf8Encoder.encode("permission:"), addressEncoder.encode(accountAddress)],
19
+ });
20
+ return pda;
21
+ }
22
+
23
+ /** Derives the DELeGG delegation PDAs for the Permission PDA (used in DelegatePermission ix). */
24
+ export async function derivePermissionDelegationPdas(permissionPda: Address): Promise<{
25
+ delegationBuffer: Address;
26
+ delegationRecord: Address;
27
+ delegationMetadata: Address;
28
+ }> {
29
+ const [delegationRecord] = await getProgramDerivedAddress({
30
+ programAddress: DELEGATION_PROGRAM,
31
+ seeds: [utf8Encoder.encode("delegation"), addressEncoder.encode(permissionPda)],
32
+ });
33
+ const [delegationMetadata] = await getProgramDerivedAddress({
34
+ programAddress: DELEGATION_PROGRAM,
35
+ seeds: [utf8Encoder.encode("delegation-metadata"), addressEncoder.encode(permissionPda)],
36
+ });
37
+ // Buffer PDA is under the ownerProgram (PERMISSION_PROGRAM for the Permission PDA)
38
+ const [delegationBuffer] = await getProgramDerivedAddress({
39
+ programAddress: PERMISSION_PROGRAM,
40
+ seeds: [utf8Encoder.encode("buffer"), addressEncoder.encode(permissionPda)],
41
+ });
42
+ return { delegationBuffer, delegationRecord, delegationMetadata };
43
+ }
44
+
11
45
  export const QUEUE_SEED = "queue";
12
46
  export const TENANT_SEED = "tenant";
13
47
  export const TICKET_SEED = "ticket";
package/src/encryption.ts DELETED
@@ -1,154 +0,0 @@
1
- export class EncryptionProvider {
2
- private keyPair: CryptoKeyPair | null = null;
3
-
4
- // Check for crypto availability
5
- private get crypto(): Crypto {
6
- if (typeof globalThis.crypto !== 'undefined') {
7
- return globalThis.crypto;
8
- }
9
- // Fallback for Node < 19 if global crypto not set (though SDK likely targets modern envs)
10
- try {
11
- return require('crypto').webcrypto;
12
- } catch (e) {
13
- throw new Error("WebCrypto API not available.");
14
- }
15
- }
16
-
17
- /**
18
- * Generate a fresh ephemeral keypair for the session (X25519/P-256).
19
- */
20
- async generateSessionKey(): Promise<CryptoKey> {
21
- this.keyPair = await this.crypto.subtle.generateKey(
22
- {
23
- name: "ECDH",
24
- namedCurve: "P-256",
25
- },
26
- true,
27
- ["deriveKey", "deriveBits"]
28
- ) as CryptoKeyPair;
29
-
30
- return this.keyPair.publicKey;
31
- }
32
-
33
- /**
34
- * Derive shared secret and encrypt payload.
35
- */
36
- async encryptPayload(
37
- data: Uint8Array,
38
- teePublicKeyBytes: Uint8Array
39
- ): Promise<{ encrypted: Uint8Array, clientPublicKey: Uint8Array }> {
40
- if (!this.keyPair) {
41
- await this.generateSessionKey();
42
- }
43
-
44
- // Import TEE Public Key
45
- // Fix: Explicitly cast to BufferSource/any because TS gets confused with SharedArrayBuffer in some envs
46
- const teeKey = await this.crypto.subtle.importKey(
47
- "raw",
48
- teePublicKeyBytes as unknown as BufferSource,
49
- { name: "ECDH", namedCurve: "P-256" },
50
- false,
51
- []
52
- );
53
-
54
- // Derive Shared Secret (AES-GCM Key)
55
- const sharedKey = await this.crypto.subtle.deriveKey(
56
- {
57
- name: "ECDH",
58
- public: teeKey,
59
- },
60
- this.keyPair!.privateKey,
61
- {
62
- name: "AES-GCM",
63
- length: 256,
64
- },
65
- false,
66
- ["encrypt", "decrypt"]
67
- );
68
-
69
- // Encrypt Data
70
- const iv = this.crypto.getRandomValues(new Uint8Array(12));
71
- const encryptedBuffer = await this.crypto.subtle.encrypt(
72
- {
73
- name: "AES-GCM",
74
- iv: iv,
75
- },
76
- sharedKey,
77
- data as unknown as BufferSource
78
- );
79
-
80
- // Concatenate IV + CipherText
81
- const encrypted = new Uint8Array(iv.length + encryptedBuffer.byteLength);
82
- encrypted.set(iv);
83
- encrypted.set(new Uint8Array(encryptedBuffer), iv.length);
84
-
85
- // Export Client Public Key
86
- const clientPubRaw = await this.crypto.subtle.exportKey("raw", this.keyPair!.publicKey);
87
-
88
- return {
89
- encrypted,
90
- clientPublicKey: new Uint8Array(clientPubRaw)
91
- };
92
- }
93
-
94
- /**
95
- * Decrypt a response from the TEE.
96
- */
97
- async decryptResponse(
98
- encryptedData: Uint8Array,
99
- teePublicKeyBytes: Uint8Array
100
- ): Promise<Uint8Array> {
101
- if (!this.keyPair) throw new Error("No session key");
102
-
103
- // Import TEE Public Key
104
- const teeKey = await this.crypto.subtle.importKey(
105
- "raw",
106
- teePublicKeyBytes as unknown as BufferSource,
107
- { name: "ECDH", namedCurve: "P-256" },
108
- false,
109
- []
110
- );
111
-
112
- // Derive Shared Secret
113
- const sharedKey = await this.crypto.subtle.deriveKey(
114
- {
115
- name: "ECDH",
116
- public: teeKey,
117
- },
118
- this.keyPair!.privateKey,
119
- {
120
- name: "AES-GCM",
121
- length: 256,
122
- },
123
- false,
124
- ["encrypt", "decrypt"]
125
- );
126
-
127
- const iv = encryptedData.slice(0, 12);
128
- const ciphertext = encryptedData.slice(12);
129
-
130
- const decrypted = await this.crypto.subtle.decrypt(
131
- {
132
- name: "AES-GCM",
133
- iv: iv,
134
- },
135
- sharedKey,
136
- ciphertext as unknown as BufferSource
137
- );
138
-
139
- return new Uint8Array(decrypted);
140
- }
141
-
142
- /**
143
- * Helper to generate a valid random P-256 Public Key (65 bytes) for testing.
144
- */
145
- async createMockValidatorKey(): Promise<Uint8Array> {
146
- const pair = await this.crypto.subtle.generateKey(
147
- { name: "ECDH", namedCurve: "P-256" },
148
- true,
149
- ["deriveKey"]
150
- ) as CryptoKeyPair;
151
- const raw = await this.crypto.subtle.exportKey("raw", pair.publicKey);
152
- return new Uint8Array(raw);
153
- }
154
- }