@openzeppelin/compact-contracts 0.2.0 → 0.3.0-alpha

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 (33) hide show
  1. package/README.md +3 -3
  2. package/access/AccessControl.compact +3 -35
  3. package/access/Ownable.compact +5 -51
  4. package/access/ShieldedAccessControl.compact +2 -2
  5. package/access/ZOwnablePK.compact +2 -2
  6. package/crypto/ElGamal.compact +473 -0
  7. package/multisig/ForwarderPrivate.compact +229 -0
  8. package/multisig/ForwarderShielded.compact +149 -0
  9. package/multisig/ForwarderUnshielded.compact +152 -0
  10. package/multisig/ProposalManager.compact +328 -0
  11. package/multisig/ShieldedTreasury.compact +201 -0
  12. package/multisig/ShieldedTreasuryStateless.compact +76 -0
  13. package/multisig/Signer.compact +312 -0
  14. package/multisig/SignerManager.compact +205 -0
  15. package/multisig/UnshieldedTreasury.compact +114 -0
  16. package/multisig/presets/ShieldedMultiSig.compact +236 -0
  17. package/multisig/presets/ShieldedMultiSigV2.compact +280 -0
  18. package/multisig/presets/ShieldedMultiSigV3.compact +368 -0
  19. package/multisig/presets/forwarder/ForwarderPrivate.compact +125 -0
  20. package/multisig/presets/forwarder/ForwarderShielded.compact +62 -0
  21. package/multisig/presets/forwarder/ForwarderUnshielded.compact +64 -0
  22. package/package.json +9 -5
  23. package/security/Allowlist.compact +101 -0
  24. package/security/Blocklist.compact +99 -0
  25. package/security/Initializable.compact +2 -2
  26. package/security/Pausable.compact +2 -2
  27. package/token/FungibleToken.compact +24 -82
  28. package/token/MultiToken.compact +21 -80
  29. package/token/NativeShieldedToken.compact +285 -0
  30. package/token/NativeShieldedTokenCore.compact +343 -0
  31. package/token/NativeShieldedTokenFamily.compact +287 -0
  32. package/token/NonFungibleToken.compact +25 -83
  33. package/utils/Utils.compact +86 -2
@@ -0,0 +1,328 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/ProposalManager.compact)
3
+
4
+ pragma language_version >= 0.23.0;
5
+
6
+ /**
7
+ * @module ProposalManager
8
+ * @description Token-agnostic proposal lifecycle management for multisig
9
+ * governance contracts.
10
+ *
11
+ * Supports shielded and unshielded proposals through a unified
12
+ * Recipient type with a RecipientKind tag. Typed helper circuits
13
+ * provide safe construction of recipients without exposing the
14
+ * internal Bytes<32> representation to consumers.
15
+ */
16
+ module ProposalManager {
17
+ import CompactStandardLibrary;
18
+
19
+ // ─── Types ──────────────────────────────────────────────────────
20
+
21
+ export enum ProposalStatus {
22
+ Inactive,
23
+ Active,
24
+ Executed,
25
+ Cancelled
26
+ }
27
+
28
+ export enum RecipientKind {
29
+ ShieldedUser,
30
+ UnshieldedUser,
31
+ Contract
32
+ }
33
+
34
+ export struct Recipient {
35
+ kind: RecipientKind,
36
+ address: Bytes<32>
37
+ }
38
+
39
+ export struct Proposal {
40
+ to: Recipient,
41
+ color: Bytes<32>,
42
+ amount: Uint<128>,
43
+ status: ProposalStatus
44
+ }
45
+
46
+ // ─── State ──────────────────────────────────────────────────────
47
+
48
+ export ledger _nextProposalId: Counter;
49
+ export ledger _proposals: Map<Uint<64>, Proposal>;
50
+
51
+ // ─── Recipient Helpers ──────────────────────────────────────────
52
+
53
+ /**
54
+ * @description Constructs a shielded user recipient.
55
+ *
56
+ * @param {ZswapCoinPublicKey} key - The shielded recipient's public key.
57
+ *
58
+ * @returns {Recipient} The typed recipient.
59
+ */
60
+ export circuit shieldedUserRecipient(key: ZswapCoinPublicKey): Recipient {
61
+ return Recipient { kind: RecipientKind.ShieldedUser, address: key.bytes };
62
+ }
63
+
64
+ /**
65
+ * @description Constructs an unshielded user recipient.
66
+ *
67
+ * @param {UserAddress} addr - The unshielded recipient's address.
68
+ *
69
+ * @returns {Recipient} The typed recipient.
70
+ */
71
+ export circuit unshieldedUserRecipient(addr: UserAddress): Recipient {
72
+ return Recipient { kind: RecipientKind.UnshieldedUser, address: addr.bytes };
73
+ }
74
+
75
+ /**
76
+ * @description Constructs a contract recipient.
77
+ *
78
+ * @param {ContractAddress} addr - The contract address.
79
+ *
80
+ * @returns {Recipient} The typed recipient.
81
+ */
82
+ export circuit contractRecipient(addr: ContractAddress): Recipient {
83
+ return Recipient { kind: RecipientKind.Contract, address: addr.bytes };
84
+ }
85
+
86
+ /**
87
+ * @description Converts a Recipient to a shielded send recipient.
88
+ * Handles both ShieldedUser and Contract kinds.
89
+ *
90
+ * Requirements:
91
+ *
92
+ * - Recipient kind must be ShieldedUser or Contract.
93
+ *
94
+ * @param {Recipient} r - The recipient.
95
+ *
96
+ * @returns {Either<ZswapCoinPublicKey, ContractAddress>} The shielded recipient.
97
+ */
98
+ export circuit toShieldedRecipient(r: Recipient): Either<ZswapCoinPublicKey, ContractAddress> {
99
+ if (r.kind == RecipientKind.ShieldedUser) {
100
+ return left<ZswapCoinPublicKey, ContractAddress>(
101
+ ZswapCoinPublicKey { bytes: r.address }
102
+ );
103
+ }
104
+ assert(r.kind == RecipientKind.Contract, "ProposalManager: invalid shielded recipient");
105
+ return right<ZswapCoinPublicKey, ContractAddress>(
106
+ ContractAddress { bytes: r.address }
107
+ );
108
+ }
109
+
110
+ /**
111
+ * @description Converts a Recipient to an unshielded send recipient.
112
+ * Handles both UnshieldedUser and Contract kinds.
113
+ *
114
+ * Requirements:
115
+ *
116
+ * - Recipient kind must be UnshieldedUser or Contract.
117
+ *
118
+ * @param {Recipient} r - The recipient.
119
+ *
120
+ * @returns {Either<ContractAddress, UserAddress>} The unshielded recipient.
121
+ */
122
+ export circuit toUnshieldedRecipient(r: Recipient): Either<ContractAddress, UserAddress> {
123
+ if (r.kind == RecipientKind.Contract) {
124
+ return left<ContractAddress, UserAddress>(
125
+ ContractAddress { bytes: r.address }
126
+ );
127
+ }
128
+ assert(r.kind == RecipientKind.UnshieldedUser, "ProposalManager: invalid unshielded recipient");
129
+ return right<ContractAddress, UserAddress>(
130
+ UserAddress { bytes: r.address }
131
+ );
132
+ }
133
+
134
+ // ─── Guards ─────────────────────────────────────────────────────
135
+
136
+ /**
137
+ * @description Asserts that a proposal exists.
138
+ *
139
+ * Requirements:
140
+ *
141
+ * - Proposal with `id` must have been created.
142
+ *
143
+ * @param {Uint<64>} id - The proposal ID.
144
+ *
145
+ * @returns {[]} Empty tuple.
146
+ */
147
+ export circuit assertProposalExists(id: Uint<64>): [] {
148
+ assert(
149
+ _proposals.member(disclose(id)),
150
+ "ProposalManager: proposal not found"
151
+ );
152
+ }
153
+
154
+ /**
155
+ * @description Asserts that a proposal exists and is active.
156
+ *
157
+ * Requirements:
158
+ *
159
+ * - Proposal must exist.
160
+ * - Proposal status must be Active.
161
+ *
162
+ * @param {Uint<64>} id - The proposal ID.
163
+ *
164
+ * @returns {[]} Empty tuple.
165
+ */
166
+ export circuit assertProposalActive(id: Uint<64>): [] {
167
+ assertProposalExists(id);
168
+ assert(
169
+ _proposals.lookup(disclose(id)).status == ProposalStatus.Active,
170
+ "ProposalManager: proposal not active"
171
+ );
172
+ }
173
+
174
+ // ─── Proposal Lifecycle ─────────────────────────────────────────
175
+
176
+ /**
177
+ * @description Creates a new proposal.
178
+ *
179
+ * @notice Access control is NOT enforced here.
180
+ * The consuming contract must gate this behind its own
181
+ * authorization policy.
182
+ *
183
+ * Requirements:
184
+ *
185
+ * - `amount` must be greater than 0.
186
+ *
187
+ * @param {Recipient} to - The recipient (constructed via helper circuits).
188
+ * @param {Bytes<32>} color - The token color.
189
+ * @param {Uint<128>} amount - The amount to transfer.
190
+ *
191
+ * @returns {Uint<64>} The new proposal ID.
192
+ */
193
+ export circuit _createProposal(
194
+ to: Recipient,
195
+ color: Bytes<32>,
196
+ amount: Uint<128>
197
+ ): Uint<64> {
198
+ assert(amount > 0, "ProposalManager: zero amount");
199
+
200
+ _nextProposalId.increment(1);
201
+ const id = _nextProposalId;
202
+
203
+ _proposals.insert(id, disclose(Proposal {
204
+ to: to,
205
+ color: color,
206
+ amount: amount,
207
+ status: ProposalStatus.Active
208
+ }));
209
+
210
+ return id;
211
+ }
212
+
213
+ /**
214
+ * @description Cancels a proposal.
215
+ *
216
+ * @notice Access control is NOT enforced here.
217
+ * The consuming contract must gate this behind its own
218
+ * authorization policy.
219
+ *
220
+ * Requirements:
221
+ *
222
+ * - Proposal must be active.
223
+ *
224
+ * @param {Uint<64>} id - The proposal ID.
225
+ *
226
+ * @returns {[]} Empty tuple.
227
+ */
228
+ export circuit _cancelProposal(id: Uint<64>): [] {
229
+ assertProposalActive(id);
230
+
231
+ const proposal = _proposals.lookup(disclose(id));
232
+ _proposals.insert(disclose(id), Proposal {
233
+ to: proposal.to,
234
+ color: proposal.color,
235
+ amount: proposal.amount,
236
+ status: ProposalStatus.Cancelled
237
+ });
238
+ }
239
+
240
+ /**
241
+ * @description Marks a proposal as executed.
242
+ *
243
+ * @notice Access control is NOT enforced here.
244
+ * The consuming contract must gate this behind its own
245
+ * authorization policy.
246
+ *
247
+ * Requirements:
248
+ *
249
+ * - Proposal must be active.
250
+ *
251
+ * @param {Uint<64>} id - The proposal ID.
252
+ *
253
+ * @returns {[]} Empty tuple.
254
+ */
255
+ export circuit _markExecuted(id: Uint<64>): [] {
256
+ assertProposalActive(id);
257
+
258
+ const proposal = _proposals.lookup(disclose(id));
259
+ _proposals.insert(disclose(id), Proposal {
260
+ to: proposal.to,
261
+ color: proposal.color,
262
+ amount: proposal.amount,
263
+ status: ProposalStatus.Executed
264
+ });
265
+ }
266
+
267
+ // ─── View ───────────────────────────────────────────────────────
268
+
269
+ /**
270
+ * @description Returns the full proposal data.
271
+ *
272
+ * @param {Uint<64>} id - The proposal ID.
273
+ *
274
+ * @returns {Proposal} The proposal.
275
+ */
276
+ export circuit getProposal(id: Uint<64>): Proposal {
277
+ assertProposalExists(id);
278
+ return _proposals.lookup(disclose(id));
279
+ }
280
+
281
+ /**
282
+ * @description Returns the recipient of a proposal.
283
+ *
284
+ * @param {Uint<64>} id - The proposal ID.
285
+ *
286
+ * @returns {Recipient} The recipient.
287
+ */
288
+ export circuit getProposalRecipient(id: Uint<64>): Recipient {
289
+ assertProposalExists(id);
290
+ return _proposals.lookup(disclose(id)).to;
291
+ }
292
+
293
+ /**
294
+ * @description Returns the amount of a proposal.
295
+ *
296
+ * @param {Uint<64>} id - The proposal ID.
297
+ *
298
+ * @returns {Uint<128>} The amount.
299
+ */
300
+ export circuit getProposalAmount(id: Uint<64>): Uint<128> {
301
+ assertProposalExists(id);
302
+ return _proposals.lookup(disclose(id)).amount;
303
+ }
304
+
305
+ /**
306
+ * @description Returns the token color of a proposal.
307
+ *
308
+ * @param {Uint<64>} id - The proposal ID.
309
+ *
310
+ * @returns {Bytes<32>} The token color.
311
+ */
312
+ export circuit getProposalColor(id: Uint<64>): Bytes<32> {
313
+ assertProposalExists(id);
314
+ return _proposals.lookup(disclose(id)).color;
315
+ }
316
+
317
+ /**
318
+ * @description Returns the status of a proposal.
319
+ *
320
+ * @param {Uint<64>} id - The proposal ID.
321
+ *
322
+ * @returns {ProposalStatus} The proposal status.
323
+ */
324
+ export circuit getProposalStatus(id: Uint<64>): ProposalStatus {
325
+ assertProposalExists(id);
326
+ return _proposals.lookup(disclose(id)).status;
327
+ }
328
+ }
@@ -0,0 +1,201 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/ShieldedTreasury.compact)
3
+
4
+ pragma language_version >= 0.23.0;
5
+
6
+ /**
7
+ * @module ShieldedTreasury
8
+ * @description Manages shielded (private) token deposits, accounting,
9
+ * and transfers for multisig governance contracts.
10
+ *
11
+ * Coins are stored on the contract ledger in a map keyed by token color,
12
+ * with one UTXO per color. Deposits are merged with existing coins of
13
+ * the same color via `mergeCoinImmediate`. This simplifies coin selection
14
+ * at spend time — the executor doesn't need to choose between multiple
15
+ * UTXOs of the same color.
16
+ *
17
+ * Cumulative received and sent totals are tracked per color for audit
18
+ * purposes. The canonical balance query is `getTokenBalance`, which
19
+ * reads the actual coin value from the UTXO map.
20
+ *
21
+ * Underscore-prefixed circuits (_deposit, _send) have no access control
22
+ * enforcement. The consuming contract must gate these behind its own
23
+ * authorization policy.
24
+ */
25
+ module ShieldedTreasury {
26
+ import CompactStandardLibrary;
27
+ import { selfAsRecipient, UINT128_MAX } from "../utils/Utils" prefix Utils_;
28
+
29
+ // ─── State ──────────────────────────────────────────────────────
30
+
31
+ export ledger _coins: Map<Bytes<32>, QualifiedShieldedCoinInfo>;
32
+ export ledger _received: Map<Bytes<32>, Uint<128>>;
33
+ export ledger _sent: Map<Bytes<32>, Uint<128>>;
34
+
35
+ // ─── Deposit ────────────────────────────────────────────────────
36
+
37
+ /**
38
+ * @description Receives a shielded coin into the treasury.
39
+ *
40
+ * The coin is first claimed at the protocol level via `receiveShielded`,
41
+ * which allocates the Merkle tree index required by `insertCoin`.
42
+ * The coin is then merged with any existing coin of the same color,
43
+ * or inserted as a new entry if no coin of that color exists.
44
+ *
45
+ * Zero-value deposits are permitted. While currently a no-op
46
+ * economically, they may serve as signaling mechanisms when events
47
+ * are supported, or as decoy transactions for privacy.
48
+ *
49
+ * @notice Access control is NOT enforced here.
50
+ * The consuming contract must gate this behind its own
51
+ * authorization policy.
52
+ *
53
+ * Requirements:
54
+ *
55
+ * - Deposit must not cause received total overflow.
56
+ *
57
+ * @param {ShieldedCoinInfo} coin - The incoming shielded coin descriptor.
58
+ *
59
+ * @returns {[]} Empty tuple.
60
+ */
61
+ export circuit _deposit(coin: ShieldedCoinInfo): [] {
62
+ const currentReceived = getReceivedTotal(coin.color);
63
+ assert(currentReceived <= Utils_UINT128_MAX() - coin.value, "ShieldedTreasury: overflow");
64
+
65
+ receiveShielded(disclose(coin));
66
+
67
+ const coinColor = disclose(coin.color);
68
+
69
+ if (_coins.member(coinColor)) {
70
+ const merged = mergeCoinImmediate(_coins.lookup(coinColor), disclose(coin));
71
+ _coins.insertCoin(coinColor, merged, Utils_selfAsRecipient());
72
+ } else {
73
+ _coins.insertCoin(coinColor, disclose(coin), Utils_selfAsRecipient());
74
+ }
75
+
76
+ _received.insert(
77
+ coinColor,
78
+ disclose(currentReceived + coin.value as Uint<128>)
79
+ );
80
+ }
81
+
82
+ // ─── Send ───────────────────────────────────────────────────────
83
+
84
+ /**
85
+ * @description Sends shielded tokens from the treasury.
86
+ *
87
+ * Looks up the stored coin by color, verifies sufficient value,
88
+ * and executes the shielded send. If the send produces change,
89
+ * it is sent back to the contract via `sendImmediateShielded`.
90
+ *
91
+ * @notice Access control is NOT enforced here.
92
+ * The consuming contract must gate this behind its own
93
+ * authorization policy.
94
+ *
95
+ * Requirements:
96
+ *
97
+ * - A coin of the given `color` must exist in the treasury.
98
+ * - The coin's value must be >= `amount`.
99
+ * - Send must not cause sent total overflow.
100
+ *
101
+ * @param {Either<ZswapCoinPublicKey, ContractAddress>} recipient - The recipient.
102
+ * @param {Bytes<32>} color - The token color.
103
+ * @param {Uint<128>} amount - The amount to send.
104
+ *
105
+ * @returns {ShieldedSendResult} The result containing sent coin and any change.
106
+ */
107
+ export circuit _send(
108
+ recipient: Either<ZswapCoinPublicKey, ContractAddress>,
109
+ color: Bytes<32>,
110
+ amount: Uint<128>
111
+ ): ShieldedSendResult {
112
+ assert(_coins.member(disclose(color)), "ShieldedTreasury: no balance");
113
+
114
+ const coin = _coins.lookup(disclose(color));
115
+ assert(coin.value >= amount, "ShieldedTreasury: coin value insufficient");
116
+
117
+ const currentSent = getSentTotal(color);
118
+ assert(currentSent <= Utils_UINT128_MAX() - amount, "ShieldedTreasury: overflow");
119
+
120
+ const result = sendShielded(coin, disclose(recipient), disclose(amount));
121
+
122
+ if (disclose(result.change.is_some)) {
123
+ const changeCoin = result.change.value;
124
+ sendImmediateShielded(
125
+ changeCoin,
126
+ Utils_selfAsRecipient(),
127
+ changeCoin.value
128
+ );
129
+ _coins.insertCoin(disclose(color), changeCoin, Utils_selfAsRecipient());
130
+ } else {
131
+ _coins.remove(disclose(color));
132
+ }
133
+
134
+ _sent.insert(
135
+ disclose(color),
136
+ disclose(currentSent + amount as Uint<128>)
137
+ );
138
+
139
+ return result;
140
+ }
141
+
142
+ // ─── View ───────────────────────────────────────────────────────
143
+
144
+ /**
145
+ * @description Returns the current token balance for a color.
146
+ * Reads the actual coin value from the UTXO map.
147
+ *
148
+ * @param {Bytes<32>} color - The token color.
149
+ *
150
+ * @returns {Uint<128>} The current balance.
151
+ */
152
+ export circuit getTokenBalance(color: Bytes<32>): Uint<128> {
153
+ if (!_coins.member(disclose(color))) {
154
+ return 0;
155
+ }
156
+ return _coins.lookup(disclose(color)).value;
157
+ }
158
+
159
+ // ─── Accounting ─────────────────────────────────────────────────
160
+
161
+ /**
162
+ * @description Returns the cumulative received total for a color.
163
+ *
164
+ * @param {Bytes<32>} color - The token color.
165
+ *
166
+ * @returns {Uint<128>} Total received.
167
+ */
168
+ export circuit getReceivedTotal(color: Bytes<32>): Uint<128> {
169
+ if (!_received.member(disclose(color))) {
170
+ return 0;
171
+ }
172
+ return _received.lookup(disclose(color));
173
+ }
174
+
175
+ /**
176
+ * @description Returns the cumulative sent total for a color.
177
+ *
178
+ * @param {Bytes<32>} color - The token color.
179
+ *
180
+ * @returns {Uint<128>} Total sent.
181
+ */
182
+ export circuit getSentTotal(color: Bytes<32>): Uint<128> {
183
+ if (!_sent.member(disclose(color))) {
184
+ return 0;
185
+ }
186
+ return _sent.lookup(disclose(color));
187
+ }
188
+
189
+ /**
190
+ * @description Returns the difference between cumulative received
191
+ * and cumulative sent totals for a color. Should equal
192
+ * `getTokenBalance` if accounting is consistent.
193
+ *
194
+ * @param {Bytes<32>} color - The token color.
195
+ *
196
+ * @returns {Uint<128>} Received minus sent.
197
+ */
198
+ export circuit getReceivedMinusSent(color: Bytes<32>): Uint<128> {
199
+ return getReceivedTotal(color) - getSentTotal(color) as Uint<128>;
200
+ }
201
+ }
@@ -0,0 +1,76 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/ShieldedTreasuryStateless.compact)
3
+
4
+ pragma language_version >= 0.23.0;
5
+
6
+ /**
7
+ * @module ShieldedTreasury
8
+ * @description Manages shielded (private) token deposits, accounting,
9
+ * and transfers for multisig governance contracts.
10
+ *
11
+ * Coins are stored on the contract ledger in a map keyed by token color,
12
+ * with one UTXO per color. Deposits are merged with existing coins of
13
+ * the same color via `mergeCoinImmediate`. This simplifies coin selection
14
+ * at spend time — the executor doesn't need to choose between multiple
15
+ * UTXOs of the same color.
16
+ *
17
+ * Cumulative received and sent totals are tracked per color for audit
18
+ * purposes. The canonical balance query is `getTokenBalance`, which
19
+ * reads the actual coin value from the UTXO map.
20
+ */
21
+ module ShieldedTreasuryStateless {
22
+ import CompactStandardLibrary;
23
+ import { selfAsRecipient } from "../utils/Utils" prefix Utils_;
24
+
25
+ // ─── Deposit ────────────────────────────────────────────────────
26
+
27
+ /**
28
+ * @description Receives a shielded coin into the treasury.
29
+ */
30
+ export circuit _deposit(coin: ShieldedCoinInfo): [] {
31
+ receiveShielded(disclose(coin));
32
+ }
33
+
34
+ // ─── Send ───────────────────────────────────────────────────────
35
+
36
+ /**
37
+ * @description Sends shielded tokens using a caller-provided coin.
38
+ *
39
+ * The coin is supplied by the caller rather than looked up from
40
+ * on-chain state; the token color is derived from `coin.color`.
41
+ * Executes the shielded send and, if change is produced, returns it
42
+ * to the contract via `sendImmediateShielded`. No balance or
43
+ * received/sent accounting is tracked on the ledger.
44
+ *
45
+ * @notice Access control is NOT enforced here.
46
+ * The consuming contract must gate this behind its own
47
+ * authorization policy.
48
+ *
49
+ * Requirements:
50
+ *
51
+ * - `coin.value` must be >= `amount`.
52
+ *
53
+ * @param {QualifiedShieldedCoinInfo} coin - The coin to spend (provided by the caller).
54
+ * @param {Either<ZswapCoinPublicKey, ContractAddress>} recipient - The recipient.
55
+ * @param {Uint<128>} amount - The amount to send.
56
+ *
57
+ * @returns {ShieldedSendResult} The result containing sent coin and any change.
58
+ */
59
+ export circuit _send(
60
+ coin: QualifiedShieldedCoinInfo,
61
+ recipient: Either<ZswapCoinPublicKey, ContractAddress>,
62
+ amount: Uint<128>
63
+ ): ShieldedSendResult {
64
+ const result = sendShielded(disclose(coin), disclose(recipient), disclose(amount));
65
+
66
+ if (disclose(result.change.is_some)) {
67
+ sendImmediateShielded(
68
+ disclose(result.change.value),
69
+ Utils_selfAsRecipient(),
70
+ disclose(result.change.value.value)
71
+ );
72
+ }
73
+
74
+ return result;
75
+ }
76
+ }