diff --git a/README.md b/README.md
index da2a2b4..1c90911 100644
--- a/README.md
+++ b/README.md
@@ -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);
+}
+```
diff --git a/packages/example/src/components/WalletDetails.tsx b/packages/example/src/components/WalletDetails.tsx
index 32d2314..504a5e7 100644
--- a/packages/example/src/components/WalletDetails.tsx
+++ b/packages/example/src/components/WalletDetails.tsx
@@ -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 {
@@ -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?.(
{
@@ -101,6 +122,35 @@ const WalletDetails = () => {
) : (
)}
+ {isSmartWalletProvisioned && (
+
+
Exit Offer
+
+
setOfferIdToExit(e.target.value)}
+ style={{ padding: '8px', borderRadius: '4px', border: '1px solid #ccc' }}
+ />
+
+ {exitOfferStatus && (
+
+ {exitOfferStatus}
+
+ )}
+
+
+ )}
);
};
diff --git a/packages/react-components/src/lib/context/AgoricContext.ts b/packages/react-components/src/lib/context/AgoricContext.ts
index ba88bcf..50aad51 100644
--- a/packages/react-components/src/lib/context/AgoricContext.ts
+++ b/packages/react-components/src/lib/context/AgoricContext.ts
@@ -36,6 +36,7 @@ export type AgoricState = {
makeOffer?: (
...params: Parameters
) => void;
+ exitOffer?: AgoricWalletConnection['exitOffer'];
};
export const AgoricContext = createContext({});
diff --git a/packages/react-components/src/lib/context/AgoricProvider.tsx b/packages/react-components/src/lib/context/AgoricProvider.tsx
index df77b9b..54a3d4d 100644
--- a/packages/react-components/src/lib/context/AgoricProvider.tsx
+++ b/packages/react-components/src/lib/context/AgoricProvider.tsx
@@ -208,6 +208,7 @@ export const AgoricProvider = ({
isSmartWalletProvisioned,
provisionSmartWallet: walletConnection?.provisionSmartWallet,
makeOffer: walletConnection?.makeOffer,
+ exitOffer: walletConnection?.exitOffer,
smartWalletProvisionFee,
smartWalletProvisionFeeUnit,
};
diff --git a/packages/web-components/src/wallet-connection/walletConnection.ts b/packages/web-components/src/wallet-connection/walletConnection.ts
index 42bdd80..e1f3b74 100644
--- a/packages/web-components/src/wallet-connection/walletConnection.ts
+++ b/packages/web-components/src/wallet-connection/walletConnection.ts
@@ -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,
diff --git a/packages/web-components/test/wallet-connection/walletConnection.test.js b/packages/web-components/test/wallet-connection/walletConnection.test.js
index 4f3df0d..bd7f5db 100644
--- a/packages/web-components/test/wallet-connection/walletConnection.test.js
+++ b/packages/web-components/test/wallet-connection/walletConnection.test.js
@@ -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',
+ );
+ });
+});