Skip to content

Commit 01e26d9

Browse files
authored
fix: sql transaction consistency (#1410)
* feat: savepoint transactions * refactor: db-controller * refactor: use transactions in all routes * test: fix unit tests * fix: bns test status code * fix: name redirects * fix: bns status tests * fix: rosetta test suite * ci: security patch * fix: instead of savepoint, run queries directly * feat: use async_hooks to check for open transactions * fix: add usage name to transaction ctx * feat: pg tx consistency in write store * feat: store scoped connection in async context * fix: revert unnecessary changes * test: transaction consistency * fix: bns tests * fix: rosetta status tests * fix: bns integration tests
1 parent 0a552b8 commit 01e26d9

20 files changed

+1213
-977
lines changed

src/api/controllers/db-controller.ts

Lines changed: 364 additions & 332 deletions
Large diffs are not rendered by default.

src/api/routes/address.ts

Lines changed: 286 additions & 264 deletions
Large diffs are not rendered by default.

src/api/routes/bns/names.ts

Lines changed: 75 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ import {
1111
setETagCacheHeaders,
1212
} from '../../../api/controllers/cache-controller';
1313

14+
class NameRedirectError extends Error {
15+
constructor(message: string) {
16+
super(message);
17+
this.message = message;
18+
this.name = this.constructor.name;
19+
}
20+
}
21+
1422
export function createBnsNamesRouter(db: PgStore, chainId: ChainID): express.Router {
1523
const router = express.Router();
1624
const cacheHandler = getETagCacheHandler(db);
@@ -85,64 +93,77 @@ export function createBnsNamesRouter(db: PgStore, chainId: ChainID): express.Rou
8593
asyncHandler(async (req, res, next) => {
8694
const { name } = req.params;
8795
const includeUnanchored = isUnanchoredRequest(req, res, next);
88-
let nameInfoResponse: BnsGetNameInfoResponse;
89-
// Subdomain case
90-
if (name.split('.').length == 3) {
91-
const subdomainQuery = await db.getSubdomain({ subdomain: name, includeUnanchored });
92-
if (!subdomainQuery.found) {
93-
const namePart = name.split('.').slice(1).join('.');
94-
const resolverResult = await db.getSubdomainResolver({ name: namePart });
95-
if (resolverResult.found) {
96-
if (resolverResult.result === '') {
97-
res.status(404).json({ error: `missing resolver from a malformed zonefile` });
98-
return;
96+
97+
await db
98+
.sqlTransaction(async sql => {
99+
let nameInfoResponse: BnsGetNameInfoResponse;
100+
// Subdomain case
101+
if (name.split('.').length == 3) {
102+
const subdomainQuery = await db.getSubdomain({
103+
subdomain: name,
104+
includeUnanchored,
105+
});
106+
if (!subdomainQuery.found) {
107+
const namePart = name.split('.').slice(1).join('.');
108+
const resolverResult = await db.getSubdomainResolver({ name: namePart });
109+
if (resolverResult.found) {
110+
if (resolverResult.result === '') {
111+
throw { error: `missing resolver from a malformed zonefile` };
112+
}
113+
throw new NameRedirectError(`${resolverResult.result}/v1/names${req.url}`);
114+
}
115+
throw { error: `cannot find subdomain ${name}` };
116+
}
117+
const { result } = subdomainQuery;
118+
119+
nameInfoResponse = {
120+
address: result.owner,
121+
blockchain: bnsBlockchain,
122+
last_txid: result.tx_id,
123+
resolver: result.resolver,
124+
status: 'registered_subdomain',
125+
zonefile: result.zonefile,
126+
zonefile_hash: result.zonefile_hash,
127+
};
128+
} else {
129+
const nameQuery = await db.getName({
130+
name,
131+
includeUnanchored: includeUnanchored,
132+
chainId: chainId,
133+
});
134+
if (!nameQuery.found) {
135+
throw { error: `cannot find name ${name}` };
99136
}
100-
res.redirect(`${resolverResult.result}/v1/names${req.url}`);
101-
return;
137+
const { result } = nameQuery;
138+
nameInfoResponse = {
139+
address: result.address,
140+
blockchain: bnsBlockchain,
141+
expire_block: result.expire_block,
142+
grace_period: result.grace_period,
143+
last_txid: result.tx_id ? result.tx_id : '',
144+
resolver: result.resolver,
145+
status: result.status ? result.status : '',
146+
zonefile: result.zonefile,
147+
zonefile_hash: result.zonefile_hash,
148+
};
102149
}
103-
res.status(404).json({ error: `cannot find subdomain ${name}` });
104-
return;
105-
}
106-
const { result } = subdomainQuery;
107150

108-
nameInfoResponse = {
109-
address: result.owner,
110-
blockchain: bnsBlockchain,
111-
last_txid: result.tx_id,
112-
resolver: result.resolver,
113-
status: 'registered_subdomain',
114-
zonefile: result.zonefile,
115-
zonefile_hash: result.zonefile_hash,
116-
};
117-
} else {
118-
const nameQuery = await db.getName({
119-
name,
120-
includeUnanchored: includeUnanchored,
121-
chainId: chainId,
151+
const response = Object.fromEntries(
152+
Object.entries(nameInfoResponse).filter(([_, v]) => v != null)
153+
);
154+
return response;
155+
})
156+
.then(response => {
157+
setETagCacheHeaders(res);
158+
res.json(response);
159+
})
160+
.catch(error => {
161+
if (error instanceof NameRedirectError) {
162+
res.redirect(error.message);
163+
} else {
164+
res.status(400).json(error);
165+
}
122166
});
123-
if (!nameQuery.found) {
124-
res.status(404).json({ error: `cannot find name ${name}` });
125-
return;
126-
}
127-
const { result } = nameQuery;
128-
nameInfoResponse = {
129-
address: result.address,
130-
blockchain: bnsBlockchain,
131-
expire_block: result.expire_block,
132-
grace_period: result.grace_period,
133-
last_txid: result.tx_id ? result.tx_id : '',
134-
resolver: result.resolver,
135-
status: result.status ? result.status : '',
136-
zonefile: result.zonefile,
137-
zonefile_hash: result.zonefile_hash,
138-
};
139-
}
140-
141-
const response = Object.fromEntries(
142-
Object.entries(nameInfoResponse).filter(([_, v]) => v != null)
143-
);
144-
setETagCacheHeaders(res);
145-
res.json(response);
146167
})
147168
);
148169

src/api/routes/bns/namespaces.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,31 @@ export function createBnsNamespacesRouter(db: PgStore): express.Router {
3636
const { tld } = req.params;
3737
const page = parsePagingQueryInput(req.query.page ?? 0);
3838
const includeUnanchored = isUnanchoredRequest(req, res, next);
39-
const response = await db.getNamespace({ namespace: tld, includeUnanchored });
40-
if (!response.found) {
41-
res.status(404).json(BnsErrors.NoSuchNamespace);
42-
} else {
43-
const { results } = await db.getNamespaceNamesList({
44-
namespace: tld,
45-
page,
46-
includeUnanchored,
47-
});
48-
if (results.length === 0 && req.query.page) {
49-
res.status(400).json(BnsErrors.InvalidPageNumber);
50-
} else {
39+
await db
40+
.sqlTransaction(async sql => {
41+
const response = await db.getNamespace({ namespace: tld, includeUnanchored });
42+
if (!response.found) {
43+
throw BnsErrors.NoSuchNamespace;
44+
} else {
45+
const { results } = await db.getNamespaceNamesList({
46+
namespace: tld,
47+
page,
48+
includeUnanchored,
49+
});
50+
if (results.length === 0 && req.query.page) {
51+
throw BnsErrors.InvalidPageNumber;
52+
} else {
53+
return results;
54+
}
55+
}
56+
})
57+
.then(results => {
5158
setETagCacheHeaders(res);
5259
res.json(results);
53-
}
54-
}
60+
})
61+
.catch(error => {
62+
res.status(400).json(error);
63+
});
5564
})
5665
);
5766

0 commit comments

Comments
 (0)