@revibase/transaction-manager 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @revibase/transaction-manager
2
2
 
3
- Transaction verification and policy-based signing for the Revibase multi-wallet system.
3
+ Server-side transaction verification and policy-based signing for the Revibase multi-wallet system.
4
4
 
5
- This package verifies incoming Solana transaction intents, extracts the on-chain instructions and verified signers, and lets you apply custom allow/deny policies before producing Ed25519 signatures.
5
+ This package verifies incoming Solana transaction intents, extracts the on-chain instructions and verified signers, and lets you apply your own allow/deny policies before producing Ed25519 signatures.
6
6
 
7
7
  ---
8
8
 
@@ -18,119 +18,207 @@ npm install @revibase/transaction-manager
18
18
 
19
19
  A **transaction manager** is a server-side signer that:
20
20
 
21
- 1. Verifies a transaction request using your custom rules
22
- 2. Applies your own business / security policy
21
+ 1. Verifies a transaction request using your rules
22
+ 2. Applies your business and security policy
23
23
  3. Signs the Solana **transaction message bytes** (Ed25519)
24
24
  4. Returns base58-encoded signatures to the caller
25
25
 
26
+ Use this package when you want a single, auditable place in your backend where every multi-wallet transaction is verified and approved before it is signed.
27
+
26
28
  ---
27
29
 
28
30
  ## Usage
29
31
 
30
- ### Basic signing endpoint
32
+ ### 1. Generate a transaction manager keypair
33
+
34
+ Generate one keypair for the manager and keep the private key server-side only.
35
+
36
+ ```ts
37
+ import { getBase58Decoder } from "gill";
38
+
39
+ const keyPair = await crypto.subtle.generateKey({ name: "Ed25519" }, true, [
40
+ "sign",
41
+ "verify",
42
+ ]);
43
+
44
+ const [pubRaw, privJwk] = await Promise.all([
45
+ crypto.subtle.exportKey("raw", keyPair.publicKey),
46
+ crypto.subtle.exportKey("jwk", keyPair.privateKey),
47
+ ]);
48
+
49
+ console.log({
50
+ publicKey: getBase58Decoder().decode(new Uint8Array(pubRaw)),
51
+ privateKey: JSON.stringify(privJwk),
52
+ });
53
+ ```
54
+
55
+ Store:
56
+
57
+ - **`publicKey`**: as the transaction manager public key (base58).
58
+ - **`privateKey`**: as a JWK JSON string in a secure secret store.
59
+
60
+ ### 2. Configure environment
61
+
62
+ Set the following environment variables for your signing service:
63
+
64
+ - **`TX_MANAGER_PRIVATE_KEY`**: Manager private key (JWK JSON string).
65
+ - **`TX_MANAGER_PUBLIC_KEY`**: Manager public key (base58).
66
+ - **`TX_MANAGER_URL`**: Public HTTPS URL of your signing endpoint.
67
+ - **`RPC_URL`** (optional): Solana RPC URL. Defaults to `https://api.mainnet-beta.solana.com`.
68
+
69
+ ### 3. Implement a basic signing endpoint
70
+
71
+ Expose a public HTTPS endpoint, for example:
72
+
73
+ `https://your-transaction-manager.com/sign`
74
+
75
+ This endpoint:
76
+
77
+ - Verifies that the request is intended for this transaction manager.
78
+ - Calls `verifyTransaction` to decode and verify the transaction.
79
+ - Applies your custom policy.
80
+ - Signs the verified transaction message bytes.
81
+ - Returns base58-encoded signatures.
31
82
 
32
83
  ```ts
33
84
  import { verifyTransaction } from "@revibase/transaction-manager";
34
- import { createSolanaRpc, getBase58Encoder } from "gill";
85
+ import { createSolanaRpc, getBase58Decoder } from "gill";
86
+ import { enforcePolicies } from "@/lib/policy";
35
87
 
36
- const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");
88
+ const rpc = createSolanaRpc(
89
+ process.env.RPC_URL ?? "https://api.mainnet-beta.solana.com",
90
+ );
37
91
 
38
92
  const transactionManagerConfig = {
39
- /**
40
- * Base58-encoded Ed25519 public key of this transaction manager
41
- */
42
- publicKey: "YOUR_TRANSACTION_MANAGER_PUBLIC_KEY",
43
-
44
- /**
45
- * Public URL of this signing endpoint
46
- */
47
- url: "https://your-transaction-manager.com/sign",
93
+ publicKey: process.env.TX_MANAGER_PUBLIC_KEY!, // base58
94
+ url: process.env.TX_MANAGER_URL!, // public HTTPS URL of this endpoint
48
95
  };
49
96
 
50
- export async function sign(request: Request): Promise<Response> {
51
- const { publicKey, payload } = await request.json();
52
-
53
- if (publicKey !== transactionManagerConfig.publicKey) {
54
- return new Response(
55
- JSON.stringify({ error: "Invalid transaction manager public key" }),
56
- {
57
- status: 400,
58
- headers: { "Content-Type": "application/json" },
59
- },
97
+ export async function POST(req: Request) {
98
+ try {
99
+ const { publicKey, payload } = (await req.json()) as {
100
+ publicKey: string;
101
+ payload: {
102
+ transaction: string;
103
+ transactionMessageBytes?: string;
104
+ authResponses?: unknown[];
105
+ }[];
106
+ };
107
+
108
+ if (publicKey !== transactionManagerConfig.publicKey) {
109
+ return Response.json(
110
+ { error: "Invalid transaction manager public key" },
111
+ { status: 400 },
112
+ );
113
+ }
114
+
115
+ const jwk = JSON.parse(process.env.TX_MANAGER_PRIVATE_KEY!);
116
+ const privateKey = await crypto.subtle.importKey(
117
+ "jwk",
118
+ jwk,
119
+ { name: "Ed25519" },
120
+ false,
121
+ ["sign"],
60
122
  );
61
- }
62
123
 
63
- // Load the Ed25519 private key corresponding to `transactionManagerConfig.publicKey`
64
- const privateKey = await loadTransactionManagerPrivateKey(publicKey);
124
+ const signatures: string[] = [];
65
125
 
66
- const signatures: string[] = [];
126
+ for (const payloadItem of payload) {
127
+ const result = await verifyTransaction(
128
+ rpc,
129
+ transactionManagerConfig,
130
+ payloadItem,
131
+ );
67
132
 
68
- for (const payloadItem of payload) {
69
- const { messageBytes, verificationResults } = await verifyTransaction(
70
- rpc,
71
- transactionManagerConfig,
72
- payloadItem,
73
- );
133
+ await enforcePolicies(result);
74
134
 
75
- /**
76
- * ------------------------------------------------------------------
77
- * Custom policy enforcement
78
- * ------------------------------------------------------------------
79
- *
80
- * `verificationResults` contains fully verified metadata such as:
81
- *
82
- * - `instructions`: decoded Solana instructions that will be sent on-chain
83
- * - `verifiedSigners`: wallets, members, and credentials involved
84
- *
85
- * Use this information to:
86
- * - allow only transfers
87
- * - reject config changes
88
- * - restrict destination addresses
89
- * - enforce amount limits
90
- */
91
-
92
- // Sign the Solana *message* bytes (not the full transaction)
93
- const signatureBytes = await crypto.subtle.sign(
94
- { name: "Ed25519" },
95
- privateKey,
96
- messageBytes,
97
- );
135
+ const signatureBytes = await crypto.subtle.sign(
136
+ { name: "Ed25519" },
137
+ privateKey,
138
+ result.transactionMessage,
139
+ );
98
140
 
99
- // Return base58-encoded signatures
100
- signatures.push(getBase58Encoder().encode(signatureBytes));
101
- }
141
+ signatures.push(
142
+ getBase58Decoder().decode(new Uint8Array(signatureBytes)),
143
+ );
144
+ }
102
145
 
103
- return new Response(JSON.stringify({ signatures }), {
104
- headers: { "Content-Type": "application/json" },
105
- });
146
+ return Response.json({ signatures });
147
+ } catch (e) {
148
+ const msg = e instanceof Error ? e.message : String(e);
149
+ return Response.json({ error: msg }, { status: 500 });
150
+ }
106
151
  }
107
152
  ```
108
153
 
109
154
  ---
110
155
 
111
- ## Key Management
156
+ ## Policy checks
157
+
158
+ Your transaction manager should express your security model in one place.
159
+
160
+ `verifyTransaction` returns a `VerificationResults` object with:
161
+
162
+ - **`transactionMessage`**: Raw transaction message bytes to sign.
163
+ - **`verificationResults`**: An array of batches, where each batch contains:
164
+ - **`instructions`**: Decoded on-chain instructions.
165
+ - **`signers`**: The signers that successfully passed verification for those instructions.
166
+
167
+ The example below:
168
+
169
+ - Allows only native SOL transfers.
170
+ - Requires the request to originate from `https://app.revibase.com`.
171
+ - Rejects any non-system-program instruction.
172
+ - Caps each transfer at **1 SOL**.
112
173
 
113
174
  ```ts
114
- async function loadTransactionManagerPrivateKey(
115
- publicKey: string,
116
- ): Promise<CryptoKey> {
117
- /**
118
- * Fetch the private key corresponding to `publicKey`.
119
- *
120
- * - Must be an Ed25519 private key
121
- * - SHOULD be stored in a secure system (KMS / HSM / Secrets Manager)
122
- * - MUST NOT be hard-coded in source code
123
- */
124
-
125
- const jwk = await fetchPrivateKeyJwkFromSecureStore(publicKey);
126
-
127
- return crypto.subtle.importKey(
128
- "jwk",
129
- jwk,
130
- { name: "Ed25519" },
131
- false, // non-extractable
132
- ["sign"],
133
- );
175
+ import type { VerificationResults } from "@revibase/transaction-manager";
176
+ import {
177
+ SYSTEM_PROGRAM_ADDRESS,
178
+ identifySystemInstruction,
179
+ parseTransferSolInstruction,
180
+ parseTransferSolWithSeedInstruction,
181
+ SystemInstruction,
182
+ } from "gill";
183
+
184
+ const ALLOWED_ORIGINS = new Set(["https://app.revibase.com"]);
185
+ const MAX_TRANSFER_LAMPORTS = 1_000_000_000n; // 1 SOL
186
+
187
+ export async function enforcePolicies(results: VerificationResults) {
188
+ for (const batch of results.verificationResults) {
189
+ const { signers, instructions } = batch;
190
+
191
+ for (const signer of signers) {
192
+ const origin = "client" in signer ? signer.client?.origin : undefined;
193
+ if (origin && !ALLOWED_ORIGINS.has(origin)) {
194
+ throw new Error("Unauthorized app origin");
195
+ }
196
+ }
197
+
198
+ for (const ix of instructions) {
199
+ if (ix.programAddress.toString() !== SYSTEM_PROGRAM_ADDRESS.toString()) {
200
+ throw new Error("Unauthorized program");
201
+ }
202
+
203
+ const ixKind = identifySystemInstruction(ix.data);
204
+
205
+ if (
206
+ ixKind !== SystemInstruction.TransferSol &&
207
+ ixKind !== SystemInstruction.TransferSolWithSeed
208
+ ) {
209
+ throw new Error("Unauthorized instruction");
210
+ }
211
+
212
+ const parsed =
213
+ ixKind === SystemInstruction.TransferSol
214
+ ? parseTransferSolInstruction(ix)
215
+ : parseTransferSolWithSeedInstruction(ix);
216
+
217
+ if (parsed.data.amount > MAX_TRANSFER_LAMPORTS) {
218
+ throw new Error("Transfer limit exceeded");
219
+ }
220
+ }
221
+ }
134
222
  }
135
223
  ```
136
224
 
@@ -141,17 +229,25 @@ async function loadTransactionManagerPrivateKey(
141
229
 
142
230
  ---
143
231
 
144
- ## What `verifyTransaction` Does
232
+ ## API surface
233
+
234
+ This package exports the following public API:
235
+
236
+ - **`verifyTransaction(rpc, transactionManagerConfig, payload, wellKnownProxyUrl?)`**
237
+ - Decodes and verifies a serialized Solana transaction.
238
+ - Returns a `VerificationResults` object with the transaction message bytes and verification batches.
145
239
 
146
- `verifyTransaction` performs:
240
+ - **`TransactionManagerConfig`**
241
+ - `publicKey`: Transaction manager public key (base58 string).
242
+ - `url`: Public URL of your transaction manager endpoint.
147
243
 
148
- - Signature verification of all required members
149
- - Instruction decoding and validation
150
- - Wallet, member, and permission checks
244
+ - **`VerificationResults`**
245
+ - `transactionMessage`: Transaction message bytes to sign.
246
+ - `verificationResults`: Array of `{ instructions, signers }` batches used by your policy.
151
247
 
152
248
  ---
153
249
 
154
- ## What This Package Does _Not_ Do
250
+ ## What this package does _not_ do
155
251
 
156
252
  - ❌ Store private keys for you
157
253
  - ❌ Enforce your business rules automatically