Skip to content

Commit

Permalink
feat(ui): make an offer and conclusion
Browse files Browse the repository at this point in the history
  • Loading branch information
samsiegart committed May 1, 2024
1 parent aa536ca commit 4545fe4
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 5 deletions.
2 changes: 1 addition & 1 deletion main/guides/ui-tutorial/agoric-provider/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This makes it easy to handle all the platform-specific implementation details, s
Install the following dependencies:

```
yarn add -D @agoric/react-components cosmos-kit@2.8.5 @interchain-ui/react@1.22.11
yarn add -D @agoric/react-components@0.1.1-dev-1329752.0 cosmos-kit@2.8.5 @interchain-ui/react@1.22.11
```

The `cosmos-kit` dependency is used to provide different wallets in the wallet connection modal.
Expand Down
20 changes: 20 additions & 0 deletions main/guides/ui-tutorial/conclusion/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## Conclusion

Throughout this tutorial, you've accomplished all the basics of building an Agoric Dapp UI:

- Scaffolding a new React App.
- Setting up `AgoricProvider` and connecting to a wallet.
- Reading and rendering purse balances.
- Reading contract data with `chainStorageWatcher`.
- Making an offer to exchange some on-chain assets.

With these basics in place, the world of Agoric UI development has opened up to you. To expand on your
newly acquired knowledge, consider trying out the following tasks:

- Spruce up your UI with `interchain-ui` components and try customizing the theme.
- Add automated e2e testing using https://github.com/agoric-labs/synpress
- Add a new item to the "Offer Up" contract and implement UI support for it.
- Deploying the contract to a testnet and adding the `NetworkDropdown` component to your dapp to switch between networks.
- Write your own dapp and implement a custom UI for it.

Check out the #dev channel in the Discord with any questions or feedback for future improvements on this guide.
2 changes: 0 additions & 2 deletions main/guides/ui-tutorial/connect-wallet/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,7 @@ const Purses = () => {
{istPurse ? (
stringifyAmountValue(
istPurse.currentAmount,
// @ts-expect-error displayInfo missing type
istPurse.displayInfo.assetKind,
// @ts-expect-error displayInfo missing type
istPurse.displayInfo.decimalPlaces
)
) : (
Expand Down
3 changes: 2 additions & 1 deletion main/guides/ui-tutorial/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# UI Tutorial

In this tutorial you will build your own UI from scratch. Along the way, you'll learn how to connect to a wallet, query the blockchain and smart contract state, sign transactions, and more!
In this tutorial you will build your own UI from scratch. Along the way, you'll learn how to connect to a wallet, query the blockchain and smart contract state, sign transactions, and more! To follow along, check your work, and help if you get stuck, you can use the https://github.com/agoric-labs/ui-tutorial repo as a reference. Each section of the tutorial has an associated branch in the repo
to act as a checkpoint.

## Prerequisites

Expand Down
280 changes: 280 additions & 0 deletions main/guides/ui-tutorial/making-an-offer/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
## Making an Offer

If you've made it this far, you've created a React app that connects to the wallet, renders
the IST purse balance of the user, and reads the chain with `chainStorageWatcher`. If you've
run into an issues, you can check out the `checkpoint-4` branch for reference.

In this final tutorial, you'll see how to make offers from your app, tying everything together
in a basic end-to-end experience for the user.

### Building out the UI

Before we can submit an offer, we'll need to build out some basic inputs so the user can specify
the Items they want. There's 3 types of items available for sale in the contract, so start by
creating an array to list them in `Trade.tsx`:

```ts
const allItems = ['scroll', 'map', 'potion'];
```

Next, add another component to `Trade.tsx` for letting the user choose the amount of each item in the offer:

```tsx
const Item = ({
label,
value,
onChange,
inputStep,
}: {
label: string;
value: number | string;
onChange: React.ChangeEventHandler<HTMLInputElement>;
inputStep?: string;
}) => (
<div className="item-col">
<h4>{label}</h4>
<input
title={label}
type="number"
min="0"
max="3"
value={value}
step={inputStep || '1'}
onChange={onChange}
className="input"
/>
</div>
);
```

And add some styles to `App.css`:

```css
.item-col {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 15px 25px 15px;
margin: 5px;
}

.row-center {
display: flex;
flex-direction: row;
align-items: center;
}

.input {
border: none;
background: #242424;
text-align: center;
padding: 5px 10px;
border-radius: 8px;
font-size: 1.2rem;
width: 75px;
}

.input[type='number']::-webkit-inner-spin-button {
opacity: 1;
}
```

Next, in your `Trade` component, render an `Item` for each item in the list:

```tsx
const Trade = () => {
...

const [choices, setChoices] = useState<Record<string, bigint>>({
map: 1n,
scroll: 2n,
});

const changeChoice = (ev: FormEvent) => {
if (!ev.target) return;
const elt = ev.target as HTMLInputElement;
const title = elt.title;
if (!title) return;
const qty = BigInt(elt.value);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [title]: _old, ...rest } = choices;
const newChoices = qty > 0 ? { ...rest, [title]: qty } : rest;
setChoices(newChoices);
};

return (
<div className="trade">
<h3>Want: Choose up to 3 items</h3>
<div className="row-center">
{allItems.map(title => (
<Item
key={title}
value={Number(choices[title] || 0n)}
label={title}
onChange={changeChoice}
/>
))}
</div>
</div>
);
}
```

As you can see, you're storing `choices` with a `useState` hook. This way, as the user changes the inputs,
the number of items of each type is updated. Later on you'll use `choices` to specify the offer.


Next, you'll add an input to let the user specify the amount of IST they want to give in exchange for the items.
First, get a reference to the IST purse with the `usePurse` you created earlier, and create a state hook for the IST value:

```tsx
const Trade = () => {
const istPurse = usePurse('IST');
const [giveValue, setGiveValue] = useState(0n);

...
}
```

Next, use the `<AmountInput>` component to add an input for the IST "give" amount:

```tsx
import { AmountInput } from '@agoric/react-components';

...

// In your 'Trade' component:
{istPurse && (
<>
<h3>Give: At least 0.25 IST</h3>
<div className="row-center">
<AmountInput
className="input"
value={giveValue}
onChange={setGiveValue}
decimalPlaces={istPurse.displayInfo.decimalPlaces as number}
/>
</div>
</>
)}
```

### Submitting the Offer

With these components in place, the user is able to select their Item and IST amounts, and the app
is able to store those in its state. Now, you'll see how to use the `makeOffer` function to sign
a transaction and make an offer to the smart contract with the selected amounts.

Recall the `useContract` hook you added to your `Trade` component previously. Now, you'll need the
brands and instance from that to submit the offer:

```ts
const { brands, instance } = useContract();
```

Next, get the `makeOffer` function from the `useAgoric()` hook:

```ts
const { makeOffer } = useAgoric();
```

Now, create a function to submit the offer. For more details on how this works, see [making an offer](../../getting-started/explainer-how-to-make-an-offer.md):

```ts
import { makeCopyBag } from '@agoric/store';

// Inside your 'Trade' component:
const submitOffer = () => {
assert(brands && instance && makeOffer);
const value = makeCopyBag(Object.entries(choices));
const want = { Items: { brand: brands.Item, value } };
const give = { Price: { brand: brands.IST, value: giveValue } };

makeOffer(
{
source: 'contract',
instance,
publicInvitationMaker: 'makeTradeInvitation',
},
{ give, want },
undefined,
(update: { status: string; data?: unknown }) => {
console.log('UPDATE', update);
if (update.status === 'error') {
alert(`Offer error: ${update.data}`);
}
if (update.status === 'accepted') {
alert('Offer accepted');
}
if (update.status === 'refunded') {
alert('Offer rejected');
}
}
);
};
```

This uses the `makeCopyBag` util to construct the Item amount in a way that the contract can understand.
Add it to your dependencies:

```
yarn add -D @agoric/store@0.9.2
```

And add the type to `vite-env.d.ts`:

```ts
declare module '@agoric/store' {
export const makeCopyBag;
}
```

Now, simply add a button to submit the offer:

```tsx
{!!(brands && instance && makeOffer && istPurse) && (
<button onClick={submitOffer}>Make Offer</button>
)}
```

Upon clicking the offer, you should see a Keplr window pop up to approve the transaction with the "Give" and "Want"
you selected. Try selecting 3 items, and giving 0.25 IST, and the offer should be accepted. See what happens
if you select more than 3 items, or give less than 0.25 IST... it should reject the offer, and you should be
refunded your IST (see [offer safety](../../../guides/zoe/offer-safety.md))

### Rendering the Items Purse

So, you've made a successful offer and acquired some items, but where are they? You can render the items purse
similarly to the IST purse. In `Purses.tsx`, add the following:

```tsx
...

const itemsPurse = usePurse('Item');
...

<div>
<b>Items: </b>
{itemsPurse ? (
<ul style={{ marginTop: 0, textAlign: 'left' }}>
{itemsPurse.currentAmount.value.payload.map(
// @ts-expect-error ignore 'any' type
([name, number]) => (
<li key={name}>
{String(number)} {name}
</li>
)
)}
</ul>
) : (
'None'
)}
</div>
```

Now, make another offer, and see that your items purse automatically updates after the offer is accepted. To see the complete
solution for this example, check out the `checkpoint-5` branch.

## Next

[Conclusion](../conclusion/index.md)
3 changes: 2 additions & 1 deletion main/guides/ui-tutorial/querying-contract-data/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ And add some styling for it in `App.css` while we're at it:
background: #171717;
border-radius: 24px;
margin-bottom: 16px;
padding: 16px;
}
```

Expand Down Expand Up @@ -115,7 +116,7 @@ As you can see, this hook makes use of `chainStorageWatcher` to watch two vstora

Go ahead and add this hook to the `Trade` component you made before this:

```
```tsx
import { useContract } from './hooks';

const Trade = () => {
Expand Down

0 comments on commit 4545fe4

Please sign in to comment.