Skip to content

Commit 478b160

Browse files
committed
feat: enhance proxy transaction handling with blocked UTxO management
- Integrated functions to extract and filter blocked UTxOs from pending transactions, improving the robustness of proxy transaction processing. - Updated `MeshProxyContract` to utilize blocked UTxO references when selecting UTxOs for spending, ensuring only available UTxOs are used. - Enhanced `ProxyControl` component to manage blocked UTxOs and ensure proper handling of available UTxOs for transactions. - Refactored UTxO selection logic to improve clarity and maintainability in the proxy transaction workflow.
1 parent 84035f9 commit 478b160

3 files changed

Lines changed: 149 additions & 158 deletions

File tree

src/components/multisig/proxy/ProxyControl.tsx

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import ProxySetup from "./ProxySetup";
1111
import ProxySpend from "./ProxySpend";
1212
import UTxOSelector from "@/components/pages/wallet/new-transaction/utxoSelector";
1313
import { getProvider } from "@/utils/get-provider";
14+
import {
15+
extractBlockedUtxoRefsFromPendingTxJson,
16+
filterBlockedUtxos,
17+
} from "@/lib/server/proxyUtxos";
1418
import type { MeshTxBuilder, UTxO } from "@meshsdk/core";
1519
import { useProxy } from "@/hooks/useProxy";
1620
import { useProxyData } from "@/lib/zustand/proxy";
@@ -119,6 +123,23 @@ export default function ProxyControl() {
119123
},
120124
});
121125

126+
const { data: pendingTransactions } = api.transaction.getPendingTransactions.useQuery(
127+
{ walletId: appWallet?.id ?? "" },
128+
{
129+
enabled: !!appWallet?.id,
130+
staleTime: 30 * 1000,
131+
refetchOnWindowFocus: false,
132+
},
133+
);
134+
135+
const blockedUtxoRefs = useMemo(
136+
() =>
137+
(pendingTransactions ?? []).flatMap((transaction) =>
138+
extractBlockedUtxoRefsFromPendingTxJson(transaction.txJson),
139+
),
140+
[pendingTransactions],
141+
);
142+
122143
// State management
123144
const [proxyContract, setProxyContract] = useState<MeshProxyContract | null>(null);
124145
const [isProxySetup, setIsProxySetup] = useState<boolean>(false);
@@ -163,9 +184,14 @@ export default function ProxyControl() {
163184
if (!utxos || utxos.length === 0) {
164185
throw new Error("No UTxOs found at multisig wallet address");
165186
}
187+
188+
const freeUtxos = filterBlockedUtxos(utxos, blockedUtxoRefs);
189+
if (freeUtxos.length === 0) {
190+
throw new Error("No free UTxOs found at multisig wallet address");
191+
}
166192

167-
return { utxos, walletAddress: appWallet.address };
168-
}, [appWallet?.address, network]);
193+
return { utxos: freeUtxos, walletAddress: appWallet.address };
194+
}, [appWallet?.address, blockedUtxoRefs, network]);
169195

170196
// Initialize proxy contract
171197
const contractInitializedRef = useRef(false);
@@ -176,7 +202,7 @@ export default function ProxyControl() {
176202
// Only initialize once
177203
if (!contractInitializedRef.current) {
178204
try {
179-
const txBuilder = getTxBuilder(network);
205+
const txBuilder = getTxBuilder(network, true);
180206
const contract = new MeshProxyContract(
181207
{
182208
mesh: txBuilder,
@@ -615,7 +641,7 @@ export default function ProxyControl() {
615641

616642
const selectedProxyContract = new MeshProxyContract(
617643
{
618-
mesh: getTxBuilder(network),
644+
mesh: getTxBuilder(network, true),
619645
wallet: activeWallet,
620646
networkId: network,
621647
},
@@ -628,7 +654,12 @@ export default function ProxyControl() {
628654

629655
// Pass multisig inputs to spend as well
630656
const { utxos, walletAddress } = await getMsInputs();
631-
const txHex = await selectedProxyContract.spendProxySimple(validOutputs, utxos, walletAddress);
657+
const txHex = await selectedProxyContract.spendProxySimple(
658+
validOutputs,
659+
utxos,
660+
walletAddress,
661+
blockedUtxoRefs,
662+
);
632663
if (appWallet?.scriptCbor) {
633664
await newTransaction({
634665
txBuilder: txHex,

src/components/multisig/proxy/offchain.ts

Lines changed: 44 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ import {
88
import type { UTxO, MeshTxBuilder } from "@meshsdk/core";
99
// import { parseDatumCbor } from "@meshsdk/core-cst";
1010
import { parseProposalId } from "@/lib/governance";
11+
import { buildProxySpendTx } from "@/lib/server/proxyTxBuilders";
12+
import {
13+
selectFreeAuthTokenUtxo,
14+
selectProxyUtxosForOutputs,
15+
type UtxoRef,
16+
} from "@/lib/server/proxyUtxos";
17+
import { getTxBuilder } from "@/utils/get-tx-builder";
1118

1219
import { MeshTxInitiator } from "./common";
1320
import type { MeshTxInitiatorInput } from "./common";
@@ -193,6 +200,7 @@ export class MeshProxyContract extends MeshTxInitiator {
193200
outputs: { address: string; unit: string; amount: string }[],
194201
msUtxos?: UTxO[],
195202
msWalletAddress?: string,
203+
blockedUtxoRefs: UtxoRef[] = [],
196204
) => {
197205
if (this.msCbor && !msUtxos && !msWalletAddress) {
198206
throw new Error(
@@ -202,7 +210,6 @@ export class MeshProxyContract extends MeshTxInitiator {
202210
const walletInfo = await this.getWalletInfoForTx();
203211
let { utxos, walletAddress } = walletInfo;
204212
const { collateral } = walletInfo;
205-
// If multisig inputs are provided, use them instead of the wallet inputs
206213
if (this.msCbor && msUtxos && msWalletAddress) {
207214
utxos = msUtxos;
208215
walletAddress = msWalletAddress;
@@ -219,164 +226,51 @@ export class MeshProxyContract extends MeshTxInitiator {
219226
if (this.proxyAddress === undefined) {
220227
throw new Error("Proxy address not set. Please setupProxy first.");
221228
}
229+
if (!this.paramUtxo.txHash) {
230+
throw new Error("Proxy param UTxO is not set. Please setupProxy first.");
231+
}
222232
const blockchainProvider = this.mesh.fetcher;
223233
if (!blockchainProvider) {
224234
throw new Error("Blockchain provider not found");
225235
}
226236

227-
const proxyUtxos = await blockchainProvider.fetchAddressUTxOs(
228-
this.proxyAddress,
237+
const policyIdAT = resolveScriptHash(this.getAuthTokenCbor(), "V3");
238+
const authTokenUtxo = selectFreeAuthTokenUtxo(
239+
utxos,
240+
policyIdAT,
241+
blockedUtxoRefs,
229242
);
230-
231-
// Calculate spend requirements and ensure coverage by proxy UTxOs
232-
const REQUIRED_FEE_BUFFER = BigInt(500_000); // 0.5 ADA buffer in lovelace
233-
234-
const requiredByUnit = new Map<string, bigint>();
235-
for (const out of outputs) {
236-
const prev = requiredByUnit.get(out.unit) ?? BigInt(0);
237-
requiredByUnit.set(out.unit, prev + BigInt(out.amount));
238-
}
239-
// Add buffer to lovelace
240-
const lovelaceNeed =
241-
(requiredByUnit.get("lovelace") ?? BigInt(0)) + REQUIRED_FEE_BUFFER;
242-
requiredByUnit.set("lovelace", lovelaceNeed);
243-
244-
const availableByUnit = new Map<string, bigint>();
245-
for (const utxo of proxyUtxos) {
246-
for (const asset of utxo.output.amount) {
247-
const prev = availableByUnit.get(asset.unit) ?? BigInt(0);
248-
availableByUnit.set(asset.unit, prev + BigInt(asset.quantity));
249-
}
243+
if ("error" in authTokenUtxo) {
244+
throw new Error(authTokenUtxo.error);
250245
}
251246

252-
for (const [unit, needed] of requiredByUnit.entries()) {
253-
const available = availableByUnit.get(unit) ?? BigInt(0);
254-
if (available < needed) {
255-
throw new Error(
256-
`Insufficient proxy balance for ${unit}. Needed: ${needed.toString()}, Available: ${available.toString()}`,
257-
);
258-
}
259-
}
260-
261-
// Select as few UTxOs as possible to cover required amounts
262-
const remainingByUnit = new Map<string, bigint>(requiredByUnit);
263-
const candidateUtxos = [...proxyUtxos];
264-
const selectedUtxos: typeof proxyUtxos = [];
265-
266-
const hasRemaining = () => {
267-
for (const value of remainingByUnit.values()) {
268-
if (value > BigInt(0)) return true;
269-
}
270-
return false;
271-
};
272-
273-
const contributionScore = (utxo: (typeof proxyUtxos)[number]) => {
274-
let score = BigInt(0);
275-
for (const asset of utxo.output.amount) {
276-
const remaining = remainingByUnit.get(asset.unit) ?? BigInt(0);
277-
if (remaining > BigInt(0)) {
278-
const qty = BigInt(asset.quantity);
279-
score += qty < remaining ? qty : remaining;
280-
}
281-
}
282-
return score;
283-
};
284-
285-
while (hasRemaining()) {
286-
let bestIdx = -1;
287-
let bestScore = BigInt(0);
288-
for (let i = 0; i < candidateUtxos.length; i++) {
289-
const s = contributionScore(candidateUtxos[i]!);
290-
if (s > bestScore) {
291-
bestScore = s;
292-
bestIdx = i;
293-
}
294-
}
295-
if (bestIdx === -1 || bestScore === BigInt(0)) {
296-
throw new Error(
297-
"Unable to select proxy UTxOs to cover required amounts.",
298-
);
299-
}
300-
const chosen = candidateUtxos.splice(bestIdx, 1)[0]!;
301-
selectedUtxos.push(chosen);
302-
// Decrease remaining by chosen utxo's amounts
303-
for (const asset of chosen.output.amount) {
304-
const remaining = remainingByUnit.get(asset.unit) ?? BigInt(0);
305-
if (remaining > BigInt(0)) {
306-
const qty = BigInt(asset.quantity);
307-
const newRemaining = remaining - (qty < remaining ? qty : remaining);
308-
remainingByUnit.set(asset.unit, newRemaining);
309-
}
310-
}
311-
}
312-
313-
const freeProxyUtxos = selectedUtxos;
314-
const paramScriptAT = this.getAuthTokenCbor();
315-
const policyIdAT = resolveScriptHash(paramScriptAT, "V3");
316-
const authTokenUtxos = utxos.filter((utxo) =>
317-
utxo.output.amount.some((asset) => asset.unit === policyIdAT),
247+
const proxyUtxos = await blockchainProvider.fetchAddressUTxOs(
248+
this.proxyAddress,
318249
);
319-
320-
if (!authTokenUtxos || authTokenUtxos.length === 0) {
321-
throw new Error("No AuthToken found at control wallet address");
322-
}
323-
//ToDo check if AuthToken utxo is used in a pending transaction and blocked then use a free AuthToken
324-
const authTokenUtxo = authTokenUtxos[0];
325-
if (!authTokenUtxo) {
326-
throw new Error("No AuthToken found");
327-
}
328-
const authTokenUtxoAmt = authTokenUtxo.output.amount;
329-
if (!authTokenUtxoAmt) {
330-
throw new Error("No AuthToken amount found");
331-
}
332-
333-
//prepare Proxy spend
334-
//1 Get
335-
const txHex = this.mesh;
336-
337-
for (const input of freeProxyUtxos) {
338-
txHex
339-
.spendingPlutusScriptV3()
340-
.txIn(
341-
input.input.txHash,
342-
input.input.outputIndex,
343-
input.output.amount,
344-
input.output.address,
345-
)
346-
.txInScript(this.getProxyCbor())
347-
.txInInlineDatumPresent()
348-
.txInRedeemerValue(mConStr0([]));
349-
}
350-
351-
txHex
352-
.txIn(
353-
authTokenUtxo.input.txHash,
354-
authTokenUtxo.input.outputIndex,
355-
authTokenUtxo.output.amount,
356-
authTokenUtxo.output.address,
357-
)
358-
.txInCollateral(
359-
collateral.input.txHash,
360-
collateral.input.outputIndex,
361-
collateral.output.amount,
362-
collateral.output.address,
363-
)
364-
.txOut(walletAddress, [{ unit: policyIdAT, quantity: "1" }]);
365-
366-
for (const output of outputs) {
367-
txHex.txOut(output.address, [
368-
{ unit: output.unit, quantity: output.amount },
369-
]);
370-
}
371-
372-
txHex.changeAddress(this.proxyAddress);
373-
374-
// Add the multisig script cbor if it exists (like in setupProxy)
375-
if (this.msCbor) {
376-
txHex.txInScript(this.msCbor);
377-
}
378-
379-
return txHex;
250+
const selectedProxyUtxos = selectProxyUtxosForOutputs({
251+
proxyUtxos,
252+
outputs,
253+
});
254+
if ("error" in selectedProxyUtxos) {
255+
throw new Error(selectedProxyUtxos.error);
256+
}
257+
258+
const txBuilder = getTxBuilder(this.networkId, true);
259+
buildProxySpendTx({
260+
txBuilder,
261+
network: this.networkId,
262+
proxyAddress: this.proxyAddress,
263+
paramUtxo: this.paramUtxo,
264+
walletUtxos: [authTokenUtxo],
265+
proxyUtxos: selectedProxyUtxos,
266+
authTokenUtxo,
267+
collateral,
268+
outputs,
269+
walletAddress,
270+
multisigScriptCbor: this.msCbor,
271+
});
272+
273+
return txBuilder;
380274
};
381275

382276
manageProxyDrep = async (

src/lib/server/proxyUtxos.ts

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,80 @@ export async function resolveCollateralRefFromChain(args: {
118118
return { collateral: resolved.utxo };
119119
}
120120

121+
export function filterBlockedUtxos(
122+
utxos: UTxO[],
123+
blockedRefs: UtxoRef[],
124+
): UTxO[] {
125+
if (blockedRefs.length === 0) {
126+
return utxos;
127+
}
128+
129+
const blocked = new Set(
130+
blockedRefs.map((ref) => `${ref.txHash}#${ref.outputIndex}`),
131+
);
132+
133+
return utxos.filter(
134+
(utxo) => !blocked.has(`${utxo.input.txHash}#${utxo.input.outputIndex}`),
135+
);
136+
}
137+
138+
export function extractBlockedUtxoRefsFromPendingTxJson(txJson: string): UtxoRef[] {
139+
try {
140+
const parsed = JSON.parse(txJson) as {
141+
inputs?: Array<{ txIn?: { txHash?: string; txIndex?: number } }>;
142+
};
143+
if (!Array.isArray(parsed.inputs)) {
144+
return [];
145+
}
146+
147+
return parsed.inputs
148+
.map((input) => ({
149+
txHash: typeof input.txIn?.txHash === "string" ? input.txIn.txHash : "",
150+
outputIndex:
151+
typeof input.txIn?.txIndex === "number" && Number.isInteger(input.txIn.txIndex)
152+
? input.txIn.txIndex
153+
: -1,
154+
}))
155+
.filter((ref) => ref.txHash.length > 0 && ref.outputIndex >= 0);
156+
} catch {
157+
return [];
158+
}
159+
}
160+
161+
export function selectFreeAuthTokenUtxo(
162+
utxos: UTxO[],
163+
authTokenId: string,
164+
blockedRefs: UtxoRef[] = [],
165+
): UTxO | { error: string } {
166+
const freeUtxos = filterBlockedUtxos(utxos, blockedRefs);
167+
const authTokenUtxos = freeUtxos.filter((utxo) => hasAsset(utxo, authTokenId));
168+
if (authTokenUtxos.length === 0) {
169+
return {
170+
error:
171+
"No free proxy auth-token UTxO found at the multisig wallet address. Cancel or complete pending transactions that use the auth token, then try again.",
172+
};
173+
}
174+
175+
return authTokenUtxos.sort((left, right) => {
176+
const lovelaceDelta = Number(getLovelace(right) - getLovelace(left));
177+
if (lovelaceDelta !== 0) {
178+
return lovelaceDelta;
179+
}
180+
if (left.input.txHash !== right.input.txHash) {
181+
return left.input.txHash.localeCompare(right.input.txHash);
182+
}
183+
return left.input.outputIndex - right.input.outputIndex;
184+
})[0]!;
185+
}
186+
121187
export function requireAuthTokenUtxo(
122188
utxos: UTxO[],
123189
authTokenId: string,
124190
): UTxO | { error: string; status: number } {
125-
const authTokenUtxo = utxos.find((utxo) => hasAsset(utxo, authTokenId));
126-
if (!authTokenUtxo) {
191+
const authTokenUtxo = selectFreeAuthTokenUtxo(utxos, authTokenId);
192+
if ("error" in authTokenUtxo) {
127193
return {
128-
error: "No proxy auth-token UTxO found at the multisig wallet address",
194+
error: authTokenUtxo.error,
129195
status: 400,
130196
};
131197
}

0 commit comments

Comments
 (0)