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
:
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:
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
:
.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:
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:
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:
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:
const { brands, instance } = useContract();
Next, get the makeOffer
function from the useAgoric()
hook:
const { makeOffer } = useAgoric();
Now, create a function to submit the offer. For more details on how this works, see making an offer:
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');
}
}
);
};
For specifics on how offers work, see Specifying Offers. The makeOffer
function allows you to specify an InvitationSpec
, automatically handles the marshalling aspect, and makes it easy to handle updates to the offer status.
The function you just wrote 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
:
declare module '@agoric/store' {
export const makeCopyBag;
}
Now, simply add a button to submit the offer:
{
!!(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)
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:
...
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.
Result
Curious to know how it looks after implementation? Check out our guide for the result.