Skip to content
Merged
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
24 changes: 24 additions & 0 deletions packages/portfolio-contract/src/portfolio.contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ import {
OrchestrationPowersShape,
registerChainsAndAssets,
withOrchestration,
type AccountId,
type Bech32Address,
type ChainInfo,
type Denom,
type DenomAmount,
type DenomDetail,
type OrchestrationPowers,
type OrchestrationTools,
Expand Down Expand Up @@ -265,6 +267,7 @@ export const contract = async (
void vowTools.when(contractAccountV, acct => {
const addr = acct.getAddress();
publishStatus(storageNode, harden({ contractAccount: addr.value }));
trace('published contractAccount', addr.value);
});

const ctx1: flows.PortfolioInstanceContext = {
Expand Down Expand Up @@ -408,6 +411,9 @@ export const contract = async (
M.string(),
M.remotable('Instance'),
).returns(),
withdrawFees: M.callWhen(M.string())
.optional(M.record())
.returns(M.record()),
}),
{
makeResolverInvitation() {
Expand Down Expand Up @@ -448,6 +454,24 @@ export const contract = async (
await E(pfP).deliverPayment(address, invitation);
trace('delivered planner invitation');
},
/**
* Withdraw from contractAccount; for example, before terminating the contract
*
* @param toAccount
* @param optAmount - defaults to BLD balance
*/
async withdrawFees(toAccount: AccountId, optAmount?: DenomAmount) {
const traceWithdraw = trace.sub('withdrawFees');
traceWithdraw('to', toAccount);
// LCA operations are prompt
const { when } = vowTools;
const acct = await when(contractAccountV);
const amount = await (optAmount || when(acct.getBalance('ubld')));
Copy link
Preview

Copilot AI Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parentheses are incorrectly placed. This will always await optAmount if it exists (which could be a synchronous value), or await the balance if not. It should be const amount = optAmount || await when(acct.getBalance('ubld')); to properly handle the optional parameter.

Suggested change
const amount = await (optAmount || when(acct.getBalance('ubld')));
const amount = optAmount || await when(acct.getBalance('ubld'));

Copilot uses AI. Check for mistakes.

Copy link
Member Author

@dckc dckc Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot, This is intentional. We prefer to not have code paths that sometimes await and sometimes don't. cf. Zalgo and all that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, there's an await on the prior line. Does that allow the nested await?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The await above means our lint rule is satisfied. But my impression is that in any case, it's best not to have code paths that are sometimes a turn boundary and sometimes not.

traceWithdraw('amount', amount);
await when(acct.send(toAccount, amount));
traceWithdraw({ amount, from: acct.getAddress().value, toAccount });
return amount;
},
},
);

Expand Down
63 changes: 63 additions & 0 deletions packages/portfolio-contract/test/portfolio.contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1084,3 +1084,66 @@ test('withdraw using planner', async t => {
]);
t.is(833332500n, (3333330000n * 25n) / 100n);
});

test('creatorFacet.withdrawFees', async t => {
const { started, common } = await setupTrader(t);
const { storage } = common.bootstrap;
const { inspectLocalBridge } = common.utils;

const { contractAccount } = storage
.getDeserialized(`${ROOT_STORAGE_PATH}`)
.at(-1) as unknown as { contractAccount: string };
t.log('contractAddress for fees', contractAccount);

const { bld } = common.brands;
const bld2k = bld.units(2_000);
{
const pmt = await common.utils.pourPayment(bld2k);
const { bankManager } = common.bootstrap;
const bank = E(bankManager).getBankForAddress(contractAccount);
const purse = E(bank).getPurse(bld2k.brand);
await E(purse).deposit(pmt);
t.log('deposited', bld2k, 'for fees');
}

const { creatorFacet } = started;

const dest = `cosmos:agoric-3:${makeTestAddress(5)}` as const;

{
const actual = await E(creatorFacet).withdrawFees(dest, {
denom: 'ubld',
value: 100n,
});
t.log('withdrew some', actual);

t.deepEqual(actual, { denom: 'ubld', value: 100n });

const [tx] = inspectLocalBridge().filter(
obj => obj.type === 'VLOCALCHAIN_EXECUTE_TX',
);
t.like(tx.messages[0], {
'@type': '/cosmos.bank.v1beta1.MsgSend',
fromAddress: 'agoric1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqp7zqht',
toAddress: 'agoric1q5qqqqqqqqqqqqqqqqqqqqqqqqqqqqqq8ee25y',
amount: [{ denom: 'ubld', amount: '100' }],
});
}

{
const amt = await E(creatorFacet).withdrawFees(dest);

t.log('withdrew all', amt);
t.deepEqual(amt, { denom: 'ubld', value: bld2k.value });

const [_, tx] = inspectLocalBridge().filter(
obj => obj.type === 'VLOCALCHAIN_EXECUTE_TX',
);
t.like(tx.messages[0], {
'@type': '/cosmos.bank.v1beta1.MsgSend',
fromAddress: 'agoric1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqp7zqht',
toAddress: 'agoric1q5qqqqqqqqqqqqqqqqqqqqqqqqqqqqqq8ee25y',
amount: [{ denom: 'ubld', amount: '2000000000' }],
});
}
});
Loading