fix(rosetta): off-by-one nonce returned with rosetta /account/balance endpoint #961 (#964)

This commit is contained in:
Matthew Little
2022-01-07 15:36:50 +01:00
committed by GitHub
parent 62ecf0845a
commit 64a440122a
7 changed files with 84 additions and 11 deletions

View File

@@ -544,8 +544,8 @@ export function createAddressRouter(db: DataStore, chainId: ChainID): RouterWith
return;
}
const results: AddressNonces = {
last_executed_tx_nonce: nonceQuery.result.nonce,
possible_next_nonce: nonceQuery.result.nonce + 1,
last_executed_tx_nonce: nonceQuery.result.lastExecutedTxNonce as number,
possible_next_nonce: nonceQuery.result.possibleNextNonce,
// Note: OpenAPI type generator doesn't support `nullable: true` so force cast it here
last_mempool_tx_nonce: (null as unknown) as number,
detected_missing_nonces: [],

View File

@@ -71,6 +71,7 @@ export function createRosettaAccountRouter(db: DataStore, chainId: ChainID): Rou
stxAddress: accountIdentifier.address,
blockIdentifier: { height: block.block_height },
});
const sequenceNumber = accountNonceQuery.found ? accountNonceQuery.result.possibleNextNonce : 0;
const extra_metadata: any = {};
@@ -120,7 +121,7 @@ export function createRosettaAccountRouter(db: DataStore, chainId: ChainID): Rou
},
],
metadata: {
sequence_number: accountNonceQuery.found ? accountNonceQuery.result.nonce ?? 0 : 0,
sequence_number: sequenceNumber,
},
};

View File

@@ -780,7 +780,7 @@ export interface DataStore extends DataStoreEventEmitter {
getAddressNonceAtBlock(args: {
stxAddress: string;
blockIdentifier: BlockIdentifier;
}): Promise<FoundOrNot<{ nonce: number }>>;
}): Promise<FoundOrNot<{ lastExecutedTxNonce: number | null; possibleNextNonce: number }>>;
getAddressNonces(args: {
stxAddress: string;

View File

@@ -573,7 +573,7 @@ export class MemoryDataStore
getAddressNonceAtBlock(args: {
stxAddress: string;
blockIdentifier: BlockIdentifier;
}): Promise<FoundOrNot<{ nonce: number }>> {
}): Promise<FoundOrNot<{ lastExecutedTxNonce: number | null; possibleNextNonce: number }>> {
throw new Error('not yet implemented');
}

View File

@@ -1725,13 +1725,13 @@ export class PgDataStore
async getAddressNonceAtBlock(args: {
stxAddress: string;
blockIdentifier: BlockIdentifier;
}): Promise<FoundOrNot<{ nonce: number }>> {
}): Promise<FoundOrNot<{ lastExecutedTxNonce: number | null; possibleNextNonce: number }>> {
return await this.queryTx(async client => {
const dbBlock = await this.getBlockInternal(client, args.blockIdentifier);
if (!dbBlock.found) {
return { found: false };
}
const executedTxNonce = await client.query<{ nonce: number | null }>(
const nonceQuery = await client.query<{ nonce: number | null }>(
`
SELECT MAX(nonce) nonce
FROM txs
@@ -1741,8 +1741,15 @@ export class PgDataStore
`,
[args.stxAddress, dbBlock.result.block_height]
);
const nonce = executedTxNonce.rows[0]?.nonce ?? 0;
return { found: true, result: { nonce } };
let lastExecutedTxNonce: number | null = null;
let possibleNextNonce = 0;
if (nonceQuery.rows.length > 0 && typeof nonceQuery.rows[0].nonce === 'number') {
lastExecutedTxNonce = nonceQuery.rows[0].nonce;
possibleNextNonce = lastExecutedTxNonce + 1;
} else {
possibleNextNonce = 0;
}
return { found: true, result: { lastExecutedTxNonce, possibleNextNonce } };
});
}

View File

@@ -628,6 +628,8 @@ describe('Rosetta API', () => {
const testAddr1 = 'STNN931GWC0XMRBWXYJQXTEKT4YFB1Z7YTCV3RZN';
const testAddr1Key = '532d5ff9f0d4980225a031f65a2dff75b351d675b086766917d43372cedf762901';
const testAddr2 = 'ST2WFY0H48AS2VYPA7N69V2VJ8VKS8FSPQSPFE1Z8';
const testAddr3 = 'ST5F760KN84TZK3VTZCTVFYCVXQBEVKNV9M7H2CW';
let expectedTxId: string = '';
const broadcastTx = new Promise<DbTx>(resolve => {
const listener: (txId: string) => void = async txId => {
@@ -726,7 +728,7 @@ describe('Rosetta API', () => {
},
}],
metadata: {
sequence_number: 1,
sequence_number: 2,
},
};
expect(JSON.parse(nonceResult1.text)).toEqual(expectedResponse1);
@@ -759,10 +761,44 @@ describe('Rosetta API', () => {
},
}],
metadata: {
sequence_number: 0,
sequence_number: 1,
},
};
expect(JSON.parse(nonceResult2.text)).toEqual(expectedResponse2);
// Test account without any existing txs, should have "next nonce" value of 0
const request3: RosettaAccountBalanceRequest = {
network_identifier: {
blockchain: 'stacks',
network: 'testnet',
},
block_identifier: {
index: tx2!.block_height,
},
account_identifier: {
address: testAddr3,
},
};
const nonceResult3 = await supertest(api.server).post(`/rosetta/v1/account/balance/`).send(request3);
expect(nonceResult3.status).toBe(200);
expect(nonceResult3.type).toBe('application/json');
const expectedResponse3: RosettaAccountBalanceResponse = {
block_identifier: {
hash: tx2!.block_hash,
index: tx2!.block_height,
},
balances: [{
value: '0',
currency: {
symbol: 'STX',
decimals: 6,
},
}],
metadata: {
sequence_number: 0,
},
};
expect(JSON.parse(nonceResult3.text)).toEqual(expectedResponse3);
});
test('account/balance - fees calculated properly', async () => {

View File

@@ -4016,6 +4016,7 @@ describe('api tests', () => {
test('address nonce', async () => {
const testAddr1 = 'ST3DWSXBPYDB484QXFTR81K4AWG4ZB5XZNFF3H70C';
const testAddr2 = 'ST5F760KN84TZK3VTZCTVFYCVXQBEVKNV9M7H2CW';
const block1 = new TestBlockBuilder({
block_height: 1,
@@ -4117,6 +4118,34 @@ describe('api tests', () => {
expect(nonceResults4.type).toBe('application/json');
expect(nonceResults4.body).toEqual(expectedNonceResults4);
// Get nonce for account with no transactions
const expectedNonceResultsNoTxs1 = {
detected_missing_nonces: [],
last_executed_tx_nonce: null,
last_mempool_tx_nonce: null,
possible_next_nonce: 0,
};
const nonceResultsNoTxs1 = await supertest(api.server).get(
`/extended/v1/address/${testAddr2}/nonces`
);
expect(nonceResultsNoTxs1.status).toBe(200);
expect(nonceResultsNoTxs1.type).toBe('application/json');
expect(nonceResultsNoTxs1.body).toEqual(expectedNonceResultsNoTxs1);
// Get nonce for account with no transactions
const expectedNonceResultsNoTxs2 = {
detected_missing_nonces: [],
last_executed_tx_nonce: null,
last_mempool_tx_nonce: null,
possible_next_nonce: 0,
};
const nonceResultsNoTxs2 = await supertest(api.server).get(
`/extended/v1/address/${testAddr2}/nonces?block_height=${block2.block.block_height}`
);
expect(nonceResultsNoTxs2.status).toBe(200);
expect(nonceResultsNoTxs2.type).toBe('application/json');
expect(nonceResultsNoTxs2.body).toEqual(expectedNonceResultsNoTxs2);
// Bad requests
const nonceResults5 = await supertest(api.server).get(
`/extended/v1/address/${testAddr1}/nonces?block_hash=xcvbnmn`