@@ -11,8 +11,7 @@ import {
1111 type UTxO ,
1212 type BrowserWallet ,
1313} from "@meshsdk/core" ;
14- import { getPythScriptHash } from "@pythnetwork/pyth-lazer-cardano-js" ;
15-
14+ import { addVKeyWitnessSetToTransaction } from "@meshsdk/core-cst" ;
1615import {
1716 UNPARAMETERISED_SCRIPT_CBOR ,
1817 PARAMS ,
@@ -59,6 +58,14 @@ export async function buildBurnTx(
5958 const provider = new BlockfrostProvider ( blockfrostKey ) ;
6059 const { scriptCbor, scriptHash, poolAddress } = getScript ( ) ;
6160
61+ // Bounded validity range required by the Pyth verify script.
62+ const latestBlockResp = await fetch (
63+ "https://cardano-preprod.blockfrost.io/api/v0/blocks/latest" ,
64+ { headers : { project_id : blockfrostKey } }
65+ ) ;
66+ const latestBlock = await latestBlockResp . json ( ) ;
67+ const currentSlot : number = latestBlock . slot ;
68+
6269 // ── 1. Fetch UTxOs ────────────────────────────────────────────────────────
6370
6471 // Pool UTxO — the single UTxO locked at the script address.
@@ -72,8 +79,10 @@ export async function buildBurnTx(
7279 if ( pythUtxos . length === 0 ) throw new Error ( "Pyth State NFT UTxO not found" ) ;
7380 const stateUtxo = pythUtxos [ 0 ] ;
7481
75- // Read the withdraw script hash dynamically from the Pyth State inline datum.
76- const withdrawScriptHash = getPythScriptHash ( stateUtxo as any ) ;
82+ // Use the hardcoded withdraw script hash (same approach as mint.ts).
83+ // getPythScriptHash() fails because Blockfrost returns the datum in a format
84+ // the function doesn't recognise.
85+ const withdrawScriptHash = PYTH . WITHDRAW_SCRIPT_HASH ;
7786 const withdrawAddress = serializeRewardAddress ( withdrawScriptHash , true , 0 ) ;
7887
7988 // Find the UTxO at the Pyth State address that has the withdraw script published as a reference script.
@@ -83,32 +92,61 @@ export async function buildBurnTx(
8392 ) ;
8493 if ( ! withdrawRefUtxo ) throw new Error ( "Pyth withdraw reference script UTxO not found" ) ;
8594
86- // User UTxOs — for synth token input and collateral.
87- const walletUtxos : UTxO [ ] = await wallet . getUtxos ( ) ;
88- const collateral : UTxO [ ] = await wallet . getCollateral ( ) ;
89- if ( collateral . length === 0 )
90- throw new Error ( "No collateral set in wallet. Enable collateral in your wallet settings." ) ;
95+ const walletAddress = await ( wallet as any ) . getChangeAddressBech32 ( ) ;
9196
92- const walletAddress = await wallet . getChangeAddress ( ) ;
97+ // wallet.getUtxos() also returns raw CBOR — use Blockfrost to get decoded UTxOs instead.
98+ const walletUtxos : UTxO [ ] = await provider . fetchAddressUTxOs ( walletAddress ) ;
99+
100+ // wallet.getCollateral() returns raw CIP-30 CBOR hex strings, not decoded UTxOs.
101+ // CBOR: 82 82 5820 <32B txHash> <uint outputIndex> 82 5839 <57B address> <uint lovelace>
102+ const collateralRaw : string [ ] = ( await wallet . getCollateral ( ) ) as unknown as string [ ] ;
103+ if ( collateralRaw . length === 0 )
104+ throw new Error ( "No collateral set in wallet. Enable collateral in your wallet settings." ) ;
105+ const colTxHash = collateralRaw [ 0 ] . slice ( 8 , 72 ) ;
106+ const colOiByte = parseInt ( collateralRaw [ 0 ] . slice ( 72 , 74 ) , 16 ) ;
107+ const colIndex = colOiByte <= 23 ? colOiByte : parseInt ( collateralRaw [ 0 ] . slice ( 74 , 76 ) , 16 ) ;
108+ // Construct collateral UTxO directly — address is always the user's wallet address.
109+ const col : UTxO = {
110+ input : { txHash : colTxHash , outputIndex : colIndex } ,
111+ output : { address : walletAddress , amount : [ { unit : "lovelace" , quantity : "5000000" } ] } ,
112+ } ;
93113
94114 // ── 2. Compute amounts ────────────────────────────────────────────────────
95115
96116 const adaToReturn = computeBurnReturn ( synthToBurn , adaUsdPrice ) ;
117+ console . log ( "[buildBurnTx] synthToBurn:" , synthToBurn . toString ( ) , "adaUsdPrice:" , adaUsdPrice , "adaToReturn:" , adaToReturn . toString ( ) ) ;
97118 if ( adaToReturn <= 0n ) throw new Error ( "ADA return amount too small" ) ;
98119
99120 const currentPoolLovelace = BigInt (
100121 poolUtxo . output . amount . find ( ( a ) => a . unit === "lovelace" ) ?. quantity ?? "0"
101122 ) ;
102- if ( adaToReturn > currentPoolLovelace )
103- throw new Error ( "Insufficient ADA in pool for this burn amount" ) ;
123+ console . log ( "[buildBurnTx] currentPoolLovelace:" , currentPoolLovelace . toString ( ) ) ;
124+ if ( adaToReturn > currentPoolLovelace ) {
125+ const rawPrice = BigInt ( Math . round ( adaUsdPrice * 1e8 ) ) ;
126+ const maxBurnable = ( currentPoolLovelace * rawPrice ) / 100_000_000n ;
127+ throw new Error (
128+ `Insufficient ADA in pool. Max burnable synth at current price: ${ maxBurnable } micro-synth (${ Number ( maxBurnable ) / 1e6 } synth). ` +
129+ `You may have old tokens from a previous contract deployment — burn only what you minted in this session.`
130+ ) ;
131+ }
104132
105133 const newPoolLovelace = currentPoolLovelace - adaToReturn ;
106134
107135 // ── 3. Build datums and redeemers ─────────────────────────────────────────
108136
109137 // Read owner PKH from the pool datum — this is what the validator checks.
110138 // PoolDatum is Constr(0, [owner_pkh_hex]).
111- const datumOwnerPkh : string = ( poolUtxo . output . plutusData as any ) ?. fields ?. [ 0 ] ?? "" ;
139+ // Blockfrost decodes inline datums as { constructor: N, fields: [{ bytes: "..." }] }
140+ // so we need .fields[0].bytes, not .fields[0] directly.
141+ // plutusData from Blockfrost is raw CBOR hex, e.g.:
142+ // d8799f581c<28-byte-pkh>ff
143+ // d879 = tag 121 (Constr 0), 9f = indef array, 581c = 28-byte bytestring
144+ // Extract the 28-byte (56 hex char) PKH directly from the CBOR string.
145+ const plutusCbor = poolUtxo . output . plutusData as string ;
146+ const CONSTR0_PREFIX = "d8799f581c" ; // Constr(0,[ByteArray(28)])
147+ const datumOwnerPkh : string = plutusCbor ?. startsWith ( CONSTR0_PREFIX )
148+ ? plutusCbor . slice ( CONSTR0_PREFIX . length , CONSTR0_PREFIX . length + 56 )
149+ : "" ;
112150 if ( ! datumOwnerPkh ) throw new Error ( "Could not read owner from pool datum" ) ;
113151
114152 // Fail fast if the connected wallet is not the position owner.
@@ -125,10 +163,13 @@ export async function buildBurnTx(
125163 // Pyth withdraw redeemer — List<ByteArray> with the signed price message.
126164 const pythRedeemer = [ pythHex ] ;
127165
128- const col = collateral [ 0 ] ;
129166
130167 // ── 4. Build transaction ──────────────────────────────────────────────────
131168
169+ const exSpend = { mem : 2_000_000 , steps : 1_000_000_000 } ;
170+ const exMint = { mem : 4_000_000 , steps : 2_000_000_000 } ;
171+ const exWithdraw = { mem : 8_000_000 , steps : 5_000_000_000 } ;
172+
132173 const txBuilder = new MeshTxBuilder ( { fetcher : provider , submitter : provider } ) ;
133174
134175 await txBuilder
@@ -141,7 +182,7 @@ export async function buildBurnTx(
141182 poolUtxo . output . address
142183 )
143184 . txInInlineDatumPresent ( )
144- . txInRedeemerValue ( burnRedeemer , "Mesh" )
185+ . txInRedeemerValue ( burnRedeemer , "Mesh" , exSpend )
145186 . txInScript ( scriptCbor )
146187
147188 // Return pool UTxO with decreased ADA + same datum.
@@ -152,23 +193,23 @@ export async function buildBurnTx(
152193 . mintPlutusScriptV3 ( )
153194 . mint ( ( - synthToBurn ) . toString ( ) , scriptHash , "" )
154195 . mintingScript ( scriptCbor )
155- . mintRedeemerValue ( burnRedeemer , "Mesh" )
196+ . mintRedeemerValue ( burnRedeemer , "Mesh" , exMint )
156197
157198 // Pyth State NFT as reference input (never spent).
158199 . readOnlyTxInReference ( stateUtxo . input . txHash , stateUtxo . input . outputIndex )
159200
160201 // Zero-ADA withdrawal from Pyth verify script — carries the signed price message.
161202 // Address and script hash derived dynamically from the Pyth State datum.
162203 // Script is referenced by UTxO (no CBOR needed).
163- . withdrawal ( withdrawAddress , "0" )
164204 . withdrawalPlutusScriptV3 ( )
205+ . withdrawal ( withdrawAddress , "0" )
165206 . withdrawalTxInReference (
166207 withdrawRefUtxo . input . txHash ,
167208 withdrawRefUtxo . input . outputIndex ,
168209 String ( withdrawRefUtxo . output . scriptRef ?. length ? withdrawRefUtxo . output . scriptRef . length / 2 : 0 ) ,
169210 withdrawScriptHash
170211 )
171- . withdrawalRedeemerValue ( pythRedeemer , "Mesh" )
212+ . withdrawalRedeemerValue ( pythRedeemer , "Mesh" , exWithdraw )
172213
173214 // Collateral + change.
174215 . txInCollateral (
@@ -180,9 +221,12 @@ export async function buildBurnTx(
180221 . changeAddress ( walletAddress )
181222 . selectUtxosFrom ( walletUtxos )
182223 . requiredSignerHash ( datumOwnerPkh ) // Burn requires owner signature (on-chain check)
224+ . invalidBefore ( currentSlot - 60 )
225+ . invalidHereafter ( currentSlot + 600 )
183226 . complete ( ) ;
184227
185228 const unsignedTx = txBuilder . txHex ;
186- const signedTx = await wallet . signTx ( unsignedTx ) ;
187- return wallet . submitTx ( signedTx ) ;
229+ const witnessSet = await wallet . signTx ( unsignedTx ) ;
230+ const signedTx = addVKeyWitnessSetToTransaction ( unsignedTx , witnessSet ) ;
231+ return provider . submitTx ( signedTx ) ;
188232}
0 commit comments