@fundtokens/builders 0.1.0-rc5

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.
@@ -0,0 +1,336 @@
1
+ import {
2
+ Contract,
3
+ TransactionBuilder,
4
+ } from 'cashscript';
5
+ import {
6
+ swapEndianness,
7
+ hash256,
8
+ hexToBin,
9
+ binToHex,
10
+ cashAddressToLockingBytecode,
11
+ } from '@bitauth/libauth';
12
+ import {
13
+ getBestFee,
14
+ getFundBin,
15
+ getFundHex,
16
+ getRandomInt,
17
+ withDust,
18
+ } from './utils.js';
19
+ import FundTokenTransactionBuilder from './FundTokenTransactionBuilder.js';
20
+
21
+ import feeJson from './art/fee.json' with { type: 'json' };
22
+ import startupJson from './art/startup.json' with { type: 'json' };
23
+ import mintInflowJson from './art/mint_inflow.json' with { type: 'json' };
24
+ import mintOutflowJson from './art/mint_outflow.json' with { type: 'json' };
25
+ import managerJson from './art/manager.json' with { type: 'json' };
26
+ import fundJson from './art/fund.json' with { type: 'json' };
27
+ import assetJson from './art/asset.json' with { type: 'json' };
28
+ import publicJson from './art/public.json' with { type: 'json' };
29
+ import simpleVaultJson from './art/simple_vault.json' with { type: 'json' };
30
+ import authHeadVaultJson from './art/authhead_vault.json' with { type: 'json' };
31
+ import publicFundVaultJson from './art/public_vault.json' with { type: 'json' };
32
+
33
+ export default class PublicFundTransactionBuilder extends TransactionBuilder {
34
+ #system = {
35
+ inflow: '', // 32 byte, token id
36
+ outflow: '', // 32 byte, token id
37
+ publicFund: '', // 32 byte, token id
38
+ authorization: '', // 32 byte, token id
39
+ fees: {
40
+ create: {
41
+ nft: '', // 32 byte, tx id/token id
42
+ value: -1n, // bigint
43
+ },
44
+ execute: {
45
+ nft: '', // 32 byte, tx id/token id
46
+ value: -1n, // bigint
47
+ }
48
+ },
49
+ };
50
+ #swapped = {
51
+ inflow: '',
52
+ outflow: '',
53
+ publicFund: '',
54
+ authorization: '',
55
+ fees: {
56
+ create: {
57
+ nft: '',
58
+ },
59
+ execute: {
60
+ nft: '',
61
+ },
62
+ },
63
+ };
64
+ #contracts = {
65
+ startupContract: null,
66
+ mintInflowContract: null,
67
+ mintOutflowContract: null,
68
+ createFundFeeContract: null,
69
+ executeFundFeeContract: null,
70
+ publicFundContract: null,
71
+ feeVaultContract: null,
72
+ authHeadVaultContract: null,
73
+ publicFundVaultContract: null,
74
+ };
75
+ #logger = null;
76
+
77
+ constructor({
78
+ provider,
79
+ system,
80
+ logger,
81
+ }) {
82
+ if (!system) {
83
+ throw new Error('No system configuration provided, unable to continue');
84
+ }
85
+ super({ provider });
86
+ this.#system = system;
87
+ this.#swapped = {
88
+ inflow: swapEndianness(system.inflow),
89
+ outflow: swapEndianness(system.outflow),
90
+ publicFund: swapEndianness(system.publicFund),
91
+ authorization: swapEndianness(system.authorization),
92
+ fees: {
93
+ create: {
94
+ nft: swapEndianness(system.fees.create.nft),
95
+ },
96
+ execute: {
97
+ nft: swapEndianness(system.fees.execute.nft),
98
+ }
99
+ }
100
+ };
101
+ this.#logger = logger ?? this.#logger;
102
+ this.#buildContracts();
103
+ }
104
+
105
+ // build and get the contracts
106
+ #buildContracts() {
107
+ const feeVaultContract = new Contract(simpleVaultJson, [this.#swapped.authorization], { provider: this.provider });
108
+ const feeVaultLockingBytecode = binToHex(cashAddressToLockingBytecode(feeVaultContract.tokenAddress).bytecode);
109
+
110
+ const createFundFeeContract = new Contract(feeJson, [this.#swapped.authorization, feeVaultLockingBytecode, this.#swapped.fees.create.nft, BigInt(this.#system.fees.create.value)], { provider: this.provider });
111
+ const executeFundFeeContract = new Contract(feeJson, [this.#swapped.authorization, feeVaultLockingBytecode, this.#swapped.fees.execute.nft, BigInt(this.#system.fees.execute.value)], { provider: this.provider });
112
+
113
+ const startupContract = new Contract(startupJson, [
114
+ binToHex(hash256(hexToBin(createFundFeeContract.bytecode))),
115
+ this.#swapped.inflow,
116
+ this.#swapped.outflow,
117
+ ], { provider: this.provider });
118
+ const startupContractHash = binToHex(hash256(hexToBin(startupContract.bytecode)))
119
+
120
+ const mintInflowContract = new Contract(mintInflowJson, [
121
+ startupContractHash,
122
+ this.#swapped.inflow,
123
+ this.#swapped.outflow,
124
+ binToHex(hash256(hexToBin(executeFundFeeContract.bytecode))),
125
+ hexToBin(managerJson.debug.bytecode),
126
+ hexToBin(fundJson.debug.bytecode),
127
+ hexToBin(assetJson.debug.bytecode),
128
+ ], { provider: this.provider });
129
+
130
+ const mintOutflowContract = new Contract(mintOutflowJson, [
131
+ startupContractHash,
132
+ this.#swapped.inflow,
133
+ this.#swapped.outflow,
134
+ binToHex(hash256(hexToBin(executeFundFeeContract.bytecode))),
135
+ hexToBin(managerJson.debug.bytecode),
136
+ hexToBin(fundJson.debug.bytecode),
137
+ hexToBin(assetJson.debug.bytecode),
138
+ ], { provider: this.provider });
139
+
140
+ const authHeadVaultContract = new Contract(authHeadVaultJson, [this.#swapped.authorization], { provider: this.provider });
141
+ const authHeadVaultLockingBytecode = binToHex(cashAddressToLockingBytecode(authHeadVaultContract.tokenAddress).bytecode);
142
+
143
+ const publicFundVaultContract = new Contract(publicFundVaultJson, [this.#swapped.publicFund, this.#swapped.authorization], { provider: this.provider });
144
+ const publicFundVaultLockingBytecode = binToHex(cashAddressToLockingBytecode(publicFundVaultContract.tokenAddress).bytecode);
145
+
146
+ const publicFundContract = new Contract(publicJson, [
147
+ authHeadVaultLockingBytecode,
148
+ publicFundVaultLockingBytecode,
149
+ this.#swapped.publicFund,
150
+ startupContractHash,
151
+ fundJson.debug.bytecode,
152
+ this.#swapped.inflow,
153
+ this.#swapped.outflow,
154
+ ], { provider: this.provider });
155
+
156
+
157
+ this.#contracts = {
158
+ startupContract,
159
+ mintInflowContract,
160
+ mintOutflowContract,
161
+ createFundFeeContract,
162
+ executeFundFeeContract,
163
+ publicFundContract,
164
+ feeVaultContract,
165
+ authHeadVaultContract,
166
+ publicFundVaultContract,
167
+ };
168
+ }
169
+
170
+ getContracts() {
171
+ return this.#contracts;
172
+ }
173
+
174
+ async addBroadcast({
175
+ fund,
176
+ payBy,
177
+ }) {
178
+ const {
179
+ feeVaultContract,
180
+ createFundFeeContract,
181
+ startupContract,
182
+ mintInflowContract,
183
+ mintOutflowContract,
184
+ publicFundContract,
185
+ authHeadVaultContract,
186
+ publicFundVaultContract,
187
+ } = this.#contracts;
188
+
189
+ if(this.inputs.length === 0) {
190
+ throw new Error('User genesis input is expected to be added prior to calling this function');
191
+ }
192
+
193
+ if(this.outputs.length > 0) {
194
+ throw new Error('No outputs should be added to the transaction');
195
+ }
196
+
197
+ const genesisUtxo = this.inputs[0];
198
+
199
+ if(genesisUtxo.vout !== 0 || genesisUtxo.token) {
200
+ throw new Error('First input must be a genesis input (vout is 0) with no tokens');
201
+ }
202
+
203
+
204
+ const bestFee = await getBestFee({ feeVaultContract, feeContract: createFundFeeContract, payBy, fee: this.#system.fees.create });
205
+
206
+ const broadcastUtxos = await startupContract.getUtxos();
207
+ const mintInflowUtxos = await mintInflowContract.getUtxos();
208
+ const mintOutflowUtxos = await mintOutflowContract.getUtxos();
209
+ const publicUtxos = await publicFundContract.getUtxos();
210
+ const inflowUtxos = mintInflowUtxos.filter(u => u.token?.category === this.#system.inflow);
211
+ const outflowUtxos = mintOutflowUtxos.filter(u => u.token?.category === this.#system.outflow);
212
+ const publicFundUtxos = publicUtxos.filter(u => u.token?.category === this.#system.publicFund)
213
+
214
+
215
+ const broadcastUtxo = broadcastUtxos[getRandomInt(broadcastUtxos.length)];
216
+ const inflowUtxo = inflowUtxos[getRandomInt(inflowUtxos.length)];
217
+ const outflowUtxo = outflowUtxos[getRandomInt(outflowUtxos.length)];
218
+ const publicFundUtxo = publicFundUtxos[getRandomInt(publicFundUtxos.length)];
219
+
220
+ var { managerContract, fundContract } = new FundTokenTransactionBuilder({ provider: this.provider, system: { ...this.#system, fee: this.#system.fees.execute }, fund }).getContracts();
221
+
222
+ const fundTokenAmount = 9223372036854775807n;
223
+
224
+ this.addInputs([
225
+ {
226
+ ...broadcastUtxo,
227
+ unlocker: startupContract.unlock.start(getFundBin(fund)),
228
+ },
229
+ {
230
+ ...inflowUtxo,
231
+ unlocker: mintInflowContract.unlock.mint(),
232
+ },
233
+ {
234
+ ...outflowUtxo,
235
+ unlocker: mintOutflowContract.unlock.mint(),
236
+ },
237
+ {
238
+ ...bestFee.utxo,
239
+ unlocker: createFundFeeContract.unlock.pay(),
240
+ },
241
+ {
242
+ ...publicFundUtxo,
243
+ unlocker: publicFundContract.unlock.broadcast(getFundBin(fund))
244
+ }
245
+ ])
246
+ .addOutputs([
247
+ withDust({
248
+ to: authHeadVaultContract.tokenAddress,
249
+ }),
250
+ {
251
+ to: startupContract.tokenAddress,
252
+ amount: broadcastUtxo.satoshis,
253
+ token: broadcastUtxo.token,
254
+ },
255
+ {
256
+ to: mintInflowContract.tokenAddress,
257
+ amount: inflowUtxo.satoshis,
258
+ token: inflowUtxo.token,
259
+ },
260
+ {
261
+ to: mintOutflowContract.tokenAddress,
262
+ amount: outflowUtxo.satoshis,
263
+ token: outflowUtxo.token,
264
+ },
265
+ ...bestFee.outputs,
266
+ withDust({
267
+ to: managerContract.tokenAddress,
268
+ token: {
269
+ ...inflowUtxo.token,
270
+ nft: {
271
+ capability: 'none',
272
+ commitment: swapEndianness(genesisUtxo.txid) + binToHex(hash256(getFundBin(fund))),
273
+ }
274
+ }
275
+ }),
276
+ withDust({
277
+ to: managerContract.tokenAddress,
278
+ token: {
279
+ ...outflowUtxo.token,
280
+ nft: {
281
+ capability: 'none',
282
+ commitment: swapEndianness(genesisUtxo.txid) + binToHex(hash256(getFundBin(fund))),
283
+ }
284
+ }
285
+ }),
286
+ withDust({
287
+ to: fundContract.tokenAddress,
288
+ token: {
289
+ category: genesisUtxo.txid,
290
+ amount: fundTokenAmount,
291
+ }
292
+ }),
293
+ withDust({
294
+ to: publicFundContract.tokenAddress,
295
+ token: {
296
+ category: this.#system.publicFund,
297
+ amount: 0n,
298
+ nft: {
299
+ capability: 'minting',
300
+ commitment: '',
301
+ }
302
+ }
303
+ }),
304
+ ]);
305
+
306
+
307
+ const maxSize = 128 * 2;
308
+
309
+ const fundHex = getFundHex(fund);
310
+ const fundHexParts = [];
311
+
312
+ let curr = 0;
313
+ let next = maxSize;
314
+
315
+
316
+ while(curr < fundHex.length) {
317
+ fundHexParts.push(fundHex.slice(curr, next));
318
+ curr = next;
319
+ next += maxSize
320
+ }
321
+
322
+ fundHexParts.forEach(part => {
323
+ this.addOutput(withDust({
324
+ to: publicFundVaultContract.tokenAddress,
325
+ token: {
326
+ category: this.#system.publicFund,
327
+ amount: 0n,
328
+ nft: {
329
+ capability: 'none',
330
+ commitment: part
331
+ }
332
+ }
333
+ }))
334
+ });
335
+ }
336
+ }
package/art/asset.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "contractName": "AssetManager",
3
+ "constructorInputs": [
4
+ {
5
+ "name": "outflowToken",
6
+ "type": "bytes32"
7
+ },
8
+ {
9
+ "name": "fundHash",
10
+ "type": "bytes32"
11
+ },
12
+ {
13
+ "name": "assetCategory",
14
+ "type": "bytes32"
15
+ }
16
+ ],
17
+ "abi": [
18
+ {
19
+ "name": "release",
20
+ "inputs": []
21
+ }
22
+ ],
23
+ "bytecode": "OP_0 OP_0 OP_BEGIN OP_DUP OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUAL OP_IF OP_DUP OP_UTXOTOKENCOMMITMENT 40 OP_SPLIT OP_DROP 20 OP_SPLIT OP_NIP OP_4 OP_PICK OP_EQUAL OP_IF OP_1 OP_ROT OP_DROP OP_SWAP OP_ENDIF OP_ENDIF OP_DUP OP_1ADD OP_NIP OP_DUP OP_TXINPUTCOUNT OP_LESSTHAN OP_2 OP_PICK OP_NOT OP_BOOLAND OP_NOT OP_UNTIL OP_SWAP OP_VERIFY 0000000000000000000000000000000000000000000000000000000000000000 OP_4 OP_PICK OP_EQUAL OP_IF OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ELSE OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_4 OP_PICK OP_EQUALVERIFY OP_ENDIF OP_2DROP OP_2DROP OP_1",
24
+ "source": "pragma cashscript ~0.13.0;\r\n\r\n/**\r\n * AssetManager: Holds individual fund assets\r\n * \r\n * Each asset in a fund has a dedicated AssetManager contract instance.\r\n * Releases assets only when outflow token is present with matching fund hash.\r\n * \r\n * This contract gates asset access during redemption (outflow) transactions.\r\n * Amount verification is performed by the TransactionManager contract.\r\n * \r\n * Parameters:\r\n * outflowToken: Token category that authorizes redemption\r\n * fundHash: Hash of fund parameters (prevents spending wrong fund's assets)\r\n * assetCategory: The specific asset category this contract holds\r\n * (0x00...00 = Bitcoin satoshis, else = token category)\r\n */\r\ncontract AssetManager(bytes32 outflowToken, bytes32 fundHash, bytes32 assetCategory) \r\n{\r\n /**\r\n * release(): Releases held assets when outflow is authorized\r\n * \r\n * Ensures:\r\n * - Outflow token with matching fund hash is present (correct redemption)\r\n * - Asset type is verified (Bitcoin satoshis or tokens)\r\n * \r\n * Note: AssetManager relies on TransactionManager for amount validation.\r\n * This contract only gates access; amounts are verified elsewhere.\r\n */\r\n function release() {\r\n // Check for outflow token with matching fund hash\r\n bool outflowTokenSeen = false;\r\n int inputIndex = 0;\r\n do {\r\n if(tx.inputs[inputIndex].tokenCategory == outflowToken) {\r\n // Extract fund hash from NFT commitment [0:32] = category, [32:64] = hash\r\n if(tx.inputs[inputIndex].nftCommitment.slice(32, 64) == fundHash) {\r\n // Correct fund's outflow token found - authorized to release\r\n outflowTokenSeen = true;\r\n }\r\n }\r\n inputIndex = inputIndex + 1;\r\n } while(inputIndex < tx.inputs.length && !outflowTokenSeen);\r\n // Must have found the outflow token for this specific fund\r\n require(outflowTokenSeen);\r\n \r\n // Verify asset type matches\r\n bytes32 satoshiAsset = 0x0000000000000000000000000000000000000000000000000000000000000000;\r\n if(assetCategory == satoshiAsset) {\r\n // For Bitcoin: input should have no token (satoshis only)\r\n require(tx.inputs[this.activeInputIndex].tokenCategory == 0x);\r\n } else {\r\n // For tokens: input must have the correct asset category\r\n require(tx.inputs[this.activeInputIndex].tokenCategory == assetCategory);\r\n }\r\n }\r\n}",
25
+ "debug": {
26
+ "bytecode": "00006576ce5379876376cf01407f7501207f7754798763517b757c6868768b7776c39f5279919a91667c6920000000000000000000000000000000000000000000000000000000000000000054798763c0ce008867c0ce547988686d6d51",
27
+ "sourceMap": "32:32:32:37;33:25:33:26;34:8:43:68;35:25:35:35;:15::50:1;:54::66:0;;:15:::1;:68:41:13:0;37:29:37:39;:19::54:1;:65::67:0;:19::68:1;;:61::63:0;:19::68:1;;:72::80:0;;:19:::1;:82:40:17:0;39:39:39:43;:20::44:1;;;37:82:40:17;35:68:41:13;42:25:42:35:0;:::39:1;:12::40;43:16:43:26:0;:29::45;:16:::1;:50::66:0;;:49:::1;:16;34:8::68;;45:16:45:32:0;:8::34:1;48:31:48:97:0;49:11:49:24;;:::40:1;:42:52:9:0;51:30:51:51;:20::66:1;:70::72:0;:12::74:1;52:15:55:9:0;54:30:54:51;:20::66:1;:70::83:0;;:12::85:1;52:15:55:9;30:4:56:5;;",
28
+ "logs": [],
29
+ "requires": [
30
+ {
31
+ "ip": 43,
32
+ "line": 45
33
+ },
34
+ {
35
+ "ip": 52,
36
+ "line": 51
37
+ },
38
+ {
39
+ "ip": 58,
40
+ "line": 54
41
+ }
42
+ ]
43
+ },
44
+ "compiler": {
45
+ "name": "cashc",
46
+ "version": "0.13.0-next.6",
47
+ "options": {
48
+ "enforceFunctionParameterTypes": true
49
+ }
50
+ },
51
+ "updatedAt": "2026-04-21T19:21:04.268Z"
52
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "contractName": "AuthHeadVault",
3
+ "constructorInputs": [
4
+ {
5
+ "name": "authToken",
6
+ "type": "bytes32"
7
+ }
8
+ ],
9
+ "abi": [
10
+ {
11
+ "name": "release",
12
+ "inputs": []
13
+ }
14
+ ],
15
+ "bytecode": "OP_INPUTINDEX OP_0 OP_NUMEQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_0 OP_0 OP_BEGIN OP_DUP OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUAL OP_IF OP_1 OP_ROT OP_DROP OP_SWAP OP_ENDIF OP_DUP OP_1ADD OP_NIP OP_DUP OP_TXINPUTCOUNT OP_LESSTHAN OP_2 OP_PICK OP_NOT OP_BOOLAND OP_NOT OP_UNTIL OP_DROP OP_NIP",
16
+ "source": "pragma cashscript ~0.13.0;\r\n\r\n/**\r\n * AuthHeadVault: Authorization vault with strict positioning requirements\r\n * \r\n * Extends SimpleVault with additional constraints to maintain authhead w/ no token:\r\n * - Must be the first input (index 0)\r\n * - First output must not contain any tokens\r\n * \r\n * Used specifically for authorizing fund's BCMR update operations.\r\n * \r\n * Parameters:\r\n * authToken: Token category that authorizes fund creation\r\n */\r\ncontract AuthHeadVault(bytes32 authToken)\r\n{\r\n /**\r\n * release(): Authorizes authhead operations with strict transaction structure\r\n * \r\n * Ensures:\r\n * - This vault is the first input (positioning control)\r\n * - First output has no tokens (clean state to maintain authhead integrity)\r\n * - Authorization token is present in inputs\r\n */\r\n function release() {\r\n // This vault MUST be the first input\r\n require(this.activeInputIndex == 0, \"expected to be the first input\");\r\n \r\n // First output must have no tokens (ensures clean state to maintain authhead integrity)\r\n require(tx.outputs[0].tokenCategory == 0x, \"no token allowed on authhead\");\r\n\r\n // Check for authorization token in ANY input\r\n bool authorized = false;\r\n int inputIndex = 0;\r\n do {\r\n if(tx.inputs[inputIndex].tokenCategory == authToken) {\r\n authorized = true;\r\n }\r\n inputIndex = inputIndex + 1;\r\n } while(inputIndex < tx.inputs.length && !authorized);\r\n require(authorized, \"unauthorized user\");\r\n }\r\n}",
17
+ "debug": {
18
+ "bytecode": "c0009d00d1008800006576ce53798763517b757c68768b7776c39f5279919a91667577",
19
+ "sourceMap": "27:16:27:37;:41::42;:8::78:1;30:27:30:28:0;:16::43:1;:47::49:0;:8::83:1;33:26:33:31:0;34:25:34:26;35:8:40:62;36:25:36:35;:15::50:1;:54::63:0;;:15:::1;:65:38:13:0;37:29:37:33;:16::34:1;;;36:65:38:13;39:25:39:35:0;:::39:1;:12::40;40:16:40:26:0;:29::45;:16:::1;:50::60:0;;:49:::1;:16;35:8::62;;25:4:42:5;",
20
+ "logs": [],
21
+ "requires": [
22
+ {
23
+ "ip": 3,
24
+ "line": 27,
25
+ "message": "expected to be the first input"
26
+ },
27
+ {
28
+ "ip": 7,
29
+ "line": 30,
30
+ "message": "no token allowed on authhead"
31
+ },
32
+ {
33
+ "ip": 34,
34
+ "line": 41,
35
+ "message": "unauthorized user"
36
+ }
37
+ ]
38
+ },
39
+ "compiler": {
40
+ "name": "cashc",
41
+ "version": "0.13.0-next.6",
42
+ "options": {
43
+ "enforceFunctionParameterTypes": true
44
+ }
45
+ },
46
+ "updatedAt": "2026-04-21T19:21:06.353Z"
47
+ }
package/art/fee.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "contractName": "FeeManager",
3
+ "constructorInputs": [
4
+ {
5
+ "name": "authToken",
6
+ "type": "bytes32"
7
+ },
8
+ {
9
+ "name": "destination",
10
+ "type": "bytes"
11
+ },
12
+ {
13
+ "name": "feeToken",
14
+ "type": "bytes32"
15
+ },
16
+ {
17
+ "name": "defaultValue",
18
+ "type": "int"
19
+ }
20
+ ],
21
+ "abi": [
22
+ {
23
+ "name": "close",
24
+ "inputs": []
25
+ },
26
+ {
27
+ "name": "pay",
28
+ "inputs": []
29
+ }
30
+ ],
31
+ "bytecode": "OP_4 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_0 OP_0 OP_BEGIN OP_DUP OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUAL OP_IF OP_1 OP_ROT OP_DROP OP_SWAP OP_ENDIF OP_DUP OP_1ADD OP_NIP OP_DUP OP_TXINPUTCOUNT OP_LESSTHAN OP_2 OP_PICK OP_NOT OP_BOOLAND OP_NOT OP_UNTIL OP_SWAP OP_VERIFY OP_0 OP_BEGIN OP_DUP OP_OUTPUTTOKENCATEGORY OP_5 OP_PICK OP_EQUAL OP_NOT OP_VERIFY OP_DUP OP_1ADD OP_NIP OP_DUP OP_TXOUTPUTCOUNT OP_GREATERTHANOREQUAL OP_UNTIL OP_2DROP OP_2DROP OP_2DROP OP_DROP OP_1 OP_ELSE OP_4 OP_ROLL OP_1 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_UTXOBYTECODE OP_INPUTINDEX OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_INPUTINDEX OP_OUTPUTTOKENCATEGORY OP_EQUALVERIFY OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_INPUTINDEX OP_OUTPUTTOKENCOMMITMENT OP_EQUALVERIFY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_3 OP_ROLL OP_EQUAL OP_IF OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT 20 OP_SPLIT OP_DUP OP_8 OP_SPLIT OP_DUP OP_SIZE OP_NIP OP_0 OP_GREATERTHAN OP_IF OP_INPUTINDEX OP_1ADD OP_OUTPUTBYTECODE OP_OVER OP_EQUALVERIFY OP_ELSE OP_INPUTINDEX OP_1ADD OP_OUTPUTBYTECODE OP_6 OP_PICK OP_EQUALVERIFY OP_ENDIF OP_3 OP_PICK OP_BIN2NUM OP_0 OP_NUMEQUAL OP_IF OP_INPUTINDEX OP_1ADD OP_OUTPUTVALUE OP_2 OP_PICK OP_BIN2NUM OP_NUMEQUALVERIFY OP_ELSE OP_INPUTINDEX OP_1ADD OP_OUTPUTTOKENCATEGORY OP_4 OP_PICK OP_EQUALVERIFY OP_INPUTINDEX OP_1ADD OP_OUTPUTTOKENAMOUNT OP_2 OP_PICK OP_BIN2NUM OP_NUMEQUALVERIFY OP_ENDIF OP_2DROP OP_2DROP OP_ELSE OP_INPUTINDEX OP_1ADD OP_OUTPUTBYTECODE OP_2 OP_PICK OP_EQUALVERIFY OP_INPUTINDEX OP_1ADD OP_OUTPUTVALUE OP_3 OP_PICK OP_NUMEQUALVERIFY OP_ENDIF OP_2DROP OP_DROP OP_1 OP_ENDIF",
32
+ "source": "pragma cashscript ~0.13.0;\r\n\r\n/**\r\n * FeeManager: Validates fee payments and routing\r\n * \r\n * Every fund transaction pays fees through this contract.\r\n * Validates that fees match their encoding and route to correct destination.\r\n * \r\n * Fee Token commitment format (stored in NFT):\r\n * [fee_category (32) | fee_amount (8) | fee_destination (variable)]\r\n * \r\n * Where:\r\n * - fee_category: 0x00 = Bitcoin satoshis, else = CashToken category\r\n * - fee_amount: Amount due (satoshis or token amount)\r\n * - fee_destination: Optional override destination (empty = use default)\r\n * Note: Variable-length intentionally - allows forward compatibility with future address formats\r\n * \r\n * Authorization:\r\n * - close() requires authToken to be present (protocol steward only)\r\n * - pay() validates the chosen fee is paid and routed\r\n * \r\n * Parameters:\r\n * authToken: Authorization token category (controls who can close)\r\n * destination: Default fee destination address (until override)\r\n * feeToken: Fee token category (with expected 'none' capability)\r\n * defaultValue: Default fee amount in satoshis (if no fee token encoding)\r\n */\r\ncontract FeeManager(bytes32 authToken, bytes destination, bytes32 feeToken, int defaultValue)\r\n{\r\n /**\r\n * close(): Close this fee by burning all fee tokens\r\n * \r\n * Allows protocol steward (authToken holder) to close the manager and cleanup.\r\n * Ensures no active fee tokens remain (no dangling fees).\r\n * \r\n * Validates:\r\n * - Transaction includes authToken (proves authorization)\r\n * - No FeeManager fee tokens exist in any output (burned)\r\n * \r\n * Used at fund wind-down when all fees have been collected.\r\n */\r\n function close() {\r\n // Check for authorization token in any input\r\n bool authorized = false;\r\n int inputIndex = 0;\r\n do {\r\n if(tx.inputs[inputIndex].tokenCategory == authToken) {\r\n // Found authorization token - user is authorized to close\r\n authorized = true;\r\n }\r\n inputIndex = inputIndex + 1;\r\n } while(inputIndex < tx.inputs.length && !authorized);\r\n require(authorized, \"unauthorized user\");\r\n\r\n // Verify all fee tokens are burned (not present in any output)\r\n int outputIndex = 0;\r\n do {\r\n // Prevent any output from receiving fee tokens (forces burn)\r\n require(tx.outputs[outputIndex].tokenCategory != feeToken);\r\n outputIndex = outputIndex + 1;\r\n } while(outputIndex < tx.outputs.length);\r\n }\r\n\r\n /**\r\n * pay(): Validate fee payment according to encoding\r\n * \r\n * Routes fee to destination encoded in fee token NFT or default destination.\r\n * Supports two fee types: Bitcoin satoshis or CashTokens.\r\n * \r\n * Validates:\r\n * - This FeeManager UTXO returns to itself (no state change)\r\n * - Fee amount matches encoding (satoshis or token amount)\r\n * - Fee routes to correct destination (from encoding or default)\r\n * - Token category (if token fee) matches encoding\r\n * \r\n * Transaction Structure:\r\n * Input[activeInputIndex]: This FeeManager with fee token\r\n * Output[activeInputIndex]: This FeeManager (self-return)\r\n * Output[activeInputIndex+1]: Fee payment to destination\r\n * \r\n * Fee Encoding Logic:\r\n * - If fee_category == 0x00: Pay satoshis = fee_amount\r\n * - If fee_category != 0x00: Pay tokens = fee_amount of fee_category\r\n * - If fee_destination present: Pay to fee_destination\r\n * - Else: Pay to default destination\r\n */\r\n function pay() {\r\n // This FeeManager must return to itself (no state change)\r\n require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode);\r\n // Preserve fee token category for continuation\r\n require(tx.inputs[this.activeInputIndex].tokenCategory == tx.outputs[this.activeInputIndex].tokenCategory);\r\n // Preserve fee token NFT commitment (maintains encoding)\r\n require(tx.inputs[this.activeInputIndex].nftCommitment == tx.outputs[this.activeInputIndex].nftCommitment);\r\n\r\n // Extract fee parameters from NFT commitment if this is fee token\r\n if(tx.inputs[this.activeInputIndex].tokenCategory == feeToken) {\r\n // Decode fee commitment: [category | amount | destination]\r\n bytes fee_category, bytes fee_next1 = tx.inputs[this.activeInputIndex].nftCommitment.split(32);\r\n bytes fee_amount, bytes fee_destination = fee_next1.split(8);\r\n\r\n // Determine fee destination (override or default)\r\n if(fee_destination.length > 0) {\r\n // Fee override destination specified in encoding\r\n require(tx.outputs[this.activeInputIndex + 1].lockingBytecode == fee_destination);\r\n } else {\r\n // No override - use default destination\r\n require(tx.outputs[this.activeInputIndex + 1].lockingBytecode == destination);\r\n }\r\n \r\n // Validate fee amount and type\r\n if(int(fee_category) == 0) {\r\n // Category 0 = Bitcoin satoshis (not tokens)\r\n // Output must have exact satoshi amount as encoded\r\n require(tx.outputs[this.activeInputIndex + 1].value == int(fee_amount));\r\n } else {\r\n // Non-zero category = CashToken fee type\r\n // Output must have correct token category and amount\r\n require(tx.outputs[this.activeInputIndex + 1].tokenCategory == fee_category);\r\n require(tx.outputs[this.activeInputIndex + 1].tokenAmount == int(fee_amount));\r\n }\r\n } else {\r\n // No fee token encoding - use default values\r\n // Pay default satoshi amount to default destination\r\n require(tx.outputs[this.activeInputIndex + 1].lockingBytecode == destination);\r\n require(tx.outputs[this.activeInputIndex + 1].value == defaultValue);\r\n }\r\n }\r\n}\r\n",
33
+ "debug": {
34
+ "bytecode": "5479009c6300006576ce53798763517b757c68768b7776c39f5279919a91667c69006576d15579879169768b7776c4a2666d6d6d755167547a519dc0c7c0cd88c0cec0d188c0cfc0d288c0ce537a8763c0cf01207f76587f76827700a063c08bcd788867c08bcd56798868537981009c63c08bcc5279819d67c08bd1547988c08bd35279819d686d6d67c08bcd527988c08bcc53799d686d755168",
35
+ "sourceMap": "42:4:62:5;;;;;44:26:44:31;45:25:45:26;46:8:52:62;47:25:47:35;:15::50:1;:54::63:0;;:15:::1;:65:50:13:0;49:29:49:33;:16::34:1;;;47:65:50:13;51:25:51:35:0;:::39:1;:12::40;52:16:52:26:0;:29::45;:16:::1;:50::60:0;;:49:::1;:16;46:8::62;;53:16:53:26:0;:8::49:1;56:26:56:27:0;57:8:61:49;59:31:59:42;:20::57:1;:61::69:0;;:20:::1;;:12::71;60:26:60:37:0;:::41:1;:12::42;61:16:61:27:0;:30::47;57:8::49:1;;42:4:62:5;;;;;;87::127::0;;;;89:26:89:47;:16::64:1;:79::100:0;:68::117:1;:8::119;91:26:91:47:0;:16::62:1;:77::98:0;:66::113:1;:8::115;93:26:93:47:0;:16::62:1;:77::98:0;:66::113:1;:8::115;96:21:96:42:0;:11::57:1;:61::69:0;;:11:::1;:71:121:9:0;98:60:98:81;:50::96:1;:103::105:0;:50::106:1;99:54:99:63:0;:70::71;:54::72:1;102:15:102:30:0;:::37:1;;:40::41:0;:15:::1;:43:105:13:0;104:35:104:56;:::60:1;:24::77;:81::96:0;:16::98:1;105:19:108:13:0;107:35:107:56;:::60:1;:24::77;:81::92:0;;:16::94:1;105:19:108:13;111::111:31:0;;:15::32:1;:36::37:0;:15:::1;:39:115:13:0;114:35:114:56;:::60:1;:24::67;:75::85:0;;:71::86:1;:16::88;115:19:120:13:0;118:35:118:56;:::60:1;:24::75;:79::91:0;;:16::93:1;119:35:119:56:0;:::60:1;:24::73;:81::91:0;;:77::92:1;:16::94;115:19:120:13;96:71:121:9;;121:15:126::0;124:31:124:52;:::56:1;:20::73;:77::88:0;;:12::90:1;125:31:125:52:0;:::56:1;:20::63;:67::79:0;;:12::81:1;121:15:126:9;87:4:127:5;;;28:0:128:1",
36
+ "logs": [],
37
+ "requires": [
38
+ {
39
+ "ip": 36,
40
+ "line": 53,
41
+ "message": "unauthorized user"
42
+ },
43
+ {
44
+ "ip": 45,
45
+ "line": 59
46
+ },
47
+ {
48
+ "ip": 67,
49
+ "line": 89
50
+ },
51
+ {
52
+ "ip": 72,
53
+ "line": 91
54
+ },
55
+ {
56
+ "ip": 77,
57
+ "line": 93
58
+ },
59
+ {
60
+ "ip": 101,
61
+ "line": 104
62
+ },
63
+ {
64
+ "ip": 108,
65
+ "line": 107
66
+ },
67
+ {
68
+ "ip": 122,
69
+ "line": 114
70
+ },
71
+ {
72
+ "ip": 129,
73
+ "line": 118
74
+ },
75
+ {
76
+ "ip": 136,
77
+ "line": 119
78
+ },
79
+ {
80
+ "ip": 146,
81
+ "line": 124
82
+ },
83
+ {
84
+ "ip": 152,
85
+ "line": 125
86
+ }
87
+ ]
88
+ },
89
+ "compiler": {
90
+ "name": "cashc",
91
+ "version": "0.13.0-next.6",
92
+ "options": {
93
+ "enforceFunctionParameterTypes": true
94
+ }
95
+ },
96
+ "updatedAt": "2026-04-21T19:21:05.225Z"
97
+ }
@@ -0,0 +1,82 @@
1
+ {
2
+ "contractName": "FeeMinter",
3
+ "constructorInputs": [
4
+ {
5
+ "name": "authorization",
6
+ "type": "bytes32"
7
+ },
8
+ {
9
+ "name": "token",
10
+ "type": "bytes32"
11
+ },
12
+ {
13
+ "name": "destination",
14
+ "type": "bytes"
15
+ }
16
+ ],
17
+ "abi": [
18
+ {
19
+ "name": "mint",
20
+ "inputs": []
21
+ }
22
+ ],
23
+ "bytecode": "OP_INPUTINDEX OP_UTXOTOKENCATEGORY 20 OP_SPLIT OP_DROP OP_2 OP_PICK OP_EQUALVERIFY OP_INPUTINDEX OP_UTXOBYTECODE OP_INPUTINDEX OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_INPUTINDEX OP_OUTPUTTOKENCATEGORY OP_EQUALVERIFY OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_INPUTINDEX OP_OUTPUTTOKENCOMMITMENT OP_EQUALVERIFY OP_0 OP_0 OP_BEGIN OP_DUP OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUAL OP_IF OP_1 OP_ROT OP_DROP OP_SWAP OP_ENDIF OP_DUP OP_1ADD OP_NIP OP_DUP OP_TXINPUTCOUNT OP_LESSTHAN OP_2 OP_PICK OP_NOT OP_BOOLAND OP_NOT OP_UNTIL OP_SWAP OP_VERIFY OP_0 OP_BEGIN OP_DUP OP_INPUTINDEX OP_NUMNOTEQUAL OP_OVER OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUAL OP_NOT OP_BOOLAND OP_IF OP_DUP OP_OUTPUTTOKENCATEGORY 20 OP_SPLIT OP_DROP OP_4 OP_PICK OP_EQUAL OP_IF OP_DUP OP_OUTPUTBYTECODE OP_5 OP_PICK OP_EQUALVERIFY OP_DUP OP_OUTPUTTOKENCATEGORY OP_4 OP_PICK OP_EQUALVERIFY OP_DUP OP_OUTPUTTOKENCOMMITMENT OP_SIZE OP_NIP OP_0 OP_GREATERTHAN OP_VERIFY OP_DUP OP_OUTPUTTOKENCOMMITMENT 20 OP_SPLIT OP_OVER OP_0 OP_EQUAL OP_NOT OP_VERIFY OP_DUP OP_8 OP_SPLIT OP_DROP OP_DUP OP_BIN2NUM OP_0 OP_GREATERTHAN OP_VERIFY OP_2DROP OP_DROP OP_ENDIF OP_ENDIF OP_DUP OP_1ADD OP_NIP OP_DUP OP_TXOUTPUTCOUNT OP_GREATERTHANOREQUAL OP_UNTIL OP_2DROP OP_2DROP OP_DROP OP_1",
24
+ "source": "pragma cashscript ~0.13.0;\r\n\r\n/**\r\n * FeeMinter: Authorization-controlled fee token minting with encoded fee commitments\r\n * \r\n * Similar to SimpleMinter, but allows encoding fee details in NFT commitments.\r\n * Fee tokens carry commitment data: [category | amount | destination_override]\r\n * Used to create tokens that encode specific fee amounts and routing.\r\n * \r\n * Destination override is variable-length by design - allows forward compatibility\r\n * with future address formats or multisig patterns without redeploying contract.\r\n * \r\n * Parameters:\r\n * authorization: Token category that authorizes minting\r\n * token: Fee token category being minted\r\n * destination: Default destination for fee payments\r\n */\r\ncontract FeeMinter(bytes32 authorization, bytes32 token, bytes destination)\r\n{\r\n /**\r\n * mint(): Mints fee tokens with commitment encoding\r\n * \r\n * Fee commitment format: [fee_category (32) | fee_amount (8) | destination_override (0+)]\r\n * - Category: 0x00 = Bitcoin satoshis, else = token category\r\n * - Amount: int64 LE integer\r\n * - Destination: Optional locking bytecode override for fee routing\r\n */\r\n function mint() {\r\n // Verify this is a fee minting token\r\n require(tx.inputs[this.activeInputIndex].tokenCategory.slice(0, 32) == token);\r\n\r\n // Contract returns to itself (locked pattern)\r\n require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode);\r\n \r\n // Keep the fee minting token\r\n require(tx.inputs[this.activeInputIndex].tokenCategory == tx.outputs[this.activeInputIndex].tokenCategory);\r\n \r\n // Preserve NFT commitment (minting capability)\r\n require(tx.inputs[this.activeInputIndex].nftCommitment == tx.outputs[this.activeInputIndex].nftCommitment);\r\n\r\n\r\n // Require owner authorization\r\n bool authorized = false;\r\n int inputIndex = 0;\r\n do {\r\n if(tx.inputs[inputIndex].tokenCategory == authorization) {\r\n authorized = true;\r\n }\r\n inputIndex = inputIndex + 1;\r\n } while(inputIndex < tx.inputs.length && !authorized);\r\n require(authorized, \"unauthorized user\");\r\n\r\n\r\n // Verify all outputs minting fee tokens have proper commitment\r\n int outputIndex = 0;\r\n do {\r\n if(outputIndex != this.activeInputIndex && tx.outputs[outputIndex].tokenCategory != 0x) {\r\n if(tx.outputs[outputIndex].tokenCategory.slice(0, 32) == token) {\r\n // Fee tokens go to the destination\r\n require(tx.outputs[outputIndex].lockingBytecode == destination);\r\n require(tx.outputs[outputIndex].tokenCategory == token);\r\n\r\n // Fee token MUST have a commitment encoding fee details\r\n require(tx.outputs[outputIndex].nftCommitment.length > 0, \"must provide a commitment\");\r\n \r\n // Parse commitment: [fee_category | fee_amount | destination_override]\r\n bytes fee_category, bytes fee_next1 = tx.outputs[outputIndex].nftCommitment.split(32);\r\n // Fee category must be specified (0x00 for Bitcoin, or token category)\r\n require(fee_category != 0x);\r\n \r\n bytes fee_amount = fee_next1.slice(0, 8);\r\n // Fee amount must be positive\r\n require(int(fee_amount) > 0);\r\n // Destination is optional (0x for default, or locking bytecode)\r\n }\r\n }\r\n outputIndex = outputIndex + 1;\r\n } while(outputIndex < tx.outputs.length);\r\n }\r\n}",
25
+ "debug": {
26
+ "bytecode": "c0ce01207f75527988c0c7c0cd88c0cec0d188c0cfc0d28800006576ce53798763517b757c68768b7776c39f5279919a91667c69006576c09e78d10087919a6376d101207f755479876376cd55798876d154798876d2827700a06976d201207f780087916976587f75768100a0696d756868768b7776c4a2666d6d7551",
27
+ "sourceMap": "30:26:30:47;:16::62:1;:72::74:0;:16::75:1;;:79::84:0;;:8::86:1;33:26:33:47:0;:16::64:1;:79::100:0;:68::117:1;:8::119;36:26:36:47:0;:16::62:1;:77::98:0;:66::113:1;:8::115;39:26:39:47:0;:16::62:1;:77::98:0;:66::113:1;:8::115;43:26:43:31:0;44:25:44:26;45:8:50:62;46:25:46:35;:15::50:1;:54::67:0;;:15:::1;:69:48:13:0;47:29:47:33;:16::34:1;;;46:69:48:13;49:25:49:35:0;:::39:1;:12::40;50:16:50:26:0;:29::45;:16:::1;:50::60:0;;:49:::1;:16;45:8::62;;51:16:51:26:0;:8::49:1;55:26:55:27:0;56:8:78:49;57:15:57:26;:30::51;:15:::1;:66::77:0;:55::92:1;:96::98:0;:55:::1;;:15;:100:76:13:0;58:30:58:41;:19::56:1;:66::68:0;:19::69:1;;:73::78:0;;:19:::1;:80:75:17:0;60:39:60:50;:28::67:1;:71::82:0;;:20::84:1;61:39:61:50:0;:28::65:1;:69::74:0;;:20::76:1;64:39:64:50:0;:28::65:1;:::72;;:75::76:0;:28:::1;:20::107;67:69:67:80:0;:58::95:1;:102::104:0;:58::105:1;69:28:69:40:0;:44::46;:28:::1;;:20::48;71:39:71::0;:58::59;:39::60:1;;73:32:73:42:0;:28::43:1;:46::47:0;:28:::1;:20::49;58:80:75:17;;;57:100:76:13;77:26:77:37:0;:::41:1;:12::42;78:16:78:27:0;:30::47;56:8::49:1;;28:4:79:5;;;",
28
+ "logs": [],
29
+ "requires": [
30
+ {
31
+ "ip": 10,
32
+ "line": 30
33
+ },
34
+ {
35
+ "ip": 15,
36
+ "line": 33
37
+ },
38
+ {
39
+ "ip": 20,
40
+ "line": 36
41
+ },
42
+ {
43
+ "ip": 25,
44
+ "line": 39
45
+ },
46
+ {
47
+ "ip": 53,
48
+ "line": 51,
49
+ "message": "unauthorized user"
50
+ },
51
+ {
52
+ "ip": 79,
53
+ "line": 60
54
+ },
55
+ {
56
+ "ip": 84,
57
+ "line": 61
58
+ },
59
+ {
60
+ "ip": 91,
61
+ "line": 64,
62
+ "message": "must provide a commitment"
63
+ },
64
+ {
65
+ "ip": 100,
66
+ "line": 69
67
+ },
68
+ {
69
+ "ip": 109,
70
+ "line": 73
71
+ }
72
+ ]
73
+ },
74
+ "compiler": {
75
+ "name": "cashc",
76
+ "version": "0.13.0-next.6",
77
+ "options": {
78
+ "enforceFunctionParameterTypes": true
79
+ }
80
+ },
81
+ "updatedAt": "2026-04-21T19:21:07.014Z"
82
+ }