Skip to content

Commit 00c9319

Browse files
committed
docs: adding frontend and backend patterns
1 parent 71b9004 commit 00c9319

21 files changed

Lines changed: 2200 additions & 1 deletion
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
---
2+
title: Gasless Voucher Server
3+
---
4+
5+
## About
6+
7+
The gasless voucher server is the off-chain service that makes voucher-based UX practical. It holds the issuer account, submits voucher extrinsics, exposes a REST API for frontend clients, and manages the lifecycle of vouchers tied to a specific target program.
8+
9+
Without this backend layer, a frontend can know that gasless UX is desirable, but it has no trusted place to keep the issuer key or coordinate voucher issuance safely.
10+
11+
## Why the pattern matters
12+
13+
Gasless UX is not a single call. A real application needs to manage the full voucher lifecycle:
14+
15+
- issue a voucher for a user,
16+
- avoid issuing duplicates unnecessarily,
17+
- inspect whether a voucher is active,
18+
- prolong it when more balance or time is needed,
19+
- revoke it when the session or entitlement should end.
20+
21+
That is why the example is a server pattern rather than a one-off helper.
22+
23+
## High-level flow
24+
25+
There are two layers in the implementation:
26+
27+
1. the HTTP server in `src/index.ts`,
28+
2. the chain-facing `GaslessService` in `src/lib.ts`.
29+
30+
The flow for voucher issuance looks like this:
31+
32+
1. frontend calls `POST /gasless/voucher/request`,
33+
2. Express validates the request body,
34+
3. the route calls `gaslessService.issueIfNeeded(...)`,
35+
4. `GaslessService` checks whether an active voucher already exists,
36+
5. if none exists, it builds `api.voucher.issue(...)`,
37+
6. the backend signer submits the extrinsic,
38+
7. the service waits for `VoucherIssued`,
39+
8. the route returns `{ voucherId }` to the client.
40+
41+
The HTTP route in `src/index.ts` stays intentionally small:
42+
43+
```ts
44+
app.post("/gasless/voucher/request", async (req, res) => {
45+
const { account, amount = 20_000_000_000_000, durationInSec = 3_600 } = req.body;
46+
47+
if (!account) {
48+
return res.status(400).json({ error: "account is required" });
49+
}
50+
51+
const voucherId: HexString = await gaslessService.issueIfNeeded(
52+
account,
53+
programId,
54+
amount,
55+
Number(durationInSec)
56+
);
57+
58+
return res.status(200).json({ voucherId });
59+
});
60+
```
61+
62+
That is a good backend sign: the route owns input validation and HTTP response shape, while the service owns every chain-specific concern.
63+
64+
## Why `issueIfNeeded(...)` matters
65+
66+
The route uses an idempotent entrypoint:
67+
68+
```ts
69+
const voucherId: HexString = await gaslessService.issueIfNeeded(
70+
account,
71+
programId,
72+
amount,
73+
Number(durationInSec)
74+
);
75+
```
76+
77+
This is more than a convenience. Frontends often request gasless support on mount, reconnect, or page refresh. Without an idempotent server-side path, those UX patterns could burn issuer funds by creating duplicate vouchers.
78+
79+
The implementation protects against duplication in two different ways:
80+
81+
1. `inFlightVoucherIssues` prevents the same `(account, programId)` request from being started twice concurrently,
82+
2. `api.voucher.getAllForAccount(account)` checks whether an acceptable voucher already exists on-chain.
83+
84+
That is exactly the kind of backend logic that is easy to miss if the article only describes the endpoint shape.
85+
86+
## Service-level architecture
87+
88+
`GaslessService` owns:
89+
90+
- the Gear API connection,
91+
- the voucher issuer account,
92+
- an in-memory submission queue,
93+
- in-flight deduplication for voucher creation,
94+
- helper methods for issue, status, prolong, revoke, and lookup.
95+
96+
That separation keeps HTTP concerns out of the chain-integration layer.
97+
98+
The service constructor makes that boundary explicit:
99+
100+
```ts
101+
constructor() {
102+
this.api = new GearApi({ providerAddress: process.env.NODE_URL });
103+
this.voucherAccount = this.getVoucherAccount();
104+
}
105+
```
106+
107+
The route layer never deals with API construction or signer loading directly.
108+
109+
## Deduplication and queueing
110+
111+
Two details in `GaslessService` are especially important for a production-oriented design.
112+
113+
### In-flight deduplication
114+
115+
The service stores in-flight voucher requests in:
116+
117+
```ts
118+
private readonly inFlightVoucherIssues = new Map<VoucherIssueKey, Promise<HexString>>();
119+
```
120+
121+
This prevents duplicate concurrent issuance attempts for the same `(account, programId)` pair.
122+
123+
### Submission queue
124+
125+
The service also serializes issuer operations through:
126+
127+
```ts
128+
private submissionQueue: Promise<void> = Promise.resolve();
129+
```
130+
131+
That matters because nonce handling on a single signer account becomes fragile under concurrency.
132+
133+
These two fields are some of the most important backend-specific code in the example. Without them, the service would appear correct in light testing and then become unstable under concurrent usage.
134+
135+
## Core issuance flow
136+
137+
The chain-side issuance happens through `issueInternal(...)`:
138+
139+
1. wait for API and crypto readiness,
140+
2. convert duration from seconds to blocks,
141+
3. normalize the spender account,
142+
4. build `api.voucher.issue(...)`,
143+
5. fetch the next nonce,
144+
6. sign and send with the issuer account,
145+
7. wait for `VoucherIssued`,
146+
8. cache voucher metadata for later reads.
147+
148+
The core fragment is:
149+
150+
```ts
151+
const { extrinsic } = await this.api.voucher.issue(
152+
accountId,
153+
amount,
154+
durationInBlocks,
155+
[programId],
156+
false
157+
);
158+
```
159+
160+
Then the service resolves only when the block contains `VoucherIssued`.
161+
162+
The event-driven promise is the most important code block in the file:
163+
164+
```ts
165+
const voucherId = await new Promise<HexString>((resolve, reject) => {
166+
extrinsic.signAndSend(
167+
this.voucherAccount,
168+
{ nonce },
169+
({ events, status }) => {
170+
if (!status.isInBlock) return;
171+
172+
const viEvent = events.find(
173+
({ event }) => event.method === "VoucherIssued"
174+
);
175+
176+
if (viEvent) {
177+
const data = viEvent.event.data as any;
178+
const id = data.voucherId.toHex() as HexString;
179+
voucherInfoStorage[id] = { durationInSec, amount };
180+
resolve(id);
181+
return;
182+
}
183+
184+
const efEvent = events.find(
185+
({ event }) => event.method === "ExtrinsicFailed"
186+
);
187+
reject(
188+
efEvent
189+
? this.api.getExtrinsicFailedError(efEvent.event)
190+
: new Error("VoucherIssued event not found in block")
191+
);
192+
}
193+
);
194+
});
195+
```
196+
197+
That is what lets the backend return a concrete `voucherId` only after it has real evidence of success.
198+
199+
## Why event-based resolution is used
200+
201+
The service does not assume that submitting the extrinsic is enough. It waits for chain events so that it can:
202+
203+
- extract the actual `voucherId`,
204+
- distinguish success from `ExtrinsicFailed`,
205+
- return a concrete result to the frontend.
206+
207+
That mirrors the same philosophy as the stronger contract/frontend patterns: real success should be tied to the relevant event boundary.
208+
209+
## Status and normalization flow
210+
211+
The status route contains one more small but useful backend detail:
212+
213+
```ts
214+
const normalizedId = voucherId.startsWith("0x")
215+
? (voucherId as `0x${string}`)
216+
: (`0x${voucherId}` as `0x${string}`);
217+
```
218+
219+
This makes the HTTP interface slightly more forgiving while keeping the service layer strict about identifier format.
220+
221+
## Status, prolong, and revoke flows
222+
223+
The server also exposes:
224+
225+
- `GET /gasless/voucher/:voucherId/status`
226+
- `POST /prolong`
227+
- `POST /revoke`
228+
229+
These routes are important because gasless UX is rarely just “issue once and forget.” Long-lived sessions and repeated interactions often require voucher inspection or renewal.
230+
231+
Even though those routes are short, they expose non-trivial service behavior:
232+
233+
- sequenced signer operations,
234+
- voucher lifecycle mutation,
235+
- error mapping from chain failures into HTTP responses,
236+
- one place for nonce-sensitive issuer activity.
237+
238+
## Production guidance
239+
240+
- Make issuance idempotent from the server side, not only from the frontend side.
241+
- Serialize submissions per issuer account or use a dedicated nonce manager.
242+
- Scope vouchers tightly to the intended program.
243+
- Keep issuer keys only on the backend.
244+
- Treat event-based confirmation as the real success boundary for issuance.
245+
- Keep route handlers tiny and push event handling, nonce sensitivity, and voucher lifecycle rules into the service layer.
246+
247+
## Source code
248+
249+
- [`infrastructure/gasless-server`](https://github.com/gear-foundation/vara-dapp-patterns/tree/master/infrastructure/gasless-server)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
title: Backend Patterns
3+
---
4+
5+
The backend examples in `vara-dapp-patterns/infrastructure` cover the trusted off-chain services that often sit beside a Vara application.
6+
7+
## Included patterns
8+
9+
| Pattern | Focus | Source |
10+
| --- | --- | --- |
11+
| [REST Gateway for Sails Programs](/docs/vara-network/examples/Patterns/backend/rest-gateway) | Expose on-chain commands and queries through a conventional HTTP API | [`infrastructure/gateway`](https://github.com/gear-foundation/vara-dapp-patterns/tree/master/infrastructure/gateway) |
12+
| [Gasless Voucher Server](/docs/vara-network/examples/Patterns/backend/gasless-voucher-server) | Issue, prolong, revoke, and inspect vouchers on behalf of users | [`infrastructure/gasless-server`](https://github.com/gear-foundation/vara-dapp-patterns/tree/master/infrastructure/gasless-server) |
13+
| [Token-Gated Authentication](/docs/vara-network/examples/Patterns/backend/token-gated-auth) | Verify wallet signatures, read VFT balances, and issue JWT access tokens | [`infrastructure/token-gate-server`](https://github.com/gear-foundation/vara-dapp-patterns/tree/master/infrastructure/token-gate-server) |
14+
15+
## When you need these patterns
16+
17+
Use a backend pattern when your architecture needs one of the following:
18+
19+
- a trusted signer that should not live in the browser,
20+
- voucher lifecycle management for gasless UX,
21+
- server-side authentication or authorization based on on-chain state,
22+
- a conventional REST interface for frontend or third-party clients.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"title": "Backend",
3+
"pages": [
4+
"rest-gateway",
5+
"gasless-voucher-server",
6+
"token-gated-auth",
7+
"..."
8+
]
9+
}

0 commit comments

Comments
 (0)