Skip to content

Commit 45741cc

Browse files
committed
wip on tx function #2
1 parent e78f6b7 commit 45741cc

3 files changed

Lines changed: 188 additions & 28 deletions

File tree

frontend/src/tx/burn.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ import {
22
BlockfrostProvider,
33
MeshTxBuilder,
44
applyParamsToScript,
5-
serializeAddressObj,
5+
serializePlutusScript,
6+
resolvePlutusScriptHash,
67
deserializeAddress,
78
mConStr0,
89
mConStr1,
9-
mBytes,
10-
mList,
1110
type UTxO,
1211
type BrowserWallet,
1312
} from "@meshsdk/core";
@@ -29,11 +28,9 @@ function getScript() {
2928
PARAMS.LIQUIDATION_THRESHOLD,
3029
]);
3130

32-
const { scriptHash } = deserializeAddress(
33-
serializeAddressObj({ scriptHash: scriptCbor }, 0)
34-
);
35-
36-
const poolAddress = serializeAddressObj({ scriptHash }, 0);
31+
const script = { code: scriptCbor, version: "V3" as const };
32+
const poolAddress = serializePlutusScript(script, undefined, 0).address;
33+
const scriptHash = resolvePlutusScriptHash(poolAddress);
3734

3835
return { scriptCbor, scriptHash, poolAddress };
3936
}
@@ -63,13 +60,13 @@ export async function buildBurnTx(
6360
// ── 1. Fetch UTxOs ────────────────────────────────────────────────────────
6461

6562
// Pool UTxO — the single UTxO locked at the script address.
66-
const poolUtxos: UTxO[] = await provider.fetchAddressUtxos(poolAddress);
63+
const poolUtxos: UTxO[] = await provider.fetchAddressUTxOs(poolAddress);
6764
if (poolUtxos.length === 0) throw new Error("Pool UTxO not found");
6865
const poolUtxo = poolUtxos[0];
6966

7067
// Pyth State NFT UTxO — reference input carrying the oracle state.
7168
const pythStateUnit = PARAMS.PYTH_POLICY_ID + PYTH.STATE_ASSET_NAME;
72-
const pythUtxos: UTxO[] = await provider.fetchAddressUtxos(PYTH.STATE_ADDRESS, pythStateUnit);
69+
const pythUtxos: UTxO[] = await provider.fetchAddressUTxOs(PYTH.STATE_ADDRESS, pythStateUnit);
7370
if (pythUtxos.length === 0) throw new Error("Pyth State NFT UTxO not found");
7471
const stateUtxo = pythUtxos[0];
7572

@@ -97,14 +94,15 @@ export async function buildBurnTx(
9794
// ── 3. Build datums and redeemers ─────────────────────────────────────────
9895

9996
// Keep the existing pool datum (owner stays the same).
97+
// In Mesh "Data" format: ByteArray is a plain hex string, Constr is mConStr0.
10098
const ownerPkh = deserializeAddress(walletAddress).pubKeyHash;
101-
const poolDatum = mConStr0([mBytes(ownerPkh)]);
99+
const poolDatum = mConStr0([ownerPkh]);
102100

103101
// Action.Burn — Constr(1, [])
104102
const burnRedeemer = mConStr1([]);
105103

106104
// Pyth withdraw redeemer — List<ByteArray> with the signed price message.
107-
const pythRedeemer = mList([mBytes(pythHex)]);
105+
const pythRedeemer = [pythHex];
108106

109107
const col = collateral[0];
110108

frontend/src/tx/liquidate.ts

Lines changed: 163 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,163 @@
1-
// TODO: implement buildLiquidateTx
1+
import {
2+
BlockfrostProvider,
3+
MeshTxBuilder,
4+
applyParamsToScript,
5+
serializePlutusScript,
6+
resolvePlutusScriptHash,
7+
mConStr0,
8+
mConStr2,
9+
type UTxO,
10+
type BrowserWallet,
11+
} from "@meshsdk/core";
12+
13+
import {
14+
UNPARAMETERISED_SCRIPT_CBOR,
15+
PARAMS,
16+
PYTH,
17+
computeBurnReturn,
18+
} from "./contract";
19+
20+
// ── Derive parameterised script once ─────────────────────────────────────────
21+
22+
function getScript() {
23+
const scriptCbor = applyParamsToScript(UNPARAMETERISED_SCRIPT_CBOR, [
24+
PARAMS.PYTH_POLICY_ID,
25+
PARAMS.ADA_USD_FEED_ID,
26+
PARAMS.COLLATERAL_RATIO,
27+
PARAMS.LIQUIDATION_THRESHOLD,
28+
]);
29+
30+
const script = { code: scriptCbor, version: "V3" as const };
31+
const poolAddress = serializePlutusScript(script, undefined, 0).address;
32+
const scriptHash = resolvePlutusScriptHash(poolAddress);
33+
34+
return { scriptCbor, scriptHash, poolAddress };
35+
}
36+
37+
// ── buildLiquidateTx ──────────────────────────────────────────────────────────
38+
39+
/**
40+
* Build, sign, and submit a Liquidate transaction.
41+
*
42+
* Anyone can call this when the position's health ratio drops below
43+
* liquidation_threshold. No owner signature required.
44+
*
45+
* @param wallet Connected CIP-30 browser wallet (MeshSDK BrowserWallet)
46+
* @param synthToBurn Synth token amount to burn (in micro-USD, 6 decimals)
47+
* @param pythHex Signed Pyth price message (solanaPayload from backend)
48+
* @param adaUsdPrice Current ADA/USD price as a float (e.g. 0.70)
49+
* @param blockfrostKey Blockfrost preprod project ID
50+
* @returns Submitted transaction hash
51+
*/
52+
export async function buildLiquidateTx(
53+
wallet: BrowserWallet,
54+
synthToBurn: bigint,
55+
pythHex: string,
56+
adaUsdPrice: number,
57+
blockfrostKey: string
58+
): Promise<string> {
59+
const provider = new BlockfrostProvider(blockfrostKey);
60+
const { scriptCbor, scriptHash, poolAddress } = getScript();
61+
62+
// ── 1. Fetch UTxOs ────────────────────────────────────────────────────────
63+
64+
// Pool UTxO — the single UTxO locked at the script address.
65+
const poolUtxos: UTxO[] = await provider.fetchAddressUTxOs(poolAddress);
66+
if (poolUtxos.length === 0) throw new Error("Pool UTxO not found");
67+
const poolUtxo = poolUtxos[0];
68+
69+
// Pyth State NFT UTxO — reference input carrying the oracle state.
70+
const pythStateUnit = PARAMS.PYTH_POLICY_ID + PYTH.STATE_ASSET_NAME;
71+
const pythUtxos: UTxO[] = await provider.fetchAddressUTxOs(PYTH.STATE_ADDRESS, pythStateUnit);
72+
if (pythUtxos.length === 0) throw new Error("Pyth State NFT UTxO not found");
73+
const stateUtxo = pythUtxos[0];
74+
75+
// Liquidator UTxOs — for collateral; ADA reward goes to liquidator's change address.
76+
const walletUtxos: UTxO[] = await wallet.getUtxos();
77+
const collateral: UTxO[] = await wallet.getCollateral();
78+
if (collateral.length === 0)
79+
throw new Error("No collateral set in wallet. Enable collateral in your wallet settings.");
80+
81+
const walletAddress = await wallet.getChangeAddress();
82+
83+
// ── 2. Compute amounts ────────────────────────────────────────────────────
84+
85+
const adaToReturn = computeBurnReturn(synthToBurn, adaUsdPrice);
86+
if (adaToReturn <= 0n) throw new Error("ADA return amount too small");
87+
88+
const currentPoolLovelace = BigInt(
89+
poolUtxo.output.amount.find((a) => a.unit === "lovelace")?.quantity ?? "0"
90+
);
91+
if (adaToReturn > currentPoolLovelace)
92+
throw new Error("Insufficient ADA in pool for this liquidation amount");
93+
94+
const newPoolLovelace = currentPoolLovelace - adaToReturn;
95+
96+
// ── 3. Build datums and redeemers ─────────────────────────────────────────
97+
98+
// Preserve the existing pool datum owner from the inline datum on the pool UTxO.
99+
// In Mesh "Data" format: ByteArray is a plain hex string, Constr is mConStr0.
100+
const existingOwnerPkh =
101+
(poolUtxo.output.plutusData as any)?.fields?.[0] ?? "";
102+
const poolDatum = mConStr0([existingOwnerPkh]);
103+
104+
// Action.Liquidate — Constr(2, [])
105+
const liquidateRedeemer = mConStr2([]);
106+
107+
// Pyth withdraw redeemer — List<ByteArray> with the signed price message.
108+
const pythRedeemer = [pythHex];
109+
110+
const col = collateral[0];
111+
112+
// ── 4. Build transaction ──────────────────────────────────────────────────
113+
114+
const txBuilder = new MeshTxBuilder({ fetcher: provider, submitter: provider });
115+
116+
await txBuilder
117+
// Spend the pool UTxO (spend validator delegates to mint validator).
118+
.spendingPlutusScriptV3()
119+
.txIn(
120+
poolUtxo.input.txHash,
121+
poolUtxo.input.outputIndex,
122+
poolUtxo.output.amount,
123+
poolUtxo.output.address
124+
)
125+
.txInInlineDatumPresent()
126+
.txInRedeemerValue(liquidateRedeemer, "Mesh")
127+
.txInScript(scriptCbor)
128+
129+
// Return pool UTxO with decreased ADA + same datum.
130+
.txOut(poolAddress, [{ unit: "lovelace", quantity: newPoolLovelace.toString() }])
131+
.txOutInlineDatumValue(poolDatum, "Mesh")
132+
133+
// Burn synth tokens (negative mint amount).
134+
.mintPlutusScriptV3()
135+
.mint((-synthToBurn).toString(), scriptHash, "")
136+
.mintingScript(scriptCbor)
137+
.mintRedeemerValue(liquidateRedeemer, "Mesh")
138+
139+
// Pyth State NFT as reference input (never spent).
140+
.readOnlyTxInReference(stateUtxo.input.txHash, stateUtxo.input.outputIndex)
141+
142+
// Zero-ADA withdrawal from Pyth verify script — carries the signed price message.
143+
.withdrawal(PYTH.WITHDRAW_ADDRESS, "0")
144+
.withdrawalPlutusScriptV3()
145+
.withdrawalScript(PYTH.WITHDRAW_SCRIPT_CBOR)
146+
.withdrawalRedeemerValue(pythRedeemer, "Mesh")
147+
148+
// Collateral + change (liquidator receives the ADA profit via change).
149+
.txInCollateral(
150+
col.input.txHash,
151+
col.input.outputIndex,
152+
col.output.amount,
153+
col.output.address
154+
)
155+
.changeAddress(walletAddress)
156+
.selectUtxosFrom(walletUtxos)
157+
// No requiredSignerHash — liquidation is permissionless.
158+
.complete();
159+
160+
const unsignedTx = txBuilder.txHex;
161+
const signedTx = await wallet.signTx(unsignedTx);
162+
return wallet.submitTx(signedTx);
163+
}

frontend/src/tx/mint.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ import {
22
BlockfrostProvider,
33
MeshTxBuilder,
44
applyParamsToScript,
5-
serializeAddressObj,
5+
serializePlutusScript,
6+
resolvePlutusScriptHash,
67
deserializeAddress,
78
mConStr0,
8-
mBytes,
9-
mList,
109
type UTxO,
1110
type BrowserWallet,
1211
} from "@meshsdk/core";
@@ -31,13 +30,13 @@ function getScript() {
3130
PARAMS.LIQUIDATION_THRESHOLD,
3231
]);
3332

34-
// The policy ID is the Blake2b-224 hash of the parameterised script.
35-
const { scriptHash } = deserializeAddress(
36-
serializeAddressObj({ scriptHash: scriptCbor }, 0) // derive hash only
37-
);
33+
const script = { code: scriptCbor, version: "V3" as const };
3834

3935
// Bech32 script address on preprod (network id = 0).
40-
const poolAddress = serializeAddressObj({ scriptHash }, 0);
36+
const poolAddress = serializePlutusScript(script, undefined, 0).address;
37+
38+
// The policy ID is the Blake2b-224 hash of the parameterised script.
39+
const scriptHash = resolvePlutusScriptHash(poolAddress);
4140

4241
return { scriptCbor, scriptHash, poolAddress };
4342
}
@@ -67,14 +66,14 @@ export async function buildMintTx(
6766
// ── 1. Fetch UTxOs ────────────────────────────────────────────────────────
6867

6968
// Pool UTxO — the single UTxO locked at the script address.
70-
const poolUtxos: UTxO[] = await provider.fetchAddressUtxos(poolAddress);
69+
const poolUtxos: UTxO[] = await provider.fetchAddressUTxOs(poolAddress);
7170
if (poolUtxos.length === 0) throw new Error("Pool UTxO not found");
7271
const poolUtxo = poolUtxos[0];
7372

7473
// Pyth State NFT UTxO — reference input carrying the oracle state.
75-
// The UTxO changes on every oracle update, so we query by address + asset name.
74+
// The UTxO changes on every oracle update, so we query by address + asset unit.
7675
const pythStateUnit = PARAMS.PYTH_POLICY_ID + PYTH.STATE_ASSET_NAME;
77-
const pythUtxos: UTxO[] = await provider.fetchAddressUtxos(PYTH.STATE_ADDRESS, pythStateUnit);
76+
const pythUtxos: UTxO[] = await provider.fetchAddressUTxOs(PYTH.STATE_ADDRESS, pythStateUnit);
7877
if (pythUtxos.length === 0) throw new Error("Pyth State NFT UTxO not found");
7978
const stateUtxo = pythUtxos[0];
8079

@@ -99,16 +98,17 @@ export async function buildMintTx(
9998
// ── 3. Build datums and redeemers ─────────────────────────────────────────
10099

101100
// PoolDatum { owner: ByteArray } — Constr(0, [owner_pkh])
101+
// In Mesh "Data" format: ByteArray is a plain hex string, Constr is mConStr0.
102102
const ownerPkh = deserializeAddress(walletAddress).pubKeyHash;
103-
const poolDatum = mConStr0([mBytes(ownerPkh)]);
103+
const poolDatum = mConStr0([ownerPkh]);
104104

105105
// Action.Mint — Constr(0, [])
106106
const mintRedeemer = mConStr0([]);
107107

108108
// Pyth withdraw redeemer — List<ByteArray> with the signed price message.
109-
// The backend now returns solanaPayload (Solana wire format, magic b9011a82),
110-
// which is what the on-chain Pyth library expects.
111-
const pythRedeemer = mList([mBytes(pythHex)]);
109+
// In Mesh "Data" format: List is a plain JS array, ByteArray is a hex string.
110+
// The backend returns solanaPayload (Solana wire format, magic b9011a82).
111+
const pythRedeemer = [pythHex];
112112

113113
const col = collateral[0];
114114

0 commit comments

Comments
 (0)