@easy1staking/cip113-sdk-ts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +49 -0
  2. package/blueprints/standard/v0.3.0/plutus.json +596 -0
  3. package/blueprints/substandards/dummy/v0.1.0/plutus.json +58 -0
  4. package/blueprints/substandards/freeze-and-seize/v0.1.0/plutus.json +380 -0
  5. package/dist/core/evo-utils.d.ts +160 -0
  6. package/dist/core/evo-utils.d.ts.map +1 -0
  7. package/dist/core/evo-utils.js +401 -0
  8. package/dist/core/evo-utils.js.map +1 -0
  9. package/dist/core/registry.d.ts +31 -0
  10. package/dist/core/registry.d.ts.map +1 -0
  11. package/dist/core/registry.js +72 -0
  12. package/dist/core/registry.js.map +1 -0
  13. package/dist/index.d.ts +89 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +158 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/provider/address-utils.d.ts +15 -0
  18. package/dist/provider/address-utils.d.ts.map +1 -0
  19. package/dist/provider/address-utils.js +29 -0
  20. package/dist/provider/address-utils.js.map +1 -0
  21. package/dist/provider/tx-utils.d.ts +15 -0
  22. package/dist/provider/tx-utils.d.ts.map +1 -0
  23. package/dist/provider/tx-utils.js +18 -0
  24. package/dist/provider/tx-utils.js.map +1 -0
  25. package/dist/standard/blueprint.d.ts +31 -0
  26. package/dist/standard/blueprint.d.ts.map +1 -0
  27. package/dist/standard/blueprint.js +49 -0
  28. package/dist/standard/blueprint.js.map +1 -0
  29. package/dist/standard/params.d.ts +6 -0
  30. package/dist/standard/params.d.ts.map +1 -0
  31. package/dist/standard/params.js +5 -0
  32. package/dist/standard/params.js.map +1 -0
  33. package/dist/standard/scripts.d.ts +51 -0
  34. package/dist/standard/scripts.d.ts.map +1 -0
  35. package/dist/standard/scripts.js +110 -0
  36. package/dist/standard/scripts.js.map +1 -0
  37. package/dist/substandards/dummy/index.d.ts +16 -0
  38. package/dist/substandards/dummy/index.d.ts.map +1 -0
  39. package/dist/substandards/dummy/index.js +158 -0
  40. package/dist/substandards/dummy/index.js.map +1 -0
  41. package/dist/substandards/freeze-and-seize/index.d.ts +17 -0
  42. package/dist/substandards/freeze-and-seize/index.d.ts.map +1 -0
  43. package/dist/substandards/freeze-and-seize/index.js +769 -0
  44. package/dist/substandards/freeze-and-seize/index.js.map +1 -0
  45. package/dist/substandards/freeze-and-seize/scripts.d.ts +25 -0
  46. package/dist/substandards/freeze-and-seize/scripts.d.ts.map +1 -0
  47. package/dist/substandards/freeze-and-seize/scripts.js +54 -0
  48. package/dist/substandards/freeze-and-seize/scripts.js.map +1 -0
  49. package/dist/substandards/freeze-and-seize/types.d.ts +18 -0
  50. package/dist/substandards/freeze-and-seize/types.d.ts.map +1 -0
  51. package/dist/substandards/freeze-and-seize/types.js +8 -0
  52. package/dist/substandards/freeze-and-seize/types.js.map +1 -0
  53. package/dist/substandards/interface.d.ts +166 -0
  54. package/dist/substandards/interface.d.ts.map +1 -0
  55. package/dist/substandards/interface.js +10 -0
  56. package/dist/substandards/interface.js.map +1 -0
  57. package/dist/types.d.ts +93 -0
  58. package/dist/types.d.ts.map +1 -0
  59. package/dist/types.js +8 -0
  60. package/dist/types.js.map +1 -0
  61. package/package.json +53 -0
@@ -0,0 +1,769 @@
1
+ /**
2
+ * Freeze-and-Seize substandard.
3
+ *
4
+ * Uses Evolution SDK directly — no adapter abstraction.
5
+ *
6
+ * Capabilities: register, mint, burn, transfer, freeze, unfreeze, seize.
7
+ */
8
+ import { Address as EvoAddress, Assets, Data, Transaction, TransactionHash as EvoTransactionHash, TransactionInput as EvoTransactionInput, } from "@evolution-sdk/evolution";
9
+ import { sortTxInputs, findRefInputIndex, findRegistryNode, findCoveringNode, utxoToTxInput, } from "../../core/registry.js";
10
+ import { buildEvoScript, scriptAddress, rewardAddress, baseAddress, stakingCredentialHash, paymentCredentialHash, stringToHex, MAX_NEXT, voidData, registryNodeDatum, blacklistNodeDatum, issuanceRedeemerFirstMint, issuanceRedeemerRefInput, registryInsertRedeemer, transferActRedeemer, thirdPartyActRedeemer, blacklistInitRedeemer, blacklistAddRedeemer, blacklistRemoveRedeemer, extractConstrBytesField, extractCredentialField, getInlineDatum, utxoUnitQty, utxoLovelace, utxoTxHash, utxoOutputIndex, outputAssets, mintAssetsFromMap, Credential, KeyHash, InlineDatum, labeledAssetName, buildCIP68FTDatum, } from "../../core/evo-utils.js";
11
+ import { createFESScripts } from "./scripts.js";
12
+ // ---------------------------------------------------------------------------
13
+ // Helpers
14
+ // ---------------------------------------------------------------------------
15
+ /**
16
+ * Build the transaction and extract unsigned CBOR + txHash.
17
+ * Works with both ReadOnlyClient (TransactionResultBase) and SigningClient (SignBuilder).
18
+ */
19
+ async function buildAndSerialize(
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ builder, changeAddress, availableUtxos, passAdditionalUtxos = false) {
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ const buildOpts = {
24
+ changeAddress: EvoAddress.fromBech32(changeAddress),
25
+ };
26
+ if (availableUtxos) {
27
+ buildOpts.availableUtxos = availableUtxos;
28
+ }
29
+ if (passAdditionalUtxos) {
30
+ buildOpts.passAdditionalUtxos = true;
31
+ }
32
+ const result = await builder.build(buildOpts);
33
+ const tx = await result.toTransaction();
34
+ const cbor = Transaction.toCBORHex(tx);
35
+ // Extract txHash and chainResult — SignBuilder has chainResult
36
+ let txHash = "";
37
+ let chainAvailable;
38
+ if (typeof result.chainResult === "function") {
39
+ const cr = result.chainResult();
40
+ txHash = cr.txHash;
41
+ chainAvailable = cr.available;
42
+ }
43
+ return { cbor, txHash, chainAvailable, _signBuilder: result };
44
+ }
45
+ function selectUtxosForAmount(utxos, unit, requiredAmount) {
46
+ const selected = [];
47
+ let total = 0n;
48
+ for (const utxo of utxos) {
49
+ const amount = utxoUnitQty(utxo, unit);
50
+ if (amount <= 0n)
51
+ continue;
52
+ selected.push(utxo);
53
+ total += amount;
54
+ if (total >= requiredAmount)
55
+ break;
56
+ }
57
+ if (total < requiredAmount) {
58
+ throw new Error(`Insufficient token balance: have ${total}, need ${requiredAmount}`);
59
+ }
60
+ return { selected, totalTokenAmount: total };
61
+ }
62
+ /** Find protocol params UTxO by searching for the protocol params NFT */
63
+ async function findProtocolParamsUtxo(client, networkId, deployment) {
64
+ const ppUnit = deployment.protocolParams.policyId + stringToHex("ProtocolParams");
65
+ const addr = EvoAddress.fromBech32(scriptAddress(networkId, deployment.protocolParams.alwaysFailScriptHash));
66
+ const utxos = await client.getUtxosWithUnit(addr, ppUnit);
67
+ if (utxos.length > 0)
68
+ return utxos[0];
69
+ throw new Error(`Protocol params UTxO not found (unit: ${ppUnit})`);
70
+ }
71
+ /** Find issuance CBOR hex UTxO */
72
+ async function findIssuanceCborHexUtxo(client, networkId, deployment) {
73
+ const icUnit = deployment.issuance.policyId + stringToHex("IssuanceCborHex");
74
+ const addr = EvoAddress.fromBech32(scriptAddress(networkId, deployment.issuance.alwaysFailScriptHash));
75
+ const utxos = await client.getUtxosWithUnit(addr, icUnit);
76
+ if (utxos.length > 0)
77
+ return utxos[0];
78
+ throw new Error(`Issuance CBOR hex UTxO not found (unit: ${icUnit})`);
79
+ }
80
+ /** Find the NFT unit in a covering node's value that belongs to a given policy */
81
+ function findCoveringNodeNftUnit(utxo, policyId) {
82
+ const units = Assets.getUnits(utxo.assets);
83
+ for (const unit of units) {
84
+ if (unit === "lovelace" || unit === "")
85
+ continue;
86
+ if (unit.startsWith(policyId))
87
+ return unit;
88
+ }
89
+ return undefined;
90
+ }
91
+ /**
92
+ * Find a blacklist node proving non-membership for a given staking credential.
93
+ * Non-membership: node.key < stakingHash < node.next
94
+ */
95
+ function findBlacklistCoveringNode(blacklistUtxos, stakingHash) {
96
+ return blacklistUtxos.find((utxo) => {
97
+ const datum = getInlineDatum(utxo);
98
+ if (!datum)
99
+ return false;
100
+ const key = extractConstrBytesField(datum, 0) ?? "";
101
+ const next = extractConstrBytesField(datum, 1) ?? MAX_NEXT;
102
+ return key < stakingHash && stakingHash < next;
103
+ });
104
+ }
105
+ // ---------------------------------------------------------------------------
106
+ // Factory
107
+ // ---------------------------------------------------------------------------
108
+ export function freezeAndSeizeSubstandard(config) {
109
+ let ctx;
110
+ let scripts;
111
+ let networkId;
112
+ return {
113
+ id: "freeze-and-seize",
114
+ version: "0.1.0",
115
+ blueprint: config.blueprint,
116
+ init(context) {
117
+ ctx = context;
118
+ networkId = ctx.client.chain.id;
119
+ const { adminPkh, assetName, blacklistNodePolicyId, blacklistInitTxInput } = config.deployment;
120
+ const fes = createFESScripts(config.blueprint);
121
+ const plbHash = ctx.standardScripts.programmableLogicBase.hash;
122
+ const issuerAdmin = fes.buildIssuerAdmin(adminPkh, assetName);
123
+ const transfer = fes.buildTransfer(plbHash, blacklistNodePolicyId);
124
+ const blacklistMint = fes.buildBlacklistMint(blacklistInitTxInput, adminPkh);
125
+ const blacklistSpend = fes.buildBlacklistSpend(blacklistNodePolicyId);
126
+ const issuanceMint = ctx.standardScripts.buildIssuanceMint(issuerAdmin.hash);
127
+ scripts = {
128
+ issuerAdmin,
129
+ transfer,
130
+ blacklistMint,
131
+ blacklistSpend,
132
+ issuanceMint,
133
+ tokenPolicyId: issuanceMint.hash,
134
+ };
135
+ },
136
+ // ====================================================================
137
+ // REGISTER — first mint + registry insert
138
+ // ====================================================================
139
+ async register(params) {
140
+ const { feePayerAddress, assetName, quantity, recipientAddress } = params;
141
+ const recipient = recipientAddress || feePayerAddress;
142
+ const chainedUtxos = (params.chainedUtxos ?? []);
143
+ const assetNameHex = stringToHex(assetName);
144
+ const hasCIP68 = !!params.cip68Metadata;
145
+ const client = ctx.client;
146
+ // When CIP-68 is enabled, prefix asset names with CIP-67 labels
147
+ const userAssetNameHex = hasCIP68 ? labeledAssetName(333, assetNameHex) : assetNameHex;
148
+ const unit = scripts.tokenPolicyId + userAssetNameHex;
149
+ // Reference token (only when CIP-68)
150
+ const refAssetNameHex = hasCIP68 ? labeledAssetName(100, assetNameHex) : null;
151
+ const refUnit = refAssetNameHex ? scripts.tokenPolicyId + refAssetNameHex : null;
152
+ // 1. Find covering registry node
153
+ const registrySpendAddr = scriptAddress(networkId, ctx.standardScripts.registrySpend.hash);
154
+ const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(registrySpendAddr));
155
+ const coveringNodeUtxo = findCoveringNode(registryUtxos, scripts.tokenPolicyId);
156
+ if (!coveringNodeUtxo)
157
+ throw new Error("Could not find covering registry node for insertion");
158
+ const coveringDatum = getInlineDatum(coveringNodeUtxo);
159
+ const coveringKey = extractConstrBytesField(coveringDatum, 0) ?? "";
160
+ const coveringNext = extractConstrBytesField(coveringDatum, 1) ?? MAX_NEXT;
161
+ // 2. Get reference inputs
162
+ const protocolParamsUtxo = await findProtocolParamsUtxo(client, networkId, ctx.deployment);
163
+ const issuanceCborHexUtxo = await findIssuanceCborHexUtxo(client, networkId, ctx.deployment);
164
+ // 3. Build datums
165
+ const coveringTransferCred = extractCredentialField(coveringDatum, 2) ?? { type: "key", hash: "" };
166
+ const coveringThirdPartyCred = extractCredentialField(coveringDatum, 3) ?? { type: "key", hash: "" };
167
+ const updatedCoveringDatum = registryNodeDatum({
168
+ key: coveringKey,
169
+ next: scripts.tokenPolicyId,
170
+ transferLogicScript: coveringTransferCred,
171
+ thirdPartyTransferLogicScript: coveringThirdPartyCred,
172
+ globalStateCs: extractConstrBytesField(coveringDatum, 4) ?? "",
173
+ });
174
+ const newRegistryNodeDatum = registryNodeDatum({
175
+ key: scripts.tokenPolicyId,
176
+ next: coveringNext,
177
+ transferLogicScript: { type: "script", hash: scripts.transfer.hash },
178
+ thirdPartyTransferLogicScript: { type: "script", hash: scripts.issuerAdmin.hash },
179
+ globalStateCs: "",
180
+ });
181
+ // 4. Build redeemers — registry output index shifts when CIP-68 adds an extra output
182
+ const registryOutputIndex = hasCIP68 ? 3 : 2;
183
+ const issuanceRedeemer = issuanceRedeemerFirstMint(scripts.issuerAdmin.hash, registryOutputIndex);
184
+ const registryMintRedeemer = registryInsertRedeemer(scripts.issuanceMint.hash, scripts.issuerAdmin.hash);
185
+ const tokenDatum = voidData();
186
+ // 5. Determine if chaining from initCompliance
187
+ const useChaining = chainedUtxos.length > 0;
188
+ // 6. Build addresses
189
+ const plbHash = ctx.standardScripts.programmableLogicBase.hash;
190
+ const recipientPlbAddr = baseAddress(networkId, plbHash, recipient);
191
+ const registryMintPolicyId = ctx.standardScripts.registryMint.hash;
192
+ // 7. Build asset maps — include CIP-68 ref token in the same mint (same policy + redeemer)
193
+ const mintEntries = new Map([[unit, quantity]]);
194
+ if (hasCIP68 && refUnit) {
195
+ mintEntries.set(refUnit, 1n);
196
+ }
197
+ const tokenAssets = mintAssetsFromMap(mintEntries);
198
+ const registryNftUnit = registryMintPolicyId + scripts.tokenPolicyId;
199
+ const registryNftAssets = mintAssetsFromMap(new Map([[registryNftUnit, 1n]]));
200
+ const coveringNftUnit = findCoveringNodeNftUnit(coveringNodeUtxo, registryMintPolicyId);
201
+ // 8. Build transaction
202
+ let tx = client.newTx();
203
+ tx = tx.collectFrom({ inputs: [coveringNodeUtxo], redeemer: voidData() });
204
+ tx = tx.withdraw({
205
+ stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(scripts.issuerAdmin.hash, "hex"))),
206
+ amount: 0n,
207
+ redeemer: voidData(),
208
+ });
209
+ tx = tx.mintAssets({ assets: tokenAssets, redeemer: issuanceRedeemer });
210
+ tx = tx.mintAssets({ assets: registryNftAssets, redeemer: registryMintRedeemer });
211
+ // Output 0: user token to recipient (with label 333 prefix if CIP-68)
212
+ tx = tx.payToAddress({
213
+ address: EvoAddress.fromBech32(recipientPlbAddr),
214
+ assets: outputAssets(1300000n, new Map([[unit, quantity]])),
215
+ datum: new InlineDatum.InlineDatum({ data: tokenDatum }),
216
+ });
217
+ // Output 1 (CIP-68 only): reference token to issuer's PLB address with metadata datum
218
+ if (hasCIP68 && refUnit) {
219
+ const issuerPlbAddr = baseAddress(networkId, plbHash, feePayerAddress);
220
+ const cip68Datum = buildCIP68FTDatum(params.cip68Metadata);
221
+ tx = tx.payToAddress({
222
+ address: EvoAddress.fromBech32(issuerPlbAddr),
223
+ assets: outputAssets(3000000n, new Map([[refUnit, 1n]])),
224
+ datum: new InlineDatum.InlineDatum({ data: cip68Datum }),
225
+ });
226
+ }
227
+ // Updated covering node (output 1 or 2)
228
+ const coveringNodeTokenMap = new Map();
229
+ if (coveringNftUnit)
230
+ coveringNodeTokenMap.set(coveringNftUnit, 1n);
231
+ tx = tx.payToAddress({
232
+ address: EvoAddress.fromBech32(registrySpendAddr),
233
+ assets: outputAssets(utxoLovelace(coveringNodeUtxo), coveringNodeTokenMap),
234
+ datum: new InlineDatum.InlineDatum({ data: updatedCoveringDatum }),
235
+ });
236
+ // New registry node (output 2 or 3)
237
+ tx = tx.payToAddress({
238
+ address: EvoAddress.fromBech32(registrySpendAddr),
239
+ assets: outputAssets(2000000n, new Map([[registryNftUnit, 1n]])),
240
+ datum: new InlineDatum.InlineDatum({ data: newRegistryNodeDatum }),
241
+ });
242
+ // Reference inputs
243
+ tx = tx.readFrom({ referenceInputs: [protocolParamsUtxo, issuanceCborHexUtxo] });
244
+ // Attach scripts
245
+ tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.registrySpend.compiledCode) });
246
+ tx = tx.attachScript({ script: buildEvoScript(scripts.issuerAdmin.compiledCode) });
247
+ tx = tx.attachScript({ script: buildEvoScript(scripts.issuanceMint.compiledCode) });
248
+ tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.registryMint.compiledCode) });
249
+ // Required signer
250
+ tx = tx.addSigner({ keyHash: KeyHash.fromHex(config.deployment.adminPkh) });
251
+ const built = await buildAndSerialize(tx, feePayerAddress, useChaining ? chainedUtxos : undefined, useChaining);
252
+ return {
253
+ cbor: built.cbor,
254
+ txHash: built.txHash,
255
+ _signBuilder: built._signBuilder,
256
+ tokenPolicyId: scripts.tokenPolicyId,
257
+ metadata: {
258
+ issuerAdminScriptHash: scripts.issuerAdmin.hash,
259
+ transferScriptHash: scripts.transfer.hash,
260
+ ...(hasCIP68 && {
261
+ cip68Enabled: true,
262
+ userAssetNameHex,
263
+ refAssetNameHex,
264
+ }),
265
+ },
266
+ };
267
+ },
268
+ // ====================================================================
269
+ // MINT — subsequent mint with RefInput proof
270
+ // ====================================================================
271
+ async mint(params) {
272
+ const { feePayerAddress, tokenPolicyId, assetName, quantity, recipientAddress } = params;
273
+ const recipient = recipientAddress || feePayerAddress;
274
+ const unit = tokenPolicyId + assetName;
275
+ const client = ctx.client;
276
+ if (tokenPolicyId !== scripts.tokenPolicyId) {
277
+ throw new Error(`Token policy ${tokenPolicyId} does not match this FES instance (${scripts.tokenPolicyId})`);
278
+ }
279
+ // 1. Find registry node as RefInput proof
280
+ const registrySpendAddr = scriptAddress(networkId, ctx.standardScripts.registrySpend.hash);
281
+ const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(registrySpendAddr));
282
+ const registryUtxo = findRegistryNode(registryUtxos, tokenPolicyId);
283
+ if (!registryUtxo)
284
+ throw new Error(`Registry node not found for ${tokenPolicyId}`);
285
+ // 2. Sort reference inputs
286
+ const regRef = utxoToTxInput(registryUtxo);
287
+ const sortedRefInputs = sortTxInputs([regRef]);
288
+ const registryRefIdx = findRefInputIndex(sortedRefInputs, regRef);
289
+ // 3. Build redeemers
290
+ const issuanceRedeemer = issuanceRedeemerRefInput(scripts.issuerAdmin.hash, registryRefIdx);
291
+ const tokenDatum = voidData();
292
+ // 4. Build PLB address
293
+ const plbHash = ctx.standardScripts.programmableLogicBase.hash;
294
+ const recipientPlbAddr = baseAddress(networkId, plbHash, recipient);
295
+ // 5. Get wallet UTxOs
296
+ const walletUtxos = await client.getUtxos(EvoAddress.fromBech32(feePayerAddress));
297
+ // 6. Build transaction
298
+ const tokenAssets = mintAssetsFromMap(new Map([[unit, quantity]]));
299
+ let tx = client.newTx();
300
+ tx = tx.collectFrom({ inputs: walletUtxos.slice(0, 2) });
301
+ tx = tx.withdraw({
302
+ stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(scripts.issuerAdmin.hash, "hex"))),
303
+ amount: 0n,
304
+ redeemer: voidData(),
305
+ });
306
+ tx = tx.mintAssets({ assets: tokenAssets, redeemer: issuanceRedeemer });
307
+ tx = tx.payToAddress({
308
+ address: EvoAddress.fromBech32(recipientPlbAddr),
309
+ assets: outputAssets(1300000n, new Map([[unit, quantity]])),
310
+ datum: new InlineDatum.InlineDatum({ data: tokenDatum }),
311
+ });
312
+ tx = tx.readFrom({ referenceInputs: [registryUtxo] });
313
+ tx = tx.attachScript({ script: buildEvoScript(scripts.issuerAdmin.compiledCode) });
314
+ tx = tx.attachScript({ script: buildEvoScript(scripts.issuanceMint.compiledCode) });
315
+ tx = tx.addSigner({ keyHash: KeyHash.fromHex(config.deployment.adminPkh) });
316
+ const { cbor, txHash } = await buildAndSerialize(tx, feePayerAddress, walletUtxos);
317
+ return { cbor, txHash, tokenPolicyId };
318
+ },
319
+ // ====================================================================
320
+ // BURN
321
+ // ====================================================================
322
+ async burn(params) {
323
+ const { feePayerAddress, tokenPolicyId, assetName, utxoTxHash: targetTxHash, utxoOutputIndex: targetIdx } = params;
324
+ const holder = params.holderAddress || feePayerAddress;
325
+ const unit = tokenPolicyId + assetName;
326
+ const client = ctx.client;
327
+ if (tokenPolicyId !== scripts.tokenPolicyId) {
328
+ throw new Error(`Token policy ${tokenPolicyId} does not match this FES instance`);
329
+ }
330
+ // 1. Find UTxO to burn at the holder's PLB address
331
+ const plbHash = ctx.standardScripts.programmableLogicBase.hash;
332
+ const holderPlbAddr = baseAddress(networkId, plbHash, holder);
333
+ const allUtxos = await client.getUtxos(EvoAddress.fromBech32(holderPlbAddr));
334
+ const utxoToBurn = allUtxos.find(u => utxoTxHash(u) === targetTxHash && utxoOutputIndex(u) === targetIdx);
335
+ if (!utxoToBurn)
336
+ throw new Error(`UTxO ${targetTxHash}#${targetIdx} not found`);
337
+ const burnAmount = utxoUnitQty(utxoToBurn, unit);
338
+ if (burnAmount <= 0n)
339
+ throw new Error(`No tokens of ${unit} in UTxO`);
340
+ // 2. Find reference inputs
341
+ const protocolParamsUtxo = await findProtocolParamsUtxo(client, networkId, ctx.deployment);
342
+ const registrySpendAddr = scriptAddress(networkId, ctx.standardScripts.registrySpend.hash);
343
+ const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(registrySpendAddr));
344
+ const registryUtxo = findRegistryNode(registryUtxos, tokenPolicyId);
345
+ if (!registryUtxo)
346
+ throw new Error(`Registry node not found for ${tokenPolicyId}`);
347
+ // 3. Sort reference inputs
348
+ const allRefInputRefs = [utxoToTxInput(protocolParamsUtxo), utxoToTxInput(registryUtxo)];
349
+ const sortedRefInputs = sortTxInputs(allRefInputRefs);
350
+ const registryIdx = findRefInputIndex(sortedRefInputs, utxoToTxInput(registryUtxo));
351
+ // 4. Build redeemers
352
+ const issuanceRedeemer = issuanceRedeemerRefInput(scripts.issuerAdmin.hash, registryIdx);
353
+ const plgRedeemer = thirdPartyActRedeemer(registryIdx, 0);
354
+ const tokenDatum = voidData();
355
+ // 5. Compute remaining assets (remove burned token's policy)
356
+ const remainingTokens = new Map();
357
+ const allUnits = Assets.getUnits(utxoToBurn.assets);
358
+ for (const u of allUnits) {
359
+ if (u === "lovelace" || u === "")
360
+ continue;
361
+ if (u.startsWith(tokenPolicyId))
362
+ continue;
363
+ const qty = Assets.getByUnit(utxoToBurn.assets, u);
364
+ if (qty > 0n)
365
+ remainingTokens.set(u, qty);
366
+ }
367
+ // 6. Build burn assets
368
+ const burnAssets = mintAssetsFromMap(new Map([[unit, -burnAmount]]));
369
+ // 7. Get wallet UTxOs
370
+ const walletUtxos = await client.getUtxos(EvoAddress.fromBech32(feePayerAddress));
371
+ // 8. Build transaction
372
+ let tx = client.newTx();
373
+ tx = tx.collectFrom({ inputs: walletUtxos.slice(0, 2) });
374
+ tx = tx.collectFrom({ inputs: [utxoToBurn], redeemer: voidData() });
375
+ tx = tx.withdraw({
376
+ stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(scripts.issuerAdmin.hash, "hex"))),
377
+ amount: 0n,
378
+ redeemer: voidData(),
379
+ });
380
+ tx = tx.withdraw({
381
+ stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.programmableLogicGlobal.hash, "hex"))),
382
+ amount: 0n,
383
+ redeemer: plgRedeemer,
384
+ });
385
+ tx = tx.payToAddress({
386
+ address: utxoToBurn.address,
387
+ assets: outputAssets(utxoLovelace(utxoToBurn), remainingTokens.size > 0 ? remainingTokens : undefined),
388
+ datum: new InlineDatum.InlineDatum({ data: tokenDatum }),
389
+ });
390
+ tx = tx.mintAssets({ assets: burnAssets, redeemer: issuanceRedeemer });
391
+ tx = tx.readFrom({ referenceInputs: [protocolParamsUtxo, registryUtxo] });
392
+ tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicBase.compiledCode) });
393
+ tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicGlobal.compiledCode) });
394
+ tx = tx.attachScript({ script: buildEvoScript(scripts.issuerAdmin.compiledCode) });
395
+ tx = tx.attachScript({ script: buildEvoScript(scripts.issuanceMint.compiledCode) });
396
+ tx = tx.addSigner({ keyHash: KeyHash.fromHex(config.deployment.adminPkh) });
397
+ const { cbor, txHash } = await buildAndSerialize(tx, feePayerAddress, walletUtxos);
398
+ return { cbor, txHash };
399
+ },
400
+ // ====================================================================
401
+ // TRANSFER
402
+ // ====================================================================
403
+ async transfer(params) {
404
+ const { senderAddress, recipientAddress, tokenPolicyId, assetName, quantity } = params;
405
+ const unit = tokenPolicyId + assetName;
406
+ const client = ctx.client;
407
+ if (tokenPolicyId !== scripts.tokenPolicyId) {
408
+ throw new Error(`Token policy ${tokenPolicyId} does not match this FES instance (${scripts.tokenPolicyId})`);
409
+ }
410
+ const plbHash = ctx.standardScripts.programmableLogicBase.hash;
411
+ // 1. Build PLB addresses
412
+ const senderPlbAddr = baseAddress(networkId, plbHash, senderAddress);
413
+ const recipientPlbAddr = baseAddress(networkId, plbHash, recipientAddress);
414
+ // 2. Find sender's token UTxOs
415
+ const allUtxos = await client.getUtxos(EvoAddress.fromBech32(senderPlbAddr));
416
+ const tokenUtxos = allUtxos.filter(u => utxoUnitQty(u, unit) > 0n);
417
+ if (tokenUtxos.length === 0) {
418
+ throw new Error(`No token UTxOs found at ${senderPlbAddr} for ${unit}`);
419
+ }
420
+ // 3. Select enough UTxOs
421
+ const { selected, totalTokenAmount } = selectUtxosForAmount(tokenUtxos, unit, quantity);
422
+ const returningAmount = totalTokenAmount - quantity;
423
+ // 4. Find registry node reference input
424
+ const registrySpendAddr = scriptAddress(networkId, ctx.standardScripts.registrySpend.hash);
425
+ const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(registrySpendAddr));
426
+ const registryUtxo = findRegistryNode(registryUtxos, tokenPolicyId);
427
+ if (!registryUtxo)
428
+ throw new Error(`Registry node not found for ${tokenPolicyId}`);
429
+ // 5. Find protocol params reference input
430
+ const protocolParamsUtxo = await findProtocolParamsUtxo(client, networkId, ctx.deployment);
431
+ // 6. Find blacklist non-membership proofs
432
+ const senderStakingHash = stakingCredentialHash(senderAddress);
433
+ const blacklistSpendAddr = scriptAddress(networkId, scripts.blacklistSpend.hash);
434
+ const blacklistUtxos = await client.getUtxos(EvoAddress.fromBech32(blacklistSpendAddr));
435
+ const proofUtxos = [];
436
+ for (const _inputUtxo of selected) {
437
+ const proofUtxo = findBlacklistCoveringNode(blacklistUtxos, senderStakingHash);
438
+ if (!proofUtxo) {
439
+ throw new Error(`Sender ${senderStakingHash} is blacklisted — transfer denied`);
440
+ }
441
+ if (!proofUtxos.some(p => utxoTxHash(p) === utxoTxHash(proofUtxo) && utxoOutputIndex(p) === utxoOutputIndex(proofUtxo))) {
442
+ proofUtxos.push(proofUtxo);
443
+ }
444
+ }
445
+ // 7. Sort ALL reference inputs
446
+ const allRefInputRefs = [
447
+ ...proofUtxos.map(utxoToTxInput),
448
+ utxoToTxInput(protocolParamsUtxo),
449
+ utxoToTxInput(registryUtxo),
450
+ ];
451
+ const sortedRefInputs = sortTxInputs(allRefInputRefs);
452
+ const proofIndices = selected.map(() => findRefInputIndex(sortedRefInputs, utxoToTxInput(proofUtxos[0])));
453
+ const registryIdx = findRefInputIndex(sortedRefInputs, utxoToTxInput(registryUtxo));
454
+ // 8. Get sender's wallet UTxOs
455
+ const senderWalletUtxos = await client.getUtxos(EvoAddress.fromBech32(senderAddress));
456
+ // 9. Build redeemers
457
+ const fesTransferRedeemer = Data.list(proofIndices.map((idx) => Data.constr(0n, [Data.int(BigInt(idx))])));
458
+ const plgRedeemer = transferActRedeemer([{ type: "exists", nodeIdx: registryIdx }]);
459
+ const spendRdmr = voidData();
460
+ const tokenDatum = voidData();
461
+ // 10. Build transaction
462
+ let tx = client.newTx();
463
+ tx = tx.collectFrom({ inputs: selected, redeemer: spendRdmr });
464
+ tx = tx.withdraw({
465
+ stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.programmableLogicGlobal.hash, "hex"))),
466
+ amount: 0n,
467
+ redeemer: plgRedeemer,
468
+ });
469
+ tx = tx.withdraw({
470
+ stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(scripts.transfer.hash, "hex"))),
471
+ amount: 0n,
472
+ redeemer: fesTransferRedeemer,
473
+ });
474
+ if (returningAmount > 0n) {
475
+ tx = tx.payToAddress({
476
+ address: EvoAddress.fromBech32(senderPlbAddr),
477
+ assets: outputAssets(1300000n, new Map([[unit, returningAmount]])),
478
+ datum: new InlineDatum.InlineDatum({ data: tokenDatum }),
479
+ });
480
+ }
481
+ tx = tx.payToAddress({
482
+ address: EvoAddress.fromBech32(recipientPlbAddr),
483
+ assets: outputAssets(1300000n, new Map([[unit, quantity]])),
484
+ datum: new InlineDatum.InlineDatum({ data: tokenDatum }),
485
+ });
486
+ tx = tx.readFrom({ referenceInputs: [...proofUtxos, protocolParamsUtxo, registryUtxo] });
487
+ tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicBase.compiledCode) });
488
+ tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicGlobal.compiledCode) });
489
+ tx = tx.attachScript({ script: buildEvoScript(scripts.transfer.compiledCode) });
490
+ tx = tx.addSigner({ keyHash: KeyHash.fromHex(senderStakingHash) });
491
+ const { cbor, txHash } = await buildAndSerialize(tx, senderAddress, senderWalletUtxos);
492
+ return { cbor, txHash };
493
+ },
494
+ // ====================================================================
495
+ // INIT COMPLIANCE — initialize blacklist
496
+ // ====================================================================
497
+ async initCompliance(params) {
498
+ const { feePayerAddress, adminAddress, assetName } = params;
499
+ const client = ctx.client;
500
+ // 1. Build blacklist origin node
501
+ const originDatum = blacklistNodeDatum("", MAX_NEXT);
502
+ // 2. Compute addresses
503
+ const blacklistSpendAddr = scriptAddress(networkId, scripts.blacklistSpend.hash);
504
+ // 3. Build blacklist origin NFT
505
+ const blacklistOriginUnit = scripts.blacklistMint.hash + "";
506
+ const blacklistOriginAssets = mintAssetsFromMap(new Map([[blacklistOriginUnit, 1n]]));
507
+ // 4. Get the bootstrap UTxO (must be consumed for one-shot minting policy)
508
+ let bootstrapUtxos;
509
+ if (params.bootstrapUtxo) {
510
+ bootstrapUtxos = [params.bootstrapUtxo];
511
+ }
512
+ else {
513
+ const { blacklistInitTxInput } = config.deployment;
514
+ bootstrapUtxos = await client.getUtxosByOutRef([
515
+ new EvoTransactionInput.TransactionInput({
516
+ transactionId: EvoTransactionHash.fromHex(blacklistInitTxInput.txHash),
517
+ index: BigInt(blacklistInitTxInput.outputIndex),
518
+ }),
519
+ ]);
520
+ }
521
+ if (bootstrapUtxos.length === 0)
522
+ throw new Error("Bootstrap UTxO not found on-chain");
523
+ // 5. Build transaction
524
+ let tx = client.newTx();
525
+ // Must consume bootstrap UTxO (one-shot check in blacklist mint validator)
526
+ tx = tx.collectFrom({ inputs: bootstrapUtxos });
527
+ tx = tx.mintAssets({ assets: blacklistOriginAssets, redeemer: blacklistInitRedeemer() });
528
+ // Output: chain output (40 ADA for next tx)
529
+ tx = tx.payToAddress({
530
+ address: EvoAddress.fromBech32(feePayerAddress),
531
+ assets: outputAssets(40000000n),
532
+ });
533
+ // Output: blacklist origin node
534
+ tx = tx.payToAddress({
535
+ address: EvoAddress.fromBech32(blacklistSpendAddr),
536
+ assets: outputAssets(1300000n, new Map([[blacklistOriginUnit, 1n]])),
537
+ datum: new InlineDatum.InlineDatum({ data: originDatum }),
538
+ });
539
+ // Register stake addresses only if not already registered on-chain.
540
+ // Check via backend API (reliable, unlike Blockfrost getDelegation).
541
+ const stakeScripts = [
542
+ { hash: scripts.issuerAdmin.hash, code: scripts.issuerAdmin.compiledCode },
543
+ { hash: scripts.transfer.hash, code: scripts.transfer.compiledCode },
544
+ ];
545
+ for (const s of stakeScripts) {
546
+ const stakeAddr = rewardAddress(networkId, s.hash);
547
+ const registered = ctx.checkStakeRegistration
548
+ ? await ctx.checkStakeRegistration(stakeAddr)
549
+ : false; // no callback → assume not registered → register
550
+ console.log(`[CIP-113] Stake ${stakeAddr}: registered=${registered}`);
551
+ if (!registered) {
552
+ tx = tx.registerStake({
553
+ stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(s.hash, "hex"))),
554
+ redeemer: voidData(),
555
+ });
556
+ tx = tx.attachScript({ script: buildEvoScript(s.code) });
557
+ }
558
+ }
559
+ tx = tx.attachScript({ script: buildEvoScript(scripts.blacklistMint.compiledCode) });
560
+ const built = await buildAndSerialize(tx, feePayerAddress);
561
+ return {
562
+ cbor: built.cbor,
563
+ txHash: built.txHash,
564
+ chainAvailable: built.chainAvailable,
565
+ _signBuilder: built._signBuilder,
566
+ metadata: {
567
+ blacklistNodePolicyId: scripts.blacklistMint.hash,
568
+ blacklistSpendScriptHash: scripts.blacklistSpend.hash,
569
+ issuerAdminScriptHash: scripts.issuerAdmin.hash,
570
+ transferScriptHash: scripts.transfer.hash,
571
+ },
572
+ };
573
+ },
574
+ // ====================================================================
575
+ // FREEZE — add address to blacklist
576
+ // ====================================================================
577
+ async freeze(params) {
578
+ const { feePayerAddress, tokenPolicyId: _tokenPolicyId, assetName: _assetName, targetAddress } = params;
579
+ const client = ctx.client;
580
+ const targetStakingHash = stakingCredentialHash(targetAddress);
581
+ const blacklistSpendAddr = scriptAddress(networkId, scripts.blacklistSpend.hash);
582
+ const blacklistUtxos = await client.getUtxos(EvoAddress.fromBech32(blacklistSpendAddr));
583
+ const coveringNode = findBlacklistCoveringNode(blacklistUtxos, targetStakingHash);
584
+ if (!coveringNode)
585
+ throw new Error(`Cannot find blacklist covering node for ${targetStakingHash} — may already be blacklisted`);
586
+ const coveringDatum = getInlineDatum(coveringNode);
587
+ const coveringKey = extractConstrBytesField(coveringDatum, 0) ?? "";
588
+ const coveringNext = extractConstrBytesField(coveringDatum, 1) ?? MAX_NEXT;
589
+ const updatedCoveringDatum = blacklistNodeDatum(coveringKey, targetStakingHash);
590
+ const newNodeDatum = blacklistNodeDatum(targetStakingHash, coveringNext);
591
+ const nftUnit = scripts.blacklistMint.hash + targetStakingHash;
592
+ const nftAssets = mintAssetsFromMap(new Map([[nftUnit, 1n]]));
593
+ const walletUtxos = await client.getUtxos(EvoAddress.fromBech32(feePayerAddress));
594
+ let tx = client.newTx();
595
+ tx = tx.collectFrom({ inputs: walletUtxos.slice(0, 2) });
596
+ tx = tx.collectFrom({ inputs: [coveringNode], redeemer: voidData() });
597
+ tx = tx.mintAssets({ assets: nftAssets, redeemer: blacklistAddRedeemer(targetStakingHash) });
598
+ // Output 0: updated covering node
599
+ const coveringNftUnit = findCoveringNodeNftUnit(coveringNode, scripts.blacklistMint.hash);
600
+ const coveringTokenMap = new Map();
601
+ if (coveringNftUnit)
602
+ coveringTokenMap.set(coveringNftUnit, 1n);
603
+ tx = tx.payToAddress({
604
+ address: EvoAddress.fromBech32(blacklistSpendAddr),
605
+ assets: outputAssets(utxoLovelace(coveringNode), coveringTokenMap),
606
+ datum: new InlineDatum.InlineDatum({ data: updatedCoveringDatum }),
607
+ });
608
+ // Output 1: new blacklist node
609
+ tx = tx.payToAddress({
610
+ address: EvoAddress.fromBech32(blacklistSpendAddr),
611
+ assets: outputAssets(2000000n, new Map([[nftUnit, 1n]])),
612
+ datum: new InlineDatum.InlineDatum({ data: newNodeDatum }),
613
+ });
614
+ tx = tx.attachScript({ script: buildEvoScript(scripts.blacklistSpend.compiledCode) });
615
+ tx = tx.attachScript({ script: buildEvoScript(scripts.blacklistMint.compiledCode) });
616
+ const managerPkh = paymentCredentialHash(feePayerAddress);
617
+ tx = tx.addSigner({ keyHash: KeyHash.fromHex(managerPkh) });
618
+ const { cbor, txHash } = await buildAndSerialize(tx, feePayerAddress, walletUtxos);
619
+ return { cbor, txHash };
620
+ },
621
+ // ====================================================================
622
+ // UNFREEZE — remove address from blacklist
623
+ // ====================================================================
624
+ async unfreeze(params) {
625
+ const { feePayerAddress, tokenPolicyId: _tokenPolicyId, assetName: _assetName, targetAddress } = params;
626
+ const client = ctx.client;
627
+ const targetStakingHash = stakingCredentialHash(targetAddress);
628
+ const blacklistSpendAddr = scriptAddress(networkId, scripts.blacklistSpend.hash);
629
+ const blacklistUtxos = await client.getUtxos(EvoAddress.fromBech32(blacklistSpendAddr));
630
+ const nodeToRemove = blacklistUtxos.find(u => {
631
+ const datum = getInlineDatum(u);
632
+ return datum ? extractConstrBytesField(datum, 0) === targetStakingHash : false;
633
+ });
634
+ if (!nodeToRemove)
635
+ throw new Error(`Blacklist node not found for ${targetStakingHash}`);
636
+ const precedingNode = blacklistUtxos.find(u => {
637
+ const datum = getInlineDatum(u);
638
+ return datum ? extractConstrBytesField(datum, 1) === targetStakingHash : false;
639
+ });
640
+ if (!precedingNode)
641
+ throw new Error(`Preceding blacklist node not found for ${targetStakingHash}`);
642
+ const precedingDatum = getInlineDatum(precedingNode);
643
+ const precedingKey = extractConstrBytesField(precedingDatum, 0) ?? "";
644
+ const removedDatum = getInlineDatum(nodeToRemove);
645
+ const removedNext = extractConstrBytesField(removedDatum, 1) ?? MAX_NEXT;
646
+ const updatedPrecedingDatum = blacklistNodeDatum(precedingKey, removedNext);
647
+ const nftUnit = scripts.blacklistMint.hash + targetStakingHash;
648
+ const burnAssets = mintAssetsFromMap(new Map([[nftUnit, -1n]]));
649
+ const walletUtxos = await client.getUtxos(EvoAddress.fromBech32(feePayerAddress));
650
+ let tx = client.newTx();
651
+ tx = tx.collectFrom({ inputs: walletUtxos.slice(0, 2) });
652
+ tx = tx.collectFrom({ inputs: [nodeToRemove], redeemer: voidData() });
653
+ tx = tx.collectFrom({ inputs: [precedingNode], redeemer: voidData() });
654
+ tx = tx.mintAssets({ assets: burnAssets, redeemer: blacklistRemoveRedeemer(targetStakingHash) });
655
+ // Output: updated preceding node
656
+ const precedingNftUnit = findCoveringNodeNftUnit(precedingNode, scripts.blacklistMint.hash);
657
+ const precedingTokenMap = new Map();
658
+ if (precedingNftUnit)
659
+ precedingTokenMap.set(precedingNftUnit, 1n);
660
+ tx = tx.payToAddress({
661
+ address: EvoAddress.fromBech32(blacklistSpendAddr),
662
+ assets: outputAssets(utxoLovelace(precedingNode), precedingTokenMap),
663
+ datum: new InlineDatum.InlineDatum({ data: updatedPrecedingDatum }),
664
+ });
665
+ tx = tx.attachScript({ script: buildEvoScript(scripts.blacklistSpend.compiledCode) });
666
+ tx = tx.attachScript({ script: buildEvoScript(scripts.blacklistMint.compiledCode) });
667
+ const managerPkh = paymentCredentialHash(feePayerAddress);
668
+ tx = tx.addSigner({ keyHash: KeyHash.fromHex(managerPkh) });
669
+ const { cbor, txHash } = await buildAndSerialize(tx, feePayerAddress, walletUtxos);
670
+ return { cbor, txHash };
671
+ },
672
+ // ====================================================================
673
+ // SEIZE
674
+ // ====================================================================
675
+ async seize(params) {
676
+ const { feePayerAddress, tokenPolicyId, assetName, utxoTxHash: targetTxHash, utxoOutputIndex: targetIdx, destinationAddress } = params;
677
+ const unit = tokenPolicyId + assetName;
678
+ const client = ctx.client;
679
+ if (tokenPolicyId !== scripts.tokenPolicyId) {
680
+ throw new Error(`Token policy ${tokenPolicyId} does not match this FES instance`);
681
+ }
682
+ // 1. Find UTxO to seize
683
+ const plbHash = ctx.standardScripts.programmableLogicBase.hash;
684
+ let utxoToSeize;
685
+ // Search the holder's PLB address first, then fall back to feePayer and destination
686
+ const searchAddresses = [];
687
+ if (params.holderAddress) {
688
+ searchAddresses.push(baseAddress(networkId, plbHash, params.holderAddress));
689
+ }
690
+ searchAddresses.push(baseAddress(networkId, plbHash, feePayerAddress), baseAddress(networkId, plbHash, destinationAddress));
691
+ // Deduplicate
692
+ const uniqueAddresses = [...new Set(searchAddresses)];
693
+ for (const addr of uniqueAddresses) {
694
+ const utxos = await client.getUtxos(EvoAddress.fromBech32(addr));
695
+ utxoToSeize = utxos.find(u => utxoTxHash(u) === targetTxHash && utxoOutputIndex(u) === targetIdx);
696
+ if (utxoToSeize)
697
+ break;
698
+ }
699
+ if (!utxoToSeize)
700
+ throw new Error(`UTxO ${targetTxHash}#${targetIdx} not found`);
701
+ const seizedAmount = utxoUnitQty(utxoToSeize, unit);
702
+ if (seizedAmount <= 0n)
703
+ throw new Error(`No tokens of ${unit} in UTxO`);
704
+ // 2. Find reference inputs
705
+ const protocolParamsUtxo = await findProtocolParamsUtxo(client, networkId, ctx.deployment);
706
+ const registrySpendAddr = scriptAddress(networkId, ctx.standardScripts.registrySpend.hash);
707
+ const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(registrySpendAddr));
708
+ const registryUtxo = findRegistryNode(registryUtxos, tokenPolicyId);
709
+ if (!registryUtxo)
710
+ throw new Error(`Registry node not found for ${tokenPolicyId}`);
711
+ // 3. Sort reference inputs
712
+ const allRefInputRefs = [utxoToTxInput(protocolParamsUtxo), utxoToTxInput(registryUtxo)];
713
+ const sortedRefInputs = sortTxInputs(allRefInputRefs);
714
+ const registryIdx = findRefInputIndex(sortedRefInputs, utxoToTxInput(registryUtxo));
715
+ // 4. Build redeemers
716
+ const plgRedeemer = thirdPartyActRedeemer(registryIdx, 1);
717
+ const tokenDatum = voidData();
718
+ // 5. Build recipient PLB address
719
+ const recipientPlbAddr = baseAddress(networkId, plbHash, destinationAddress);
720
+ // 6. Compute remaining assets
721
+ const remainingTokens = new Map();
722
+ const allUnits = Assets.getUnits(utxoToSeize.assets);
723
+ for (const u of allUnits) {
724
+ if (u === "lovelace" || u === "" || u === unit)
725
+ continue;
726
+ const qty = Assets.getByUnit(utxoToSeize.assets, u);
727
+ if (qty > 0n)
728
+ remainingTokens.set(u, qty);
729
+ }
730
+ // 7. Get wallet UTxOs
731
+ const walletUtxos = await client.getUtxos(EvoAddress.fromBech32(feePayerAddress));
732
+ // 8. Build transaction
733
+ let tx = client.newTx();
734
+ tx = tx.collectFrom({ inputs: walletUtxos.slice(0, 2) });
735
+ tx = tx.collectFrom({ inputs: [utxoToSeize], redeemer: voidData() });
736
+ tx = tx.withdraw({
737
+ stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(scripts.issuerAdmin.hash, "hex"))),
738
+ amount: 0n,
739
+ redeemer: voidData(),
740
+ });
741
+ tx = tx.withdraw({
742
+ stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.programmableLogicGlobal.hash, "hex"))),
743
+ amount: 0n,
744
+ redeemer: plgRedeemer,
745
+ });
746
+ // Output 0: seized tokens to recipient
747
+ tx = tx.payToAddress({
748
+ address: EvoAddress.fromBech32(recipientPlbAddr),
749
+ assets: outputAssets(1300000n, new Map([[unit, seizedAmount]])),
750
+ datum: new InlineDatum.InlineDatum({ data: tokenDatum }),
751
+ });
752
+ // Output 1: remaining value to original address
753
+ tx = tx.payToAddress({
754
+ address: utxoToSeize.address,
755
+ assets: outputAssets(utxoLovelace(utxoToSeize), remainingTokens.size > 0 ? remainingTokens : undefined),
756
+ datum: new InlineDatum.InlineDatum({ data: tokenDatum }),
757
+ });
758
+ tx = tx.readFrom({ referenceInputs: [protocolParamsUtxo, registryUtxo] });
759
+ tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicBase.compiledCode) });
760
+ tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicGlobal.compiledCode) });
761
+ tx = tx.attachScript({ script: buildEvoScript(scripts.issuerAdmin.compiledCode) });
762
+ tx = tx.addSigner({ keyHash: KeyHash.fromHex(config.deployment.adminPkh) });
763
+ const { cbor, txHash } = await buildAndSerialize(tx, feePayerAddress, walletUtxos);
764
+ return { cbor, txHash };
765
+ },
766
+ };
767
+ }
768
+ export { createFESScripts } from "./scripts.js";
769
+ //# sourceMappingURL=index.js.map