This is the starter template for Overmind's Pay me a River - Remastered FE quest. The original Pay me a River quest can be view here.
- Pay me a River - Remastered
- Table of Contents
- Tech Stack
- Developer Cheat Sheet
- Quest
- Yarn package manager
- React library for building user interfaces
- Next.js framework for React
- Tailwind CSS for styling
- shadcn/ui for UI components using Radix UI and Tailwind CSS
- Aptos wallet adapter, Aptos TS SDK, and Aptos API for interacting with the Aptos blockchain
This FE quest is built on top of the Pay me a River smart contract quest. The Pay me a River module is a smart contract that allows users to stream APT to others via a real-time payment. This real-time payment holds APT which is streamed to the recipient. The recipient can claim the streamed APT at anytime. The module code can be viewed here.
The Pay me a River module has the following entry functions:
create_stream
: Allows a user to create a new payment stream. The user must provide the following arguments:receiver_address
: The address of the recipient of the payment streamamount
: The amount of APT to be streamed to the recipientduration_in_seconds
: The duration of the payment stream in seconds
accept_stream
: Allows the recipient to accept a pending payment stream. The user must provide the following arguments:sender_address
: The address of the sender of the payment stream to be accepted
claim_stream
: Allows the recipient to claim all of the APT that has been streamed so far. This will close the stream if the stream is completed. The user must provide the following arguments:sender_address
: The address of the sender of the payment stream to be claimed
cancel_stream
: Allows the sender or recipient of the payment stream to cancel the stream. The user must provide the following arguments:sender_address
: The address of the sender of the payment stream to be cancelledreceiver_address
: The address of the recipient of the payment stream to be cancelled
The Pay me a River module has the following view functions:
get_receivers_streams
: Returns a list of all payment streams that the user is the recipient of. The user must provide the following arguments:receiver_address
: The address of the recipient of the payment streams
get_senders_streams
: Returns a list of all payment streams that the user is the sender of. The user must provide the following arguments:sender_address
: The address of the sender of the payment streams]
The Pay me a River module has the following events (stored in the ModuleEventStore
resource):
stream_create_events
: Emitted when a new payment stream is created. The event contains the following data:sender_address
: The address of the sender of the payment streamreceiver_address
: The address of the recipient of the payment streamamount
: The amount of APT to be streamed to the recipientduration_in_seconds
: The duration of the payment stream in secondsstream_id
: The unique id of the payment streamtimestamp
: The timestamp when the payment stream was created
stream_accept_events
: Emitted when a payment stream is accepted. The event contains the following data:sender_address
: The address of the sender of the payment streamreceiver_address
: The address of the recipient of the payment streamstream_id
: The unique id of the payment streamtimestamp
: The timestamp when the payment stream was created
stream_claim_events
: Emitted when a payment stream is claimed. The event contains the following data:sender_address
: The address of the sender of the payment streamreceiver_address
: The address of the recipient of the payment streamamount
: The amount of APT that was claimedstream_id
: The unique id of the payment streamtimestamp
: The timestamp when the payment stream was created
stream_close_events
: Emitted when a payment stream is closed. The event contains the following data:sender_address
: The address of the sender of the payment streamreceiver_address
: The address of the recipient of the payment streamamount_to_receiver
: The amount of APT sent to the recipientamount_to_sender
: The amount of APT refunded to the senderstream_id
: The unique id of the payment streamtimestamp
: The timestamp when the payment stream was created
This dapp interacts with a deployed pay_me_a_river
instance on Aptos Testnet. The deployed module has the following properties:
- module address:
0x70a6f54475da6bdb55b5d938249a7d496797c7515a3d24aa2fa778c0a8d7a36f
- module name:
pay_me_a_river
In this contract, the admin/module deployer is used to store all of the module's resources instead of a resource account. Because of this the resource account constant is the same as the module address: 0x70a6f54475da6bdb55b5d938249a7d496797c7515a3d24aa2fa778c0a8d7a36f
These properties are set up in the next.config.js
and can be used in the tsx file with:
- module address:
process.env.MODULE_ADDRESS
- module name:
process.env.MODULE_NAME
- module's resource account address:
process.env.RESOURCE_ACCOUNT_ADDRESS
The deployed module can be viewed here.
This dapp is built using React and Next.js. React is a JavaScript library for building user interfaces. Next.js is a React framework that provides a number of features including server-side rendering, file-based routing, and automatic code splitting.
This dapp uses conditional rendering to display different components based on the state of the dapp. The following examples show how conditional rendering is used in this dapp.
The following code snippet shows how conditional rendering is used to display a loading message while the wallet is loading:
// Get the isLoading property from the wallet adapter (boolean indicating if the wallet is loading)
const { isLoading } = useWallet();
// Conditional rendering to display a loading message
<>
{
isLoading &&
(
<div>
<h1>Loading...</h1>
</div>
)
}
</>
The following code snippet shows how conditional rendering is used with the ternary operator to display a message based on the connected state of the wallet:
// Get the connected property from the wallet adapter (boolean indicating if the wallet is connected)
const { connected } = useWallet();
// Conditional rendering to display a message based on the connected state
<>
{
connected ?
(
<div>
<h1>Connected</h1>
</div>
) :
(
<div>
<h1>Not connected</h1>
</div>
)
}
</>
There are times when a list of data needs to be rendered. If the list is static, the list can be hard-coded in the tsx file. If the list is dynamic, the list can be stored in a state variable and rendered using the map
function. This is called dynamic rendering and is used in this dapp.
This dapp uses the map
function to render lists of data. The following examples show how the map
function is used in this dapp.
The following code snippet shows how the map
function is used to render a list of wallets:
// Get the wallets property from the wallet adapter
const { wallets } = useWallet();
/*
Iterates through each wallets and returns a div with the wallet name. The index is used as the key
to ensure that each div is unique.
*/
<>
<h1>Wallets</h1>
{
wallets.map((wallet, index) => {
return (
<div key={index}>
<h2>{wallet.name}</h2>
</div>
)
})
}
</>
The Aptos wallet adapter contains the logic for connecting to the Aptos wallet and submitting transactions.
The wallet adapter provider is initialized in the app/layout.tsx
file.
All wallet properties and functions are provided with the useWallet
hook. The hook must be imported from the wallet adapter package:
import { useWallet } from '@aptos-labs/aptos-wallet-adapter-react';
Once the hook is imported, you can use the provide properties and functions:
const {
connect, // connect to the wallet
account, // connected wallet's account information
network, // connected wallet's network
connected, // boolean indicating if the wallet is connected
disconnect, // disconnect from the wallet
isLoading, // boolean indicating if the wallet is loading
wallet, // connected wallet information
wallets, // list of information for all supported wallets
signAndSubmitTransaction, // sign and submit a transaction
signAndSubmitBCSTransaction, // sign and submit a BCS transaction
signTransaction, // sign a transaction
signMessage, // sign a message
signMessageAndVerify, // sign a message and verify
} = useWallet();
To connect to a wallet, use the connect
function provided by the useWallet
hook as follows:
// Get the connect function from the wallet adapter
const { connect } = useWallet();
// Function to connect to a wallet
const handleConnect = (walletName: string) => {
connect(walletName);
};
// Wallet selector component for a single wallet
<div>
<h1>{wallet.name}</h1>
<Button
variant="secondary"
onClick={() => handleConnect(wallet.name)}
>
Connect
</Button>
</div>
Similarly, to disconnect from the wallet, use the disconnect
function provided by the useWallet
hook as follows:
// Get the disconnect function from the wallet adapter
const { disconnect } = useWallet();
// Function to disconnect from the wallet
const handleDisconnect = () => {
disconnect();
};
// Disconnect button
<Button onClick={() => handleDisconnect()}>
Disconnect
</Button>
To sign and submit a transaction, use the signAndSubmitTransaction
function provided by the useWallet
hook as follows:
/*
Function to sign and submit a transaction. In this case, the transaction is a call to transfer 10
APT to the address 0xabc123.
Note: The Types.TransactionPayload type is imported from the Aptos TS SDK. This is the only use of
the TS SDK in this dapp.
Note: The signer argument is provided by the wallet adapter internally. There is no need to
provide this argument.
*/
const handleSignAndSubmitTransaction = async () => {
// Transaction payload
const payload: Types.TransactionPayload = {
type: "entry_function_payload", // The type of transaction payload
function: `0x1::coin::transfer`, // The address::module::function to call
type_arguments: ['0x1::aptos_coin::AptosCoin'],
arguments: [
'0xabc123', // recipient address
1000000000, // 10 APT
],
};
/*
Sign and submit the transaction in a try/catch block
NOTE: The sleep helper function to ensure that the transaction is reliably viewable on the
blockchain. The minimum amount of seconds to wait to get reliable results is stored in the
environment under `TRANSACTION_DELAY_MILLISECONDS`. Even though the Aptos TS SDK has a
helper function, waitForTransaction, we have found it more reliable to use the sleep
function.
*/
try {
const result = await signAndSubmitTransaction(payload);
await sleep(parseInt(process.env.TRANSACTION_DELAY_MILLISECONDS || '0'))
console.log(result);
} catch (e) {
console.log(e);
}
};
// Sign and submit transaction button
<Button onClick={() => handleSignAndSubmitTransaction()}>
Transfer 10 APT to 0xabc123
</Button>
The account
property provided by the useWallet
hook contains the following information:
declare type AccountInfo = {
address: string;
publicKey: string | string[];
minKeysRequired?: number;
ansName?: string | null;
};
// Div to display account address
<div>
<h1>Address: {account.address}</h1>
</div>
The network
property provided by the useWallet
hook contains the following information:
declare type NetworkInfo = {
name: NetworkName;
chainId?: string;
url?: string;
};
enum NetworkName {
Mainnet = "mainnet",
Testnet = "testnet",
Devnet = "devnet"
}
// Example conditional rendering based on network
<>
{
network.name.toString() !== 'Testnet' &&
(
<div>
<h1>Warning! You are on the wrong network. Please switch to Testnet.</h1>
</div>
)
}
</>
The wallet
property provided by the useWallet
hook contains the following information:
declare type WalletInfo = {
name: WalletName; // name of the wallet as a string
icon: string;
url: string;
};
// Div to display wallet name
<div>
<h1>Wallet: {wallet.name}</h1>
</div>
The wallets
property provided by the useWallet
hook contains the following information:
declare type Wallet<Name extends string = string> = AdapterPlugin<Name> & {
readyState?: WalletReadyState;
};
declare type AdapterPlugin<Name extends string = string> = AdapterPluginProps<Name> & AdapterPluginEvents;
interface AdapterPluginProps<Name extends string = string> {
name: WalletName<Name>;
url: string;
icon: `data:image/${"svg+xml" | "webp" | "png" | "gif"};base64,${string}`;
providerName?: string;
provider: any;
deeplinkProvider?: (data: {
url: string;
}) => string;
connect(): Promise<any>;
disconnect: () => Promise<any>;
network: () => Promise<any>;
signAndSubmitTransaction<T extends Types.TransactionPayload, V>(transaction: T, options?: V): Promise<{
hash: Types.HexEncodedBytes;
}>;
signMessage<T extends SignMessagePayload>(message: T): Promise<SignMessageResponse>;
}
interface AdapterPluginEvents {
onNetworkChange: OnNetworkChange;
onAccountChange(callback: any): Promise<any>;
}
declare enum WalletReadyState {
/**
* User-installable wallets can typically be detected by scanning for an API
* that they've injected into the global context. If such an API is present,
* we consider the wallet to have been installed.
*/
Installed = "Installed",
NotDetected = "NotDetected",
/**
* Loadable wallets are always available to you. Since you can load them at
* any time, it's meaningless to say that they have been detected.
*/
Loadable = "Loadable",
/**
* If a wallet is not supported on a given platform (eg. server-rendering, or
* mobile) then it will stay in the `Unsupported` state.
*/
Unsupported = "Unsupported"
}
/*
div to display all wallets and provide a connect or install button based on the wallet's ready
state
*/
<>
{
wallets.map((wallet, index) => (
<div key={index}>
<h1>{wallet.name}</h1>
{
wallet.readyState === WalletReadyState.Installed &&
(
<Button
variant="secondary"
onClick={() => handleConnect(wallet.name)}
>
Connect
</Button>
)
}
{
wallet.readyState === WalletReadyState.NotDetected &&
(
<a href={wallet.url} target="_blank">
<Button
variant="secondary"
>
Install
</Button>
</a>
)
}
</div>
))
}
</>
The Aptos API is used to read data from the Aptos blockchain. In this dapp, the API is used to call view functions as well as query events emitted by the pay me a river module.
The Aptos API can be used to call view functions. The API view function endpoint is:
https://fullnode.testnet.aptoslabs.com/v1/view
Making a request to this endpoint can be done the following way:
// State to store balance with default value of "0"
const [balance, setBalance] = useState<string>("0");
// Get the account and connected state from the wallet adapter
const { account, connected } = useWallet();
// Calls the getBalance function when the account or connected state changes
useEffect(() => {
if (connected && account) {
getBalance(account.address);
}
}, [connected, account]);
// Function to call the balance view function
const getBalance = async (address: string) => {
const body = {
function:
"0x1::coin::balance",
type_arguments: ["0x1::aptos_coin::AptosCoin"],
arguments: [address],
};
let res;
try {
res = await fetch(
`https://fullnode.testnet.aptoslabs.com/v1/view`,
{
method: 'POST',
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
}
)
} catch (e) {
setBalance("0");
return;
}
const data = await res.json();
setBalance((data / 100000000).toLocaleString());
}
// Div to display balance
<div>
<h1>Balance: {balance}</h1>
</div>
The Aptos API can be used to retrieve account data. The API account endpoint is:
https://fullnode.testnet.aptoslabs.com/v1/accounts/{address}
where {address}
is the address of the account to retrieve data for.
Making a request to this endpoint can done the following way:
const getAccountData = async () => {
// Making the API request
const response = await fetch (
`https://fullnode.testnet.aptoslabs.com/v1/accounts/${account.address}`,
{
method: 'GET'
}
);
// Parsing the response into a json
const accountData = await response.json();
// If the response is the error code for account not found, the account has not been initialized
if (accountData.error_code == 'account_not_found') {
initializeAccount();
} else {
setAccountData(accountData);
}
console.log(accountData);
}
The Aptos API can be used to query events emitted by the pay me a river module. The API event endpoint is:
https://fullnode.testnet.aptoslabs.com/v1/accounts/{address}/events/{event_handle}/{field_name}
where {address}
is the address of the account that holds the object containing the events, {event_handle}
is the type of the object that contains the events, and {field_name}
is the name of the field that contains the events.
Making a request to this endpoint can be done the following way:
// Event types
type Event = {
type: "stream_created" | "stream_accepted" | "stream_claimed" | "stream_cancelled" | "unknown",
timestamp: number,
data: {
amount?: number,
amount_to_sender?: number,
amount_to_recipient?: number,
}
}
// connected and account state from the wallet adapter
const { connected, account } = useWallet();
// State to store events
const [events, setEvents] = useState([]);
// Updates the events state when the account or connected state changes
useEffect(() => {
if (connected && account) {
getEvents();
} else {
setEvents([]);
}
}, [connected, account]);
/*
function to query `stream_create_events` events emitted by the pay me a river module
NOTE: The limit query parameter is set to 10000 to ensure that all events are returned. The
default limit is 75.
*/
const getEvents = async () => {
const response = await fetch (
`https://fullnode.testnet.aptoslabs.com/v1/accounts/${process.env.RESOURCE_ACCOUNT_ADDRESS}/events/${process.env.MODULE_ADDRESS}::${process.env.MODULE_NAME}::ModuleEventStore/stream_create_events?limit=10000`,
{
method: 'GET'
}
);
const eventData = await response.json();
console.log(eventData);
setEvents(eventData);
}
// Div to display events
<div>
<h1>Events</h1>
{
events.map((event) => (
<div key={event.id}>
<h2>Type: {event.type}</h2>
<h3>Timestamp: {event.timestamp}</h3>
{
event.data.amount &&
(
<h3>Amount: {event.data.amount}</h3>
)
}
{
event.data.amount_to_sender &&
(
<h3>Amount to sender: {event.data.amount_to_sender}</h3>
)
}
{
event.data.amount_to_recipient &&
(
<h3>Amount to recipient: {event.data.amount_to_recipient}</h3>
)
}
</div>
))
}
</div>
The Aptos TS SDK is used in this quest to fund uninitialized accounts.
The SDK provides a client to help fund accounts with APT. If an account does not exist, the client will initialize the account as well as fund it. An aptos account can be initialized and funded by the following:
// Import the client from the TS SDK as well as the network enum
import { FaucetClient, Network } from "aptos";
// create a new client
const faucetClient = new FaucetClient(Network.TESTNET, "https://faucet.testnet.aptoslabs.com");
/*
Fund the account with 1 APT
NOTE: The faucet's fundAccount will most likely fail when verifying the transactions. This is
expected behavior and the account will still be initialized and funded.
*/
try {
/*
The faucet's fundAccount function takes the address of the account to fund, the amount of APT
to fund the account with, and the number of seconds to wait before timing out.
*/
await faucetClient.fundAccount(account.address, 100000000, 1);
} catch (e) {
console.log(e);
}
The Aptos explorer can be used to view transaction details. The explorer transaction endpoint is:
https://explorer.aptoslabs.com/txn/${transaction_hash}?network=testnet
where {transaction_hash}
is the hash of the transaction to view.
The Aptos explorer can be used to view account details. The explorer account endpoint is:
https://explorer.aptoslabs.com/account/${account_address}?network=testnet
where {account_address}
is the address of the account to view.
- Navigate to the
app
directory - Run
yarn install
to install dependencies - Run
yarn dev
to start the development server - Open http://localhost:3000 with your browser to see the result
- Read through the Developer Cheat Sheet above to understand the dapp and the supporting dependencies. Look back to that section for reference as you complete the quest.
- Visit and try out the demo dapp here. Feel free to use this demo as a reference as you complete the quest.
- Deploy and open the dapp locally as described above.
- Complete the quests by following the TODO comments in the following files (recommended order):