Skip to content

Commit

Permalink
feat: Use API for nonce detection if available (#1704)
Browse files Browse the repository at this point in the history
* feat: use API for nonce detection if available

* test: update test mocks

* test: fix cli mock tests

---------

Co-authored-by: janniks <[email protected]>
  • Loading branch information
janniks and janniks committed Jun 17, 2024
1 parent 33ca645 commit 855ca69
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 20 deletions.
5 changes: 1 addition & 4 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import * as path from 'path';
const c32check = require('c32check');

import { UserData } from '@stacks/auth';
import crossfetch from 'cross-fetch';
import 'cross-fetch/polyfill';

import { StackerInfo, StackingClient } from '@stacks/stacking';

Expand Down Expand Up @@ -1735,7 +1735,6 @@ async function canStack(network: CLINetworkAdapter, args: string[]): Promise<str
const txNetwork = network.isMainnet() ? new StacksMainnet() : new StacksTestnet();

const apiConfig = new Configuration({
fetchApi: crossfetch,
basePath: txNetwork.coreApiUrl,
});
const accounts = new AccountsApi(apiConfig);
Expand Down Expand Up @@ -1799,7 +1798,6 @@ async function stack(network: CLINetworkAdapter, args: string[]): Promise<string
const txVersion = txNetwork.isMainnet() ? TransactionVersion.Mainnet : TransactionVersion.Testnet;

const apiConfig = new Configuration({
fetchApi: crossfetch,
basePath: txNetwork.coreApiUrl,
});
const accounts = new AccountsApi(apiConfig);
Expand Down Expand Up @@ -1936,7 +1934,6 @@ function faucetCall(_: CLINetworkAdapter, args: string[]): Promise<string> {
// console.log(address);

const apiConfig = new Configuration({
fetchApi: crossfetch,
basePath: 'https://api.testnet.hiro.so',
});

Expand Down
13 changes: 9 additions & 4 deletions packages/cli/tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ describe('BNS', () => {
const mockedResponse = JSON.stringify(TEST_FEE_ESTIMATE);

fetchMock.mockOnce(mockedResponse);
fetchMock.mockRejectOnce();
fetchMock.mockOnce(JSON.stringify({ nonce: 1000 }));
fetchMock.mockOnce(JSON.stringify('success'));

Expand All @@ -303,6 +304,7 @@ describe('BNS', () => {
const mockedResponse = JSON.stringify(TEST_FEE_ESTIMATE);

fetchMock.mockOnce(mockedResponse);
fetchMock.mockRejectOnce();
fetchMock.mockOnce(JSON.stringify({ nonce: 1000 }));
fetchMock.mockOnce(JSON.stringify('success'));

Expand All @@ -323,7 +325,7 @@ describe('Subdomain Migration', () => {
string,
string,
{ txid: string; error: string | null; status: number } | string,
boolean
boolean,
][] = [
[
'sound idle panel often situate develop unit text design antenna vendor screen opinion balcony share trigger accuse scatter visa uniform brass update opinion media',
Expand Down Expand Up @@ -429,6 +431,9 @@ describe('Subdomain Migration', () => {

test('can_stack', async () => {
fetchMock.resetMocks();
fetchMock.mockOnce(
`{"stx":{"balance":"16216000000000","total_sent":"0","total_received":"0","total_fees_sent":"0","total_miner_rewards_received":"0","lock_tx_id":"","locked":"0","lock_height":0,"burnchain_lock_height":0,"burnchain_unlock_height":0},"fungible_tokens":{},"non_fungible_tokens":{}}`
);
fetchMock.mockOnce(
'{"contract_id":"ST000000000000000000002AMW42H.pox","pox_activation_threshold_ustx":827381723155441,"first_burnchain_block_height":2000000,"prepare_phase_block_length":50,"reward_phase_block_length":1000,"reward_slots":2000,"rejection_fraction":12,"total_liquid_supply_ustx":41369086157772050,"current_cycle":{"id":269,"min_threshold_ustx":5180000000000,"stacked_ustx":0,"is_pox_active":false},"next_cycle":{"id":270,"min_threshold_ustx":5180000000000,"min_increment_ustx":5171135769721,"stacked_ustx":5600000000000,"prepare_phase_start_block_height":2283450,"blocks_until_prepare_phase":146,"reward_phase_start_block_height":2283500,"blocks_until_reward_phase":196,"ustx_until_pox_rejection":4964290338932640},"min_amount_ustx":5180000000000,"prepare_cycle_length":50,"reward_cycle_id":269,"reward_cycle_length":1050,"rejection_votes_left_required":4964290338932640,"next_reward_cycle_in":196}'
);
Expand All @@ -446,9 +451,9 @@ test('can_stack', async () => {
const response = await canStack(testnetNetwork, params.split(' '));
expect(response.eligible).toBe(true);

expect(fetchMock.mock.calls).toHaveLength(4);
expect(fetchMock.mock.calls[3][0]).toContain('/pox/can-stack-stx');
expect(fetchMock.mock.calls[3][1]?.body).toBe(
expect(fetchMock.mock.calls).toHaveLength(5);
expect(fetchMock.mock.calls[4][0]).toContain('/pox/can-stack-stx');
expect(fetchMock.mock.calls[4][1]?.body).toBe(
'{"sender":"ST3VJVZ265JZMG1N61YE3EQ7GNTQHF6PXP0E7YACV","arguments":["0x0c000000020968617368627974657302000000147046a658021260485e1ba9eb6c3e4c26b60953290776657273696f6e020000000100","0x010000000000000000000005a74678d000","0x010000000000000000000000000000010d","0x010000000000000000000000000000000a"]}'
);
});
Expand Down
24 changes: 18 additions & 6 deletions packages/transactions/src/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,21 +70,33 @@ import { StacksTransaction } from './transaction';
import { createLPList } from './types';
import { cvToHex, omit, parseReadOnlyResponse, validateTxId } from './utils';

/** @internal */
async function _getNonceApi(address: string, network: StacksNetwork): Promise<bigint> {
const url = `${network.coreApiUrl}/extended/v1/address/${address}/nonces`;
const response = await network.fetchFn(url);
const result = await response.json();
return BigInt(result.possible_next_nonce);
}

/**
* Lookup the nonce for an address from a core node
*
* @param {string} address - the c32check address to look up
* @param {StacksNetworkName | StacksNetwork} network - the Stacks network to look up address on
*
* @return a promise that resolves to an integer
* Lookup the nonce for an address from an API or core node
* @return a promise that resolves to a bigint
*/
export async function getNonce(
/** The Stacks (c32check) address to look up */
address: string,
/** The Stacks network to look up the address on */
network?: StacksNetworkName | StacksNetwork
): Promise<bigint> {
const derivedNetwork = StacksNetwork.fromNameOrNetwork(network ?? new StacksMainnet());
const url = derivedNetwork.getAccountApiUrl(address);

// Try API first
try {
return await _getNonceApi(address, derivedNetwork);
} catch (e) {}

// Try node if API endpoint isn't available
const response = await derivedNetwork.fetchFn(url);
if (!response.ok) {
let msg = '';
Expand Down
52 changes: 46 additions & 6 deletions packages/transactions/tests/builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,13 @@ test('API key middleware - get nonce', async () => {
const fetchFn = createFetchFn(createApiKeyMiddleware({ apiKey }));
const network = new StacksMainnet({ fetchFn });

fetchMock.mockRejectOnce();
fetchMock.mockOnce(`{"balance": "0", "nonce": "123"}`);

const fetchNonce = await getNonce(senderAddress, network);
expect(fetchNonce).toBe(123n);
expect(fetchMock.mock.calls.length).toEqual(1);
expect(fetchMock.mock.calls[0][0]).toEqual(
expect(fetchMock.mock.calls.length).toEqual(2);
expect(fetchMock.mock.calls[1][0]).toEqual(
'https://api.mainnet.hiro.so/v2/accounts/STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6?proof=0'
);
const callHeaders = new Headers(fetchMock.mock.calls[0][1]?.headers);
Expand Down Expand Up @@ -1404,10 +1405,12 @@ test('Make STX token transfer with fetch account nonce', async () => {
const network = new StacksTestnet();
const apiUrl = network.getAccountApiUrl(senderAddress);

fetchMock.mockRejectOnce();
fetchMock.mockOnce(`{"balance":"0", "nonce":${nonce}}`);

const fetchNonce = await getNonce(senderAddress, network);

fetchMock.mockRejectOnce();
fetchMock.mockOnce(`{"balance":"0", "nonce":${nonce}}`);

const transaction = await makeSTXTokenTransfer({
Expand All @@ -1420,9 +1423,9 @@ test('Make STX token transfer with fetch account nonce', async () => {
anchorMode: AnchorMode.Any,
});

expect(fetchMock.mock.calls.length).toEqual(2);
expect(fetchMock.mock.calls[0][0]).toEqual(apiUrl);
expect(fetchMock.mock.calls.length).toEqual(4);
expect(fetchMock.mock.calls[1][0]).toEqual(apiUrl);
expect(fetchMock.mock.calls[3][0]).toEqual(apiUrl);
expect(fetchNonce.toString()).toEqual(nonce.toString());
expect(transaction.auth.spendingCondition?.nonce?.toString()).toEqual(nonce.toString());
});
Expand Down Expand Up @@ -1786,12 +1789,13 @@ test('Make sponsored contract call with sponsor nonce fetch', async () => {
fee: sponsorFee,
};

fetchMock.mockRejectOnce();
fetchMock.mockOnce(`{"balance":"100000", "nonce":${sponsorNonce}}`);

const sponsorSignedTx = await sponsorTransaction(sponsorOptions);

expect(fetchMock.mock.calls.length).toEqual(1);
expect(fetchMock.mock.calls[0][0]).toEqual(network.getAccountApiUrl(sponsorAddress));
expect(fetchMock.mock.calls.length).toEqual(2);
expect(fetchMock.mock.calls[1][0]).toEqual(network.getAccountApiUrl(sponsorAddress));

const sponsorSignedTxSerialized = sponsorSignedTx.serialize();

Expand Down Expand Up @@ -2164,6 +2168,42 @@ test('Get contract map entry - no match', async () => {
expect(result.type).toBe(ClarityType.OptionalNone);
});

describe(getNonce.name, () => {
test('without API', async () => {
const nonce = 123n;
const address = 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6';

const network = new StacksTestnet();

fetchMock.mockRejectOnce(); // missing API
fetchMock.mockOnce(`{"balance":"0", "nonce":${nonce}}`);

await expect(getNonce(address, network)).resolves.toEqual(nonce);

expect(fetchMock.mock.calls.length).toEqual(2);
expect(fetchMock.mock.calls[0][0]).toContain('https://api.testnet.hiro.so/extended/');
expect(fetchMock.mock.calls[1][0]).toContain('https://api.testnet.hiro.so/v2/');
});

test('with API', async () => {
const nonce = 123n;
const address = 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6';

const network = new StacksTestnet();

fetchMock.mockOnce(
`{"last_executed_tx_nonce":${nonce - 2n},"last_mempool_tx_nonce":${
nonce - 1n
},"possible_next_nonce":${nonce},"detected_missing_nonces":[],"detected_mempool_nonces":[]}`
);

await expect(getNonce(address, network)).resolves.toEqual(nonce);

expect(fetchMock.mock.calls.length).toEqual(1);
expect(fetchMock.mock.calls[0][0]).toContain('https://api.testnet.hiro.so/extended/');
});
});

test('Post-conditions with amount larger than 8 bytes throw an error', () => {
const amount = BigInt('0xffffffffffffffff') + 1n;

Expand Down

0 comments on commit 855ca69

Please sign in to comment.