@openzeppelin/relayer-plugin-channels 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/LICENSE +21 -0
- package/README.md +504 -0
- package/dist/build.d.ts +21 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +100 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +89 -0
- package/dist/constants.d.ts +46 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +56 -0
- package/dist/fee.d.ts +11 -0
- package/dist/fee.d.ts.map +1 -0
- package/dist/fee.js +41 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +115 -0
- package/dist/management.d.ts +11 -0
- package/dist/management.d.ts.map +1 -0
- package/dist/management.js +166 -0
- package/dist/pool.d.ts +31 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +129 -0
- package/dist/simulation.d.ts +14 -0
- package/dist/simulation.d.ts.map +1 -0
- package/dist/simulation.js +52 -0
- package/dist/submit.d.ts +20 -0
- package/dist/submit.d.ts.map +1 -0
- package/dist/submit.js +102 -0
- package/dist/test/config.test.d.ts +2 -0
- package/dist/test/config.test.d.ts.map +1 -0
- package/dist/test/config.test.js +43 -0
- package/dist/test/fee.test.d.ts +2 -0
- package/dist/test/fee.test.d.ts.map +1 -0
- package/dist/test/fee.test.js +34 -0
- package/dist/test/helpers/fakeKV.d.ts +17 -0
- package/dist/test/helpers/fakeKV.d.ts.map +1 -0
- package/dist/test/helpers/fakeKV.js +60 -0
- package/dist/test/management.test.d.ts +2 -0
- package/dist/test/management.test.d.ts.map +1 -0
- package/dist/test/management.test.js +66 -0
- package/dist/test/pool.busy.test.d.ts +2 -0
- package/dist/test/pool.busy.test.d.ts.map +1 -0
- package/dist/test/pool.busy.test.js +23 -0
- package/dist/test/pool.test.d.ts +2 -0
- package/dist/test/pool.test.d.ts.map +1 -0
- package/dist/test/pool.test.js +29 -0
- package/dist/test/tx.test.d.ts +2 -0
- package/dist/test/tx.test.d.ts.map +1 -0
- package/dist/test/tx.test.js +29 -0
- package/dist/test/validation.test.d.ts +2 -0
- package/dist/test/validation.test.d.ts.map +1 -0
- package/dist/test/validation.test.js +31 -0
- package/dist/tx.d.ts +8 -0
- package/dist/tx.d.ts.map +1 -0
- package/dist/tx.js +45 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/validation.d.ts +9 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +84 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 OpenZeppelin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
# Channels Plugin
|
|
2
|
+
|
|
3
|
+
A plugin for OpenZeppelin Relayer that enables parallel transaction submission on Stellar using channel accounts with fee bumping. Channel accounts provide unique sequence numbers for parallel transaction submission, preventing sequence number conflicts.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
**Want to get started quickly?** Check out the [Channels Plugin Example](https://github.com/OpenZeppelin/openzeppelin-relayer/tree/main/examples/channels-plugin-example) which includes a pre-configured relayer setup, Docker Compose configuration, and step-by-step instructions. This is the fastest way to get the Channels plugin up and running.
|
|
8
|
+
|
|
9
|
+
For manual installation and configuration details, continue reading below.
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
- Node.js >= 18
|
|
14
|
+
- pnpm >= 10
|
|
15
|
+
- OpenZeppelin Relayer
|
|
16
|
+
|
|
17
|
+
## Installation & Setup
|
|
18
|
+
|
|
19
|
+
The Channels plugin can be added to any OpenZeppelin Relayer in two ways:
|
|
20
|
+
|
|
21
|
+
### 1. Install from npm (recommended)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# From the root of your Relayer repository
|
|
25
|
+
cd plugins
|
|
26
|
+
mkdir channels
|
|
27
|
+
cd channels
|
|
28
|
+
pnpm add @openzeppelin/relayer-plugin-channels
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 2. Use a local build (for development / debugging)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Clone and build the plugin
|
|
35
|
+
git clone https://github.com/openzeppelin/relayer-plugin-channels.git
|
|
36
|
+
cd relayer-plugin-channels
|
|
37
|
+
pnpm install
|
|
38
|
+
pnpm build
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Now reference the local build from your Relayer's `plugins/package.json`:
|
|
42
|
+
|
|
43
|
+
```jsonc
|
|
44
|
+
{
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@openzeppelin/relayer-plugin-channels": "file:../../relayer-plugin-channels",
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Install dependencies:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pnpm install
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### Create the plugin wrapper
|
|
60
|
+
|
|
61
|
+
Inside the Relayer create a directory for the plugin and expose its handler:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
mkdir -p plugins/channels
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`plugins/channels/index.ts`
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
export { handler } from "@openzeppelin/relayer-plugin-channels";
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Configure the Relayer
|
|
74
|
+
|
|
75
|
+
Before setting environment variables, you need to configure your Relayer's `config.json` with the fund account and channel accounts. Create or update your `config/config.json`:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"relayers": [
|
|
80
|
+
{
|
|
81
|
+
"id": "channels-fund",
|
|
82
|
+
"name": "Channels Fund Account",
|
|
83
|
+
"network": "testnet",
|
|
84
|
+
"paused": false,
|
|
85
|
+
"network_type": "stellar",
|
|
86
|
+
"signer_id": "channels-fund-signer",
|
|
87
|
+
"policies": {
|
|
88
|
+
"concurrent_transactions": true
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"id": "channel-001",
|
|
93
|
+
"name": "Channel Account 001",
|
|
94
|
+
"network": "testnet",
|
|
95
|
+
"paused": false,
|
|
96
|
+
"network_type": "stellar",
|
|
97
|
+
"signer_id": "channel-001-signer"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"id": "channel-002",
|
|
101
|
+
"name": "Channel Account 002",
|
|
102
|
+
"network": "testnet",
|
|
103
|
+
"paused": false,
|
|
104
|
+
"network_type": "stellar",
|
|
105
|
+
"signer_id": "channel-002-signer"
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
"notifications": [],
|
|
109
|
+
"signers": [
|
|
110
|
+
{
|
|
111
|
+
"id": "channels-fund-signer",
|
|
112
|
+
"type": "local",
|
|
113
|
+
"config": {
|
|
114
|
+
"path": "config/keys/channels-fund.json",
|
|
115
|
+
"passphrase": {
|
|
116
|
+
"type": "env",
|
|
117
|
+
"value": "KEYSTORE_PASSPHRASE_FUND"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"id": "channel-001-signer",
|
|
123
|
+
"type": "local",
|
|
124
|
+
"config": {
|
|
125
|
+
"path": "config/keys/channel-001.json",
|
|
126
|
+
"passphrase": {
|
|
127
|
+
"type": "env",
|
|
128
|
+
"value": "KEYSTORE_PASSPHRASE_CHANNEL_001"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"id": "channel-002-signer",
|
|
134
|
+
"type": "local",
|
|
135
|
+
"config": {
|
|
136
|
+
"path": "config/keys/channel-002.json",
|
|
137
|
+
"passphrase": {
|
|
138
|
+
"type": "env",
|
|
139
|
+
"value": "KEYSTORE_PASSPHRASE_CHANNEL_002"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
"networks": "./config/networks",
|
|
145
|
+
"plugins": [
|
|
146
|
+
{
|
|
147
|
+
"id": "channels",
|
|
148
|
+
"path": "channel/index.ts",
|
|
149
|
+
"timeout": 30,
|
|
150
|
+
"emit_logs": true,
|
|
151
|
+
"emit_traces": true
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Important Configuration Notes:**
|
|
158
|
+
|
|
159
|
+
- **Fund Account** (`channels-fund`): Must have `"concurrent_transactions": true` in policies to enable parallel transaction processing
|
|
160
|
+
- **Channel Accounts**: Create at least 2 for better throughput (you can add more as `channel-003`, etc.)
|
|
161
|
+
- **Network**: Use `testnet` for testing or `mainnet` for production
|
|
162
|
+
- **Signers**: Each relayer references a signer by `signer_id`, and signers are defined separately with keystore paths
|
|
163
|
+
- **Keystore Files**: You'll need to create keystore files for each account - see [OpenZeppelin Relayer documentation](https://docs.openzeppelin.com/relayer) for details on creating and managing keys
|
|
164
|
+
- **Plugin Registration**: The `path` points to your plugin wrapper file relative to the plugins directory
|
|
165
|
+
|
|
166
|
+
For more details on Relayer configuration, see the [OpenZeppelin Relayer documentation](https://docs.openzeppelin.com/relayer).
|
|
167
|
+
|
|
168
|
+
### Configure Environment Variables
|
|
169
|
+
|
|
170
|
+
Set the required environment variables for the plugin:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# Required environment variables
|
|
174
|
+
export STELLAR_NETWORK="testnet" # or "mainnet"
|
|
175
|
+
export SOROBAN_RPC_URL="https://soroban-testnet.stellar.org"
|
|
176
|
+
export FUND_RELAYER_ID="channels-fund"
|
|
177
|
+
export PLUGIN_ADMIN_SECRET="your-secret-here" # Required for management API
|
|
178
|
+
|
|
179
|
+
# Optional environment variables
|
|
180
|
+
export LOCK_TTL_SECONDS=10 # default: 30, min: 3, max: 30
|
|
181
|
+
export MAX_FEE=1000000 # default: 1,000,000 stroops
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Your Relayer should now contain:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
relayer/
|
|
188
|
+
└─ plugins/
|
|
189
|
+
└─ channels/
|
|
190
|
+
├─ package.json # lists the dependency
|
|
191
|
+
└─ index.ts
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Initialize Channel Accounts
|
|
195
|
+
|
|
196
|
+
Before using the Channels plugin, you must configure channel accounts using the management API:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
curl -X POST http://localhost:8080/api/v1/plugins/channels/call \
|
|
200
|
+
-H "Authorization: Bearer YOUR_API_KEY" \
|
|
201
|
+
-H "Content-Type: application/json" \
|
|
202
|
+
-d '{
|
|
203
|
+
"params": {
|
|
204
|
+
"management": {
|
|
205
|
+
"action": "setChannelAccounts",
|
|
206
|
+
"adminSecret": "your-secret-here",
|
|
207
|
+
"relayerIds": ["channel-0001", "channel-0002", "channel-0003"]
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}'
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The Channels plugin is now ready to serve Soroban transactions 🚀
|
|
214
|
+
|
|
215
|
+
## Development
|
|
216
|
+
|
|
217
|
+
### Building from Source
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
# Install dependencies
|
|
221
|
+
pnpm install
|
|
222
|
+
|
|
223
|
+
# Build the plugin
|
|
224
|
+
pnpm build
|
|
225
|
+
|
|
226
|
+
# Run tests
|
|
227
|
+
pnpm test
|
|
228
|
+
|
|
229
|
+
# Lint and format
|
|
230
|
+
pnpm lint
|
|
231
|
+
pnpm format
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Overview
|
|
235
|
+
|
|
236
|
+
The Channels plugin accepts Soroban operations and handles all the complexity of getting them on-chain:
|
|
237
|
+
|
|
238
|
+
- Automatic fee bumping using a dedicated fund account
|
|
239
|
+
- Parallel transaction execution with a pool of channel accounts
|
|
240
|
+
- Transaction simulation and resource management
|
|
241
|
+
- Error handling and confirmation waiting
|
|
242
|
+
|
|
243
|
+
## Architecture
|
|
244
|
+
|
|
245
|
+
- **Fund Account**: Holds funds and pays for fee bumps
|
|
246
|
+
- **Channel Accounts**: Provide unique sequence numbers for parallel transaction submission
|
|
247
|
+
- The channel account is the transaction source and signer; the fund account wraps it in a fee bump
|
|
248
|
+
|
|
249
|
+
## Management API
|
|
250
|
+
|
|
251
|
+
The Channels plugin provides a management API to dynamically configure channel accounts. This API requires authentication via the `PLUGIN_ADMIN_SECRET` environment variable.
|
|
252
|
+
|
|
253
|
+
### List Channel Accounts
|
|
254
|
+
|
|
255
|
+
Get the current list of configured channel accounts:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
curl -X POST http://localhost:8080/api/v1/plugins/channels/call \
|
|
259
|
+
-H "Authorization: Bearer YOUR_API_KEY" \
|
|
260
|
+
-H "Content-Type: application/json" \
|
|
261
|
+
-d '{
|
|
262
|
+
"params": {
|
|
263
|
+
"management": {
|
|
264
|
+
"action": "listChannelAccounts",
|
|
265
|
+
"adminSecret": "your-secret-here"
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}'
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Response:**
|
|
272
|
+
|
|
273
|
+
```json
|
|
274
|
+
{
|
|
275
|
+
"relayerIds": ["channel-0001", "channel-0002", "channel-0003"]
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Set Channel Accounts
|
|
280
|
+
|
|
281
|
+
Configure the channel accounts that the plugin will use. This replaces the entire list:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
curl -X POST http://localhost:8080/api/v1/plugins/channels/call \
|
|
285
|
+
-H "Authorization: Bearer YOUR_API_KEY" \
|
|
286
|
+
-H "Content-Type: application/json" \
|
|
287
|
+
-d '{
|
|
288
|
+
"params": {
|
|
289
|
+
"management": {
|
|
290
|
+
"action": "setChannelAccounts",
|
|
291
|
+
"adminSecret": "your-secret-here",
|
|
292
|
+
"relayerIds": ["channel-0001", "channel-0002", "channel-0003"]
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}'
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Response:**
|
|
299
|
+
|
|
300
|
+
```json
|
|
301
|
+
{
|
|
302
|
+
"ok": true,
|
|
303
|
+
"appliedRelayerIds": ["channel-0001", "channel-0002", "channel-0003"]
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**Important Notes:**
|
|
308
|
+
|
|
309
|
+
- You must configure at least one channel account before the plugin can process transactions
|
|
310
|
+
- The management API will prevent removing accounts that are currently locked (in use). On failure it throws a plugin error with status 409, code `LOCKED_CONFLICT`, and `details.locked` listing blocked IDs.
|
|
311
|
+
- All relayer IDs must exist in your OpenZeppelin Relayer configuration
|
|
312
|
+
- The `adminSecret` must match the `PLUGIN_ADMIN_SECRET` environment variable
|
|
313
|
+
|
|
314
|
+
## API Usage
|
|
315
|
+
|
|
316
|
+
### Submit with Transaction XDR
|
|
317
|
+
|
|
318
|
+
Submit a complete, signed transaction:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
curl -X POST http://localhost:8080/api/v1/plugins/channels/call \
|
|
322
|
+
-H "Authorization: Bearer YOUR_API_KEY" \
|
|
323
|
+
-H "Content-Type: application/json" \
|
|
324
|
+
-d '{
|
|
325
|
+
"params": {
|
|
326
|
+
"xdr": "AAAAAgAAAAB..."
|
|
327
|
+
}
|
|
328
|
+
}'
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Submit with Function and Auth
|
|
332
|
+
|
|
333
|
+
Submit just the Soroban function and auth entries:
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
curl -X POST http://localhost:8080/api/v1/plugins/channels/call \
|
|
337
|
+
-H "Authorization: Bearer YOUR_API_KEY" \
|
|
338
|
+
-H "Content-Type: application/json" \
|
|
339
|
+
-d '{
|
|
340
|
+
"params": {
|
|
341
|
+
"func": "AAAABAAAAAEAAAAGc3ltYm9s...",
|
|
342
|
+
"auth": ["AAAACAAAAAEAAAA..."]
|
|
343
|
+
}
|
|
344
|
+
}'
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Parameters
|
|
348
|
+
|
|
349
|
+
- `xdr` (string): Complete transaction envelope XDR (signed, not fee-bump)
|
|
350
|
+
- `func` (string): Soroban host function XDR (base64)
|
|
351
|
+
- `auth` (array): Array of Soroban authorization entry XDRs (base64)
|
|
352
|
+
|
|
353
|
+
**Note**: Provide either `xdr` OR `func`+`auth`, not both.
|
|
354
|
+
|
|
355
|
+
### Response
|
|
356
|
+
|
|
357
|
+
Responses follow the Relayer envelope `{ success, data, error }`.
|
|
358
|
+
|
|
359
|
+
Success example:
|
|
360
|
+
|
|
361
|
+
```json
|
|
362
|
+
{
|
|
363
|
+
"success": true,
|
|
364
|
+
"data": {
|
|
365
|
+
"transactionId": "tx_123456",
|
|
366
|
+
"status": "confirmed",
|
|
367
|
+
"hash": "1234567890abcdef..."
|
|
368
|
+
},
|
|
369
|
+
"error": null
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Plugin error example:
|
|
374
|
+
|
|
375
|
+
```json
|
|
376
|
+
{
|
|
377
|
+
"success": false,
|
|
378
|
+
"data": {
|
|
379
|
+
"code": "POOL_CAPACITY",
|
|
380
|
+
"details": {}
|
|
381
|
+
},
|
|
382
|
+
"error": "Too many transactions queued. Please try again later"
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## How It Works
|
|
387
|
+
|
|
388
|
+
1. **Request Validation**: Validates input parameters (xdr OR func+auth)
|
|
389
|
+
2. **Channel Account Pool**: Acquires an available channel account from the pool
|
|
390
|
+
3. **Transaction Building**: For func+auth, builds transaction with channel as source
|
|
391
|
+
4. **Simulation**: Simulates transaction to obtain sorobanData and resource fee
|
|
392
|
+
5. **Signing**: Channel account signs the transaction
|
|
393
|
+
6. **Fee Calculation**: Calculates dynamic max_fee based on resource fee
|
|
394
|
+
7. **Fee Bumping**: Fund account wraps transaction with fee bump
|
|
395
|
+
8. **Submission**: Sends to Stellar network and waits for confirmation
|
|
396
|
+
9. **Pool Release**: Returns channel account to the pool
|
|
397
|
+
|
|
398
|
+
## Validation Rules
|
|
399
|
+
|
|
400
|
+
### Input Validation
|
|
401
|
+
|
|
402
|
+
- Must provide `xdr` OR `func`+`auth` (not both)
|
|
403
|
+
- XDR must not be a fee-bump envelope
|
|
404
|
+
- All parameters must be valid base64 XDR
|
|
405
|
+
|
|
406
|
+
### Transaction Validation (XDR mode)
|
|
407
|
+
|
|
408
|
+
- Envelope type must be `envelopeTypeTx` (not fee bump)
|
|
409
|
+
- TimeBounds maxTime must be within 30 seconds from now
|
|
410
|
+
|
|
411
|
+
## KV Schema
|
|
412
|
+
|
|
413
|
+
### Membership List
|
|
414
|
+
|
|
415
|
+
- **Key**: `<network>:channel:relayer-ids`
|
|
416
|
+
- **Value**: `{ relayerIds: string[] }`
|
|
417
|
+
|
|
418
|
+
### Channel Locks
|
|
419
|
+
|
|
420
|
+
- **Key**: `<network>:channel:in-use:<relayerId>`
|
|
421
|
+
- **Value**: `{ token: string, lockedAt: ISOString }`
|
|
422
|
+
- **TTL**: Configured by `LOCK_TTL_SECONDS`.
|
|
423
|
+
|
|
424
|
+
## Error Codes
|
|
425
|
+
|
|
426
|
+
- `CONFIG_MISSING`: Missing required environment variable
|
|
427
|
+
- `UNSUPPORTED_NETWORK`: Invalid network type
|
|
428
|
+
- `INVALID_PARAMS`: Invalid request parameters
|
|
429
|
+
- `INVALID_XDR`: Failed to parse XDR
|
|
430
|
+
- `INVALID_ENVELOPE_TYPE`: Not a regular transaction envelope
|
|
431
|
+
- `INVALID_TIME_BOUNDS`: TimeBounds too far in the future
|
|
432
|
+
- `NO_CHANNELS_CONFIGURED`: No channel accounts have been configured via management API
|
|
433
|
+
- `POOL_CAPACITY`: All channel accounts in use
|
|
434
|
+
- `RELAYER_UNAVAILABLE`: Relayer not found
|
|
435
|
+
- `SIMULATION_FAILED`: Transaction simulation failed
|
|
436
|
+
- `ONCHAIN_FAILED`: Transaction failed on-chain
|
|
437
|
+
- `WAIT_TIMEOUT`: Transaction wait timeout
|
|
438
|
+
- `MANAGEMENT_DISABLED`: Management API not enabled
|
|
439
|
+
- `UNAUTHORIZED`: Invalid admin secret
|
|
440
|
+
- `LOCKED_CONFLICT`: Cannot remove locked channel accounts
|
|
441
|
+
|
|
442
|
+
## License
|
|
443
|
+
|
|
444
|
+
MIT
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## Smoke Test Contract
|
|
449
|
+
|
|
450
|
+
This repo includes a minimal Soroban contract and smoke test script that exercise the Channels plugin with different authorization methods.
|
|
451
|
+
|
|
452
|
+
### Contract
|
|
453
|
+
|
|
454
|
+
- **Path**: `contracts/smoke-contract`
|
|
455
|
+
- **Functions**:
|
|
456
|
+
- `no_auth_bump(n: u32) -> u32` — No auth required; returns n+1
|
|
457
|
+
- `write_with_address_auth(addr: Address, value: u32)` — Requires address auth; writes value to storage
|
|
458
|
+
- `read_value(addr: Address) -> u32` — Reads stored value for address
|
|
459
|
+
|
|
460
|
+
Build/optimize/deploy with the Stellar CLI:
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
# Using helper script
|
|
464
|
+
bash contracts/smoke-contract/contract.sh build
|
|
465
|
+
bash contracts/smoke-contract/contract.sh optimize
|
|
466
|
+
bash contracts/smoke-contract/contract.sh deploy --network testnet --account test-account
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Smoke Test Script
|
|
470
|
+
|
|
471
|
+
- **Path**: `scripts/smoke.ts`
|
|
472
|
+
- **Requirements**:
|
|
473
|
+
- Node.js 18+
|
|
474
|
+
- Stellar CLI (`stellar`) configured with a key
|
|
475
|
+
- Channels plugin running at base URL
|
|
476
|
+
|
|
477
|
+
Environment variables:
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
export BASE_URL="http://localhost:8080" # relayer origin
|
|
481
|
+
export API_KEY="<relayer-api-key>" # required
|
|
482
|
+
export NETWORK="testnet" # or "mainnet"
|
|
483
|
+
export RPC_URL="https://soroban-testnet.stellar.org"
|
|
484
|
+
export ACCOUNT_NAME="test-account" # key in `stellar keys`
|
|
485
|
+
export CONTRACT_ID="CDXX..." # deployed smoke-contract
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
Run (args override env):
|
|
489
|
+
|
|
490
|
+
```bash
|
|
491
|
+
pnpm ts-node scripts/smoke.ts \
|
|
492
|
+
--api-key YOUR_API_KEY \
|
|
493
|
+
--account-name test-account \
|
|
494
|
+
--contract-id CDXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
|
495
|
+
|
|
496
|
+
# Show all options in the script header
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
What it does:
|
|
500
|
+
|
|
501
|
+
- Health-checks the relayer
|
|
502
|
+
- XDR submit-only: signs a small self-payment and submits via fee bump
|
|
503
|
+
- func+auth (no auth): calls `no_auth_bump(42)` using a channel account
|
|
504
|
+
- func+auth (address auth): calls `write_with_address_auth(addr, 777)` with signed auth entries
|
package/dist/build.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* build.ts
|
|
3
|
+
*
|
|
4
|
+
* Transaction rebuild logic to use channel account as source.
|
|
5
|
+
*/
|
|
6
|
+
import { Transaction } from "@stellar/stellar-sdk";
|
|
7
|
+
export interface RebuildParams {
|
|
8
|
+
inputXdr: string;
|
|
9
|
+
channelAddress: string;
|
|
10
|
+
channelSequence: string;
|
|
11
|
+
fundAddress: string;
|
|
12
|
+
networkPassphrase: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Rebuild transaction with channel account as source
|
|
16
|
+
* - Set transaction source to channel account with current sequence
|
|
17
|
+
* - Preserve memo, timeBounds, etc.
|
|
18
|
+
* - Copy operations ensuring their source equals the fund address
|
|
19
|
+
*/
|
|
20
|
+
export declare function rebuildWithChannel(params: RebuildParams): Transaction;
|
|
21
|
+
//# sourceMappingURL=build.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAe,MAAM,sBAAsB,CAAC;AAIhE,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,GAAG,WAAW,CA6GrE"}
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* build.ts
|
|
4
|
+
*
|
|
5
|
+
* Transaction rebuild logic to use channel account as source.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.rebuildWithChannel = rebuildWithChannel;
|
|
9
|
+
const stellar_sdk_1 = require("@stellar/stellar-sdk");
|
|
10
|
+
const relayer_sdk_1 = require("@openzeppelin/relayer-sdk");
|
|
11
|
+
const constants_1 = require("./constants");
|
|
12
|
+
/**
|
|
13
|
+
* Rebuild transaction with channel account as source
|
|
14
|
+
* - Set transaction source to channel account with current sequence
|
|
15
|
+
* - Preserve memo, timeBounds, etc.
|
|
16
|
+
* - Copy operations ensuring their source equals the fund address
|
|
17
|
+
*/
|
|
18
|
+
function rebuildWithChannel(params) {
|
|
19
|
+
const { inputXdr, channelAddress, channelSequence, fundAddress, networkPassphrase, } = params;
|
|
20
|
+
// Parse input transaction
|
|
21
|
+
let inputTx;
|
|
22
|
+
try {
|
|
23
|
+
const envelope = stellar_sdk_1.xdr.TransactionEnvelope.fromXDR(inputXdr, "base64");
|
|
24
|
+
// Ensure it's a regular transaction envelope (not fee bump)
|
|
25
|
+
if (envelope.switch() !== stellar_sdk_1.xdr.EnvelopeType.envelopeTypeTx()) {
|
|
26
|
+
throw (0, relayer_sdk_1.pluginError)("Input must be a regular transaction envelope (not fee bump)", {
|
|
27
|
+
code: "INVALID_ENVELOPE_TYPE",
|
|
28
|
+
status: constants_1.HTTP_STATUS.BAD_REQUEST,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
inputTx = new stellar_sdk_1.Transaction(envelope, networkPassphrase);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error.code === "INVALID_ENVELOPE_TYPE") {
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
throw (0, relayer_sdk_1.pluginError)("Failed to parse input transaction XDR", {
|
|
38
|
+
code: "INVALID_XDR",
|
|
39
|
+
status: constants_1.HTTP_STATUS.BAD_REQUEST,
|
|
40
|
+
details: {
|
|
41
|
+
message: error instanceof Error ? error.message : String(error),
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
// Validate timeBounds
|
|
46
|
+
if (inputTx.timeBounds) {
|
|
47
|
+
const now = Math.floor(Date.now() / 1000);
|
|
48
|
+
const maxTime = Number(inputTx.timeBounds.maxTime);
|
|
49
|
+
if (maxTime > 0) {
|
|
50
|
+
const maxAllowedTime = now + constants_1.TIME.MAX_TIME_BOUND_OFFSET_SECONDS;
|
|
51
|
+
if (maxTime > maxAllowedTime) {
|
|
52
|
+
throw (0, relayer_sdk_1.pluginError)(`Transaction maxTime is too far in the future. Max allowed: ${constants_1.TIME.MAX_TIME_BOUND_OFFSET_SECONDS} seconds from now`, {
|
|
53
|
+
code: "INVALID_TIME_BOUNDS",
|
|
54
|
+
status: constants_1.HTTP_STATUS.BAD_REQUEST,
|
|
55
|
+
details: { maxTime, maxAllowedTime },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Validate operations: all operation sources must be fund address or missing
|
|
61
|
+
for (let i = 0; i < inputTx.operations.length; i++) {
|
|
62
|
+
const op = inputTx.operations[i];
|
|
63
|
+
if (op.source && op.source !== fundAddress) {
|
|
64
|
+
throw (0, relayer_sdk_1.pluginError)(`Operation ${i} has source ${op.source} but must be ${fundAddress} or omitted`, {
|
|
65
|
+
code: "INVALID_OPERATION_SOURCE",
|
|
66
|
+
status: constants_1.HTTP_STATUS.BAD_REQUEST,
|
|
67
|
+
details: {
|
|
68
|
+
operationIndex: i,
|
|
69
|
+
operationSource: op.source,
|
|
70
|
+
expectedSource: fundAddress,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Manipulate the transaction envelope XDR directly to change source and sequence
|
|
76
|
+
// This is the most reliable approach that preserves all transaction details
|
|
77
|
+
const envelope = inputTx.toEnvelope();
|
|
78
|
+
const txBody = envelope.v1().tx();
|
|
79
|
+
// Update source account to channel address
|
|
80
|
+
const channelAccountId = stellar_sdk_1.StrKey.decodeEd25519PublicKey(channelAddress);
|
|
81
|
+
const channelMuxed = stellar_sdk_1.xdr.MuxedAccount.keyTypeEd25519(channelAccountId);
|
|
82
|
+
txBody.sourceAccount(channelMuxed);
|
|
83
|
+
// Update sequence number
|
|
84
|
+
// Sequence numbers in Stellar are represented as Int64 in XDR
|
|
85
|
+
const seqNum = stellar_sdk_1.xdr.Int64.fromString(channelSequence);
|
|
86
|
+
txBody.seqNum(seqNum);
|
|
87
|
+
// Update operation sources to fund address if not set
|
|
88
|
+
const fundAccountId = stellar_sdk_1.StrKey.decodeEd25519PublicKey(fundAddress);
|
|
89
|
+
const fundMuxed = stellar_sdk_1.xdr.MuxedAccount.keyTypeEd25519(fundAccountId);
|
|
90
|
+
const operations = txBody.operations();
|
|
91
|
+
for (let i = 0; i < operations.length; i++) {
|
|
92
|
+
const op = operations[i];
|
|
93
|
+
// If operation doesn't have a source, set it to fund address
|
|
94
|
+
if (!op.sourceAccount()) {
|
|
95
|
+
op.sourceAccount(fundMuxed);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Create new transaction from modified envelope
|
|
99
|
+
return new stellar_sdk_1.Transaction(envelope, networkPassphrase);
|
|
100
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config.ts
|
|
3
|
+
*
|
|
4
|
+
* Environment-driven configuration for the channel accounts plugin.
|
|
5
|
+
*/
|
|
6
|
+
export interface ChannelAccountsConfig {
|
|
7
|
+
fundRelayerId: string;
|
|
8
|
+
network: "testnet" | "mainnet";
|
|
9
|
+
rpcUrl: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Load configuration from environment variables
|
|
13
|
+
*/
|
|
14
|
+
export declare function loadConfig(): ChannelAccountsConfig;
|
|
15
|
+
/**
|
|
16
|
+
* Get the network passphrase based on the configuration
|
|
17
|
+
*/
|
|
18
|
+
export declare function getNetworkPassphrase(network: "testnet" | "mainnet"): string;
|
|
19
|
+
/**
|
|
20
|
+
* Get the per-channel lock TTL in seconds (default 30)
|
|
21
|
+
*/
|
|
22
|
+
export declare function getLockTtlSeconds(): number;
|
|
23
|
+
/**
|
|
24
|
+
* Get the max fee for fee bump transactions
|
|
25
|
+
*/
|
|
26
|
+
export declare function getMaxFee(): number;
|
|
27
|
+
/**
|
|
28
|
+
* Get the admin secret for management API
|
|
29
|
+
*/
|
|
30
|
+
export declare function getAdminSecret(): string | undefined;
|
|
31
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;CAChB;AAcD;;GAEG;AACH,wBAAgB,UAAU,IAAI,qBAAqB,CAiBlD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAE3E;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAY1C;AAED;;GAEG;AACH,wBAAgB,SAAS,IAAI,MAAM,CAQlC;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,GAAG,SAAS,CAKnD"}
|