mirror of https://github.com/lukechilds/docs.git
Patrick Gray
4 years ago
committed by
Patrick Gray
9 changed files with 645 additions and 6 deletions
After Width: | Height: | Size: 59 KiB |
@ -0,0 +1,619 @@ |
|||
--- |
|||
title: Heystack app |
|||
description: Interacting with the wallet and smart contracts from a React application |
|||
tags: |
|||
- example-app |
|||
images: |
|||
large: /images/pages/heystack-app.svg |
|||
--- |
|||
|
|||
## Introduction |
|||
|
|||
This example application demonstrates important features of the Stacks blockchain, and is a case study for how a frontend |
|||
web application can interact with a Clarity smart contract. The full source of the application is provided, this page |
|||
highlights important code snippets and design patterns to help you learn how to develop your own Stacks application. |
|||
|
|||
This app highlights the following platform features: |
|||
|
|||
- Authenticating users with the web wallet |
|||
- Using a smart contract to store data on the blockchain |
|||
- Minting new fungible tokens with a [SIP-010][] compliant smart contract |
|||
- Creating and monitoring transactions on the Stacks blockchain using [Stacks.js][] |
|||
|
|||
You can access the [online version][heystack] of the Heystack app to interact with it. The source for Heystack is also |
|||
available on [Github][heystack_gh]. This page assumes some familiarity with [React][]. |
|||
|
|||
## Heystack overview |
|||
|
|||
Heystack is a web application for chatting with other Stacks users. The application uses the [Stacks web wallet][] to |
|||
authenticate users in the frontend. When a user logs in to Heystack, they're given a genesis amount of $HEY fungible |
|||
tokens, which allows them to send and like messages on the platform. |
|||
|
|||
Heystack is powered by Clarity smart contracts so each message is a transaction on the Stacks blockchain. Each time a |
|||
user sends a message on the platform, they must sign the message with the [Stacks web wallet][] (or another compatible |
|||
wallet) and pay a small gas fee in STX. A user spends a $HEY token to send every message, and recieves a $HEY token for |
|||
every like that their messages receive. |
|||
|
|||
## Review smart contracts |
|||
|
|||
Heystack depends on two smart contracts to execute the backend functions of the app on the Stacks blockchain: a contract |
|||
for handling the messaging content, and a contract for minting and distributing the $HEY token. |
|||
|
|||
### Content contract |
|||
|
|||
The `hey.clar` contract provides two primary functions for the application, one to publish content to |
|||
the blockchain and another to like a piece of content based on its ID. This section reviews the implementation of |
|||
these primary functions, but is not a comprehensive discussion of the contract. |
|||
|
|||
In order to accomplish the two primary functions, the contract relies on a data variable `content-index` and two |
|||
[data maps][], `like-state` and `publisher-state` which contain the number of likes a piece of content has received, and |
|||
the principal address of the account that published the content. |
|||
|
|||
Note that all variables are defined at the top of the contract, which is a requirement of the Clarity language. These |
|||
include constants such as the `contract-creator`, error codes, and a treasury address. |
|||
|
|||
```clarity |
|||
;; |
|||
;; Data maps and vars |
|||
(define-data-var content-index uint u0) |
|||
|
|||
(define-read-only (get-content-index) |
|||
(ok (var-get content-index)) |
|||
) |
|||
|
|||
(define-map like-state |
|||
{ content-index: uint } |
|||
{ likes: uint } |
|||
) |
|||
|
|||
(define-map publisher-state |
|||
{ content-index: uint } |
|||
{ publisher: principal } |
|||
) |
|||
``` |
|||
|
|||
Read-only functions provide a method for getting the like count of a piece of content, and getting the principal address |
|||
of the message publisher. |
|||
|
|||
```clarity |
|||
(define-read-only (get-like-count (id uint)) |
|||
;; Checks map for like count of given id |
|||
;; defaults to 0 likes if no entry found |
|||
(ok (default-to { likes: u0 } (map-get? like-state { content-index: id }))) |
|||
) |
|||
|
|||
(define-read-only (get-message-publisher (id uint)) |
|||
;; Checks map for like count of given id |
|||
;; defaults to 0 likes if no entry found |
|||
(ok (unwrap-panic (get publisher (map-get? publisher-state { content-index: id })))) |
|||
``` |
|||
|
|||
The `get-like-count` method accepts a content ID and returns the number of likes associated with that content. The |
|||
method uses the [`default-to`][] function to return `0` if the content ID isn't found in the map of likes. |
|||
|
|||
The `get-message-publisher` method accepts a content ID and returns the principal address of the content publisher. The |
|||
method uses the [`unwrap-panic`][] function to halt execution of the method if the principal address isn't found in |
|||
the map of publishers. |
|||
|
|||
The two primary public methods are the `send-message` and `like-message` functions. These methods allow the contract |
|||
caller to store a message on the blockchain (creating entries in the data maps for the message sender and the number |
|||
of likes). Note that the message itself isn't stored in a contract variable, the frontend application reads the content |
|||
of the message directly from the transaction on the blockchain. |
|||
|
|||
```clarity |
|||
;; |
|||
;; Public functions |
|||
(define-public (send-message (content (string-utf8 140))) |
|||
(let ((id (unwrap! (increment-content-index) (err u0)))) |
|||
(print { content: content, publisher: tx-sender, index: id }) |
|||
(map-set like-state |
|||
{ content-index: id } |
|||
{ likes: u0 } |
|||
) |
|||
(map-set publisher-state |
|||
{ content-index: id } |
|||
{ publisher: tx-sender } |
|||
) |
|||
(transfer-hey u1 HEY_TREASURY) |
|||
) |
|||
) |
|||
``` |
|||
|
|||
The `send-message` method accepts a utf-8 string with a maximum length of 140 characters. The method defines an internal |
|||
variable `id` using the `let` function and assigns the next content ID to that variable by calling the |
|||
`increment-contract-index` method of the contract. The value assignment of this variable is bound by the [`unwrap!`][] |
|||
function, which returns an error and exits the control-flow if the `increment-contract-index` function isn't |
|||
successfully called. |
|||
|
|||
The method then assigns `u0` likes to the content in the `like-state` data map, and adds the principal address to the |
|||
`publisher-state` data map using the [`map-set`][] function. Finally, the private method `transfer-hey` is called to |
|||
transfer 1 $HEY token from the message sender to the $HEY treasury address stored in the `HEY_TREASURY` constant. |
|||
|
|||
```clarity |
|||
(define-public (like-message (id uint)) |
|||
(begin |
|||
;; cannot like content that doesn't exist |
|||
(asserts! (>= (var-get content-index) id) (err ERR_CANNOT_LIKE_NON_EXISTENT_CONTENT)) |
|||
;; transfer 1 HEY to the principal that created the content |
|||
(map-set like-state |
|||
{ content-index: id } |
|||
{ likes: (+ u1 (get likes (unwrap! (get-like-count id) (err u0)))) } |
|||
) |
|||
(transfer-hey u1 (unwrap-panic (get-message-publisher id))) |
|||
) |
|||
) |
|||
``` |
|||
|
|||
The `like-message` method accepts a content ID. The method checks that the ID is lower than the current content ID using |
|||
the [`asserts!`][] function, to verify that the provided ID is a valid ID. If the [`asserts!`][] assessment is `false`, |
|||
the method returns an error code. If the ID is valid, the method performs a [`map-set`][] to look up the content in the |
|||
`like-state` data map and add a like to the value stored in the map. Once again, the [`unwrap!`][] function is used to |
|||
ensure that an invalid value isn't stored in the map. |
|||
|
|||
The `hey.clar` contract provides some additional functions for working with the $HEY token contract, discussed in the |
|||
next section. |
|||
|
|||
### Token contract |
|||
|
|||
Heystack creates a native fungible token for use in the application. When a user authenticates with Heystack, they're |
|||
automatically eligible to claim 100 $HEY tokens to allow them to start messaging. |
|||
|
|||
[SIP-010][] defines the fungible token standard on Stacks, which allows Stacks compatible wallets to handle fungible |
|||
tokens through a set of standardized methods. SIP-010 defines 7 traits that a fungible token contract must have in order |
|||
to be compliant: |
|||
|
|||
- `transfer`: method for transferring the token from one principal to another |
|||
- `get-name`: returns the human-readable name of the token |
|||
- `get-symbol`: returns the ticker symbol of the token |
|||
- `get-decimals`: returns number of decimal places in the token |
|||
- `get-balance`: return the balance of a given principal |
|||
- `get-total-supply`: returns the total supply of the token |
|||
- `get-token-uri`: returns an optional string that resolves to a valid URI for the token's metadata. |
|||
|
|||
In Clarity, a contract can declare that it intends to implement a set of standard traits. |
|||
|
|||
```clarity |
|||
|
|||
;; Implement the `ft-trait` trait defined in the `ft-trait` contract |
|||
;; https://github.com/hstove/stacks-fungible-token |
|||
(impl-trait 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.ft-trait.sip-010-trait) |
|||
``` |
|||
|
|||
The [`impl-trait`][] function asserts that the smart contract is fully implementing a given set of traits defined by the |
|||
argument. Like variable definitions, `impl-trait` must be declared at the top of a smart contract definition. |
|||
|
|||
-> The contract address for SIP-010 trait definition is different depending on which network (mainnet, testnet, etc.) |
|||
your contract is deployed on. See the standard for the current addresses of the standard traits. |
|||
|
|||
The `hey-token.clar` contract implements the required 7 traits of [SIP-010][], and one additional method, the |
|||
`gift-tokens` method, that allows a principal to request tokens from the contract. |
|||
|
|||
```clarity |
|||
(define-public (gift-tokens (recipient principal)) |
|||
(begin |
|||
(asserts! (is-eq tx-sender recipient) (err u0)) |
|||
(ft-mint? hey-token u1 recipient) |
|||
) |
|||
) |
|||
``` |
|||
|
|||
## Authentication |
|||
|
|||
Authentication is handled through the [`@stacks/connect-react`][] and [`@stacks/auth`][] packages, which interact with |
|||
compatible Stacks wallet extensions and provide methods for interacting with a user session respectively. [Jotai][] |
|||
provides application state management. |
|||
|
|||
The [connect wallet button component][] implements the interface with the Stacks web wallet through the |
|||
[`@stacks/connect-react`][] package. |
|||
|
|||
```tsx |
|||
import { Button } from '@components/button'; |
|||
import React from 'react'; |
|||
import { useConnect } from '@stacks/connect-react'; |
|||
import { ButtonProps } from '@stacks/ui'; |
|||
import { useLoading } from '@hooks/use-loading'; |
|||
import { LOADING_KEYS } from '@store/ui'; |
|||
|
|||
export const ConnectWalletButton: React.FC<ButtonProps> = props => { |
|||
const { doOpenAuth } = useConnect(); |
|||
const { isLoading, setIsLoading } = useLoading(LOADING_KEYS.AUTH); |
|||
return ( |
|||
<Button |
|||
isLoading={isLoading} |
|||
onClick={() => { |
|||
void setIsLoading(true); |
|||
doOpenAuth(); |
|||
}} |
|||
{...props} |
|||
> |
|||
Connect wallet |
|||
</Button> |
|||
); |
|||
}; |
|||
``` |
|||
|
|||
Once connected, [`/src/store/auth.ts`][] populates the user session data into the Jotai store, allowing the application |
|||
to access the user information. |
|||
|
|||
You can see in the [welcome panel component][] how the presence or absence of stored user data is used to display the |
|||
wallet connect button or the signed in view. |
|||
|
|||
```tsx |
|||
... |
|||
const UserSection = memo((props: StackProps) => { |
|||
const { user } = useUser(); |
|||
|
|||
return ( |
|||
<Stack |
|||
alignItems="center" |
|||
justifyContent="center" |
|||
flexGrow={1} |
|||
spacing="loose" |
|||
textAlign="center" |
|||
{...props} |
|||
> |
|||
{!user ? <SignedOutView /> : <SignedInView onClick={() => console.log('click')} />} |
|||
</Stack> |
|||
); |
|||
}); |
|||
... |
|||
``` |
|||
|
|||
### Token faucet |
|||
|
|||
The `use-claim-hey.ts` file provides a React hook for interacting with the token faucet of the Clarity smart contract. |
|||
|
|||
```ts |
|||
import { useLoading } from '@hooks/use-loading'; |
|||
import { LOADING_KEYS } from '@store/ui'; |
|||
import { useConnect } from '@stacks/connect-react'; |
|||
import { useNetwork } from '@hooks/use-network'; |
|||
import { useCallback } from 'react'; |
|||
import { useHeyContract } from '@hooks/use-hey-contract'; |
|||
import { REQUEST_FUNCTION } from '@common/constants'; |
|||
import { principalCV } from '@stacks/transactions/dist/clarity/types/principalCV'; |
|||
import { useCurrentAddress } from '@hooks/use-current-address'; |
|||
|
|||
export function useHandleClaimHey() { |
|||
const address = useCurrentAddress(); |
|||
const { setIsLoading } = useLoading(LOADING_KEYS.CLAIM_HEY); |
|||
const { doContractCall } = useConnect(); |
|||
const [contractAddress, contractName] = useHeyContract(); |
|||
const network = useNetwork(); |
|||
|
|||
const onFinish = useCallback(() => { |
|||
void setIsLoading(false); |
|||
}, [setIsLoading]); |
|||
|
|||
const onCancel = useCallback(() => { |
|||
void setIsLoading(false); |
|||
}, [setIsLoading]); |
|||
|
|||
return useCallback(() => { |
|||
void setIsLoading(true); |
|||
void doContractCall({ |
|||
contractAddress, |
|||
contractName, |
|||
functionName: REQUEST_FUNCTION, |
|||
functionArgs: [principalCV(address)], |
|||
onFinish, |
|||
onCancel, |
|||
network, |
|||
stxAddress: address, |
|||
}); |
|||
}, [setIsLoading, onFinish, network, onCancel, address, doContractCall]); |
|||
} |
|||
``` |
|||
|
|||
The [`@stacks/connect-react`][] package exports the `doContractCall` method, which interacts with the smart contract on |
|||
the blockchain. There are more examples of transaction calls in the next section. It's important to note that it's |
|||
necessary to convert Javascript types to Clarity types using the types exported by the [`@stacks/transactions`][] package. |
|||
Further discussion of this conversion is in the [Clarity types in Javascript][] section. |
|||
|
|||
## Transactions |
|||
|
|||
Since messages in Heystack are transactions against a Clarity smart contract, the application must be able to create |
|||
transactions and read their content from the blockchain. The following sections highlight code snippets that perform |
|||
Clarity transactions and read both completed and pending transactions from the Stacks blockchain. |
|||
|
|||
### Issuing transactions |
|||
|
|||
The two primary functions of the `hey.clar` smart contract are publishing a message and accepting a like on an already |
|||
published message. The [`src/hooks/use-publish-hey.ts`][] file implements the frontend method for calling the smart |
|||
contract on the blockchain with the appropriate values. |
|||
|
|||
```ts |
|||
... |
|||
return useCallback( |
|||
(content: string, _onFinish: () => void) => { |
|||
void setShowPendingOverlay(true); |
|||
void setIsLoading(true); |
|||
|
|||
void doContractCall({ |
|||
contractAddress, |
|||
contractName, |
|||
functionName: MESSAGE_FUNCTION, |
|||
functionArgs: [ |
|||
stringUtf8CV(content), |
|||
attachmentUri !== '' ? someCV(stringUtf8CV(attachmentUri)) : noneCV(), |
|||
], |
|||
onFinish: () => { |
|||
_onFinish(); |
|||
onFinish(); |
|||
}, |
|||
postConditions: [ |
|||
createFungiblePostCondition( |
|||
address, |
|||
FungibleConditionCode.Equal, |
|||
new BN(1), |
|||
createAssetInfo(contractAddress, 'hey-token', 'hey-token') |
|||
), |
|||
], |
|||
onCancel, |
|||
network, |
|||
stxAddress: address, |
|||
}); |
|||
}, |
|||
[setIsLoading, onFinish, network, onCancel, address, doContractCall] |
|||
); |
|||
... |
|||
``` |
|||
|
|||
The frontend uses the `doContractCall` function from the [`@stacks/connect-react`][] package to perform the call to the |
|||
Clarity smart contract. In order to support the mapping of [Javascript types to Clarity types][], helpers exported from |
|||
the [`@stacks/transactions`][] package are used as arguments to the contract call. |
|||
|
|||
Note that the contract call also create post conditions to verify that a single $HEY token is transferred by the |
|||
execution of the contract call. Post conditions are a powerful feature of Clarity that can be used to prevent |
|||
rug-pulling and other detrimental behavior by smart contracts. |
|||
|
|||
### Reading transactions |
|||
|
|||
Heystack achieves pseudo-real-time messaging by reading both confirmed and pending transactions from the blockchain. |
|||
Pending transactions are read from the mempool, whereas confirmed transactions are read directly from the chain. The |
|||
[`src/store/hey.ts`][] file contains the implementation of both. |
|||
|
|||
```ts |
|||
... |
|||
export const heyTransactionsAtom = atomWithQuery<ContractCallTransaction[], string>(get => ({ |
|||
queryKey: ['hey-txs'], |
|||
...(defaultOptions as any), |
|||
refetchInterval: 500, |
|||
queryFn: async (): Promise<ContractCallTransaction[]> => { |
|||
const client = get(accountsClientAtom); |
|||
const txClient = get(transactionsClientAtom); |
|||
|
|||
const txs = await client.getAccountTransactions({ |
|||
limit: 50, |
|||
principal: HEY_CONTRACT, |
|||
}); |
|||
const txids = (txs as TransactionResults).results |
|||
.filter( |
|||
tx => |
|||
tx.tx_type === 'contract_call' && |
|||
tx.contract_call.function_name === MESSAGE_FUNCTION && |
|||
tx.tx_status === 'success' |
|||
) |
|||
.map(tx => tx.tx_id); |
|||
|
|||
const final = await Promise.all(txids.map(async txId => txClient.getTransactionById({ txId }))); |
|||
return final as ContractCallTransaction[]; |
|||
}, |
|||
})); |
|||
... |
|||
``` |
|||
|
|||
The `getAccountTransactions` from the `AccountsApi` object exported by [`@stacks/blockchain-api-client`][] is used to |
|||
read confirmed blockchain transactions against the `hey.clar` contract from the Stacks API. The list of transactions |
|||
returned by the API is filtered to only transactions representing a call to the message function that was successful, |
|||
and then mapped to an array of transaction IDs. |
|||
|
|||
Finally, the array of IDs is used to read each full transaction from the blockchain using the `getTransactionsById` |
|||
method from the `TransactionsApi` object exported by the [`@stacks/blockchain-api-client`][] package. |
|||
|
|||
Pending transactions are read from the mempool in a similar implementation. |
|||
|
|||
```ts |
|||
export const pendingTxsAtom = atomWithQuery<Heystack[], string>(get => ({ |
|||
queryKey: ['hey-pending-txs'], |
|||
refetchInterval: 1000, |
|||
...(defaultOptions as any), |
|||
queryFn: async (): Promise<Heystack[]> => { |
|||
const client = get(transactionsClientAtom); |
|||
|
|||
const txs = await client.getMempoolTransactionList({ limit: 96 }); |
|||
const heyTxs = (txs as MempoolTransactionListResponse).results |
|||
.filter( |
|||
tx => |
|||
tx.tx_type === 'contract_call' && |
|||
tx.contract_call.contract_id === HEY_CONTRACT && |
|||
tx.contract_call.function_name === MESSAGE_FUNCTION && |
|||
tx.tx_status === 'pending' |
|||
) |
|||
.map(tx => tx.tx_id); |
|||
|
|||
const final = await Promise.all(heyTxs.map(async txId => client.getTransactionById({ txId }))); |
|||
|
|||
return ( |
|||
(final as ContractCallTransaction[]).map(tx => { |
|||
const attachment = tx.contract_call.function_args?.[1].repr |
|||
.replace(`(some u"`, '') |
|||
.slice(0, -1); |
|||
|
|||
return { |
|||
sender: tx.sender_address, |
|||
content: tx.contract_call.function_args?.[0].repr |
|||
.replace(`u"`, '') |
|||
.slice(0, -1) as string, |
|||
id: tx.tx_id, |
|||
attachment: attachment === 'non' ? undefined : attachment, |
|||
timestamp: (tx as any).receipt_time, |
|||
isPending: true, |
|||
}; |
|||
}) || [] |
|||
); |
|||
}, |
|||
})); |
|||
``` |
|||
|
|||
Pending transactions are read from the mempool using the `getMempoolTransactionList` method from the `TransactionsApi` |
|||
exported by [`@stacks/blockchain-api-client`][]. Similar to confirmed transactions, the returned array is filtered to |
|||
a list of IDs, and then used to generate an array of full transactions. |
|||
|
|||
Because of differences in the data structure of the pending transactions vs. confirmed transactions, the pending |
|||
transaction list must be standardized before being returned. |
|||
|
|||
Note that for the low stakes of a messaging app, pending transactions can be treated as likely permanent state |
|||
transitions. For applications implementing higher stakes business logic (such as the transfer of representations |
|||
of value) it would be more appropriate to wait to display confirmed transactions. |
|||
|
|||
### Clarity types in Javascript |
|||
|
|||
In order to create transactions to call functions in Clarity contracts, the [`@stacks/transactions`][] package exports |
|||
classes that make it easy to construct well-typed Clarity values in Javascript. According to the Clarity language |
|||
specification, Clarity has the following types: |
|||
|
|||
- `(tuple (key-name-0 key-type 0) (key-name-1 key-type-1) ...)` - a typed tuple with named fields |
|||
- `(list max-len entry-type)` - a list of maximum length `max-len`, with entries of type `entry-type` |
|||
- `(response ok-type err-type)` - object used by public functions to commit their state changes or abort |
|||
- `(optional some-type)` - an option type for objects that can be either `(some-value)` or `none` |
|||
- `(buff max-len)` - byte buffer of maximum length |
|||
- `principal` - object representing a principal address (contract or standard) |
|||
- `bool` - boolean value (`true` or `false`) |
|||
- `int` - signed 128-bit integer |
|||
- `uint` - unsigned 128-bit integer |
|||
|
|||
To support these types in Javascript, [`@stacks/transactions`][] exports the following helpers: |
|||
|
|||
```ts |
|||
// construct boolean clarity values |
|||
const t = trueCV(); |
|||
const f = falseCV(); |
|||
|
|||
// construct optional clarity values |
|||
const nothing = noneCV(); |
|||
const something = someCV(t); |
|||
|
|||
// construct a buffer clarity value from an existing Buffer |
|||
const buffer = Buffer.from('foo'); |
|||
const bufCV = bufferCV(buffer); |
|||
|
|||
// construct signed and unsigned integer clarity values |
|||
const i = intCV(-10); |
|||
const u = uintCV(10); |
|||
|
|||
// construct principal clarity values |
|||
const address = 'SP2JXKMSH007NPYAQHKJPQMAQYAD90NQGTVJVQ02B'; |
|||
const contractName = 'contract-name'; |
|||
const spCV = standardPrincipalCV(address); |
|||
const cpCV = contractPrincipalCV(address, contractName); |
|||
|
|||
// construct response clarity values |
|||
const errCV = responseErrorCV(trueCV()); |
|||
const okCV = responseOkCV(falseCV()); |
|||
|
|||
// construct tuple clarity values |
|||
const tupCV = tupleCV({ |
|||
a: intCV(1), |
|||
b: trueCV(), |
|||
c: falseCV(), |
|||
}); |
|||
|
|||
// construct list clarity values |
|||
const l = listCV([trueCV(), falseCV()]); |
|||
``` |
|||
|
|||
You should use these helpers when calling Clarity contracts with Javascript to avoid failed contract calls due to bad |
|||
typing. |
|||
|
|||
## Reading BNS names |
|||
|
|||
An important feature of Stacks is the [Blockchain Naming System][] (BNS). BNS allows users to register a human-readable |
|||
identity to their account, that can act as both a username and a web address. |
|||
|
|||
Names registered to a user can be read from a Stacks API endpoint, as demonstrated in [`src/store/names.ts`][]. |
|||
|
|||
-> Due to ecosystem limitations, it's currently uncommon for BNS names to be registered on any testnet. For the purpose |
|||
of demonstration, Heystack looks for BNS names against the user's mainnet wallet address. |
|||
|
|||
```ts |
|||
export const namesAtom = atomFamily((address: string) => |
|||
atom(async get => { |
|||
if (!address || address === '') return; |
|||
const network = get(mainnetNetworkAtom); |
|||
if (!network) return null; |
|||
|
|||
const local = getLocalNames(network.coreApiUrl, address); |
|||
|
|||
if (local) { |
|||
const [names, timestamp] = local; |
|||
const now = Date.now(); |
|||
const isStale = now - timestamp > STALE_TIME; |
|||
if (!isStale) return names; |
|||
} |
|||
|
|||
try { |
|||
const names = await fetchNamesByAddress({ |
|||
networkUrl: network.coreApiUrl, |
|||
address, |
|||
}); |
|||
if (names?.length) { |
|||
setLocalNames(network.coreApiUrl, address, [names, Date.now()]); |
|||
} |
|||
return names || []; |
|||
} catch (e) { |
|||
console.error(e); |
|||
return []; |
|||
} |
|||
}) |
|||
); |
|||
``` |
|||
|
|||
In order to reduce network traffic, Heystack also caches names in the browser's local storage. |
|||
|
|||
A common design pattern in Stacks 2.0 apps is to check if a user has a registered BNS name (only 1 name can be tied to |
|||
an account) and display that name in the app where appropriate. If the user doesn't own a BNS name, the wallet address |
|||
is used as a stand in. Often, the wallet address is truncated to avoid displaying an overly long string. |
|||
|
|||
The account name component in [`src/components/user-area.tsx`][] demonstrates this design pattern: |
|||
|
|||
```tsx |
|||
... |
|||
const AccountNameComponent = memo(() => { |
|||
const { user } = useUser(); |
|||
const address = useCurrentMainnetAddress(); |
|||
const names = useAccountNames(address); |
|||
const name = names?.[0]; |
|||
return <Text mb="tight">{name || user?.username || truncateMiddle(address)}</Text>; |
|||
}); |
|||
... |
|||
``` |
|||
|
|||
[heystack]: https://heystack.xyz |
|||
[stacks.js]: https://github.com/blockstack/stacks.js |
|||
[stacks web wallet]: https://www.hiro.so/wallet/install-web |
|||
[react]: https://reactjs.org/ |
|||
[heystack_gh]: https://github.com/blockstack/heystack |
|||
[data maps]: /references/language-functions#define-map |
|||
[`default-to`]: /references/language-functions#default-to |
|||
[`asserts!`]: /references/language-functions#asserts |
|||
[`unwrap-panic`]: /references/language-functions#unwrap-panic |
|||
[`unwrap!`]: /references/language-functions#unwrap |
|||
[`map-set`]: /references/language-functions#map-set |
|||
[sip-010]: https://github.com/hstove/sips/blob/feat/sip-10-ft/sips/sip-010/sip-010-fungible-token-standard.md |
|||
[`impl-trait`]: /references/language-functions#impl-trait |
|||
[`@stacks/connect-react`]: https://github.com/blockstack/connect#readme |
|||
[`@stacks/auth`]: https://github.com/blockstack/stacks.js/tree/master/packages/auth |
|||
[jotai]: https://github.com/pmndrs/jotai |
|||
[connect wallet button component]: https://github.com/blockstack/heystack/blob/main/src/components/connect-wallet-button.tsx |
|||
[welcome panel component]: https://github.com/blockstack/heystack/blob/63ce30f4f6de7a9c846fcdba3acbb6c7b82b83e3/src/components/welcome-panel.tsx#L102 |
|||
[`/src/store/auth.ts`]: https://github.com/blockstack/heystack/blob/main/src/store/auth.ts |
|||
[clarity types in javascript]: /build-apps/examples/heystack#clarity-types-in-javascript |
|||
[`@stacks/transactions`]: https://github.com/blockstack/stacks.js/tree/master/packages/transactions#constructing-clarity-values |
|||
[blockchain naming system]: /build-apps/references/bns |
|||
[`src/store/names.ts`]: https://github.com/blockstack/heystack/blob/main/src/store/names.ts |
|||
[javascript types to clarity types]: /build-apps/examples/heystack#clarity-types-in-javascript |
|||
[`@stacks/blockchain-api-client`]: https://github.com/blockstack/stacks-blockchain-api/tree/master/client |
|||
[`src/common/hooks/use-publish-hey.ts`]: https://github.com/blockstack/heystack/blob/main/src/common/hooks/use-publish-hey.ts |
|||
[`src/store/hey.ts`]: https://github.com/blockstack/heystack/blob/main/src/store/hey.ts |
|||
[`src/components/user-area.tsx`]: https://github.com/blockstack/heystack/blob/22e4e9020f8bbb404e8c1e36f32f000050f90818/src/components/user-area.tsx#L62 |
Loading…
Reference in new issue