Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,23 @@ connection.makeOffer(
},
);
```

## Exiting Offers

To allow users to exit long-standing offers from your dapp UI:

```ts
import { makeAgoricChainStorageWatcher } from '@agoric/rpc';
import { makeAgoricWalletConnection } from '@agoric/web-components';

const watcher = makeAgoricChainStorageWatcher(apiAddr, chainName);
const connection = await makeAgoricWalletConnection(watcher, rpcAddr);

// Exit an offer by its id
try {
const txn = await connection.exitOffer(offerId);
console.log('Offer exit transaction:', txn);
} catch (error) {
console.error('Failed to exit offer:', error);
}
```
50 changes: 50 additions & 0 deletions packages/example/src/components/WalletDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useAgoric, type PurseJSONState } from '@agoric/react-components';
import { stringifyAmountValue, stringifyValue } from '@agoric/web-components';
import { useState } from 'react';

const WalletDetails = () => {
const {
Expand All @@ -9,10 +10,30 @@ const WalletDetails = () => {
provisionSmartWallet,
smartWalletProvisionFee,
makeOffer,
exitOffer,
} = useAgoric();
const usdcPurseBrand = purses?.find(p => p.brandPetname === 'USDC')
?.currentAmount.brand;

const [offerIdToExit, setOfferIdToExit] = useState('');
const [exitOfferStatus, setExitOfferStatus] = useState('');

const handleExitOffer = async () => {
if (!offerIdToExit) {
setExitOfferStatus('Error: Please enter an offer ID');
return;
}
try {
setExitOfferStatus('Submitting...');
await exitOffer?.(offerIdToExit);
setExitOfferStatus('Success! Offer exit submitted.');
setOfferIdToExit('');
} catch (error: unknown) {
console.error('Error exiting offer:', error);
setExitOfferStatus('Error: ' + (error as Error).message);
}
};

const testTransaction = () => {
makeOffer?.(
{
Expand Down Expand Up @@ -101,6 +122,35 @@ const WalletDetails = () => {
) : (
<button onClick={testTransaction}>Test Transaction</button>
)}
{isSmartWalletProvisioned && (
<div style={{ marginTop: '20px', paddingTop: '20px', borderTop: '1px solid #ccc' }}>
<h3>Exit Offer</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
<input
type="text"
placeholder="Enter Offer ID"
value={offerIdToExit}
onChange={(e) => setOfferIdToExit(e.target.value)}
style={{ padding: '8px', borderRadius: '4px', border: '1px solid #ccc' }}
/>
<button
onClick={handleExitOffer}
disabled={!offerIdToExit}
style={{ padding: '8px' }}
>
Exit Offer
</button>
{exitOfferStatus && (
<p style={{
marginTop: '10px',
color: exitOfferStatus.includes('Error') ? 'red' : 'green'
}}>
{exitOfferStatus}
</p>
)}
</div>
</div>
)}
</div>
);
};
Expand Down
1 change: 1 addition & 0 deletions packages/react-components/src/lib/context/AgoricContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export type AgoricState = {
makeOffer?: (
...params: Parameters<AgoricWalletConnection['makeOffer']>
) => void;
exitOffer?: AgoricWalletConnection['exitOffer'];
};

export const AgoricContext = createContext<AgoricState>({});
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ export const AgoricProvider = ({
isSmartWalletProvisioned,
provisionSmartWallet: walletConnection?.provisionSmartWallet,
makeOffer: walletConnection?.makeOffer,
exitOffer: walletConnection?.exitOffer,
smartWalletProvisionFee,
smartWalletProvisionFeeUnit,
};
Expand Down
14 changes: 14 additions & 0 deletions packages/web-components/src/wallet-connection/walletConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,22 @@ export const makeAgoricWalletConnection = async (
await watchP;
};

const exitOffer = async (offerId: string | number) => {
const { marshaller } = chainStorageWatcher;
const spendAction = marshaller.toCapData(
harden({
method: 'tryExitOffer',
offerId,
}),
);

const txn = await submitSpendAction(JSON.stringify(spendAction));
return txn;
};

return {
makeOffer,
exitOffer,
address,
provisionSmartWallet,
signingClient: client,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,84 @@ it('submits a spend action', async () => {
connection.provisionSmartWallet();
expect(mockProvisionSmartWallet).toHaveBeenCalled();
});

describe('exitOffer', () => {
it('submits a tryExitOffer spend action with string offerId', async () => {
const watcher = {
chainId: 'agoric-foo',
watchLatest: (_path, onUpdate) => {
onUpdate({ offerToPublicSubscriberPaths: 'foo' });
},
marshaller: {
toCapData: val => val,
},
};

const connection = await makeAgoricWalletConnection(
// @ts-expect-error fake partial watcher implementation
watcher,
rpc,
undefined,
{ address: testAddress, client: {} },
);

await connection.exitOffer('123');

expect(mockSubmitSpendAction).toHaveBeenCalledWith(
'{"method":"tryExitOffer","offerId":"123"}',
);
});

it('submits a tryExitOffer spend action with numeric offerId', async () => {
const watcher = {
chainId: 'agoric-foo',
watchLatest: (_path, onUpdate) => {
onUpdate({ offerToPublicSubscriberPaths: 'foo' });
},
marshaller: {
toCapData: val => val,
},
};

const connection = await makeAgoricWalletConnection(
// @ts-expect-error fake partial watcher implementation
watcher,
rpc,
undefined,
{ address: testAddress, client: {} },
);

await connection.exitOffer(456);

expect(mockSubmitSpendAction).toHaveBeenCalledWith(
'{"method":"tryExitOffer","offerId":456}',
);
});

it('throws error when submitSpendAction fails', async () => {
const watcher = {
chainId: 'agoric-foo',
watchLatest: (_path, onUpdate) => {
onUpdate({ offerToPublicSubscriberPaths: 'foo' });
},
marshaller: {
toCapData: val => val,
},
};

const connection = await makeAgoricWalletConnection(
// @ts-expect-error fake partial watcher implementation
watcher,
rpc,
undefined,
{ address: testAddress, client: {} },
);

const testError = new Error('Transaction failed');
mockSubmitSpendAction.mockRejectedValueOnce(testError);

await expect(connection.exitOffer('789')).rejects.toThrow(
'Transaction failed',
);
});
});