From ac6626f2403d1555c1ec97b11b395e5d4ba24108 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Oct 2025 17:40:46 +0000 Subject: [PATCH 1/4] Initial plan From 04cef9f182d111105f5c0244677e829e76e3e0c6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Oct 2025 17:52:29 +0000 Subject: [PATCH 2/4] Add exitOffer method to wallet connection with tests and documentation Co-authored-by: turadg <21505+turadg@users.noreply.github.com> --- README.md | 20 +++++ .../src/wallet-connection/walletConnection.ts | 15 ++++ .../walletConnection.test.js | 81 +++++++++++++++++++ 3 files changed, 116 insertions(+) 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/web-components/src/wallet-connection/walletConnection.ts b/packages/web-components/src/wallet-connection/walletConnection.ts index 42bdd80..a80d92c 100644 --- a/packages/web-components/src/wallet-connection/walletConnection.ts +++ b/packages/web-components/src/wallet-connection/walletConnection.ts @@ -105,8 +105,23 @@ export const makeAgoricWalletConnection = async ( await watchP; }; + const exitOffer = async (offerId: string | number) => { + const { marshaller } = chainStorageWatcher; + const spendAction = marshaller.toCapData( + harden({ + method: 'tryExitOffer', + offerId, + }), + ); + + await null; + 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', + ); + }); +}); From 06af5b430007013a62d36b0c73f353a6f6b4c8fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Oct 2025 17:55:53 +0000 Subject: [PATCH 3/4] Remove unnecessary await null statement Co-authored-by: turadg <21505+turadg@users.noreply.github.com> --- .../web-components/src/wallet-connection/walletConnection.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/web-components/src/wallet-connection/walletConnection.ts b/packages/web-components/src/wallet-connection/walletConnection.ts index a80d92c..e1f3b74 100644 --- a/packages/web-components/src/wallet-connection/walletConnection.ts +++ b/packages/web-components/src/wallet-connection/walletConnection.ts @@ -114,7 +114,6 @@ export const makeAgoricWalletConnection = async ( }), ); - await null; const txn = await submitSpendAction(JSON.stringify(spendAction)); return txn; }; From 531b3651715bcce0bd47f1debc0626fadbaf4939 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Oct 2025 18:11:15 +0000 Subject: [PATCH 4/4] Add exit offer UI section to example app Co-authored-by: turadg <21505+turadg@users.noreply.github.com> --- .../example/src/components/WalletDetails.tsx | 50 +++++++++++++++++++ .../src/lib/context/AgoricContext.ts | 1 + .../src/lib/context/AgoricProvider.tsx | 1 + 3 files changed, 52 insertions(+) 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 && ( +
+ {exitOfferStatus} +
+ )} +