@revibase/transaction-manager 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +188 -92
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @revibase/transaction-manager
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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
|
|
22
|
-
2. Applies your
|
|
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
|
-
###
|
|
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,
|
|
85
|
+
import { createSolanaRpc, getBase58Decoder } from "gill";
|
|
86
|
+
import { enforcePolicies } from "@/lib/policy";
|
|
35
87
|
|
|
36
|
-
const rpc = createSolanaRpc(
|
|
88
|
+
const rpc = createSolanaRpc(
|
|
89
|
+
process.env.RPC_URL ?? "https://api.mainnet-beta.solana.com",
|
|
90
|
+
);
|
|
37
91
|
|
|
38
92
|
const transactionManagerConfig = {
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
64
|
-
const privateKey = await loadTransactionManagerPrivateKey(publicKey);
|
|
124
|
+
const signatures: string[] = [];
|
|
65
125
|
|
|
66
|
-
|
|
126
|
+
for (const payloadItem of payload) {
|
|
127
|
+
const result = await verifyTransaction(
|
|
128
|
+
rpc,
|
|
129
|
+
transactionManagerConfig,
|
|
130
|
+
payloadItem,
|
|
131
|
+
);
|
|
67
132
|
|
|
68
|
-
|
|
69
|
-
const { messageBytes, verificationResults } = await verifyTransaction(
|
|
70
|
-
rpc,
|
|
71
|
-
transactionManagerConfig,
|
|
72
|
-
payloadItem,
|
|
73
|
-
);
|
|
133
|
+
await enforcePolicies(result);
|
|
74
134
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
141
|
+
signatures.push(
|
|
142
|
+
getBase58Decoder().decode(new Uint8Array(signatureBytes)),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
102
145
|
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
240
|
+
- **`TransactionManagerConfig`**
|
|
241
|
+
- `publicKey`: Transaction manager public key (base58 string).
|
|
242
|
+
- `url`: Public URL of your transaction manager endpoint.
|
|
147
243
|
|
|
148
|
-
-
|
|
149
|
-
-
|
|
150
|
-
-
|
|
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
|
|
250
|
+
## What this package does _not_ do
|
|
155
251
|
|
|
156
252
|
- ❌ Store private keys for you
|
|
157
253
|
- ❌ Enforce your business rules automatically
|