Skip to content

Commit

Permalink
feat(stacking): add getSecondsUntilStackingDeadline helper method (#1528
Browse files Browse the repository at this point in the history
)

* fix: add getSecondsUntilStackingDeadline in addition to getSecondsUntilNextCycle

* docs: update docs

* chore: add negative case

* docs: udpate links

---------

Co-authored-by: janniks <[email protected]>
  • Loading branch information
janniks and janniks authored Jul 24, 2023
1 parent 957790b commit 3c819c2
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 47 deletions.
18 changes: 11 additions & 7 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
title: Getting Started
---

:::info Connect 🌐
[Stacks.js starters](https://docs.hiro.so/stacksjs-starters) offer working templates with Stacks Connect pre-installed for a quick and easy way to get started with building Stacks enabled web apps.
:::
import StacksjsStartersNote from './includes/\_stacks.js-starters-note.mdx';

<StacksjsStartersNote/>

---

Expand All @@ -16,7 +16,9 @@ Typically we speak of "mainnet" and "testnet" as the networks of Stacks. Most wa
As the name suggests, "testnet" is a network for testing.
It's a separate blockchain state that holds test tokens, which have no value.

Developers are encouraged to use testnet for testing and development, before rolling out applications and contracts to mainnet. Stacks.js functions can be configured to use wichever network you want.
Developers are encouraged to use testnet for testing, before rolling out applications and contracts to mainnet.
For development, there is even Devnet/Mocknet for working in a local development environment.
Stacks.js functions can be configured to use wichever network you want.

```js
import { StacksMainnet, StacksTestnet } from '@stacks/network';
Expand Down Expand Up @@ -152,16 +154,18 @@ The mode can be either `Allow` or `Deny`.
- `Deny` means the transaction will fail if any asset transfers (not specified in the post conditions) are attempted.

:::note
In either case, all post conditions will still be checked. By default, transactions are set to `Deny` mode, for additional security.
In either case, all post conditions will still be checked.
By default, transactions are set to `Deny` mode, for additional security.
:::

## Broadcasting

:::info Connect 🌐
For web apps via Stacks Connect, the users' wallet will broadcast the transaction and return a txid. [Read more](https://connect.stacks.js.org/modules/_stacks_connect)
For web apps via Stacks Connect, the users' wallet will broadcast the transaction and return a txid.
[Read more](https://connect.stacks.js.org/modules/_stacks_connect)
:::

A finalized transaction can be broadcasted to the network or serialized (to bytes representation) using Stacks.js.
A finalized transaction can be broadcasted to the network or serialized (to a byte representation) using Stacks.js.

```js
import { bytesToHex } from '@stacks/common';
Expand Down
2 changes: 1 addition & 1 deletion docs/includes/_stacks.js-starters-note.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
:::info Connect 🌐
Use our prebuilt [Stacks.js starter templates](/stacksjs-starters) to kickstart your frontend web application development with your preferred JavaScript framework.
[Stacks.js starters](https://docs.hiro.so/stacksjs-starters) offer working templates with Stacks Connect pre-installed for a quick and easy way to get started with building Stacks enabled web apps.
:::
4 changes: 2 additions & 2 deletions docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ It's a collection of various JavaScript libraries allowing developers to interac

There are two main ways developers build applications on the Stacks blockchain:

- 🔒 **Without Direct Private Key Access**: For example a web app that allows users to interact with the Stacks blockchain using their Stacks wallet (browser extension or mobile). Read More in the Connect Guide
- 🔒 **Without Direct Private Key Access**: For example, a web app that allows users to interact with the Stacks blockchain using their Stacks wallet (browser extension or mobile). Read More in the Connect Guide
- 🔑 **With Private Key Access**: For example, managing funds with the Stacks.js CLI, building a backend (which can sign transactions directly).

Most users interact via their favorite Stacks wallet.
Expand All @@ -28,7 +28,7 @@ In these cases, developers can use the same libraries used by Stacks wallets for

There are three main integrations used by Stacks enabled applications:

<!-- todo: add a card and better how-to-guid for each, (eg add message signing backend checking) -->
<!-- todo: add a card and better how-to-guide for each, (eg add message signing backend checking) -->

- **Authentication**: Register and sign users in with identities on the Stacks blockchain
- **Transaction signing**: Prompt users to sign and broadcast transactions to the Stacks blockchain
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"workspaces": [
"packages/**"
],
"license": "MIT",
"prettier": "@stacks/prettier-config",
"scripts": {
"bootstrap": "lerna bootstrap",
Expand Down
6 changes: 3 additions & 3 deletions packages/stacking/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ npm install @stacks/stacking
- [Client helpers](#client-helpers)
- [Will Stacking be executed in the next cycle?](#will-stacking-be-executed-in-the-next-cycle)
- [How long (in seconds) is a Stacking cycle?](#how-long-in-seconds-is-a-stacking-cycle)
- [How much time is left (in seconds) until the next cycle begins?](#how-much-time-is-left-in-seconds-until-the-next-cycle-begins)
- [How much estimated time is left (in seconds) to submit a stacking transaction for the upcoming reward cycle?](#how-much-estimated-time-is-left-in-seconds-to-submit-a-stacking-transaction-for-the-upcoming-reward-cycle)
- [Does account have sufficient STX to meet minimum threshold?](#does-account-have-sufficient-stx-to-meet-minimum-threshold)
- [Get PoX info](#get-pox-info)
- [Get Stacks node info](#get-stacks-node-info)
Expand Down Expand Up @@ -169,10 +169,10 @@ const cycleDuration = await client.getCycleDuration();
// 120
```

### How much time is left (in seconds) until the next cycle begins?
### How much estimated time is left (in seconds) to submit a stacking transaction for the upcoming reward cycle?

```typescript
const secondsUntilNextCycle = await client.getSecondsUntilNextCycle();
const seconds = await client.getSecondsUntilStackingDeadline();

// 600000
```
Expand Down
54 changes: 28 additions & 26 deletions packages/stacking/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ export class StackingClient {
/**
* Get stacks node target block time
*
* @returns {Promise<number>} that resolves to a number if the operation succeeds
* @returns {Promise<number>} resolves to a number if the operation succeeds
*/
async getTargetBlockTime(): Promise<number> {
const url = this.network.getBlockTimeInfoUrl();
Expand All @@ -380,8 +380,7 @@ export class StackingClient {

/**
* Get account balance
*
* @returns promise resolves to a bigint if the operation succeeds
* @returns {Promise<bigint>} resolves to a bigint if the operation succeeds
*/
async getAccountBalance(): Promise<bigint> {
return this.getAccountStatus().then(res => {
Expand All @@ -391,8 +390,7 @@ export class StackingClient {

/**
* Get extended account balances
*
* @returns promise resolves to a bigint if the operation succeeds
* @returns {Promise<AccountExtendedBalances>} resolves to an AccountExtendedBalances response if the operation succeeds
*/
async getAccountExtendedBalances(): Promise<AccountExtendedBalances> {
const url = this.network.getAccountExtendedBalancesApiUrl(this.address);
Expand All @@ -401,17 +399,15 @@ export class StackingClient {

/**
* Get account balance of locked tokens
*
* @returns promise resolves to a bigint if the operation succeeds
* @returns {Promise<bigint>} resolves to a bigint if the operation succeeds
*/
async getAccountBalanceLocked(): Promise<bigint> {
return this.getAccountStatus().then(res => BigInt(res.locked));
}

/**
* Get reward cycle duration in seconds
*
* @returns {Promise<number>} that resolves to a number if the operation succeeds
* @returns {Promise<number>} resolves to a number if the operation succeeds
*/
async getCycleDuration(): Promise<number> {
const poxInfoPromise = this.getPoxInfo();
Expand All @@ -426,7 +422,6 @@ export class StackingClient {

/**
* Get the total burnchain rewards total for the set address
*
* @returns {Promise<TotalRewardsResponse | RewardsError>} that resolves to TotalRewardsResponse or RewardsError
*/
async getRewardsTotalForBtcAddress(): Promise<BurnchainRewardsTotal | RewardsError> {
Expand All @@ -436,7 +431,6 @@ export class StackingClient {

/**
* Get burnchain rewards for the set address
*
* @returns {Promise<RewardsResponse | RewardsError>} that resolves to RewardsResponse or RewardsError
*/
async getRewardsForBtcAddress(
Expand All @@ -448,7 +442,6 @@ export class StackingClient {

/**
* Get burnchain rewards holders for the set address
*
* @returns {Promise<RewardHoldersResponse | RewardsError>} that resolves to RewardHoldersResponse or RewardsError
*/
async getRewardHoldersForBtcAddress(
Expand All @@ -460,7 +453,6 @@ export class StackingClient {

/**
* Get PoX address from reward set by index
*
* @returns {Promise<RewardSetInfo | undefined>} that resolves to RewardSetInfo if the entry exists
*/
async getRewardSet(options: RewardSetOptions): Promise<RewardSetInfo | undefined> {
Expand All @@ -485,8 +477,10 @@ export class StackingClient {

/**
* Get number of seconds until next reward cycle
* @returns {Promise<number>} resolves to a number if the operation succeeds
*
* @returns {Promise<number>} that resolves to a number if the operation succeeds
* See also:
* - {@link getSecondsUntilStackingDeadline}
*/
async getSecondsUntilNextCycle(): Promise<number> {
const poxInfoPromise = this.getPoxInfo();
Expand All @@ -504,6 +498,26 @@ export class StackingClient {
);
}

/**
* Get number of seconds until the end of the stacking deadline.
* This is the estimated time stackers have to submit their stacking
* transactions to be included in the upcoming reward cycle.
* @returns {Promise<number>} resolves to a number of seconds if the operation succeeds.
* **⚠️ Attention**: The returned number of seconds can be negative if the deadline has passed and the prepare phase has started.
*
* See also:
* - {@link getSecondsUntilNextCycle}
*/
async getSecondsUntilStackingDeadline(): Promise<number> {
const poxInfoPromise = this.getPoxInfo();
const targetBlockTimePromise = this.getTargetBlockTime();

return Promise.all([poxInfoPromise, targetBlockTimePromise]).then(
([poxInfo, targetBlockTime]) =>
poxInfo.next_cycle.blocks_until_prepare_phase * targetBlockTime
);
}

/**
* Get information on current PoX operation
*
Expand Down Expand Up @@ -592,7 +606,6 @@ export class StackingClient {

/**
* Check if account has minimum require amount of Stacks for stacking
*
* @returns {Promise<boolean>} that resolves to a bool if the operation succeeds
*/
async hasMinimumStx(): Promise<boolean> {
Expand All @@ -603,9 +616,7 @@ export class StackingClient {

/**
* Check if account can lock stx
*
* @param {CanLockStxOptions} options - a required lock STX options object
*
* @returns {Promise<StackingEligibility>} that resolves to a StackingEligibility object if the operation succeeds
*/
async canStack({ poxAddress, cycles }: CanLockStxOptions): Promise<StackingEligibility> {
Expand Down Expand Up @@ -648,9 +659,7 @@ export class StackingClient {

/**
* Generate and broadcast a stacking transaction to lock STX
*
* @param {LockStxOptions} options - a required lock STX options object
*
* @returns {Promise<string>} that resolves to a broadcasted txid if the operation succeeds
*/
async stack({
Expand Down Expand Up @@ -737,9 +746,7 @@ export class StackingClient {

/**
* As a delegatee, generate and broadcast a transaction to create a delegation relationship
*
* @param {DelegateStxOptions} options - a required delegate STX options object
*
* @returns {Promise<string>} that resolves to a broadcasted txid if the operation succeeds
*/
async delegateStx({
Expand Down Expand Up @@ -774,9 +781,7 @@ export class StackingClient {

/**
* As a delegator, generate and broadcast transactions to stack for multiple delegatees. This will lock up tokens owned by the delegatees.
*
* @param {DelegateStackStxOptions} options - a required delegate stack STX options object
*
* @returns {Promise<string>} that resolves to a broadcasted txid if the operation succeeds
*/
async delegateStackStx({
Expand Down Expand Up @@ -870,9 +875,7 @@ export class StackingClient {

/**
* As a delegator, generate and broadcast a transaction to commit partially committed delegatee tokens
*
* @param {StackAggregationCommitOptions} options - a required stack aggregation commit options object
*
* @returns {Promise<string>} that resolves to a broadcasted txid if the operation succeeds
*/
async stackAggregationCommit({
Expand Down Expand Up @@ -938,7 +941,6 @@ export class StackingClient {

/**
* As a delegator, generate and broadcast a transaction to increase partial commitment committed delegatee tokens
*
* @param {StackAggregationIncreaseOptions} options - a required stack aggregation increase options object
* @category PoX-2
* @returns {Promise<string>} that resolves to a broadcasted txid if the operation succeeds
Expand Down
44 changes: 38 additions & 6 deletions packages/stacking/tests/stacking.test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
import { bigIntToBytes, bytesToHex, hexToBytes } from '@stacks/common';
import { base58CheckDecode } from '@stacks/encryption';
import { StacksTestnet } from '@stacks/network';
import { StacksMainnet, StacksTestnet } from '@stacks/network';
import {
AnchorMode,
bufferCV,
ClarityType,
ReadOnlyFunctionOptions,
SignedContractCallOptions,
TupleCV,
bufferCV,
intCV,
noneCV,
ReadOnlyFunctionOptions,
responseErrorCV,
responseOkCV,
SignedContractCallOptions,
someCV,
standardPrincipalCV,
trueCV,
tupleCV,
TupleCV,
uintCV,
validateContractCall,
} from '@stacks/transactions';
import fetchMock from 'jest-fetch-mock';
import { StackingClient } from '../src';
import { PoXAddressVersion, StackingErrors } from '../src/constants';
import { decodeBtcAddress, poxAddressToBtcAddress } from '../src/utils';
import { V2_POX_REGTEST_POX_3 } from './apiMockingHelpers';
import { V2_POX_REGTEST_POX_3, setApiMocks } from './apiMockingHelpers';

const poxInfo = {
contract_id: 'ST000000000000000000002AMW42H.pox',
Expand Down Expand Up @@ -1084,3 +1085,34 @@ test('client operations with contract principal stacker', () => {
);
expect(async () => await client.getStatus()).not.toThrow();
});

test('getSecondsUntilStackingDeadline', async () => {
const network = new StacksMainnet({ url: 'http://localhost:3999' });
const client = new StackingClient('', network);

setApiMocks({
'/extended/v1/info/network_block_times': `{"testnet":{"target_block_time":120},"mainnet":{"target_block_time":600}}`,
'/v2/pox': `{"contract_id":"ST000000000000000000002AMW42H.pox-3","pox_activation_threshold_ustx":600058115845055,"first_burnchain_block_height":0,"current_burnchain_block_height":275,"prepare_phase_block_length":1,"reward_phase_block_length":4,"reward_slots":8,"rejection_fraction":3333333333333333,"total_liquid_supply_ustx":60005811584505576,"current_cycle":{"id":54,"min_threshold_ustx":1875190000000000,"stacked_ustx":0,"is_pox_active":false},"next_cycle":{"id":55,"min_threshold_ustx":1875190000000000,"min_increment_ustx":7500726448063,"stacked_ustx":0,"prepare_phase_start_block_height":279,"blocks_until_prepare_phase":4,"reward_phase_start_block_height":280,"blocks_until_reward_phase":5,"ustx_until_pox_rejection":14656114351294034000},"min_amount_ustx":1875190000000000,"prepare_cycle_length":1,"reward_cycle_id":54,"reward_cycle_length":5,"rejection_votes_left_required":14656114351294034000,"next_reward_cycle_in":5,"contract_versions":[{"contract_id":"ST000000000000000000002AMW42H.pox","activation_burnchain_block_height":0,"first_reward_cycle_id":0},{"contract_id":"ST000000000000000000002AMW42H.pox-2","activation_burnchain_block_height":107,"first_reward_cycle_id":22},{"contract_id":"ST000000000000000000002AMW42H.pox-3","activation_burnchain_block_height":111,"first_reward_cycle_id":23}]}`,
});

let seconds = await client.getSecondsUntilStackingDeadline();
expect(seconds).toBe(4 * 10 * 60); // four blocks until prepare phase

setApiMocks({
'/extended/v1/info/network_block_times': `{"testnet":{"target_block_time":120},"mainnet":{"target_block_time":600}}`,
'/v2/pox': `{"contract_id":"ST000000000000000000002AMW42H.pox-3","pox_activation_threshold_ustx":600058812952055,"first_burnchain_block_height":0,"current_burnchain_block_height":344,"prepare_phase_block_length":1,"reward_phase_block_length":4,"reward_slots":8,"rejection_fraction":3333333333333333,"total_liquid_supply_ustx":60005881295205576,"current_cycle":{"id":68,"min_threshold_ustx":1875190000000000,"stacked_ustx":0,"is_pox_active":false},"next_cycle":{"id":69,"min_threshold_ustx":1875190000000000,"min_increment_ustx":7500735161900,"stacked_ustx":0,"prepare_phase_start_block_height":344,"blocks_until_prepare_phase":0,"reward_phase_start_block_height":345,"blocks_until_reward_phase":1,"ustx_until_pox_rejection":5198637306263702000},"min_amount_ustx":1875190000000000,"prepare_cycle_length":1,"reward_cycle_id":68,"reward_cycle_length":5,"rejection_votes_left_required":5198637306263702000,"next_reward_cycle_in":1,"contract_versions":[{"contract_id":"ST000000000000000000002AMW42H.pox","activation_burnchain_block_height":0,"first_reward_cycle_id":0},{"contract_id":"ST000000000000000000002AMW42H.pox-2","activation_burnchain_block_height":107,"first_reward_cycle_id":22},{"contract_id":"ST000000000000000000002AMW42H.pox-3","activation_burnchain_block_height":111,"first_reward_cycle_id":23}]}`,
});

seconds = await client.getSecondsUntilStackingDeadline();
expect(seconds).toBe(0); // this time we are in the prepare phase

// warning: manually changed response to negative value
setApiMocks({
'/extended/v1/info/network_block_times': `{"testnet":{"target_block_time":120},"mainnet":{"target_block_time":600}}`,
'/v2/pox': `{"contract_id":"ST000000000000000000002AMW42H.pox-3","pox_activation_threshold_ustx":600058812952055,"first_burnchain_block_height":0,"current_burnchain_block_height":344,"prepare_phase_block_length":1,"reward_phase_block_length":4,"reward_slots":8,"rejection_fraction":3333333333333333,"total_liquid_supply_ustx":60005881295205576,"current_cycle":{"id":68,"min_threshold_ustx":1875190000000000,"stacked_ustx":0,"is_pox_active":false},"next_cycle":{"id":69,"min_threshold_ustx":1875190000000000,"min_increment_ustx":7500735161900,"stacked_ustx":0,"prepare_phase_start_block_height":344,"blocks_until_prepare_phase":-50,"reward_phase_start_block_height":345,"blocks_until_reward_phase":1,"ustx_until_pox_rejection":5198637306263702000},"min_amount_ustx":1875190000000000,"prepare_cycle_length":1,"reward_cycle_id":68,"reward_cycle_length":5,"rejection_votes_left_required":5198637306263702000,"next_reward_cycle_in":1,"contract_versions":[{"contract_id":"ST000000000000000000002AMW42H.pox","activation_burnchain_block_height":0,"first_reward_cycle_id":0},{"contract_id":"ST000000000000000000002AMW42H.pox-2","activation_burnchain_block_height":107,"first_reward_cycle_id":22},{"contract_id":"ST000000000000000000002AMW42H.pox-3","activation_burnchain_block_height":111,"first_reward_cycle_id":23}]}`,
});

seconds = await client.getSecondsUntilStackingDeadline();
expect(seconds).toBeLessThan(0); // negative (deadline passed)
expect(seconds).toBe(-50 * 10 * 60); // this time we are in the prepare phase
});
Loading

0 comments on commit 3c819c2

Please sign in to comment.