@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.
- package/README.md +49 -0
- package/blueprints/standard/v0.3.0/plutus.json +596 -0
- package/blueprints/substandards/dummy/v0.1.0/plutus.json +58 -0
- package/blueprints/substandards/freeze-and-seize/v0.1.0/plutus.json +380 -0
- package/dist/core/evo-utils.d.ts +160 -0
- package/dist/core/evo-utils.d.ts.map +1 -0
- package/dist/core/evo-utils.js +401 -0
- package/dist/core/evo-utils.js.map +1 -0
- package/dist/core/registry.d.ts +31 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +72 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +158 -0
- package/dist/index.js.map +1 -0
- package/dist/provider/address-utils.d.ts +15 -0
- package/dist/provider/address-utils.d.ts.map +1 -0
- package/dist/provider/address-utils.js +29 -0
- package/dist/provider/address-utils.js.map +1 -0
- package/dist/provider/tx-utils.d.ts +15 -0
- package/dist/provider/tx-utils.d.ts.map +1 -0
- package/dist/provider/tx-utils.js +18 -0
- package/dist/provider/tx-utils.js.map +1 -0
- package/dist/standard/blueprint.d.ts +31 -0
- package/dist/standard/blueprint.d.ts.map +1 -0
- package/dist/standard/blueprint.js +49 -0
- package/dist/standard/blueprint.js.map +1 -0
- package/dist/standard/params.d.ts +6 -0
- package/dist/standard/params.d.ts.map +1 -0
- package/dist/standard/params.js +5 -0
- package/dist/standard/params.js.map +1 -0
- package/dist/standard/scripts.d.ts +51 -0
- package/dist/standard/scripts.d.ts.map +1 -0
- package/dist/standard/scripts.js +110 -0
- package/dist/standard/scripts.js.map +1 -0
- package/dist/substandards/dummy/index.d.ts +16 -0
- package/dist/substandards/dummy/index.d.ts.map +1 -0
- package/dist/substandards/dummy/index.js +158 -0
- package/dist/substandards/dummy/index.js.map +1 -0
- package/dist/substandards/freeze-and-seize/index.d.ts +17 -0
- package/dist/substandards/freeze-and-seize/index.d.ts.map +1 -0
- package/dist/substandards/freeze-and-seize/index.js +769 -0
- package/dist/substandards/freeze-and-seize/index.js.map +1 -0
- package/dist/substandards/freeze-and-seize/scripts.d.ts +25 -0
- package/dist/substandards/freeze-and-seize/scripts.d.ts.map +1 -0
- package/dist/substandards/freeze-and-seize/scripts.js +54 -0
- package/dist/substandards/freeze-and-seize/scripts.js.map +1 -0
- package/dist/substandards/freeze-and-seize/types.d.ts +18 -0
- package/dist/substandards/freeze-and-seize/types.d.ts.map +1 -0
- package/dist/substandards/freeze-and-seize/types.js +8 -0
- package/dist/substandards/freeze-and-seize/types.js.map +1 -0
- package/dist/substandards/interface.d.ts +166 -0
- package/dist/substandards/interface.d.ts.map +1 -0
- package/dist/substandards/interface.js +10 -0
- package/dist/substandards/interface.js.map +1 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- 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
|