From b2231967701a96d5713622cebf6a1d1bb02f8514 Mon Sep 17 00:00:00 2001 From: Lavanya Kasturi Date: Mon, 12 Dec 2022 11:27:16 -0600 Subject: [PATCH] docs: add stacks js remote content (#1409) --- docs/faq.md | 25 + .../authenticate-users-with-connect.md | 274 +++++++++++ docs/feature-guides/sign-messages.md | 251 ++++++++++ docs/feature-guides/sign-transactions.md | 457 ++++++++++++++++++ docs/feature-guides/store-data-securely.md | 177 +++++++ docs/getting-started.md | 5 + .../how-to-integrate-stacking-delegation.md | 212 ++++++++ .../how-to-integrate-stacking.md | 349 +++++++++++++ .../how-to-migrate-from-blockstack.js.md | 5 + .../how-to-use-stacks-connect-with-angular.md | 231 +++++++++ docs/includes/stacks.js-starters-note.mdx | 3 + docs/overview.md | 21 + 12 files changed, 2010 insertions(+) create mode 100644 docs/faq.md create mode 100644 docs/feature-guides/authenticate-users-with-connect.md create mode 100644 docs/feature-guides/sign-messages.md create mode 100644 docs/feature-guides/sign-transactions.md create mode 100644 docs/feature-guides/store-data-securely.md create mode 100644 docs/getting-started.md create mode 100644 docs/how-to-guides/how-to-integrate-stacking-delegation.md create mode 100644 docs/how-to-guides/how-to-integrate-stacking.md create mode 100644 docs/how-to-guides/how-to-migrate-from-blockstack.js.md create mode 100644 docs/how-to-guides/how-to-use-stacks-connect-with-angular.md create mode 100644 docs/includes/stacks.js-starters-note.mdx create mode 100644 docs/overview.md diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 00000000..ba1a8bfd --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,25 @@ +--- +title: FAQ's +--- + +#### **What are Post Conditions?** + +- Any supplied post-conditions are always verified, regardless of "mode" (and abort the tx if any supplied PC evaluates to false). +- The "mode" (allow/deny) only applies to any asset (stx/ft/nft) transfer that is not mentioned in the post-conditions (can be thought of as "ALLOW-additional-asset-transfer" or "DENY-additional-asset-transfer") + +Example: In deny mode, an additional asset transfer (not covered by PCs) will abort the tx. In deny mode without PCs a tx will only fail due to PCs if an asset is transferred. + +Post-conditions are less a part of clarity (the language), but more a part of transactions. +Users could send the otherwise-identical transaction (Example: contract-call, executing a function on the blockchain) with OR without different post-conditions, in allow OR deny mode. +The PCs are managed by the user/wallet/app that's creating the tx; so they are a bit different from the other "safety" features of clarity (Example: asserts, try, https://book.clarity-lang.org/ch06-00-control-flow.html) + +#### **How to fix the regenerator-runtime?** + +If using @stacks/connect with vite, rollup, svelte, or vue a package `regenerator-runtime` needs to be manually added to successfully build the project. + +(Fix is in progress) + +#### **How to fix the BigInt or how to fix if BigInt doesn’t support X?** + +BigInt’s work in all modern browsers, but some bundlers try to optimize them incorrectly. If you are targeting browsers that are too outdated, BigInts might fail. +To solve this set your project `browserslist` to the following [package.json](https://github.com/hirosystems/stacks.js-starters/blob/efb93261b59494f4eb34a7cb5db5d82a84bd3b7c/templates/template-react/package.json#L34-L40) diff --git a/docs/feature-guides/authenticate-users-with-connect.md b/docs/feature-guides/authenticate-users-with-connect.md new file mode 100644 index 00000000..9d8bfa93 --- /dev/null +++ b/docs/feature-guides/authenticate-users-with-connect.md @@ -0,0 +1,274 @@ +--- +title: Authenticate Users +--- + + +import StacksjsStartersNote from '../includes/stacks.js-starters-note.mdx'; + + + +This guide explains how to authenticate users with the [`connect`](https://github.com/hirosystems/connect#readme) package of Stacks.js. + +Authentication provides a way for users to identify themselves to an app while retaining complete control over their credentials and personal details. It can be integrated alone or used in conjunction with [transaction signing](/build-apps/transaction-signing) and [data storage](/build-apps/data-storage), for which it is a prerequisite. + +Users who register for your app can subsequently authenticate to any other app with support for the [Blockchain Naming System](https://docs.stacks.co/build-apps/references/bns) and vice versa. + +See the [To-dos example app](/example-apps/to-dos) for a concrete example of this feature in practice. + +## How it works + +The authentication flow with Stacks is similar to the typical client-server flow used by centralized sign in services (for example, OAuth). However, with Stacks the authentication flow happens entirely client-side. + +An app and authenticator, such as [the Stacks Wallet](https://www.hiro.so/wallet/install-web), communicate during the authentication flow by passing back and forth two tokens. The requesting app sends the authenticator an `authRequest` token. Once a user approves authentication, the authenticator responds to the app with an `authResponse` token. + +These tokens are based on [a JSON Web Token (JWT) standard](https://tools.ietf.org/html/rfc7519) with additional support for the `secp256k1` curve used by Bitcoin and many other cryptocurrencies. They are passed via URL query strings. + +See the [`authRequest`](#authrequest-payload-schema) and [`authResponse`](#authresponse-payload-schema) payload schemas below for more details about what data they contain. + +When a user chooses to authenticate an app, it sends the `authRequest` token to the authenticator via a URL query string with an equally named parameter: + +`https://wallet.hiro.so/...?authRequest=j902120cn829n1jnvoa...` + +When the authenticator receives the request, it generates an `authResponse` token for the app using an _ephemeral transit key_ . The ephemeral transit key is just used for the particular instance of the app, in this case, to sign the `authRequest`. + +The app stores the ephemeral transit key during request generation. The public portion of the transit key is passed in the `authRequest` token. The authenticator uses the public portion of the key to encrypt an _app private key_ which is returned via the `authResponse`. + +The authenticator generates the app private key from the user's _identity address private key_ and the app's domain. The app private key serves three functions: + +1. It is used to create credentials that give the app access to a storage bucket in the user's Gaia hub +2. It is used in the end-to-end encryption of files stored for the app in the user's Gaia storage. +3. It serves as a cryptographic secret that apps can use to perform other cryptographic functions. + +Finally, the app private key is deterministic, meaning that the same private key will always be generated for a given Stacks address and domain. + +The first two of these functions are particularly relevant to [data storage with Stacks.js](/build-apps/data-storage). + +[Learn more about keypairs](#key-pairs) used by authentication. + +## Install dependency + +The following dependency must be installed: + +``` +npm install @stacks/connect +``` + +## Initiate userSession object + +Apps keep track of user authentication state with the `userSession` object, initiated with the `UserSession` and `AppConfig` classes: + +```js +import { AppConfig, UserSession } from '@stacks/connect'; + +const appConfig = new AppConfig(['store_write', 'publish_data']); +const userSession = new UserSession({ appConfig }); +``` + +The main thing to decide here is what permission scopes your app needs from the user during authentication. + +Apps may request any of the following scopes: + +| Scope | Definition | +| -------------- | ------------------------------------------------------------------------------- | +| `store_write` | Read and write data to the user's Gaia hub in an app-specific storage bucket. | +| `publish_data` | Publish data so other users of the app can discover and interact with the user. | + +The default scopes are `['store_write']` if no `scopes` array is provided when initializing the `appConfig` object. + +We recommend you initiate the `userSession` object just once in your app then reference it using imports where needed. + +## Initiate authentication flow + +Apps prompt both new and existing users to authenticate with the `showConnect` function: + +```js +import { AppConfig, UserSession, showConnect } from '@stacks/connect'; + +const appConfig = new AppConfig(['store_write', 'publish_data']); +const userSession = new UserSession({ appConfig }); + +function authenticate() { + showConnect({ + appDetails: { + name: 'My App', + icon: window.location.origin + '/my-app-logo.svg', + }, + redirectTo: '/', + onFinish: () => { + let userData = userSession.loadUserData(); + // Save or otherwise utilize userData post-authentication + }, + userSession: userSession, + }); +} +``` + +`showConnect` triggers the display of a modal that initiates the authentication process for users, one in which they'll authenticate with a _Secret Key_ that's used to encrypt their private data. + +![Modal displayed by showConnect function](/img/todos-get-started.png) + +The `showConnect` function accepts a number of properties within a parameter object such as: + +- The app's `name` and `icon`: provided as strings comprising the `appDetails` object property. +- The `redirectTo` string: used to provide a URL to which the user should be redirected upon successful authentication. The `onFinish` callback serves a similar purpose by handling successful authentication within a context of a popup window. +- The `userSession` object initiated above. + +Once the user selects the button presented in this modal, they are passed to the Stacks Wallet for authenticator with the `authRequest` token as a GET parameter. From there they can confirm authentication and generate a new _Secret Key_ or Stacks identity before doing so, as needed before coming back to the app. + +## Handle pending authentication + +Unless the user has confirmed authentication within the context of a popup window, they will get redirected back to the app via the `redirectTo` address provided above, at which point the app needs to handle the pending authentication state using the `authResponse` value provided as a GET parameter: + +```jsx +import { AppConfig, UserSession, showConnect } from '@stacks/connect'; + +const appConfig = new AppConfig(['store_write', 'publish_data']); +const userSession = new UserSession({ appConfig }); + +window.onload = function () { + if (userSession.isSignInPending()) { + userSession.handlePendingSignIn().then(userData => { + // Save or otherwise utilize userData post-authentication + }); + } else if (userSession.isUserSignedIn()) { + // Handle case in which user is already authenticated + } +}; +``` + +The `isSignInPending` method of the `userSession` object is used to detect whether the user needs to handle a pending authentication state upon page load. + +The `handlePendingSignIn` method is then used to handle that state, returning a `userData` object with all the data needed to save the user's information into their session. + +The authenticated state can later be detected by the `isUserSignedIn` method in case any particular handling is needed then. + +:::note + +It's especially important to implement `handlePendingSignIn` within the context of mobile apps. + +::: + +If the user has indeed confirmed authentication in the context of a popup window, the authenticator will resolve the pending authentication state automatically with the app within the parent window. + +It will then trigger the `onFinish` function provided above, which can be used similarly to save the user's information into their session as retrieved with `userSession.loadUserData()`. + +## Usage in React Apps + +Import the `useConnect` from the [`connect-react`](https://github.com/hirosystems/connect/) package to integrate authentication more seamlessly into React apps. + +``` +npm install @stacks/connect-react +``` + +```jsx +import { useConnect } from '@stacks/connect-react'; + +const AuthButton = () => { + const { doOpenAuth } = useConnect(); + return ; +}; +``` + +## Key pairs + +Authentication with Stacks makes extensive use of public key cryptography generally and ECDSA with the `secp256k1` curve in particular. + +The following sections describe the three public-private key pairs used, including how they're generated, where they're used and to whom private keys are disclosed. + +### Transit private key + +The transit private is an ephemeral key that is used to encrypt secrets that +need to be passed from the authenticator to the app during the +authentication process. It is randomly generated by the app at the beginning of +the authentication response. + +The public key that corresponds to the transit private key is stored in a single +element array in the `public_keys` key of the authentication request token. The +authenticator encrypts secret data such as the app private key using this +public key and sends it back to the app when the user signs in to the app. The +transit private key signs the app authentication request. + +### Identity address private key + +The identity address private key is derived from the user's keychain phrase and +is the private key of the Stacks username that the user chooses to use to sign in +to the app. It is a secret owned by the user and never leaves the user's +instance of the authenticator. + +This private key signs the authentication response token for an app to indicate that the user approves sign in to that app. + +### App private key + +The app private key is an app-specific private key that is generated from the +user's identity address private key using the `domain_name` as input. + +The app private key is securely shared with the app on each authentication, encrypted by the authenticator with the transit public key. Because the transit key is only stored on the client side, this prevents a man-in-the-middle attack where a server or internet provider could potentially snoop on the app private key. + +## authRequest Payload Schema + +```jsx +const requestPayload = { + jti, // UUID + iat, // JWT creation time in seconds + exp, // JWT expiration time in seconds + iss, // legacy decentralized identifier generated from transit key + public_keys, // single entry array with public key of transit key + domain_name, // app origin + manifest_uri, // url to manifest file - must be hosted on app origin + redirect_uri, // url to which the authenticator redirects user on auth approval - must be hosted on app origin + version, // version tuple + do_not_include_profile, // a boolean flag asking authenticator to send profile url instead of profile object + supports_hub_url, // a boolean flag indicating gaia hub support + scopes, // an array of string values indicating scopes requested by the app +}; +``` + +## authResponse Payload Schema + +```jsx +const responsePayload = { + jti, // UUID + iat, // JWT creation time in seconds + exp, // JWT expiration time in seconds + iss, // legacy decentralized identifier (string prefix + identity address) - this uniquely identifies the user + private_key, // encrypted private key payload + public_keys, // single entry array with public key + profile, // profile object + username, // Stacks username (if any) + core_token, // encrypted core token payload + email, // email if email scope is requested & email available + profile_url, // url to signed profile token + hubUrl, // url pointing to user's gaia hub + version, // version tuple +}; +``` + +## Decode authRequest or authResponse + +To decode a token and see what data it holds: + +1. Copy the `authRequest` or `authResponse` string from the URL during authentication. +2. Navigate to [jwt.io](https://jwt.io/). +3. Paste the full token there. + + The output should look similar to below: + + ```json + { + "jti": "f65f02db-9f42-4523-bfa9-8034d8edf459", + "iat": 1555641911, + "exp": 1555645511, + "iss": "did:btc-addr:1ANL7TNdT7TTcjVnrvauP7Mq3tjcb8TsUX", + "public_keys": ["02f08d5541bf611ded745cc15db08f4447bfa55a55a2dd555648a1de9759aea5f9"], + "domain_name": "http://localhost:8080", + "manifest_uri": "http://localhost:8080/manifest.json", + "redirect_uri": "http://localhost:8080", + "version": "1.3.1", + "do_not_include_profile": true, + "supports_hub_url": true, + "scopes": ["store_write", "publish_data"], + "private_key": "4447bfa55a55a2dd555648a1d02f08d759aea5f945cc15db08f" + } + ``` + + The `iss` property is a decentralized identifier or `did`. This identifies the user and the username to the app. The specific `did` is a `btc-addr`. diff --git a/docs/feature-guides/sign-messages.md b/docs/feature-guides/sign-messages.md new file mode 100644 index 00000000..8d7c2f57 --- /dev/null +++ b/docs/feature-guides/sign-messages.md @@ -0,0 +1,251 @@ +--- +title: Sign Messages +--- + +import StacksjsStartersNote from '../includes/stacks.js-starters-note.mdx'; + + + +This guide explains how to prompt users to sign a message. + +The user will be prompted a popup from the Hiro Wallet showing the message you would like them to sign. + +The user can then click on the ‘Sign’ button which will return the signature data and the user's publicKey to your app. You can then verify the signature by passing the signature data and the public key to the [`stacks.js`](https://github.com/hirosystems/stacks.js) `verifySignature` method. + +The message can be any utf-8 string. + +Internally the string will be hashed using `sha256` and signed with `secp256k1` using the user's privateKey + +## Install dependency + +:::tip + +In order to utilize the latest transaction signing with the Hiro Wallet, use a version >= 6.6.0 of the `@stacks/connect` NPM package. + +::: + +The following dependency must be installed: + +``` +npm install @stacks/connect +``` + +## Initiate session + +Users must authenticate to an app before you request message signing. Users can install an authenticator like [the Hiro Wallet](https://www.hiro.so/wallet/install-web). + +See the [authentication guide](https://docs.hiro.so/build-apps/authentication) before proceeding to integrate the following message signing capabilities. + +## Prompt to sign a message + +Call the `openSignatureRequestPopup` function provided by the `connect`  package to trigger the display of the message signing prompt. + +```tsx +import { openSignatureRequestPopup } from '@stacks/connect'; +import { StacksTestnet } from '@stacks/network'; + +const message = 'Hello World'; + +openSignatureRequestPopup({ + message, + network: new StacksTestnet(), // for mainnet, `new StacksMainnet()` + appDetails: { + name: 'My App', + icon: window.location.origin + '/my-app-logo.svg', + }, + onFinish(data) { + console.log('Signature of the message', data.signature); + console.log('Use public key:', data.publicKey); + }, +}); +``` + +Several parameters are available for calling `openSignatureRequestPopup`. Here's the exact interface for them: + +```tsx +interface SignatureRequestOptions { + message: string; + onFinish?: (data: SignatureData) => void; + onCancel?: (data: SignatureData) => void; + appDetails: { + name: string; + icon: string; + }; + authOrigin?: string; + stxAddress?: string; + userSession?: UserSession; +} +``` + +## Getting the signed message back after completion + +The `openSignatureRequestPopup` signing method from `@stacks/connect` allows you to specify an `onFinish` callback. +This callback will be triggered after the user has successfully signed the message. + +You can get the signature of the message via the arguments passed to `onFinish`. Your callback will be fired with a single argument, which is an object with the following properties: + +```ts +export interface SignatureData { + /* Hex encoded DER signature */ + signature: string; + /* Hex encoded private string taken from privateKey */ + publicKey: string; +} +``` + +```ts +const onFinish = (data: SignatureData) => { + console.log('Signature', data.signature); + console.log('PublicKey', data.publicKey); +}; +``` + +## How to verify a signature + +You can easily verify the signature using the [`@stacks/stacks.js`](https://github.com/hirosystems/stacks.js) package as seen in the following example. + +```ts +import { verifyMessageSignatureRsv } from '@stacks/encryption'; + +const message = 'Hello World'; + +openSignatureRequestPopup({ + // ... + onFinish({ publicKey, signature }) { + const verified = verifyMessageSignatureRsv({ message, publicKey, signature }); + if (verified) { + // Trigger a notification explaining signature is verified + } + }, +}); +``` + +## Specifying the network for a transaction {#network-option} + +All of the methods included on this page accept a `network` option. By default, Connect uses a testnet network option. You can import a network configuration from the [`@stacks/network`](https://stacks.js.org/modules/network.html) package. + +```ts +import { StacksTestnet, StacksMainnet } from '@stacks/network'; + +const testnet = new StacksTestnet(); +const mainnet = new StacksMainnet(); + +// use this in your messe signing method: + +openSignatureRequestPopup({ + network: mainnet, + // other relevant options +}); +``` + +## Usage in React Apps + +Import the `useConnect` helper from [`connect-react`](https://github.com/hirosystems/connect) package to sign messages more seamlessly with React apps. +You must install a version >= 15.0.0 + +``` +npm install @stacks/connect-react +``` + +Use the function with the same parameters as outlined above. However, you don't have to specify `appDetails` since they are detected automatically if `useConnect` has been used already [for authentication](/build-apps/authentication#usage-in-react-apps). + +```tsx +import { useConnect } from '@stacks/connect-react'; + +const MyComponent = () => { + const { sign } = useConnect(); + + const onClick = async () => { + const options = { + /** See examples above */ + }; + await sign(options); + }; + + return Sign message; +}; +``` + +## Signature request / response payload + +Under the hood, `@stacks/connect` will serialize and deserialize data between your app and the Hiro Wallet. + +These payloads are tokens that conform to the [JSON Web Token (JWT) standard](https://tools.ietf.org/html/rfc7519) with additional support for the `secp256k1` curve used by Bitcoin and many other cryptocurrencies. + +### Signature Request Payload + +When an application triggers a message signing from `@stacks/connect`, the options of that signature request are serialized into a `signatureRequest` payload. The `signatureRequest` is similar to the [authRequest](https://docs.hiro.so/build-apps/authentication#authrequest-payload-schema) payload used for authentication. + +The signature request payload has the following schema, in addition to the standard JWT required fields: + +```ts +interface SignatureRequestPayload { + message: string; + publicKey: string; + /** + * Provide the Hiro Wallet with a suggested account to sign this transaction with. + * This is set by default if a `userSession` option is provided. + */ + stxAddress?: string; + appDetails?: AuthOptions['appDetails']; + network?: StacksNetwork; +} +``` + +### Signature Response payload + +After the user signs the message, a `signatureResponse` payload is sent back to your app. + +```ts +interface SignatureData { + /* Hex encoded DER signature */ + signature: string; + /* Hex encoded private string taken from privateKey */ + publicKey: string; +} +``` + +## StacksProvider injected variable + +When users have the [Hiro Wallet](https://www.hiro.so/wallet/install-web) extension installed, the extension will inject a global `StacksProvider` variable into the JavaScript context of your web app. This allows your JavaScript code to hook into the extension, and make authentication, transaction and signature requests. `@stacks/connect` automatically detects and uses this global variable for you. + +At the moment, only the Hiro Wallet extension includes a `StacksProvider`, however, ideally more wallets (and mobile wallets) will support this format, so that your app can be compatible with any Stacks wallet that has functionality to embed web applications. + +In your web application, you can check to see if the user has a compatible wallet installed by checking for the presence of `window.StacksProvider`. + +Here is the interface for the `StacksProvider` variable. + +```ts +interface StacksProvider { + /** @deprecated */ + getURL: () => Promise; + /** + * Make a transaction request + * + * @param payload - a JSON web token representing a transaction request + */ + transactionRequest(payload: string): Promise; + /** + * Make an authentication request + * + * @param payload - a JSON web token representing an auth request + * + * @returns an authResponse string in the form of a JSON web token + */ + authenticationRequest(payload: string): Promise; + signatureRequest(payload: string): Promise; + getProductInfo: + | undefined + | (() => { + version: string; + name: string; + meta?: { + tag?: string; + commit?: string; + [key: string]: any; + }; + [key: string]: any; + }); +} +``` + diff --git a/docs/feature-guides/sign-transactions.md b/docs/feature-guides/sign-transactions.md new file mode 100644 index 00000000..37894bbb --- /dev/null +++ b/docs/feature-guides/sign-transactions.md @@ -0,0 +1,457 @@ +--- +title: Sign Transactions +--- + + +import StacksjsStartersNote from '../includes/stacks.js-starters-note.mdx'; + + + +This guide explains how to prompt users to sign [transactions](https://docs.stacks.co/understand-stacks/transactions) and broadcast them to the Stacks blockchain by implementing the [`connect`](https://github.com/hirosystems/connect) package of Stacks.js. + +Transaction signing provides a way for users execute Clarity smart contracts that are relevant to your app then handle the result as appropriate. + +Users can sign transactions that exchange fungible or non-fungible tokens with upfront guarantees that help them retain control over their digital assets. + +There are three types of transactions: + +1. STX transfer +2. Contract deployment +3. Contract execution + +See the public registry tutorial for a concrete example of these capabilities in practice. + +## Install dependency + +:::tip + +In order to utilize the latest transaction signing with the Stacks Wallet, use version 5 of the `@stacks/connect` NPM package. + +::: + +The following dependency must be installed: + +``` +npm install @stacks/connect +``` + +## Initiate session + +Users must authenticate to an app before the `connect` package will work to prompt them for signing and broadcasting transactions to the Stacks blockchain with an authenticator such as [the Stacks Wallet](https://www.hiro.so/wallet/install-web). + +See the authentication guide before proceeding to integrate the following transaction signing capabilities in cases where `userSession.isUserSignedIn()` returns `true`. + +## Get the user's Stacks address + +After your user has authenticated with their Stacks Wallet, you can get their Stacks address from their `profile`. + +```ts +const profile = userSession.loadUserData().profile.stxAddress; + +const mainnetAddress = stxAddresses.mainnet; +// "SP2K5SJNTB6YP3VCTCBE8G35WZBPVN6TDMDJ96QAH" +const testnetAddress = stxAddresses.testnet; +// "ST2K5SJNTB6YP3VCTCBE8G35WZBPVN6TDMFEVESR6" +``` + +## Prompt to transfer STX + +Call the `openSTXTransfer` function provided by the `connect` package to trigger the display of a transaction signing prompt for transferring STX: + +```tsx +import { openSTXTransfer } from '@stacks/connect'; +import { StacksTestnet } from '@stacks/network'; + +openSTXTransfer({ + recipient: 'ST2EB9WEQNR9P0K28D2DC352TM75YG3K0GT7V13CV', + amount: '100', + memo: 'Reimbursement', + network: new StacksTestnet(), // for mainnet, `new StacksMainnet()` + appDetails: { + name: 'My App', + icon: window.location.origin + '/my-app-logo.svg', + }, + onFinish: data => { + console.log('Stacks Transaction:', data.stacksTransaction); + console.log('Transaction ID:', data.txId); + console.log('Raw transaction:', data.txRaw); + }, +}); +``` + +Several parameters are available for calling `openSTXTransfer`. Here's the exact interface for them: + +```tsx +interface STXTransferOptions { + recipient: string; + amount: string; + memo?: string; + network: StacksNetwork; + fee: number | string; + appDetails: { + name: string; + icon: string; + }; + onFinish: (data: FinishedTxData) => void; +} +``` + +| parameter | type | required | description | +| ---------- | ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| recipient | string | true | STX address for recipient of transfer | +| amount | string | true | Amount of microstacks (1 STX = 1,000,000 microstacks) to be transferred provided as string to prevent floating point errors. | +| appDetails | object | true | Dictionary that requires `name` and `icon` for app | +| onFinish | function | true | Callback executed by app when transaction has been signed and broadcasted. [Read more](#onFinish-option) | +| memo | string | false | Optional memo for inclusion with transaction | +| network | StacksNetwork | false | Specify the network that this transaction should be completed on. [Read more](#network-option) | +| fee | number \| string | false | Optional fee amount in microstacks (1 STX = 1,000,000 microstacks) for overwriting the wallet's default fee value. [Read more](https://forum.stacks.org/t/mempool-congestion-on-stacks-observations-and-next-steps-from-hiro/12325/5) | + +## Prompt to deploy smart contract + +Call the `openContractDeploy` function provided by the `connect` package to trigger the display of a transaction signing prompt for deploying a smart contract: + +```tsx +import { openContractDeploy } from '@stacks/connect'; + +const codeBody = '(begin (print "hello, world"))'; + +openContractDeploy({ + contractName: 'my-contract-name', + codeBody, + appDetails: { + name: 'My App', + icon: window.location.origin + '/my-app-logo.svg', + }, + onFinish: data => { + console.log('Stacks Transaction:', data.stacksTransaction); + console.log('Transaction ID:', data.txId); + console.log('Raw transaction:', data.txRaw); + }, +}); +``` + +Several parameters are available for calling `openContractDeploy`. Here's the exact interface for them: + +```tsx +interface ContractDeployOptions { + codeBody: string; + contractName: string; + network: StacksNetwork; + fee: number | string; + appDetails: { + name: string; + icon: string; + }; + onFinish: (data: FinishedTxData) => void; +} +``` + +| parameter | type | required | description | +| ------------ | ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| codeBody | string | true | Clarity source code for contract | +| contractName | string | true | Name for contract | +| appDetails | object | true | Dictionary that requires `name` and `icon` for app | +| onFinish | function | true | Callback executed by app when transaction has been signed and broadcasted. [Read more](#onFinish-option) | +| network | StacksNetwork | false | Specify the network that this transaction should be completed on. [Read more](#network-option) | +| fee | number \| string | false | Optional fee amount in microstacks (1 STX = 1,000,000 microstacks) for overwriting the wallet's default fee value. [Read more](https://forum.stacks.org/t/mempool-congestion-on-stacks-observations-and-next-steps-from-hiro/12325/5) | + +:::info + +Contracts will deploy to the Stacks address of the authenticated user. + +::: + +## Prompt to execute contract + +Call the `openContractCall` function provided by the `connect` package to trigger the display of a transaction signing prompt for executing a contract. + +As an example, consider this simple Clarity contract: + +```clarity +(define-public + (my-func + (arg-uint uint) + (arg-int int) + (arg-buff (buff 20)) + (arg-string-ascii (string-ascii 20)) + (arg-string-utf8 (string-utf8 20)) + (arg-principal principal) + (arg-bool bool) + ) + (ok u0) +) +``` + +To execute this function, invoke the `openContractCall` method. Use the `ClarityValue` types from `@stacks/transactions` to construct properly formatted arguments. + +```tsx +import { openContractCall } from '@stacks/connect'; +import { + uintCV, + intCV, + bufferCV, + stringAsciiCV, + stringUtf8CV, + standardPrincipalCV, + trueCV, +} from '@stacks/transactions'; + +const functionArgs = [ + uintCV(1234), + intCV(-234), + bufferCV(Buffer.from('hello, world')), + stringAsciiCV('hey-ascii'), + stringUtf8CV('hey-utf8'), + standardPrincipalCV('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6'), + trueCV(), +]; + +const options = { + contractAddress: 'ST22T6ZS7HVWEMZHHFK77H4GTNDTWNPQAX8WZAKHJ', + contractName: 'my-contract', + functionName: 'my-func', + functionArgs, + appDetails: { + name: 'My App', + icon: window.location.origin + '/my-app-logo.svg', + }, + onFinish: data => { + console.log('Stacks Transaction:', data.stacksTransaction); + console.log('Transaction ID:', data.txId); + console.log('Raw transaction:', data.txRaw); + }, +}; + +await openContractCall(options); +``` + +Several parameters are available for calling `openContractCall`. Here's the exact interface for them: + +```tsx +interface ContractCallOptions { + contractAddress: string; + functionName: string; + contractName: string; + functionArgs?: ClarityValue[]; + network: StacksNetwork; + fee: number | string; + appDetails: { + name: string; + icon: string; + }; + onFinish: (data: FinishedTxData) => void; +} +``` + +| parameter | type | required | description | +| --------------- | ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | +| contractAddress | string | true | Stacks address to which contract is deployed | +| contractName | string | true | Name of contract to sign | +| functionName | string | true | Name of function for signing / execution, which needs to be a [public function](https://docs.stacks.co/references/language-functions#define-public). | +| functionArgs | `ClarityValue[]` | true | Arguments for calling the function. [Learn more about constructing clarity values](https://github.com/blockstack/stacks.js/tree/master/packages/transactions#constructing-clarity-values). Defaults to `[]`. | +| appDetails | object | true | Dictionary that requires `name` and `icon` for app | +| onFinish | function | true | Callback executed by app when transaction has been signed and broadcasted. [Read more](#onFinish-option) | | +| network | StacksNetwork | false | Specify the network that this transaction should be completed on. [Read more](#network-option) | +| fee | number \| string | false | Optional fee amount in microstacks (1 STX = 1,000,000 microstacks) for overwriting the wallet's default fee value. [Read more](https://forum.stacks.org/t/mempool-congestion-on-stacks-observations-and-next-steps-from-hiro/12325/5) | + +## Getting the signed transaction back after completion {#onFinish-option} + +Each transaction signing method from `@stacks/connect` allows you to specify an `onFinish` callback. This callback will be triggered after the user has successfully broadcasted their transaction. The transaction will be broadcasted, but it will be pending until it has been mined on the Stacks blockchain. + +You can access some information about this transaction via the arguments passed to `onFinish`. Your callback will be fired with a single argument, which is an object with the following properties: + +```ts +interface FinishedTxData { + stacksTransaction: StacksTransaction; + txRaw: string; + txId: string; +} +``` + +The `StacksTransaction` type comes from the [`@stacks/transactions`](https://stacks.js.org/modules/transactions.html) library. + +The `txId` property can be used to provide a link to view the transaction in the explorer. + +```ts +const onFinish = data => { + const explorerTransactionUrl = 'https://explorer.stacks.co/txid/${data.txId}'; + console.log('View transaction in explorer:', explorerTransactionUrl); +}; +``` + +## Specifying the network for a transaction {#network-option} + +All of the methods included on this page accept a `network` option. By default, Connect uses a testnet network option. You can import a network configuration from the [`@stacks/network`](https://stacks.js.org/modules/network.html) package. + +```ts +import { StacksTestnet, StacksMainnet } from '@stacks/network'; + +const testnet = new StacksTestnet(); +const mainnet = new StacksMainnet(); + +// use this in your transaction signing methods: + +openSTXTransfer({ + network: mainnet, + // other relevant options +}); +``` + +## Usage in React Apps + +Import the `useConnect` from the [`connect-react`](https://github.com/hirosystems/connect) package to integrate transaction signing more seamlessly into React apps. + +``` +npm install @stacks/connect-react +``` + +Each transaction signing method is itself available as a function returned by `useConnect` though prefixed with `do` for consistency with React action naming standards: + +- `openContractCall` as `doContractCall` +- `openSTXTransfer` as `doSTXTransfer` +- `openContractDeploy` as `doContractDeploy` + +Use these functions with the same parameters as outlined above. However, you don't have to specify `appDetails` since they are detected automatically if `useConnect` has been used already [for authentication](/build-apps/authentication#usage-in-react-apps). + +```tsx +import { useConnect } from '@stacks/connect-react'; + +const MyComponent = () => { + const { doContractCall } = useConnect(); + + const onClick = async () => { + const options = { + /** See examples above */ + }; + await doContractCall(options); + }; + + return Call my contract; +}; +``` + +## Request testnet STX from faucet + +You may find it useful to request testnet STX from [the Explorer sandbox](https://explorer.stacks.co/sandbox/deploy?chain=testnet) while developing your app with the Stacks testnet. + +## Transaction request / response payload + +Under the hood, `@stacks/connect` will serialize and deserialize data between your app and the Stacks Wallet. + +These payloads are tokens that conform to the [JSON Web Token (JWT) standard](https://tools.ietf.org/html/rfc7519) with additional support for the `secp256k1` curve used by Bitcoin and many other cryptocurrencies. + +### Transaction Request Payload + +When an application triggers an transaction from `@stacks/connect`, the options of that transaction are serialized into a `transactionRequest` payload. The `transactionRequest` is similar to the [authRequest](/build-apps/authentication#authrequest-payload-schema) payload used for authentication. + +The transaction request payload has the following schema, in addition to the standard JWT required fields: + +```ts +interface TransactionRequest { + appDetails?: { + name: string; + icon: string; + }; + // 1 = "allow", 2 = "deny". + postConditionMode?: PostConditionMode; // number + // Serialized version of post conditions + postConditions?: string[]; + // JSON serialized version of `StacksNetwork` + // This allows the app to specify their default desired network. + // The user may switch networks before broadcasting their transaction. + network?: { + coreApiUrl: string; + chainID: ChainID; // number + }; + // `AnchorMode` defined in `@stacks/transactions` + anchorMode?: AnchorMode; // number + // The desired default stacks address to sign with. + // There is no guarantee that the transaction is signed with this address; + stxAddress?: string; + txType: TransactionDetails; // see below +} + +export enum TransactionTypes { + ContractCall = 'contract_call', + ContractDeploy = 'smart_contract', + STXTransfer = 'token_transfer', +} + +interface ContractCallPayload extends TransactionRequest { + contractAddress: string; + contractName: string; + functionName: string; + // Serialized Clarity values to be used as arguments in the contract call + functionArgs: string[]; + txType: TransactionTypes.ContractCall; +} + +interface ContractDeployPayload extends TransactinRequest { + contractName: string; + // raw source code for this contract + codeBody: string; + txType: TransactionTypes.ContractDeploy; +} + +interface StxTransferPayload extends TransactionRequest { + recipient: string; + // amount for this transaction, in microstacks + amount: string; + memo?: string; + txType: TransactionTypes.STXTransfer; +} +``` + +### Transaction Response payload + +After the user signs and broadcasts a transaction, a `transactionResponse` payload is sent back to your app. + +```ts +interface TransactionResponse { + txId: string; + // hex serialized version of this transaction + txRaw: string; +} +``` + +## StacksProvider injected variable + +When users have the [Stacks Wallet for Web](https://www.hiro.so/wallet/install-web) browser extension installed, the extension will inject a global `StacksProvider` variable into the JavaScript context of your web app. This allows your JavaScript code to hook into the extension, and make authentication and transaction requests. `@stacks/connect` automatically detects and uses this global variable for you. + +At the moment, only the Stacks Wallet for Web browser extension includes a `StacksProvider`, however, ideally more wallets (and mobile wallets) support this format, so that your app can be compatible with any Stacks Wallet that has functionality to embed web applications. + +In your web application, you can check to see if the user has a compatible wallet installed by checking for the presence of `window.StacksProvider`. + +Here is the interface for the `StacksProvider` variable. + +```ts +interface StacksProvider { + /** + * Make a transaction request + * + * @param payload - a JSON web token representing a transaction request + */ + transactionRequest(payload: string): Promise; + /** + * Make an authentication request + * + * @param payload - a JSON web token representing an auth request + * + * @returns an authResponse string in the form of a JSON web token + */ + authenticationRequest(payload: string): Promise; + getProductInfo: + | undefined + | (() => { + version: string; + name: string; + meta?: { + tag?: string; + commit?: string; + [key: string]: any; + }; + [key: string]: any; + }); +} +``` + diff --git a/docs/feature-guides/store-data-securely.md b/docs/feature-guides/store-data-securely.md new file mode 100644 index 00000000..c1beb60c --- /dev/null +++ b/docs/feature-guides/store-data-securely.md @@ -0,0 +1,177 @@ +--- +title: Store Data Securely +--- + +import StacksjsStartersNote from '../includes/stacks.js-starters-note.mdx'; + + + +This guide explains how to save and retrieve data for users with [Gaia](https://docs.stacks.co/build-apps/references/gaia) by implementing the [`connect`](https://github.com/hirosystems/connect/) and [`storage`](https://stacks.js.org/modules/storage.html) packages of Stacks.js. + +Data storage provides a way for users to save both public and private data off-chain while retaining complete control over it. + +Storing data off the Stacks blockchain ensures that apps can provide users with high performance and high availability for data reads and writes without the involvement of centralized parties that could compromise their privacy or accessibility. + +See the To-dos app tutorial for a concrete example of this feature in practice. + +## Install dependencies + +The following dependencies must be installed: + +``` +npm install @stacks/connect @stacks/storage +``` + +## Initiate session + +Users must authenticate to an app before the `storage` package will work to save or retrieve data on their behalf. + +See the authentication guide before proceeding to integrate the following data storage capabilities in cases where `userSession.isUserSignedIn()` returns `true`. + +## Save data for session user + +Gaia serves as a key-value store in which data is saved and retrieved as files to and from Gaia hubs owned by, or managed for, users. + +The default Gaia hub for users who authenticate to apps with [the Stacks Wallet](https://www.hiro.so/wallet/install-web) is run by Hiro PBC at `https://gaia.blockstack.org/`. It supports files up to 25 megabytes in size. + +:::tip + +Hiro recommends breaking data instances greater than 25 MB into several files, saving them individually, and recomposing them on retrieval. + +::: + +These files can comprise any type of data such as text, image, video or binary. + +Files are often saved as strings that represent stringified JSON objects and contain a variety of properties for a particular model. + +To save a file, first instantiate a `storage` object using the `userSession` object for an authenticated user. Then proceed to call its `putFile` method with relevant parameters: + +```js +import { AppConfig, UserSession } from '@stacks/connect'; +import { Storage } from '@stacks/storage'; + +const appConfig = new AppConfig(['store_write', 'publish_data']); +const userSession = new UserSession({ appConfig }); +const storage = new Storage({ userSession }); + +let fileName = 'car.json'; + +let fileData = { + color: 'blue', + electric: true, + purchaseDate: '2019-04-03', +}; + +const options = { + encrypt: true, +}; + +let fileUrl = storage.putFile(fileName, JSON.stringify(fileData), options).then(() => { + // Handle any execution after data has been saved +}); +``` + +The `options` parameter object contains an `encrypt` property that when set to `true` indicates that the data should be encrypted with the user's app private key before saved to their Gaia hub. All data will be encrypted as such by default if the `encrypt` property or the `options` object itself is omitted entirely. + +If the `encrypt` property is set to `false`, the data will be saved completely unencrypted and available to everyone online with public access to the user's Gaia hub. + +Whereas saving privately encrypted data is possible for all authenticated apps with the [`store_write`](https://stacks.js.org/enums/auth.AuthScope.html#store_write) scope, the user must have previously granted the [`publish_data`](https://stacks.js.org/enums/auth.AuthScope.html#publish_data) scope as well during authentication for the app to save publicly unencrypted data. + +The `putFile` method returns the URL where the the file can be retrieved from the user's Gaia hub, as used here to set the value of `fileUrl`. + +:::info + +You'll need to save an entirely new string of modified data using `putFile` with the same `fileName` every time you want to update a record. There is no separate update method. + +::: + +## Get data for session user + +To retrieve data previously saved for a user with an app, call the `getFile` method available from the `storage` object: + +```js +import { AppConfig, UserSession } from '@stacks/connect'; +import { Storage } from '@stacks/storage'; + +const appConfig = new AppConfig(['store_write', 'publish_data']); +const userSession = new UserSession({ appConfig }); +const storage = new Storage({ userSession }); + +let fileName = 'car.json'; + +const options = { + decrypt: true, +}; + +storage.getFile(fileName, options).then(fileData => { + // Handle any execution that uses decrypted fileData +}); +``` + +Note how the `decrypt` property in the `options` object here should implement the same boolean value as used for `encrypt` initially upon saving the data with `putFile`. The `decrypt` property will default to `true` if omitted. + +Encrypted files need `decrypt` set to `true` so the app knows to decrypt the data with the user's app private key before made available in the callback here as `fileData`. + +## Get data for other user + +Apps can also retrieve public data saved by users other than the one with the active session, granted those users have registered usernames via the [Blockchain Naming System](https://docs.stacks.co/build-apps/references/bns). + +Simply indicate the username of such a user in the `options` object: + +```js +import { AppConfig, UserSession } from '@stacks/connect'; +import { Storage } from '@stacks/storage'; + +const appConfig = new AppConfig(['store_write', 'publish_data']); +const userSession = new UserSession({ appConfig }); +const storage = new Storage({ userSession }); + +let fileName = 'car.json'; + +const options = { + username: 'markmhendrickson.id.blockstack', +}; + +storage.getFile(fileName, options).then(fileData => { + // Handle any execution that uses decrypted fileData +}); +``` + +This `getFile` call will retrieve data found at the given `fileName` path from the storage bucket of the Gaia hub that maps to the user who registered the given `username` and this particular app as hosted at the current domain. + +Set an additional `app` property within `options` to retrieve data for a user as saved by an app hosted at a separate domain: + +```js +const options = { + app: 'https://example.org', + username: 'markmhendrickson.id.blockstack', +}; +``` + +This will cause the `getFile` call to retrieve data found in a separate storage bucket for the indicated app on the user's Gaia hub. + +## Delete data for session user + +Call the `deleteFile` method on `storage` to remove data found at a particular file path for the active session user: + +```js +import { AppConfig, UserSession } from '@stacks/connect'; +import { Storage } from '@stacks/storage'; + +const appConfig = new AppConfig(['store_write', 'publish_data']); +const userSession = new UserSession({ appConfig }); +const storage = new Storage({ userSession }); + +let fileName = 'car.json'; + +storage.deleteFile(fileName).then(() => { + // Handle any execution after file has been deleted +}); +``` + +:::info + +Apps can save and delete data only for the active session user. + +::: + diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 00000000..bc7a320a --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,5 @@ +--- +title: Getting Started +--- + +[Stacks.js starters](https://docs.hiro.so/stacksjs-starters) provide a quick and easy way to get started with Stacks.js. diff --git a/docs/how-to-guides/how-to-integrate-stacking-delegation.md b/docs/how-to-guides/how-to-integrate-stacking-delegation.md new file mode 100644 index 00000000..675262c4 --- /dev/null +++ b/docs/how-to-guides/how-to-integrate-stacking-delegation.md @@ -0,0 +1,212 @@ +--- +title: Integrate Stacking Delegation +--- + +import StacksjsStartersNote from '../includes/stacks.js-starters-note.mdx'; + + + +In this guide, you'll learn how to integrate the Stacking delegation flow by interacting with the respective smart contract, as well as reading data from the Stacks blockchain. + +This guide highlights the following capabilities: + +- As an account holder: delegate STX tokens +- As a delegator: Stack STX token on behalf of the account holder +- As a delegator: Commit to Stacking with all delegated STX tokens + +## Prerequisites + +First, you'll need to understand the [Stacking delegation mechanism](https://docs.stacks.co/understand-stacks/stacking). + +You'll also need [NodeJS](https://nodejs.org/en/download/) `12.10.0` or higher to complete this tutorial. You can verify your installation by opening up your terminal and run the following command: + +```sh +node --version +``` + +Finally, you need to have access to at least two accounts (STX account holder and delegator). For testing purposes on the testnet, you can use the CLI to generate them: + +```sh +stacks make_keychain -t > account.json +stacks make_keychain -t > delegator.json +``` + +You can use the faucet to obtain testnet STX tokens for the test account. Replace `` below with your address: + +```sh +curl -XPOST "https://stacks-node-api.testnet.stacks.co/extended/v1/faucets/stx?address=&stacking=true" +``` + +## Step 1: Integrate libraries + +Install the stacking, network, transactions libraries, and bn.js for large number handling: + +```shell +npm install --save @stacks/stacking @stacks/network @stacks/transactions bn.js +``` + +:::info + +See additional [Stacking library reference](https://github.com/blockstack/stacks.js/tree/master/packages/stacking) + +::: + +## Step 2: Delegate STX tokens + +To get started, delegate STX tokens as an account holder. + +```js +import { getNonce } from '@stacks/transactions'; +import { StacksTestnet, StacksMainnet } from '@stacks/network'; +import { StackingClient } from '@stacks/stacking'; +import BN from 'bn.js'; + +// for mainnet: const network = new StacksMainnet(); +const network = new StacksTestnet(); + +// the stacker STX address +const address = 'ST3XKKN4RPV69NN1PHFDNX3TYKXT7XPC4N8KC1ARH'; + +const client = new StackingClient(address, network); + +// how much to stack, in microSTX +const amountMicroStx = new BN(100000000000); + +// STX address of the delegator +const delegateTo = 'ST2MCYPWTFMD2MGR5YY695EJG0G1R4J2BTJPRGM7H'; + +// burn height at which the delegation relationship should be revoked (optional) +const untilBurnBlockHeight = 5000; + +// hash of bitcoin address that the delegator has to use to receive the pool's rewards (optional) +const poxAddress = undefined; + +// private key of the account holder for transaction signing +const privateKey = 'd48f215481c16cbe6426f8e557df9b78895661971d71735126545abddcd5377001'; + +const delegetateResponse = await client.delegateStx({ + amountMicroStx, + delegateTo, + untilBurnBlockHeight, // optional + poxAddress, // optional + privateKey, +}); + +// { +// txid: '0xf6e9dbf6a26c1b73a14738606cb2232375d1b440246e6bbc14a45b3a66618481', +// } +``` + +This method calls the [`delegate-stx`](https://docs.stacks.co/references/stacking-contract#delegate-stx) method of the Stacking contract. Note, that the amount can be higher or lower than the current account balance. Delegation does not yet lock the STX tokens, users can still transfer them. + +:::tip + +To avoid handling private keys, it is recommended to use the [Stacks Wallet](https://www.hiro.so/wallet) to sign the delegation transaction + +::: + +**Congratulations!** With the completion of this step, you successfully learnt how to use the Stacking library to delegate STX tokens as an account holder. + +## Optional: Revoke delegation rights + +Delegators will be able to Stack STX tokens on the account holder's behalf until either the set burn height is reached or the account holder revokes the rights. + +To revoke delegation rights, the account holder can call the `revokeDelegatestx` method. + +```js +const revokeResponse = await client.revokeDelegateStx(privateKey); + +// { +// txid: '0xf6e9dbf6a26c1b73a14738606cb2232375d1b440246e6bbc14a45b3a66618481', +// } +``` + +This method calls the [`revoke-delegate-stx`](https://docs.stacks.co/references/stacking-contract#revoke-delegate-stx) method of the Stacking contract. + +:::tip + +To avoid handling private keys, it is recommended to use the [Stacks Wallet](https://www.hiro.so/wallet) to sign the revoke transaction + +::: + +## Step 3: Stack delegated STX tokens + +With an established delegation relationship, the delegator can stack STX tokens on behalf of the account holder. This happens usually in a different client app than the delegation. + +```js +// block height at which to stack +const burnBlockHeight = 2000; + +// the delegator initiates a different client +const delegatorAddress = 'ST22X605P0QX2BJC3NXEENXDPFCNJPHE02DTX5V74'; + +// number cycles to stack +const cycles = 3; + +// delegator private key for transaction signing +const delegatorPrivateKey = 'd48f215481c16cbe6426f8e557df9b78895661971d71735126545abddcd5377001'; + +// the BTC address for reward payouts; either to the delegator or to the BTC address set by the account holder +// must start with "1" or "3". Native Segwit (starts with "bc1") is not supported +const delegatorBtcAddress = 'msiYwJCvXEzjgq6hDwD9ueBka6MTfN962Z'; + +// if you call this method multiple times in the same block, you need to increase the nonce manually +let nonce = getNonce(delegatorAddress, network); +nonce = nonce.add(new BN(1)); + +const delegatorClient = new StackingClient(delegatorAddress, network); + +const delegetateStackResponses = await delegatorClient.delegateStackStx({ + stacker: address, + amountMicroStx, + poxAddress: delegatorBtcAddress, + burnBlockHeight, + cycles, + privateKey: delegatorPrivateKey, + nonce, // optional +}); + +// { +// txid: '0xf6e9dbf6a26c1b73a14738606cb2232375d1b440246e6bbc14a45b3a66618481', +// } +``` + +This function calls the [`delegate-stack-stx`](https://docs.stacks.co/references/stacking-contract#delegate-stack-stx) method of the Stacking contract to lock up the STX token from the account holder. + +The delegator must call this method multiple times (for all stackers), until enough tokens are locked up to participate in Stacking. This is the first part of delegated stacking for the delegator. + +:::info + +Reward slots are assigned based on the number of STX tokens locked up for a specific Bitcoin reward address + +::: + +## Step 4: Commit to Stacking + +As soon as pooling completes (minimum STX token threshold reached), the delegator needs to confirm participation for the next one or more cycles: + +```js +// reward cycle id to commit to +const rewardCycle = 12; + +const delegetateCommitResponse = await delegatorClient.stackAggregationCommit({ + poxAddress: delegatorBtcAddress, // this must be the delegator bitcoin address + rewardCycle, + privateKey: delegatorPrivateKey, +}); + +// { +// txid: '0xf6e9dbf6a26c1b73a14738606cb2232375d1b440246e6bbc14a45b3a66618481', +// } +``` + +This method calls the [`stack-aggregation-commit`](https://docs.stacks.co/references/stacking-contract#stack-aggregation-commit) function of the Stacking contract. This call also includes locked Stacks from previous cycles. This is the second part of delegated stacking for the delegator. + +This method has to be called once for each reward cycle, even if all account holders have already locked their Stacks for several cycles in a row. If no new account holders are added to the pool, then this method call can be made even several cycles before the actual rewards cycle. + +Locking delegated Stacks together with a aggregation commits can be done several times before the cycle starts as long as the minimum increment amount of locked Stacks is met. + +**Congratulations!** With the completion of this step, you successfully learnt how to use the Stacking library to ... + +- Stack STX token on behalf of an account holder +- Commit to Stacking with all delegated STX tokens diff --git a/docs/how-to-guides/how-to-integrate-stacking.md b/docs/how-to-guides/how-to-integrate-stacking.md new file mode 100644 index 00000000..90a76eee --- /dev/null +++ b/docs/how-to-guides/how-to-integrate-stacking.md @@ -0,0 +1,349 @@ +--- +title: Integrating Stacking +--- +import StacksjsStartersNote from '../includes/stacks.js-starters-note.mdx'; + + + +In this tutorial, you'll learn how to integrate Stacking by interacting with the respective smart contract, as well as reading data from the Stacks blockchain. + +This tutorial highlights the following capabilities: + +- Generate Stacks accounts +- Display stacking info +- Verify stacking eligibility +- Add stacking action +- Display stacking status + +:::tip + +Alternatively to integration using JS libraries, you can use the [Rust CLI](https://gist.github.com/kantai/c261ca04114231f0f6a7ce34f0d2499b) or [JS CLI](/tutorials/stacking-using-cli). + +::: + +## Prerequisites + +First, you'll need to understand the [Stacking mechanism](https://docs.stacks.co/understand-stacks/stacking). + +You'll also need [NodeJS](https://nodejs.org/en/download/) `12.10.0` or higher to complete this tutorial. You can verify your installation by opening up your terminal and run the following command: + +```bash +node --version +``` + +## Overview + +In this tutorial, we'll implement the Stacking flow laid out in the [Stacking guide](https://docs.stacks.co/understand-stacks/stacking#stacking-flow). + +## Step 1: Integrate libraries + +Install the stacking, network, transactions libraries and bn.js for large number handling: + +```shell +npm install --save @stacks/stacking @stacks/network @stacks/transactions bn.js +``` + +:::info + +See additional [Stacking library reference](https://github.com/blockstack/stacks.js/tree/master/packages/stacking) + +::: + +## Step 2: Generating an account and initialization + +To get started, let's create a new, random Stacks 2.0 account: + +```js +import { + makeRandomPrivKey, + privateKeyToString, + getAddressFromPrivateKey, + TransactionVersion, +} from '@stacks/transactions'; + +import { StackingClient } from '@stacks/stacking'; + +import { StacksTestnet, StacksMainnet } from '@stacks/network'; + +import BN from 'bn.js'; + +// generate random key or use an existing key +const privateKey = privateKeyToString(makeRandomPrivKey()); + +// get Stacks address +// for mainnet, remove the TransactionVersion +const stxAddress = getAddressFromPrivateKey(privateKey, TransactionVersion.Testnet); + +// instantiate the Stacker class for testnet +// for mainnet, use `new StacksMainnet()` +const client = new StackingClient(stxAddress, new StacksTestnet()); +``` + +:::info + +Review the [accounts guide](https://docs.stacks.co/understand-stacks/accounts) for more details + +::: + +## Step 3: Display stacking info + +In order to inform users about the upcoming reward cycle, we can use the following methods to obtain information for Stacking: + +With the obtained PoX info, you can present whether Stacking has been executed in the next cycle, when the next cycle begins, the duration of a cycle, and the minimum microstacks required to participate: + +```js +// will Stacking be executed in the next cycle? +const stackingEnabledNextCycle = await client.isStackingEnabledNextCycle(); +// true or false + +// how long (in seconds) is a Stacking cycle? +const cycleDuration = await client.getCycleDuration(); +// 120 + +// how much time is left (in seconds) until the next cycle begins? +const secondsUntilNextCycle = await client.getSecondsUntilNextCycle(); +// 600000 +``` + +:::note + +Cycle duration and participation thresholds will differ between mainnet and testnet + +::: + +You can also retrieve the raw PoX and core information using the methods below if required: + +```js +const poxInfo = await client.getPoxInfo(); + +// poxInfo: +// { +// contract_id: 'ST000000000000000000002AMW42H.pox', +// first_burnchain_block_height: 0, +// min_amount_ustx: 83335083333333, +// prepare_cycle_length: 30, +// rejection_fraction: 3333333333333333, +// reward_cycle_id: 17, +// reward_cycle_length: 120, +// rejection_votes_left_required: 0, +// total_liquid_supply_ustx: 40000840000000000 +// } + +const coreInfo = await client.getCoreInfo(); + +// coreInfo: +// { +// peer_version: 385875968, +// pox_consensus: 'bb88a6e6e65fa7c974d3f6e91a941d05cc3dff8e', +// burn_block_height: 2133, +// stable_pox_consensus: '2284451c3e623237def1f8caed1c11fa46b6f0cc', +// stable_burn_block_height: 2132, +// server_version: 'blockstack-core 0.0.1 => 23.0.0.0 (HEAD:a4deb7a+, release build, linux [x86_64])', +// network_id: 2147483648, +// parent_network_id: 3669344250, +// stacks_tip_height: 1797, +// stacks_tip: '016df36c6a154cb6114c469a28cc0ce8b415a7af0527f13f15e66e27aa480f94', +// stacks_tip_consensus_hash: 'bb88a6e6e65fa7c974d3f6e91a941d05cc3dff8e', +// unanchored_tip: '6b93d2c62fc07cf44302d4928211944d2debf476e5c71fb725fb298a037323cc', +// exit_at_block_height: null +// } + +const targetBlocktime = await client.getTargetBlockTime(); + +// targetBlocktime: +// 120 +``` + +Users need to have sufficient Stacks (STX) tokens to participate in Stacking. This can be verified easily: + +```js +const hasMinStxAmount = await client.hasMinimumStx(); +// true or false +``` + +For testing purposes, you can use the faucet to obtain testnet STX tokens. Replace `` below with your address: + +```shell +curl -XPOST "https://stacks-node-api.testnet.stacks.co/extended/v1/faucets/stx?address=&stacking=true" +``` + +You'll have to wait a few minutes for the transaction to complete. + +Users can select how many cycles they would like to participate in. To help with that decision, the unlocking time can be estimated: + +```js +// this would be provided by the user +let numberOfCycles = 3; + +// the projected datetime for the unlocking of tokens +const unlockingAt = new Date(new Date().getTime() + secondsUntilNextCycle); +unlockingAt.setSeconds(unlockingAt.getSeconds() + cycleDuration * numberOfCycles); +``` + +## Step 4: Verify stacking eligibility + +At this point, your app shows Stacking details. If Stacking will be executed and the user has enough funds, the user should be asked to provide input for the amount of microstacks to lockup and a Bitcoin address to receive the pay out rewards. + +With this input, and the data from previous steps, we can determine the eligibility for the next reward cycle: + +```js +// user supplied parameters +// BTC address must start with "1" or "3". Native Segwit (starts with "bc1") is not supported +let btcAddress = '1Xik14zRm29UsyS6DjhYg4iZeZqsDa8D3'; +let numberOfCycles = 3; + +const stackingEligibility = await client.canStack({ + poxAddress: btcAddress, + cycles: numberOfCycles, +}); + +// stackingEligibility: +// { +// eligible: false, +// reason: 'ERR_STACKING_INVALID_LOCK_PERIOD', +// } +``` + +:::note + +The eligibility check assumes the user will be stacking the maximum balance available in the account. The eligibility check is a read-only function call to the PoX smart contract which does not require broadcasting a transaction + +::: + +If the user is eligible, the stacking action should be enabled on the UI. If not, the respective error message should be shown to the user. + +## Step 5: Lock STX to stack + +Next, the Stacking action should be executed. + +```js +// set the amount to lock in microstacks +const amountMicroStx = new BN(100000000000); + +// set the burnchain (BTC) block for stacking lock to start +// you can find the current burnchain block height from coreInfo above +// and adding 3 blocks to provide a buffer for transaction to confirm +const burnBlockHeight = 2133 + 3; + +// execute the stacking action by signing and broadcasting a transaction to the network +client + .stack({ + amountMicroStx, + poxAddress: btcAddress, + cycles: numberOfCycles, + privateKey, + burnBlockHeight, + }) + .then(response => { + // If successful, stackingResults will contain the txid for the Stacking transaction + // otherwise an error will be returned + if (response.hasOwnProperty('error')) { + console.log(response.error); + throw new Error('Stacking transaction failed'); + } else { + console.log(`txid: ${response}`); + // txid: f6e9dbf6a26c1b73a14738606cb2232375d1b440246e6bbc14a45b3a66618481 + return response; + } + }); +``` + +The transaction completion will take several minutes. Only one stacking transaction from each account/address is active at any time. Multiple/concurrent stacking actions from the same account will fail. + +## Step 6: Confirm lock-up + +The new transaction will not be completed immediately. It'll stay in the `pending` status for a few minutes. We need to poll the status and wait until the transaction status changes to `success`. We can use the [Stacks Blockchain API client library](/get-started/stacks-blockchain-api#javascript-client-library) to check transaction status. + +```js +const { TransactionsApi } = require('@stacks/blockchain-api-client'); +const tx = new TransactionsApi(apiConfig); + +const waitForTransactionSuccess = txId => + new Promise((resolve, reject) => { + const pollingInterval = 3000; + const intervalID = setInterval(async () => { + const resp = await tx.getTransactionById({ txId }); + if (resp.tx_status === 'success') { + // stop polling + clearInterval(intervalID); + // update UI to display stacking status + return resolve(resp); + } + }, pollingInterval); + }); + +// note: txId should be defined previously +const resp = await waitForTransactionSuccess(txId); +``` + +:::info + +More details on the lifecycle of transactions can be found in the [transactions guide](https://docs.stacks.co/understand-stacks/transactions#lifecycle) + +::: + +Alternatively to the polling, the Stacks Blockchain API client library offers WebSockets. WebSockets can be used to subscribe to specific updates, like transaction status changes. Here is an example: + +```js +const client = await connectWebSocketClient('ws://stacks-node-api.blockstack.org/'); + +// note: txId should be defined previously +const sub = await client.subscribeAddressTransactions(txId, event => { + console.log(event); + // update UI to display stacking status +}); + +await sub.unsubscribe(); +``` + +## Step 6: Display Stacking status + +With the completed transactions, Stacks tokens are locked up for the lockup duration. During that time, your app can display the following details: unlocking time, amount of Stacks locked, and bitcoin address used for rewards. + +```js +const stackingStatus = await client.getStatus(); + +// If stacking is active for the account, you will receive the stacking details +// otherwise an error will be thrown +// stackingStatus: +// { +// stacked: true, +// details: { +// amount_microstx: '80000000000000', +// first_reward_cycle: 18, +// lock_period: 10, +// burnchain_unlock_height: 3020, +// pox_address: { +// version: '00', +// hashbytes: '05cf52a44bf3e6829b4f8c221cc675355bf83b7d' +// } +// } +// } +``` + +:::note + +The `pox_address` property is the PoX contract's internal representation of the reward BTC address. + +::: + +To display the unlocking time, you need to use the `firstRewardCycle` and the `lockPeriod` fields. + +**Congratulations!** With the completion of this step, you successfully learnt how to ... + +- Generate Stacks accounts +- Display stacking info +- Verify stacking eligibility +- Add stacking action +- Display stacking status + +## Optional: Rewards + +Currently, the Stacking library does not provide methods to get the paid rewards for a set address. However, the [Stacks Blockchain API exposes endpoints](https://docs.hiro.so/api#tag/Burnchain) to get more details. + +As an example, if you want to get the rewards paid to `btcAddress`, you can make the following API call: + +```shell +# for mainnet, replace `testnet` with `mainnet` +curl 'https://stacks-node-api.testnet.stacks.co/extended/v1/burnchain/rewards/' +``` diff --git a/docs/how-to-guides/how-to-migrate-from-blockstack.js.md b/docs/how-to-guides/how-to-migrate-from-blockstack.js.md new file mode 100644 index 00000000..2623bd11 --- /dev/null +++ b/docs/how-to-guides/how-to-migrate-from-blockstack.js.md @@ -0,0 +1,5 @@ +--- +title: Migrate from Blockstack +--- + +To migrate your app from blockstack.js to Stacks.js, follow the steps in the [migration guide](https://github.com/hirosystems/stacks.js/blob/master/.github/MIGRATION.md). diff --git a/docs/how-to-guides/how-to-use-stacks-connect-with-angular.md b/docs/how-to-guides/how-to-use-stacks-connect-with-angular.md new file mode 100644 index 00000000..b1bd8138 --- /dev/null +++ b/docs/how-to-guides/how-to-use-stacks-connect-with-angular.md @@ -0,0 +1,231 @@ +--- +title: Angular Authenticator +--- + +In this tutorial, you'll learn how to work with Stacks Connect when using [Angular](https://angular.io/) as your framework of choice. It builds on what you've learnt in the [Authentication Overview](/build-apps/authentication). + +> **_NOTE:_** + +> This article presumes some familiarity with [Angular](https://angular.io/), as well as [Reactive Extensions (RxJS)](https://rxjs.dev/). + + + +### Prerequisites + +We'll be using the [Angular CLI](https://cli.angular.io/) to scaffold the project, so make sure you've got the latest version installed. We're using version `10.2.0`. + +```sh +npm install --global @angular/cli +``` + +## Step 1: Scaffold and run + +Use the `ng new` command to scaffold a new project. We've named ours `ng-stacks-connect`. + +```sh +ng new --minimal --inline-style --inline-template +``` + +You'll be asked to enter some preferences: whether your app with use routing, and whether you want to use a CSS preprocessor like SASS. For sake of this tutorial, we're keeping things simple. No routing. No preprocessing. + +Inside the newly created `ng-stacks-connect` directory, let's boot up the development server which defaults to [localhost:4200](http://localhost:4200). + +```sh +cd ng-stacks-connect +ng serve +``` + +## Step 2: Add Stacks connect + +```sh +npm install --save @stacks/connect blockstack +``` + +:::info + +We're also installing the `blockstack` package, as it's a [peer dependency](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#peerdependencies) of Stacks Connect + +::: + +## Step 3: Declare missing globals + +Some dependencies of these packages were written for a Nodejs environment. In a browser environment, tools such as Webpack (v4) often abstract the polyfilling of Nodejs specific APIs. Using the Angular CLI, this must be done manually. + +:::info + +`Buffer`, for example, is a global class in a Nodejs environment. In the browser is it `undefined` so we must declare it to avoid runtime exceptions + +::: + +Add the following snippet to your `src/polyfills.ts` + +```typescript +(window as any).global = window; +(window as any).process = { + version: "", + env: {}, +}; +global.Buffer = require("buffer").Buffer; +``` + +This does 3 things: + +1. Declares `global` to `window` +2. Declares a global `Buffer` class +3. Declares a global `process` object + +## Step 4: Authentication flow + +Now everything's set up, we're ready to create our auth components + +We can use the CLI's generator to scaffold components. + +### Sign-in button + +```sh +ng generate component +``` + +Enter the name: `stacks-sign-in-button`. You'll find the newly generated component in `src/app/stacks-sign-in-button/stacks-sign-in-button.component.ts`. + +Here's our Sign In button component. Let's replace this example with the following code. + +```typescript +import { Component, OnInit, Output, EventEmitter } from "@angular/core"; + +@Component({ + selector: "app-stacks-sign-in-button", + template: ` `, +}) +export class StacksSignInButtonComponent { + @Output() onSignIn = new EventEmitter(); +} +``` + +### Connecting Stacks connect + +Let's add this button to our `app-root` component (`app.component.ts`) and wire up the `(onSignIn)` event. Make sure to import `Subject` from `rxjs`. + +```typescript +@Component({ + selector: "app-root", + template: ``, +}) +export class AppComponent { + stacksAuth$ = new Subject(); +} +``` + +Here we're using an Rxjs `Subject` to represent a stream of sign in events. `stacksAuth$` will emit when we should trigger the sign in action. + +### Authentication + +First, describe the auth options we need to pass to Connect. [Learn more about `AuthOptions` here](/build-apps/authentication). Let's modify the default component to look like this: + +```typescript +import { Component } from "@angular/core"; +import { AuthOptions, FinishedData } from "@stacks/connect"; +import { ReplaySubject, Subject } from "rxjs"; +import { switchMap } from "rxjs/operators"; + +@Component({ + selector: "app-root", + template: ` + + +
{{ authResponse$ | async | json }}
+
+ `, +}) +export class AppComponent { + stacksAuth$ = new Subject(); + authResponse$ = new ReplaySubject(1); + + authOptions: AuthOptions = { + finished: (response) => this.authResponse$.next(response), + appDetails: { + name: "Angular Stacks Connect Demo", + icon: "http://placekitten.com/g/100/100", + }, + }; + + ngOnInit() { + this.stacksAuth$ + .pipe(switchMap(() => import("@stacks/connect"))) + .subscribe((connectLibrary) => + connectLibrary.showBlockstackConnect(this.authOptions) + ); + } +} +``` + +Let's run through what's going on. In the `authOptions` field, we're using the `finished` handler to emit a value to the `authResponse$` which uses a `ReplaySubject` to persist the latest response. + +:::info + +A [`ReplaySubject`](https://rxjs.dev/api/index/class/ReplaySubject) is an Observable that starts without an initial value, but replays the latest x emissions when subscribed to + +::: + +For initial load performance, we're using `import("@stacks/connect")` to only load the Stacks Connect library when it's needed. The `switchMap` operators "switches" out the `stacksAuth$` event for the library. + +The output of `authResponse$` can be added to the template for debugging purposes. This uses Angular's `async` and `json` pipes. + +### Loading text + +One problem with the current implementation is that there's a network delay while waiting to load the Connect library. Let's keep track of the loading state and display some text in the sign in button component. You'll need to `import { tap, switchMap } from 'rxjs/operators';`. + +```typescript +// src/app/app.component.ts +isLoadingConnect$ = new BehaviorSubject(false); + +ngOnInit() { + this.stacksAuth$ + .pipe( + tap(() => this.isLoadingConnect$.next(true)), + switchMap(() => import("@stacks/connect")), + tap(() => this.isLoadingConnect$.next(false)) + ) + .subscribe(connectLibrary => + connectLibrary.showBlockstackConnect(this.authOptions) + ); +} +``` + +We can keep track of it with a [BehaviorSubject](https://rxjs.dev/api/index/class/BehaviorSubject), which always emits its initial value when subscribed to. + +Let's add a `loading` input to the `StacksSignInButtonComponent` component. + +```typescript highlight=3,6 +@Component({ + selector: "app-stacks-sign-in-button", + template: ` + + `, +}) +export class StacksSignInButtonComponent { + @Input() loading: boolean; + @Output() onSignIn = new EventEmitter(); +} +``` + +Then, pass the `isLoadingConnect$` Observable into the component, and hide it when the user has already authenticated. + +```html +// Edit src/app/app.component.ts + +``` + +### Next steps + +This tutorial has shown you how to integrate Stacks Connect with an Angular application. You may want to consider abstracting the Stacks Connect logic behind an [Angular service](https://angular.io/guide/architecture-services), or using [Material Design](https://material.angular.io/) to theme your app. diff --git a/docs/includes/stacks.js-starters-note.mdx b/docs/includes/stacks.js-starters-note.mdx new file mode 100644 index 00000000..67f2d05c --- /dev/null +++ b/docs/includes/stacks.js-starters-note.mdx @@ -0,0 +1,3 @@ +> **_NOTE:_** +> +> Use our prebuilt [Stacks.js starter templates](/stacksjs-starters) to kickstart your frontend application development with your preferred JavaScript framework. diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 00000000..bce2b2ad --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,21 @@ +--- +title: Overview +--- + +Stacks.js is an SDK with a collection of JavaScript libraries to interact with and transact on Stacks blockchain. On the Stacks blockchain, Stacks.js libraries are used to build, broadcast and sign transactions. + +This page provides information on how to build applications using [Stacks.js](https://github.com/blockstack/stacks.js) and other libraries that make integration of the Stacks blockchain easy for front-end developers. + +Following are the three main integrations: + +- **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 +- **Data storage**: Save and retrieve data for users with [Gaia](https://docs.stacks.co/build-apps/references/gaia) + +All three of these integrations can be used together to create powerful new user experiences that rival or exceed those of traditional apps—all while protecting your users' digital rights. + +While integration is possible for any type of app, the resources available here are for web developers experienced with JavaScript. + +