diff --git a/contracts/contracts/ccip/executors/context_executor/contract.tolk b/contracts/contracts/ccip/executors/context_executor/contract.tolk index 60c0fd235..c2f52dd88 100644 --- a/contracts/contracts/ccip/executors/context_executor/contract.tolk +++ b/contracts/contracts/ccip/executors/context_executor/contract.tolk @@ -5,6 +5,9 @@ import "lib" import "messages" import "types" +/// @notice Generic message interceptor deployed per async operation. +/// Waits for messages from whitelisted `forwardFrom` addresses, then forwards them +/// to the owner pool with the original context attached for finalization. contract ContextExecutor { author: "SmartContract Chainlink Limited SEZC" version: "0.1.0" diff --git a/contracts/contracts/ccip/executors/context_executor/lib.tolk b/contracts/contracts/ccip/executors/context_executor/lib.tolk index c26841483..399c732b7 100644 --- a/contracts/contracts/ccip/executors/context_executor/lib.tolk +++ b/contracts/contracts/ccip/executors/context_executor/lib.tolk @@ -5,10 +5,8 @@ import "../../../lib/utils" import "types" import "messages" -struct ContextExecutor { - data: ContextExecutor_Data; -} - +/// @notice Handles incoming messages: Set/Ask are control messages from owner, +/// all other messages from `forwardFrom` addresses are forwarded to owner. fun ContextExecutor.onInternalMessage( mutate self, in: ContextExecutor_InMessageForward, @@ -62,6 +60,9 @@ fun ContextExecutor.onInternalMessage( } else => { if (self._isForwardSender(in.senderAddress)) { + val forwardFrom = self.data.forwardFrom; + self.data.forwardFrom = []; + val reply = createMessage({ bounce: BounceMode.NoBounce, value: 0, @@ -69,7 +70,7 @@ fun ContextExecutor.onInternalMessage( body: ContextExecutor_ForwardNotification { id: self.data.id, context: self.data.context, - forwardFrom: self.data.forwardFrom, + forwardFrom, message: in.toCell(), }, }); diff --git a/contracts/contracts/ccip/executors/context_executor/messages.tolk b/contracts/contracts/ccip/executors/context_executor/messages.tolk index d99dc565d..bcf0afd1b 100644 --- a/contracts/contracts/ccip/executors/context_executor/messages.tolk +++ b/contracts/contracts/ccip/executors/context_executor/messages.tolk @@ -1,22 +1,27 @@ // SPDX-License-Identifier: BUSL-1.1 import "types" +/// @notice Sets the context and forwardFrom whitelist on an existing ContextExecutor. +/// Only callable by the owner. struct (0x44e61eec) ContextExecutor_Set { queryId: uint64; context: Cell; forwardFrom: array
; } +/// @notice Queries or updates the ContextExecutor state. +/// When `done=true`, the executor self-destructs after replying. struct (0xcad4d1d0) ContextExecutor_Ask { queryId: uint64; // Per request context - forwardPayload: cell; + forwardPayload: cell?; // Close the context executor after this request is processed. done: bool; } +/// @notice Union of all incoming messages the ContextExecutor accepts. type ContextExecutor_InMessage = | ContextExecutor_Set | ContextExecutor_Ask; @@ -29,12 +34,15 @@ struct (0x93e5bbc5) ContextExecutor_Reply { forwardFrom: array
; // Per request context - forwardPayload: cell; + forwardPayload: cell?; // Whether the context executor was closed. done: bool; } +/// @notice Forwarded notification sent to the owner pool when a message arrives +/// from a whitelisted `forwardFrom` address. Carries the original context and message. +/// @dev After forwarding, `forwardFrom` is cleared so the executor only forwards once. struct (0x55b412b9) ContextExecutor_ForwardNotification { // Notice: no queryId here, because it's not a reply to a specific request @@ -46,6 +54,7 @@ struct (0x55b412b9) ContextExecutor_ForwardNotification { message: Cell; } +/// @notice Union of all outgoing messages the ContextExecutor can send. type ContextExecutor_OutMessage = | ContextExecutor_Reply | ContextExecutor_ForwardNotification; diff --git a/contracts/contracts/ccip/executors/context_executor/types.tolk b/contracts/contracts/ccip/executors/context_executor/types.tolk index 037eec84e..1ae0146e9 100644 --- a/contracts/contracts/ccip/executors/context_executor/types.tolk +++ b/contracts/contracts/ccip/executors/context_executor/types.tolk @@ -1,12 +1,45 @@ // SPDX-License-Identifier: BUSL-1.1 +/// @notice Runtime data stored by each ContextExecutor instance. +/// A pool deploys one executor per async operation with deterministic address. struct ContextExecutor_Data { - id: uint64; // 64bit addr space for a unique identity, for more use context C + /// 64bit addr space for a unique identity, for more use context C + id: uint64; + /// Address of the owning pool contract (only owner receives forwarded messages). owner: address; + /// Arbitrary context cell encoding operation-specific data for the owner to consume. context: Cell; + /// Whitelist of addresses whose messages will be forwarded to the owner. forwardFrom: array
; } +/// @notice Wrapper providing load/store helpers for ContextExecutor instances. +struct ContextExecutor { + data: ContextExecutor_Data; +} + +fun ContextExecutor.getAddress( + code: cell, + id: uint64, + owner: address, + context: Cell, + forwardFrom: array
, +): AutoDeployAddress { + return AutoDeployAddress { + stateInit: ContractState { + code, + data: ContextExecutor_Data { + id, + owner, + context, + forwardFrom, + }.toCell(), + }, + }; +} + +/// @notice Forwarded message info passed from ContextExecutor to the owner pool. +/// Contains the original message metadata plus the message body cell. struct ContextExecutor_InMessageForward { senderAddress: address // an internal address from which the message arrived valueCoins: coins // ton amount attached to an incoming message @@ -21,7 +54,8 @@ const ContextExecutor_FACILITY_NAME = "link.chain.ton.lib.executor.ContextExecut const ContextExecutor_FACILITY_ID = ContextExecutor_FACILITY_NAME.crc32() % 640 + 10; const ContextExecutor_ERROR_CODE = ContextExecutor_FACILITY_ID * 100; -/// Error codes used by the ContextExecutor contract. +/// @notice Error codes used by the ContextExecutor contract. +/// Facility ID: 260 (based on FACILITY_NAME CRC32). enum ContextExecutor_Error { /// Thrown when a function is called by an address that is not the owner of the contract. OnlyCallableByOwner = ContextExecutor_ERROR_CODE diff --git a/contracts/contracts/ccip/pools/burn_mint/contract.tolk b/contracts/contracts/ccip/pools/burn_mint/contract.tolk index a259b482e..7e1eb9fda 100644 --- a/contracts/contracts/ccip/pools/burn_mint/contract.tolk +++ b/contracts/contracts/ccip/pools/burn_mint/contract.tolk @@ -6,8 +6,11 @@ import "@stdlib/lisp-lists" import "../../../lib/utils" import "../../../lib/access/ownable_2step" import "../../../lib/jetton/messages" +import "../../../lib/jetton/jetton_client" import "../../../lib/jetton/jetton-utils" import "../../common/types" +import "../../executors/context_executor/messages" +import "../../executors/context_executor/types" import "../../rmn_remote/lib" import "../lib/token_pool/entrypoint" import "../lib/token_pool/types" @@ -20,6 +23,9 @@ import "types" import "errors" import "storage" +/// @notice Burn-Mint Token Pool implementation for CCIP. +/// Burns jettons on the source chain and mints them on the destination chain. +/// Uses ContextExecutor contracts for async operation handling. contract BurnMintTokenPool { author: "SmartContract Chainlink Limited SEZC" version: "0.1.0" @@ -48,8 +54,12 @@ fun onInternalMessage(in: InMessage) { onClaimMinterAdmin(mutate st, in.senderAddress, msg.queryId); st.store(); } - ReturnExcessesBack => { - onReturnExcessesBack(mutate st, in.senderAddress, msg); + ContextExecutor_Reply => { + onContextExecutorReply(mutate st, in.senderAddress, msg); + st.store(); + } + ContextExecutor_ForwardNotification => { + onContextExecutorForwardNotification(mutate st, in.senderAddress, msg); st.store(); } else => { @@ -58,9 +68,145 @@ fun onInternalMessage(in: InMessage) { } } -// TODO: implement me +/// @notice Deploys a new ContextExecutor at the given AutoDeployAddress with an Ask message. +/// @notice Deploys a ContextExecutor with an Ask message (empty forwardPayload). +/// The context is already embedded in the CE state from init, so forwardPayload is not needed. +/// The pool awaits the Reply before sending the operational message, ensuring sequential +/// execution (no race condition). +/// @param executor The deterministic CE address to deploy. +/// @param queryId QueryId for correlation between Ask and Reply. +fun deployBurnMintContextExecutorWithAsk( + executor: AutoDeployAddress, + queryId: uint64, +): void { + val deploy = createMessage({ + bounce: BounceMode.NoBounce, + value: BurnMintTokenPool_CONTEXT_EXECUTOR_DEPLOY_VALUE, + dest: executor, + body: ContextExecutor_Ask { + queryId: queryId, + forwardPayload: null, + done: false, + }, + }); + deploy.send(SEND_MODE_PAY_FEES_SEPARATELY); +} + +/// @notice Validates a ContextExecutor_Reply by recomputing the expected CE address from +/// the reply fields, then dispatches the operational message based on the context type. +fun onContextExecutorReply( + mutate st: Storage, + sender: address, + msg: ContextExecutor_Reply, +) { + // Validate CE identity: recompute address from reply fields and compare to sender. + val expectedExecutor = ContextExecutor.getAddress( + st.contextExecutorCode, + msg.id, + contract.getAddress(), + msg.context, + msg.forwardFrom, + ); + assert(sender == expectedExecutor.calculateAddress(), BurnMintTokenPool_Error.InvalidContextExecutorReply); + + val context = msg.context.load(); + match (context) { + BurnMintTokenPool_BurnContext => { + sendBurnMessage(st, sender, msg.queryId, context); + } + BurnMintTokenPool_MintContext => { + sendMintMessage(st, sender, msg.queryId, context); + } + } +} + +/// @notice Sends the AskToBurn operational message after CE deployment is confirmed. +fun sendBurnMessage( + _st: Storage, + executorAddress: address, + queryId: uint64, + context: BurnMintTokenPool_BurnContext, +) { + val operation = context.forwardPayload.load(); + val prepared = lazy operation.prepared.load(); + val burnMsg = createMessage({ + bounce: BounceMode.RichBounce, + value: BurnMintTokenPool_BURN_VALUE, + dest: context.wallet, + body: AskToBurn { + queryId: queryId, + jettonAmount: prepared.destTokenAmount, + sendExcessesTo: executorAddress, + customPayload: context.toCell(), + }, + }); + burnMsg.send(SEND_MODE_PAY_FEES_SEPARATELY); +} + +/// @notice Sends the MintNewJettons operational message after CE deployment is confirmed. +fun sendMintMessage( + st: Storage, + executorAddress: address, + queryId: uint64, + context: BurnMintTokenPool_MintContext, +) { + val jettonClient = st.poolData.load().jettonClient(); + val operation = context.forwardPayload.load(); + val prepared = lazy operation.prepared.load(); + val mint = createMessage({ + bounce: BounceMode.RichBounce, + value: BurnMintTokenPool_MINT_VALUE, + dest: jettonClient.masterAddress, + body: MintNewJettons { + queryId: queryId, + mintRecipient: operation.requestMsg.load().request.load().transfer.details.load().receiver, + tonAmount: BurnMintTokenPool_MINT_VALUE, + internalTransferMsg: InternalTransferStep { + queryId: queryId, + jettonAmount: prepared.localAmount, + transferInitiator: null, + sendExcessesTo: executorAddress, + forwardTonAmount: 0, + forwardPayload: beginCell().storeBool(true).storeRef(context.toCell()).endCell().beginParse(), + }.toCell(), + }, + }); + mint.send(SEND_MODE_PAY_FEES_SEPARATELY); +} + +/// @notice Handles bounced burn/mint messages: refunds rate limit and sends failure reply. fun onBouncedMessage(in: InMessageBounced) { - // TODO: use TokenPool.onBouncedMessage (lib) handler + var st = Storage.load(); + val rich = lazy RichBounceBody.fromSlice(in.bouncedBody); + val msg = lazy BurnMintTokenPool_BouncedMessage.fromCell(rich.originalBody); + match (msg) { + AskToBurn => { + assert(msg.customPayload != null) throw BurnMintTokenPool_Error.UnexpectedBurnBounce; + val context = lazy BurnMintTokenPool_BurnContext.fromCell(msg.customPayload!); + assert(in.senderAddress == context.wallet) throw BurnMintTokenPool_Error.UnexpectedBurnBounce; + + val operation = context.forwardPayload.load(); + var pool = loadPool(st); + pool.refundLockOrBurnRateLimit(operation); + st.poolData = pool.data.toCell(); + pool.sendLockOrBurnFailure(operation, BurnMintTokenPool_Error.UnexpectedBurnBounce as uint16); + st.store(); + } + MintNewJettons => { + assert(in.senderAddress == st.poolData.load().jettonClient().masterAddress) throw BurnMintTokenPool_Error.UnexpectedMintBounce; + val internalTransfer = lazy msg.internalTransferMsg.load(); + val contextCell = loadForwardPayloadAsCell(internalTransfer.forwardPayload); + assert(contextCell != null) throw BurnMintTokenPool_Error.UnexpectedMintBounce; + val context = lazy BurnMintTokenPool_MintContext.fromCell(contextCell!); + + val operation = context.forwardPayload.load(); + var pool = loadPool(st); + pool.refundReleaseOrMintRateLimit(operation); + st.poolData = pool.data.toCell(); + pool.sendReleaseOrMintFailure(operation, BurnMintTokenPool_Error.UnexpectedMintBounce as uint16); + st.store(); + } + } } /// @notice Token pool used for burn and mint tokens. This uses a burn and mint mechanism. @@ -68,31 +214,29 @@ fun onBouncedMessage(in: InMessageBounced) { fun onLockOrBurnTransferContinue( st: Storage, sender: address, + _msg: TransferNotificationForRecipient, fwdPayload: TokenPool_LockOrBurnForwardPayload, ): Storage { - val requestMsg = lazy fwdPayload.requestMsg.load(); - val prepared = lazy fwdPayload.prepared.load(); - - assert(!st.pendingBurns.get(requestMsg.queryId).isFound, BurnMintTokenPool_Error.PendingBurnAlreadyExists); + assert(st.canTakeContextExecutorNextId()) throw BurnMintTokenPool_Error.ContextExecutorUnavailable; + val requestMsg = lazy fwdPayload.requestMsg.load(); val jettonClient = st.poolData.load().jettonClient(); - st.pendingBurns.set(requestMsg.queryId, BurnMintTokenPool_PendingBurn { - forwardPayload: fwdPayload, - expectedSender: jettonClient.masterAddress, - }.toCell()); + val context = BurnMintTokenPool_BurnContext { + wallet: sender, + forwardPayload: fwdPayload.toCell(), + }; + val contextExecutorId = st.takeContextExecutorNextId(); + val executor = ContextExecutor.getAddress( + st.contextExecutorCode, + contextExecutorId, + contract.getAddress(), + context.toCell(), + [jettonClient.masterAddress], + ); - val burnMsg = createMessage({ - bounce: BounceMode.RichBounce, // used to recover sendExcessesTo address (context) - value: BurnMintTokenPool_BURN_VALUE, - dest: sender, - body: AskToBurn { - queryId: requestMsg.queryId, - jettonAmount: prepared.destTokenAmount, - sendExcessesTo: contract.getAddress(), // TODO: use ContextExecutor addr as proxy - customPayload: null, - }, - }); - burnMsg.send(SEND_MODE_PAY_FEES_SEPARATELY); + // Deploy CE with Ask — context is already in CE state from init. + // Reply triggers the operational message (no race condition). + deployBurnMintContextExecutorWithAsk(executor, requestMsg.queryId); return st; } @@ -105,10 +249,14 @@ fun onReleaseOrMintContinue( ): Storage? { assert(ctx != null, TokenPool_Error.UnsupportedOperation); var st = ctx!; - assert(!st.pendingMints.get(msg.queryId).isFound, BurnMintTokenPool_Error.PendingMintAlreadyExists); val jettonClient = st.poolData.load().jettonClient(); val request = lazy msg.request.load(); val details = lazy request.transfer.details.load(); + val forwardPayload = TokenPool_ReleaseOrMintForwardPayload { + originalSender: sender, + requestMsg: msg.toCell(), + prepared: prepared.toCell(), + }; val recipientWallet = calcAddressOfJettonWallet( details.receiver, @@ -116,38 +264,56 @@ fun onReleaseOrMintContinue( jettonClient.jettonWalletCode, ); - // TODO: use transfer.id + val context = BurnMintTokenPool_MintContext { + forwardPayload: forwardPayload.toCell(), + }; + val contextExecutorId = st.takeContextExecutorNextId(); + val executor = ContextExecutor.getAddress( + st.contextExecutorCode, + contextExecutorId, + contract.getAddress(), + context.toCell(), + [recipientWallet], + ); - st.pendingMints.set(msg.queryId, BurnMintTokenPool_PendingMint { - replyTo: msg.replyTo, - request: request.toCell(), - out: prepared.out.toCell(), - expectedSender: recipientWallet, - }.toCell()); + // Deploy CE with Ask — context is already in CE state from init. + // Reply triggers the operational message (no race condition). + deployBurnMintContextExecutorWithAsk( + executor, + msg.queryId, + ); - val mint = createMessage({ - bounce: BounceMode.RichBounce, // used to recover sendExcessesTo address (context) - value: BurnMintTokenPool_MINT_VALUE, - dest: jettonClient.masterAddress, - body: MintNewJettons { - queryId: msg.queryId, - mintRecipient: details.receiver, - tonAmount: BurnMintTokenPool_MINT_VALUE, - internalTransferMsg: InternalTransferStep { - queryId: msg.queryId, - jettonAmount: prepared.localAmount, - transferInitiator: null, - sendExcessesTo: contract.getAddress(), - forwardTonAmount: 0, - forwardPayload: beginCell().storeUint(0, 1).endCell().beginParse(), - }.toCell(), - }, - }); - mint.send(SEND_MODE_PAY_FEES_SEPARATELY); + return st; +} +fun onReleaseOrMint(st: Storage, _msg: TokenPool_ReleaseOrMint): Storage { + assert(st.canTakeContextExecutorNextId()) throw BurnMintTokenPool_Error.ContextExecutorUnavailable; return st; } +/// @notice Transfers jettons back to the original sender after a failed lock/burn. +/// @dev Sends AskToTransfer to the jetton wallet with original sender as recipient. +fun returnLockOrBurnTransfer(sender: address, forwardPayload: TokenPool_LockOrBurnForwardPayload) { + val requestMsg = lazy forwardPayload.requestMsg.load(); + val request = lazy requestMsg.request.load(); + val details = lazy request.transfer.details.load(); + val returnTransfer = createMessage({ + bounce: BounceMode.NoBounce, + value: 0, + dest: sender, + body: AskToTransfer { + queryId: requestMsg.queryId, + jettonAmount: details.amount, + transferRecipient: details.originalSender, + sendExcessesTo: details.originalSender, + customPayload: null, + forwardTonAmount: 0, + forwardPayload: beginCell().storeBool(false).endCell().beginParse(), + }, + }); + returnTransfer.send(SEND_MODE_PAY_FEES_SEPARATELY | SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); +} + fun onClaimMinterAdmin( mutate st: Storage, sender: address, @@ -164,54 +330,46 @@ fun onClaimMinterAdmin( claim.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); } -fun onReturnExcessesBack( +/// @notice Handles ForwardNotification from ContextExecutor after burn or mint completion. +/// Verifies executor identity, then finalizes the operation based on context type. +/// @dev For burn context: calls onLockOrBurnTransferFinalize. +/// For mint context: emits event and sends ReleaseOrMintFinished reply. +fun onContextExecutorForwardNotification( mutate st: Storage, sender: address, - msg: ReturnExcessesBack, + msg: ContextExecutor_ForwardNotification, ) { - val burnEntry = st.pendingBurns.get(msg.queryId); - if (burnEntry.isFound) { - val pending = burnEntry.loadValue().load(); - assert(sender == pending.expectedSender, BurnMintTokenPool_Error.UnexpectedBurnConfirmationSender); + val expectedExecutor = ContextExecutor.getAddress( + st.contextExecutorCode, + msg.id, + contract.getAddress(), + msg.context, + msg.forwardFrom, + ); + assert(sender == expectedExecutor.calculateAddress(), TokenPool_Error.Unauthorized); - st.pendingBurns.delete(msg.queryId); + val forwarded = msg.message.load(); + val returnExcesses = ReturnExcessesBack.fromCell(forwarded.body); - var pool = loadPool(st); - return pool.onLockOrBurnTransferFinalize(pending.forwardPayload); - } + val context = msg.context.load(); + match (context) { + BurnMintTokenPool_BurnContext => { + val operation = context.forwardPayload.load(); + assert(returnExcesses.queryId == operation.requestMsg.load().queryId) throw 0xFFFF; - val mintEntry = st.pendingMints.get(msg.queryId); - assert(mintEntry.isFound, BurnMintTokenPool_Error.PendingMintNotFound); - - val pending = mintEntry.loadValue().load(); - assert(sender == pending.expectedSender, BurnMintTokenPool_Error.UnexpectedMintConfirmationSender); - - st.pendingMints.delete(msg.queryId); - val request = pending.request.load(); - val details = request.transfer.details.load(); - val out = pending.out.load(); - - emit(TOPIC_TOKEN_POOL_RELEASED_OR_MINTED, TokenPool_ReleasedOrMinted { - remoteChainSelector: details.remoteChainSelector, - details: TokenPool_ReleasedOrMintedDetails { - token: details.localToken, - sender: contract.getAddress(), // TODO: who should we put as sender? (msg.sender on EVM) - amount: out.destinationAmount, - recipient: details.receiver.toCell(), - }.toCell(), - }); + var pool = loadPool(st); + pool.onLockOrBurnTransferFinalize(operation); + st.poolData = pool.data.toCell(); - if (pending.replyTo != null) { - val notify = createMessage({ - bounce: BounceMode.NoBounce, - value: 0, - dest: pending.replyTo!, - body: TokenPool_ReleaseOrMintFinished { - queryId: msg.queryId, - out: pending.out, - }, - }); - notify.send(SEND_MODE_PAY_FEES_SEPARATELY | SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); + return; + } + BurnMintTokenPool_MintContext => { + val operation = context.forwardPayload.load(); + assert(returnExcesses.queryId == operation.requestMsg.load().queryId) throw 0xFFFF; + + var pool = loadPool(st); + pool.sendReleaseOrMintFinished(operation); + } } } @@ -228,7 +386,7 @@ fun loadPool(st: Storage): TokenPool { preflightCheck: null, onLockOrBurnTransfer: null, onLockOrBurnTransferContinue, - onReleaseOrMint: null, + onReleaseOrMint, validateReleaseOrMint: null, postflightCheck: null, onReleaseOrMintContinue, @@ -262,14 +420,6 @@ get fun offRamp(remoteChainSelector: uint64): address? { return entry.isFound ? entry.loadValue() : null; } -get fun hasPendingBurn(queryId: uint64): bool { - return Storage.load().pendingBurns.get(queryId).isFound; -} - -get fun hasPendingMint(queryId: uint64): bool { - return Storage.load().pendingMints.get(queryId).isFound; -} - get fun getRMNProxy(): address { return Storage.load().poolData.load().adminConfig.load().rmnProxy; } diff --git a/contracts/contracts/ccip/pools/burn_mint/errors.tolk b/contracts/contracts/ccip/pools/burn_mint/errors.tolk index 1d005d4a4..ad3b4ba85 100644 --- a/contracts/contracts/ccip/pools/burn_mint/errors.tolk +++ b/contracts/contracts/ccip/pools/burn_mint/errors.tolk @@ -4,10 +4,8 @@ const BurnMintTokenPool_FACILITY_NAME = "link.chain.ton.ccip.BurnMintTokenPool"; const BurnMintTokenPool_FACILITY_ID = 412; // (crc32() % 640) + 10 enum BurnMintTokenPool_Error { - PendingBurnAlreadyExists = BurnMintTokenPool_FACILITY_ID * 100 - PendingBurnNotFound - PendingMintAlreadyExists - PendingMintNotFound - UnexpectedBurnConfirmationSender - UnexpectedMintConfirmationSender + UnexpectedBurnBounce = BurnMintTokenPool_FACILITY_ID * 100 + UnexpectedMintBounce + ContextExecutorUnavailable + InvalidContextExecutorReply } diff --git a/contracts/contracts/ccip/pools/burn_mint/messages.tolk b/contracts/contracts/ccip/pools/burn_mint/messages.tolk index 2ae11bafb..8a02f82a4 100644 --- a/contracts/contracts/ccip/pools/burn_mint/messages.tolk +++ b/contracts/contracts/ccip/pools/burn_mint/messages.tolk @@ -1,5 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 import "../../../lib/jetton/messages" +import "../../executors/context_executor/messages" +import "types" struct (0x39898e4d) BurnMintTokenPool_ClaimMinterAdmin { queryId: uint64; @@ -7,9 +9,14 @@ struct (0x39898e4d) BurnMintTokenPool_ClaimMinterAdmin { type BurnMintTokenPool_InMessage = | BurnMintTokenPool_ClaimMinterAdmin - | ReturnExcessesBack; + | ContextExecutor_Reply + | ContextExecutor_ForwardNotification; type BurnMintTokenPool_OutMessage = | ClaimMinterAdmin | MintNewJettons | AskToBurn; + +type BurnMintTokenPool_BouncedMessage = + | AskToBurn + | MintNewJettons; diff --git a/contracts/contracts/ccip/pools/burn_mint/storage.tolk b/contracts/contracts/ccip/pools/burn_mint/storage.tolk index 63123bc35..97d3e52e6 100644 --- a/contracts/contracts/ccip/pools/burn_mint/storage.tolk +++ b/contracts/contracts/ccip/pools/burn_mint/storage.tolk @@ -6,13 +6,29 @@ import "../lib/token_pool/types" import "types" +/// @notice Pool storage: common pool data + ContextExecutor deployment state. struct Storage { poolData: Cell; - // Local storage used for MVP, won't scale to high volume of concurrent messages - // with 65,536 cells per contract limit. - // TODO: replace with sharded ContextExecutor per message - pendingBurns: map> = []; - pendingMints: map> = []; + /// Compiled code cell of the ContextExecutor contract (deployed per-operation). + contextExecutorCode: cell; + /// Monotonically increasing ID for deterministic ContextExecutor address derivation. + contextExecutorNextId: uint64; +} + +@inline +/// @notice Returns true if the next ContextExecutor ID has not reached max uint64. +fun Storage.canTakeContextExecutorNextId(self): bool { + return self.contextExecutorNextId != 18446744073709551615; +} + +@inline +/// @notice Atomically takes and increments the next ContextExecutor ID. +/// @dev Throws if the ID counter has reached its maximum value. +fun Storage.takeContextExecutorNextId(mutate self): uint64 { + val id = self.contextExecutorNextId; + assert(id != 18446744073709551615) throw 0xFFFF; + self.contextExecutorNextId = id + 1; + return id; } fun Storage.load(): Storage { @@ -53,7 +69,7 @@ fun Storage.init(config: BurnMintTokenPool_InitConfig): Storage { remoteChainConfigs: createEmptyMap(), tokenTransferFeeConfigs: createEmptyMap(), }.toCell(), - pendingBurns: createEmptyMap(), - pendingMints: createEmptyMap(), + contextExecutorCode: config.contextExecutorCode, + contextExecutorNextId: 1, }; } \ No newline at end of file diff --git a/contracts/contracts/ccip/pools/burn_mint/types.tolk b/contracts/contracts/ccip/pools/burn_mint/types.tolk index 093107d90..f19461dfb 100644 --- a/contracts/contracts/ccip/pools/burn_mint/types.tolk +++ b/contracts/contracts/ccip/pools/burn_mint/types.tolk @@ -8,22 +8,30 @@ const BurnMintTokenPool_CONTRACT_VERSION = "0.1.0".literalSlice(); const BurnMintTokenPool_CLAIM_ADMIN_VALUE = ton("0.05"); const BurnMintTokenPool_BURN_VALUE = ton("0.05"); const BurnMintTokenPool_MINT_VALUE = ton("0.1"); +const BurnMintTokenPool_CONTEXT_EXECUTOR_DEPLOY_VALUE = ton("0.02"); -struct BurnMintTokenPool_PendingBurn { - forwardPayload: TokenPool_LockOrBurnForwardPayload; - expectedSender: address; +/// @notice Context passed to ContextExecutor for burn (lock) operations. +/// Carries the jetton wallet address and the full forward payload for finalization. +struct (0xba302a47) BurnMintTokenPool_BurnContext { + wallet: address; + forwardPayload: Cell; } -struct BurnMintTokenPool_PendingMint { - replyTo: address?; - request: Cell; - out: Cell; - expectedSender: address; +/// @notice Context passed to ContextExecutor for mint (release) operations. +/// Carries the full forward payload for post-mint finalization. +struct (0xb3d52361) BurnMintTokenPool_MintContext { + forwardPayload: Cell; } +/// @notice Union of all ContextExecutor contexts used by BurnMintTokenPool. +type BurnMintTokenPool_ContextExecutorContext = + | BurnMintTokenPool_BurnContext + | BurnMintTokenPool_MintContext; + struct BurnMintTokenPool_InitConfig { owner: address; jettonClient: JettonClient; + contextExecutorCode: cell; tokenDecimals: uint8; router: address; rmnProxy: address; diff --git a/contracts/contracts/ccip/pools/lib/token_pool/entrypoint.tolk b/contracts/contracts/ccip/pools/lib/token_pool/entrypoint.tolk index 15182f95b..ca985ead0 100644 --- a/contracts/contracts/ccip/pools/lib/token_pool/entrypoint.tolk +++ b/contracts/contracts/ccip/pools/lib/token_pool/entrypoint.tolk @@ -1,5 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 import "@stdlib/lisp-lists" +import "@stdlib/gas-payments" import "../../../../lib/math" import "../../../../lib/utils" @@ -32,6 +33,11 @@ import "../rate_limiter" /// 0.000567 tokens. /// In the case of a burnMint pool on chain A, these funds are burned in the pool on chain A. /// In the case of a lockRelease pool on chain A, these funds accumulate in the pool on chain A. +/// TON value needed to return Jettons after a recoverable transfer failure. +/// Any value left from the inbound notification is forwarded with the failure +/// notification after the pool's pre-message balance has been reserved. +const TokenPool_RETURN_TRANSFER_VALUE = ton("0.1"); + struct TokenPool { data: TokenPool_Data; context: T? = null; @@ -89,7 +95,7 @@ struct TokenPool_Hooks { validateLockOrBurn: ((T, address, TokenPool_LockOrBurn) -> (T, bool, TokenPool_LockOrBurnPrepared))?; preflightCheck: ((T?, TokenPool_LockOrBurn, TokenPool_LockOrBurnForwardPayload) -> bool)?; onLockOrBurnTransfer: ((T, address, TransferNotificationForRecipient) -> T)?; - onLockOrBurnTransferContinue: ((T, address, TokenPool_LockOrBurnForwardPayload) -> T)?; + onLockOrBurnTransferContinue: ((T, address, TransferNotificationForRecipient, TokenPool_LockOrBurnForwardPayload) -> T)?; } @inline @@ -564,7 +570,7 @@ fun TokenPool.onInternalMessage(mutate self, msgSender: address, msgValue: co // // In the worst case, not enough funds, received Jettons are burned. TransferNotificationForRecipient => { - self.onLockOrBurnTransfer(msgSender, msg); + self.onLockOrBurnTransfer(msgSender, msg, msgValue); } TokenPool_ReleaseOrMint => { self.onReleaseOrMint(msgSender, msg); @@ -656,31 +662,12 @@ fun TokenPool.onPreflightCheckFailed( msg: TokenPool_PreflightCheckFailed, ): void { val fwdp = msg.forwardPayload.load(); - val requestMsg = lazy fwdp.requestMsg.load(); - val prepared = lazy fwdp.prepared.load(); // Refund outbound rate limit that was consumed before the hook check. - val request = requestMsg.request.load(); - val details = request.transfer.details.load(); - if (requestMsg.requestedFinalityConfig != TOKEN_POOL_WAIT_FOR_FINALITY_FLAG) { - self.releaseFastFinalityOutboundRateLimit(details.remoteChainSelector, prepared.destTokenAmount); - } else { - self.releaseOutboundRateLimit(details.remoteChainSelector, prepared.destTokenAmount); - } + self.refundLockOrBurnRateLimit(fwdp); // Send failure response back to the executor (replyTo). - if (requestMsg.replyTo != null) { - val failure = createMessage({ - bounce: false, - value: 0, - dest: requestMsg.replyTo!, - body: TokenPool_LockOrBurnFailure { - queryId: requestMsg.queryId, - errorCode: TokenPool_errorCode(TokenPool_Error.UnsupportedOperation), - }, - }); - failure.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); - } + self.sendLockOrBurnFailure(fwdp, TokenPool_Error.UnsupportedOperation as uint16); } /// Resumes the ReleaseOrMint flow after the advanced hooks confirmed the postflight check. @@ -707,30 +694,145 @@ fun TokenPool.onPostflightCheckFailed( msg: TokenPool_PostflightCheckFailed, ): void { val fwdp = msg.forwardPayload.load(); - val requestMsg = lazy fwdp.requestMsg.load(); - val prepared = lazy fwdp.prepared.load(); // Refund inbound rate limit that was consumed before the hook check. - val request = requestMsg.request.load(); - val details = request.transfer.details.load(); - if (requestMsg.requestedFinalityConfig != TOKEN_POOL_WAIT_FOR_FINALITY_FLAG) { - self.releaseFastFinalityInboundRateLimit(details.remoteChainSelector, prepared.localAmount); - } else { - self.releaseInboundRateLimit(details.remoteChainSelector, prepared.localAmount); - } + self.refundReleaseOrMintRateLimit(fwdp); // Send failure response back to the executor (replyTo). + self.sendReleaseOrMintFailure(fwdp, TokenPool_Error.UnsupportedOperation as uint16); +} + +// ================================================================ +// │ Common notification & rate-limit refund helpers │ +// ================================================================ + +/// Sends `LockOrBurnFailure` reply to the executor (replyTo address) when an outbound +/// operation fails after admission. +/// @param forwardPayload The full forward payload carrying the original request message. +/// @param errorCode The pool-specific error code to attach to the failure message. +/// @dev Only sends a message if `replyTo` is set on the original request. Instance method +/// so concrete pools can override (e.g. reserve balance before sending). +fun TokenPool.sendLockOrBurnFailure( + self, + forwardPayload: TokenPool_LockOrBurnForwardPayload, + errorCode: uint16, +): void { + val requestMsg = lazy forwardPayload.requestMsg.load(); + if (requestMsg.replyTo != null) { + val failure = createMessage({ + bounce: BounceMode.NoBounce, + value: 0, + dest: requestMsg.replyTo!, + body: TokenPool_LockOrBurnFailure { + queryId: requestMsg.queryId, + errorCode, + }, + }); + failure.send(SEND_MODE_PAY_FEES_SEPARATELY | SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); + } +} + +/// Emits `ReleasedOrMinted` event and sends `ReleaseOrMintFinished` reply to the executor +/// when an inbound operation completes successfully. +/// @param forwardPayload The full forward payload carrying the original request and prepared data. +/// @dev Event is always emitted. Reply message is only sent if `replyTo` is set. Instance method +/// so concrete pools can override (e.g. customize event or reply body). +fun TokenPool.sendReleaseOrMintFinished( + self, + forwardPayload: TokenPool_ReleaseOrMintForwardPayload, +): void { + val requestMsg = lazy forwardPayload.requestMsg.load(); + val request = lazy requestMsg.request.load(); + val details = lazy request.transfer.details.load(); + val prepared = lazy forwardPayload.prepared.load(); + + emit(TOPIC_TOKEN_POOL_RELEASED_OR_MINTED, TokenPool_ReleasedOrMinted { + remoteChainSelector: details.remoteChainSelector, + details: TokenPool_ReleasedOrMintedDetails { + token: details.localToken, + sender: contract.getAddress(), + amount: prepared.out.destinationAmount, + recipient: details.receiver.toCell(), + }.toCell(), + }); + + if (requestMsg.replyTo != null) { + val notify = createMessage({ + bounce: BounceMode.NoBounce, + value: 0, + dest: requestMsg.replyTo!, + body: TokenPool_ReleaseOrMintFinished { + queryId: requestMsg.queryId, + out: prepared.out.toCell(), + }, + }); + notify.send(SEND_MODE_PAY_FEES_SEPARATELY | SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); + } +} + +/// Sends `ReleaseOrMintFailure` reply to the executor (replyTo address) when an inbound +/// operation fails after admission. +/// @param forwardPayload The full forward payload carrying the original request message. +/// @param errorCode The pool-specific error code to attach to the failure message. +/// @dev Only sends a message if `replyTo` is set on the original request. Instance method +/// so concrete pools can override (e.g. reserve balance before sending). +fun TokenPool.sendReleaseOrMintFailure( + self, + forwardPayload: TokenPool_ReleaseOrMintForwardPayload, + errorCode: uint16, +): void { + val requestMsg = lazy forwardPayload.requestMsg.load(); if (requestMsg.replyTo != null) { val failure = createMessage({ - bounce: false, // TODO: ? + bounce: BounceMode.NoBounce, value: 0, dest: requestMsg.replyTo!, body: TokenPool_ReleaseOrMintFailure { queryId: requestMsg.queryId, - errorCode: TokenPool_errorCode(TokenPool_Error.UnsupportedOperation), + errorCode, }, }); - failure.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); + failure.send(SEND_MODE_PAY_FEES_SEPARATELY | SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); + } +} + +/// Refunds the outbound (or fast-finality outbound) rate limit capacity that was consumed +/// before a LockOrBurn operation when it later fails. +/// @param forwardPayload The forward payload carrying the original request and prepared data. +/// @dev Picks the correct rate limiter bucket based on the requested finality config. +fun TokenPool.refundLockOrBurnRateLimit( + mutate self, + forwardPayload: TokenPool_LockOrBurnForwardPayload, +): void { + val requestMsg = lazy forwardPayload.requestMsg.load(); + val request = lazy requestMsg.request.load(); + val details = lazy request.transfer.details.load(); + val prepared = lazy forwardPayload.prepared.load(); + + if (requestMsg.requestedFinalityConfig != TOKEN_POOL_WAIT_FOR_FINALITY_FLAG) { + self.releaseFastFinalityOutboundRateLimit(details.remoteChainSelector, prepared.destTokenAmount); + } else { + self.releaseOutboundRateLimit(details.remoteChainSelector, prepared.destTokenAmount); + } +} + +/// Refunds the inbound (or fast-finality inbound) rate limit capacity that was consumed +/// before a ReleaseOrMint operation when it later fails. +/// @param forwardPayload The forward payload carrying the original request and prepared data. +/// @dev Picks the correct rate limiter bucket based on the requested finality config. +fun TokenPool.refundReleaseOrMintRateLimit( + mutate self, + forwardPayload: TokenPool_ReleaseOrMintForwardPayload, +): void { + val requestMsg = lazy forwardPayload.requestMsg.load(); + val request = lazy requestMsg.request.load(); + val details = lazy request.transfer.details.load(); + val prepared = lazy forwardPayload.prepared.load(); + + if (requestMsg.requestedFinalityConfig != TOKEN_POOL_WAIT_FOR_FINALITY_FLAG) { + self.releaseFastFinalityInboundRateLimit(details.remoteChainSelector, prepared.localAmount); + } else { + self.releaseInboundRateLimit(details.remoteChainSelector, prepared.localAmount); } } @@ -983,14 +1085,45 @@ fun TokenPool.onLockOrBurnTransfer( mutate self, sender: address, msg: TransferNotificationForRecipient, + msgValue: coins, ): void { if (self.hooks != null && self.hooks.onLockOrBurnTransfer != null) { self.context = self.hooks.onLockOrBurnTransfer(self.context!, sender, msg); + + return; } - // Check the notification originates from a valid Jetton wallet + // This is a trust boundary, rather than a recoverable transfer failure. + // Never return tokens in response to a notification from an arbitrary + // contract: only this pool's Jetton wallet can have credited custody. assert(self.data.jettonClient().isWallet(sender), TokenPool_Error.Unauthorized); + try { + val forwardPayload = loadForwardPayloadAsSlice(msg.forwardPayload); + val fwdp = TokenPool_LockOrBurnForwardPayload.fromSlice(forwardPayload!); + + // A valid pool wallet has already credited the tokens before it + // delivers this notification. Once its payload is decoded, every + // failure can return custody and notify the original requester. + try { + self.processLockOrBurnTransfer(sender, msg, fwdp); + } catch (errorCode) { + self.failLockOrBurnTransfer(sender, msg, fwdp, errorCode as uint16, msgValue); + } + } catch { + // A malformed payload has no reliable reply target, but custody can + // still be returned to a valid pool wallet. + self.bestEffortReturnTransfer(sender, msg); + } +} + +/// Process a decoded Jetton transfer notification after its sender has been authenticated. +fun TokenPool.processLockOrBurnTransfer( + mutate self, + sender: address, + msg: TransferNotificationForRecipient, + fwdp: TokenPool_LockOrBurnForwardPayload, +): void { // We either get this transfer from the Router, in which case we trust the transfer is valid; // Or for a different sender (e.g., per-user deposit account), we need to validate the transfer, // which we can only do with context -> recover from ContextExecutor. @@ -998,52 +1131,93 @@ fun TokenPool.onLockOrBurnTransfer( // The context contains info about the sender, the amount, etc. that we can use to validate the transfer. // If we can't validate the transfer, we try to return the funds. - // TODO: check message value is enough to cover full flow, or error - val fwdps = loadForwardPayloadAsSlice(msg.forwardPayload); - if (fwdps == null) { - self.returnTransfer(sender, msg); - return; - } - val fwdp = TokenPool_LockOrBurnForwardPayload.fromSlice(fwdps!); - - // TODO: can we trust (any) JettonWallet forward payloads? - // assert(request.amount == msg.jettonAmount, TokenPool_Error.AmountMismatch); - - // Continue if transfer initiator is the Router, otherwise try to validate the transfer - val router = self.data.adminConfig.load().dynamicConfig.load().router; - // TODO: should check if transferInitiator == walletAddress(router) - missing info for now - // if (msg.transferInitiator != null && msg.transferInitiator! == router) { - if (msg.transferInitiator != null) { - self.onLockOrBurnTransferContinue(sender, fwdp); - return; - } + val requestMsg = lazy fwdp.requestMsg.load(); + val request = lazy requestMsg.request.load(); + val details = lazy request.transfer.details.load(); - // TODO: should throw as unsupported for now - // TODO: check if this is a registered deposit account - // ContextExecutor_Ask(fwdp: msg + fwdp) -> ContextExecutor_Reply(ctx, fwdp) - // if reply is valid, continue, else return transfer + assert(details.amount == msg.jettonAmount, TokenPool_Error.AmountMismatch); + assert(msg.transferInitiator != null, TokenPool_Error.MissingTransferInitiator); + self.onLockOrBurnTransferContinue(sender, msg, fwdp); } fun TokenPool.onLockOrBurnTransferContinue( mutate self, sender: address, + msg: TransferNotificationForRecipient, fwdPayload: TokenPool_LockOrBurnForwardPayload, ): void { if (self.hooks != null && self.hooks.onLockOrBurnTransferContinue != null) { - self.context = self.hooks.onLockOrBurnTransferContinue(self.context!, sender, fwdPayload); - + self.context = self.hooks.onLockOrBurnTransferContinue(self.context!, sender, msg, fwdPayload); return; } return self.onLockOrBurnTransferFinalize(fwdPayload); } -fun TokenPool.returnTransfer(self, sender: address, msg: TransferNotificationForRecipient): void { - // TODO: check available gas, we don't want to drain the contract with return transfers - // send back the tokens, we don't support direct transfers to the pool +/// Returns a failure notification to the original requester, carrying the remaining value of the inbound transfer. +/// The pool's balance is reserved to ensure that the notification can be sent safely (without consuming the pool's balance). +fun TokenPool.notifyLockOrBurnFailure( + self, + msgValue: coins, + forwardPayload: TokenPool_LockOrBurnForwardPayload, + errorCode: uint16, +): void { + self.reserveLockOrBurnFailureValue(msgValue); + + val requestMsg = lazy forwardPayload.requestMsg.load(); + if (requestMsg.replyTo != null) { + val failure = createMessage({ + bounce: BounceMode.NoBounce, + value: 0, + dest: requestMsg.replyTo!, + body: TokenPool_LockOrBurnFailure { + queryId: requestMsg.queryId, + errorCode, + }, + }); + failure.send(SEND_MODE_CARRY_ALL_BALANCE | SEND_MODE_IGNORE_ERRORS); + } +} + +/// Keep the pool's balance from before the inbound Jetton notification, then +/// allow recovery messages to carry only the notification's remaining value. +fun TokenPool.reserveLockOrBurnFailureValue(self, msgValue: coins): void { + // Storage accounting can leave a minimally funded pool with less balance + // than the forwarded value. Saturate rather than throwing before recovery. + val originalBalance = contract.getOriginalBalance(); + val balanceBeforeMessage = originalBalance > msgValue + ? originalBalance - msgValue + : 0 as coins; + val toLeaveOnBalance = balanceBeforeMessage + contract.getStorageDuePayment(); + reserveToncoinsOnBalance(toLeaveOnBalance, RESERVE_MODE_AT_MOST); +} + +/// Returns custody to the sender and notifies the original requester of a failed transfer. +fun TokenPool.failLockOrBurnTransfer( + self, + sender: address, + msg: TransferNotificationForRecipient, + forwardPayload: TokenPool_LockOrBurnForwardPayload, + errorCode: uint16, + msgValue: coins, +): void { + self.bestEffortReturnTransfer(sender, msg); + self.notifyLockOrBurnFailure(msgValue, forwardPayload, errorCode); +} + +fun TokenPool.bestEffortReturnTransfer(self, sender: address, msg: TransferNotificationForRecipient): void { + // Keep the helper safe if a future caller uses it directly. The outer + // entrypoint performs the same assertion before any recoverable catch. + assert(self.data.jettonClient().isWallet(sender), TokenPool_Error.Unauthorized); + + if (msg.transferInitiator == null) { + return; // nothing to do, no one to return to + } + + // Send back tokens; direct transfers to the pool are not supported. val returnTransfer = createMessage({ bounce: BounceMode.NoBounce, - value: 0, + value: TokenPool_RETURN_TRANSFER_VALUE, dest: sender, // the wallet we own which sent us the notification body: AskToTransfer { queryId: msg.queryId, @@ -1055,7 +1229,9 @@ fun TokenPool.returnTransfer(self, sender: address, msg: TransferNotification forwardPayload: beginCell().storeBool(false).endCell().beginParse(), }, }); - returnTransfer.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); + // Returning custody is best-effort. The remaining inbound value is carried + // by the subsequent failure notification, after reserving pool balance. + returnTransfer.send(SEND_MODE_PAY_FEES_SEPARATELY | SEND_MODE_IGNORE_ERRORS); } fun TokenPool.onLockOrBurnTransferFinalize( diff --git a/contracts/contracts/ccip/pools/lib/token_pool/errors.tolk b/contracts/contracts/ccip/pools/lib/token_pool/errors.tolk index 93324e967..598337bff 100644 --- a/contracts/contracts/ccip/pools/lib/token_pool/errors.tolk +++ b/contracts/contracts/ccip/pools/lib/token_pool/errors.tolk @@ -31,8 +31,3 @@ enum TokenPool_Error { InvalidRequestedFinality RateLimitExceeded } - -@inline -fun TokenPool_errorCode(local: uint16): uint16 { - return getErrorCode("link.chain.ton.ccip.TokenPool".crc32(), local); -} diff --git a/contracts/contracts/ccip/pools/lock_release/contract.tolk b/contracts/contracts/ccip/pools/lock_release/contract.tolk index d1161bbe2..0b34cfa8a 100644 --- a/contracts/contracts/ccip/pools/lock_release/contract.tolk +++ b/contracts/contracts/ccip/pools/lock_release/contract.tolk @@ -9,6 +9,8 @@ import "../../../lib/jetton/messages" import "../../../lib/jetton/jetton-utils" import "../../../lib/jetton/jetton_client" import "../../common/types" +import "../../executors/context_executor/messages" +import "../../executors/context_executor/types" import "../../rmn_remote/lib" import "../lib/token_pool/entrypoint" import "../lib/token_pool/types" @@ -45,8 +47,12 @@ fun onInternalMessage(in: InMessage) { val msg = lazy LockReleaseTokenPool_InMessage.fromSlice(in.body); match (msg) { - ReturnExcessesBack => { - onReturnExcessesBack(mutate st, in.senderAddress, msg); + ContextExecutor_Reply => { + onContextExecutorReply(mutate st, in.senderAddress, msg); + st.store(); + } + ContextExecutor_ForwardNotification => { + onContextExecutorForwardNotification(mutate st, in.senderAddress, msg); st.store(); } else => { @@ -55,13 +61,77 @@ fun onInternalMessage(in: InMessage) { } } -// TODO: implement me +/// @notice Deploys a new ContextExecutor with an Ask message. +/// The Ask carries the release context in forwardPayload. The pool awaits the Reply +/// before sending the operational transfer message, ensuring sequential execution. +/// @notice Deploys a ContextExecutor with an Ask message (empty forwardPayload). +/// The context is already embedded in the CE state from init, so forwardPayload is not needed. +/// @param executor The deterministic CE address to deploy. +/// @param queryId QueryId for correlation between Ask and Reply. +fun deployLockReleaseContextExecutor( + executor: AutoDeployAddress, + queryId: uint64, +): void { + val deploy = createMessage({ + bounce: BounceMode.NoBounce, + value: LockReleaseTokenPool_CONTEXT_EXECUTOR_DEPLOY_VALUE, + dest: executor, + body: ContextExecutor_Ask { + queryId: queryId, + forwardPayload: null, + done: false, + }, + }); + deploy.send(SEND_MODE_PAY_FEES_SEPARATELY); +} + +/// @notice Validates a ContextExecutor_Reply by recomputing the expected CE address from +/// the reply fields, then sends the AskToTransfer operational message. +fun onContextExecutorReply( + mutate st: Storage, + sender: address, + msg: ContextExecutor_Reply, +) { + // Validate CE identity: recompute address from reply fields and compare to sender. + val expectedExecutor = ContextExecutor.getAddress( + st.contextExecutorCode, + msg.id, + contract.getAddress(), + msg.context, + msg.forwardFrom, + ); + assert(sender == expectedExecutor.calculateAddress(), Error.InvalidContextExecutorReply); + + val context = msg.context.load(); + val operation = context.forwardPayload.load(); + val prepared = lazy operation.prepared.load(); + val requestIn = lazy operation.requestMsg.load().request.load(); + val details = lazy requestIn.transfer.details.load(); + + val transfer = createMessage({ + bounce: BounceMode.RichBounce, + value: LockReleaseTokenPool_RELEASE_TRANSFER_VALUE, + dest: st.poolData.load().jettonClient().walletAddress(), + body: AskToTransfer { + queryId: msg.queryId, + jettonAmount: prepared.localAmount, + transferRecipient: details.receiver, + sendExcessesTo: sender, + customPayload: context.toCell(), + forwardTonAmount: 0, + forwardPayload: beginCell().storeBool(false).endCell().beginParse(), + }, + }); + transfer.send(SEND_MODE_PAY_FEES_SEPARATELY); +} + +/// @notice Handles bounced release messages: refunds rate limit and sends failure reply. fun onBouncedMessage(in: InMessageBounced) { - // TODO: use TokenPool.onBouncedMessage (lib) handler - val msg = lazy LockReleaseTokenPool_BouncedMessage.fromSlice(in.bouncedBody.skipBouncedPrefix()); + var st = Storage.load(); + val rich = lazy RichBounceBody.fromSlice(in.bouncedBody); + val msg = lazy LockReleaseTokenPool_BouncedMessage.fromCell(rich.originalBody); match (msg) { AskToTransfer => { - var st = Storage.load(); onReleaseTransferBounced(mutate st, in.senderAddress, msg); st.store(); } @@ -76,11 +146,15 @@ fun onReleaseOrMintContinue( ): Storage? { assert(ctx != null, TokenPool_Error.UnsupportedOperation); var st = ctx!; - assert(!st.pendingReleases.get(msg.queryId).isFound, Error.PendingReleaseAlreadyExists); val jettonClient = st.poolData.load().jettonClient(); val request = lazy msg.request.load(); val details = lazy request.transfer.details.load(); + val forwardPayload = TokenPool_ReleaseOrMintForwardPayload { + originalSender: sender, + requestMsg: msg.toCell(), + prepared: prepared.toCell(), + }; val recipientWallet = calcAddressOfJettonWallet( details.receiver, @@ -88,119 +162,71 @@ fun onReleaseOrMintContinue( jettonClient.jettonWalletCode, ); - st.pendingReleases.set(msg.queryId, LockReleaseTokenPool_PendingRelease { - replyTo: msg.replyTo, - request: request.toCell(), - out: prepared.out.toCell(), - expectedSender: recipientWallet, - }.toCell()); - - val ownWallet = calcAddressOfJettonWallet( + val context = LockReleaseTokenPool_ReleaseContext { + forwardPayload: forwardPayload.toCell(), + }; + val contextExecutorId = st.takeContextExecutorNextId(); + val executor = ContextExecutor.getAddress( + st.contextExecutorCode, + contextExecutorId, contract.getAddress(), - jettonClient.masterAddress, - jettonClient.jettonWalletCode, + context.toCell(), + [recipientWallet], ); - val transfer = createMessage({ - // TODO: use RichBounce to recover context, currently breaks with exitCode: 63 b/x - // `skipBouncedPrefix` in the handler. - bounce: true, - value: LockReleaseTokenPool_RELEASE_TRANSFER_VALUE, - dest: ownWallet, - body: AskToTransfer { - queryId: msg.queryId, - jettonAmount: prepared.localAmount, - transferRecipient: details.receiver, - sendExcessesTo: contract.getAddress(), // TODO: ContextExecutor - customPayload: null, - forwardTonAmount: 0, // TODO: add fwdp amount - // TODO: add fwdp - forwardPayload: beginCell().storeBool(false).endCell().beginParse(), - }, - }); - transfer.send(SEND_MODE_PAY_FEES_SEPARATELY); + // Deploy CE with Ask — context is already in CE state from init. + // The operational message (AskToTransfer) is sent after CE confirms. + deployLockReleaseContextExecutor(executor, msg.queryId); return st; } -fun onReturnExcessesBack( +fun onReleaseOrMint(st: Storage, _msg: TokenPool_ReleaseOrMint): Storage { + assert(st.canTakeContextExecutorNextId()) throw Error.ContextExecutorUnavailable; + return st; +} + +fun onReleaseTransferBounced( mutate st: Storage, sender: address, - msg: ReturnExcessesBack, + msg: AskToTransfer, ) { - val pendingEntry = st.pendingReleases.get(msg.queryId); - assert(pendingEntry.isFound, Error.PendingReleaseNotFound); - - val pending = pendingEntry.loadValue().load(); - assert(sender == pending.expectedSender, Error.UnexpectedReleaseConfirmationSender); - - st.pendingReleases.delete(msg.queryId); - val request = lazy pending.request.load(); - val details = lazy request.transfer.details.load(); - val out = lazy pending.out.load(); - - emit(TOPIC_TOKEN_POOL_RELEASED_OR_MINTED, TokenPool_ReleasedOrMinted { - remoteChainSelector: details.remoteChainSelector, - details: TokenPool_ReleasedOrMintedDetails { - token: details.localToken, - sender: contract.getAddress(), // TODO: who should we put as sender? (msg.sender on EVM) - amount: out.destinationAmount, - recipient: details.receiver.toCell(), - }.toCell(), - }); + assert(st.poolData.load().jettonClient().isWallet(sender)) throw Error.UnexpectedReleaseBounce; + assert(msg.customPayload != null) throw Error.UnexpectedReleaseBounce; - if (pending.replyTo != null) { - val notify = createMessage({ - bounce: BounceMode.NoBounce, - value: 0, - dest: pending.replyTo!, - body: TokenPool_ReleaseOrMintFinished { - queryId: msg.queryId, - out: pending.out, - }, - }); - notify.send(SEND_MODE_PAY_FEES_SEPARATELY | SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); - } + val context = lazy LockReleaseTokenPool_ReleaseContext.fromCell(msg.customPayload!); + val operation = context.forwardPayload.load(); + var pool = loadPool(st); + pool.refundReleaseOrMintRateLimit(operation); + st.poolData = pool.data.toCell(); + pool.sendReleaseOrMintFailure(operation, Error.UnexpectedReleaseBounce as uint16); } -fun onReleaseTransferBounced( +/// @notice Handles ForwardNotification from ContextExecutor after release completion. +/// Verifies executor identity, then emits event and sends ReleaseOrMintFinished reply. +fun onContextExecutorForwardNotification( mutate st: Storage, sender: address, - msg: AskToTransfer, + msg: ContextExecutor_ForwardNotification, ) { - val jettonClient = st.poolData.load().jettonClient(); - assert(jettonClient.isWallet(sender), Error.UnexpectedReleaseBounce); - - val pendingEntry = st.pendingReleases.get(msg.queryId); - assert(pendingEntry.isFound, Error.PendingReleaseNotFound); + val expectedExecutor = ContextExecutor.getAddress( + st.contextExecutorCode, + msg.id, + contract.getAddress(), + msg.context, + msg.forwardFrom, + ); + assert(sender == expectedExecutor.calculateAddress(), TokenPool_Error.Unauthorized); - val pending = pendingEntry.loadValue().load(); - st.pendingReleases.delete(msg.queryId); + val forwarded = msg.message.load(); + val returnExcesses = ReturnExcessesBack.fromCell(forwarded.body); - // TODO: should use shared onBouncedMessage handler from TokenPool lib to - // reuse the logic across pool implementations. - // The release consumed inbound rate-limit capacity at admission, but the transfer - // bounced so no tokens were delivered. Return that capacity to the bucket. - val request = lazy pending.request.load(); - val details = lazy request.transfer.details.load(); - val out = lazy pending.out.load(); - var pool: TokenPool = loadPool(st); - pool.releaseInboundRateLimit(details.remoteChainSelector, out.destinationAmount); - st.poolData = pool.data.toCell(); + val context = msg.context.load(); + val operation = context.forwardPayload.load(); + assert(returnExcesses.queryId == operation.requestMsg.load().queryId) throw 0xFFFF; - // TODO: we should always reply back - if (pending.replyTo != null) { - val fail = createMessage({ - bounce: BounceMode.NoBounce, - value: 0, - dest: pending.replyTo!, - body: TokenPool_ReleaseOrMintFailure { - queryId: msg.queryId, - errorCode: Error.UnexpectedReleaseBounce as uint16, - }, - }); - fail.send(SEND_MODE_PAY_FEES_SEPARATELY | SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); - } + var pool = loadPool(st); + pool.sendReleaseOrMintFinished(operation); } fun loadPool(st: Storage): TokenPool { @@ -216,7 +242,7 @@ fun loadPool(st: Storage): TokenPool { preflightCheck: null, onLockOrBurnTransfer: null, onLockOrBurnTransferContinue: null, - onReleaseOrMint: null, + onReleaseOrMint, validateReleaseOrMint: null, postflightCheck: null, onReleaseOrMintContinue, @@ -250,10 +276,6 @@ get fun offRamp(remoteChainSelector: uint64): address? { return entry.isFound ? entry.loadValue() : null; } -get fun hasPendingRelease(queryId: uint64): bool { - return Storage.load().pendingReleases.get(queryId).isFound; -} - get fun getRMNProxy(): address { return Storage.load().poolData.load().adminConfig.load().rmnProxy; } diff --git a/contracts/contracts/ccip/pools/lock_release/errors.tolk b/contracts/contracts/ccip/pools/lock_release/errors.tolk index 4b91534fa..22e672a01 100644 --- a/contracts/contracts/ccip/pools/lock_release/errors.tolk +++ b/contracts/contracts/ccip/pools/lock_release/errors.tolk @@ -4,8 +4,7 @@ const LockReleaseTokenPool_FACILITY_NAME = "link.chain.ton.ccip.LockReleaseToken const LockReleaseTokenPool_FACILITY_ID = 263; // (crc32() % 640) + 10 enum Error { - PendingReleaseAlreadyExists = LockReleaseTokenPool_FACILITY_ID * 100 - PendingReleaseNotFound - UnexpectedReleaseConfirmationSender - UnexpectedReleaseBounce + UnexpectedReleaseBounce = LockReleaseTokenPool_FACILITY_ID * 100 + ContextExecutorUnavailable + InvalidContextExecutorReply } diff --git a/contracts/contracts/ccip/pools/lock_release/messages.tolk b/contracts/contracts/ccip/pools/lock_release/messages.tolk index 593cabc69..1df54b346 100644 --- a/contracts/contracts/ccip/pools/lock_release/messages.tolk +++ b/contracts/contracts/ccip/pools/lock_release/messages.tolk @@ -1,8 +1,11 @@ // SPDX-License-Identifier: BUSL-1.1 import "../../../lib/jetton/messages" +import "../../executors/context_executor/messages" +import "types" type LockReleaseTokenPool_InMessage = - | ReturnExcessesBack; + | ContextExecutor_Reply + | ContextExecutor_ForwardNotification; type LockReleaseTokenPool_OutMessage = | AskToTransfer; diff --git a/contracts/contracts/ccip/pools/lock_release/storage.tolk b/contracts/contracts/ccip/pools/lock_release/storage.tolk index 41ea7ac43..69a9cddf3 100644 --- a/contracts/contracts/ccip/pools/lock_release/storage.tolk +++ b/contracts/contracts/ccip/pools/lock_release/storage.tolk @@ -6,10 +6,29 @@ import "../lib/token_pool/entrypoint" import "types" +/// @notice Pool storage: common pool data + ContextExecutor deployment state. struct Storage { poolData: Cell; - // TODO: replace with sharded ContextExecutor per message - pendingReleases: map> = []; + /// Compiled code cell of the ContextExecutor contract (deployed per-operation). + contextExecutorCode: cell; + /// Monotonically increasing ID for deterministic ContextExecutor address derivation. + contextExecutorNextId: uint64; +} + +@inline +/// @notice Returns true if the next ContextExecutor ID has not reached max uint64. +fun Storage.canTakeContextExecutorNextId(self): bool { + return self.contextExecutorNextId != 18446744073709551615; +} + +@inline +/// @notice Atomically takes and increments the next ContextExecutor ID. +/// @dev Throws if the ID counter has reached its maximum value. +fun Storage.takeContextExecutorNextId(mutate self): uint64 { + val id = self.contextExecutorNextId; + assert(id != 18446744073709551615) throw 0xFFFF; + self.contextExecutorNextId = id + 1; + return id; } fun Storage.load(): Storage { @@ -50,6 +69,7 @@ fun Storage.init(config: LockReleaseTokenPool_InitConfig): Storage { remoteChainConfigs: createEmptyMap(), tokenTransferFeeConfigs: createEmptyMap(), }.toCell(), - pendingReleases: createEmptyMap(), + contextExecutorCode: config.contextExecutorCode, + contextExecutorNextId: 1, }; } diff --git a/contracts/contracts/ccip/pools/lock_release/types.tolk b/contracts/contracts/ccip/pools/lock_release/types.tolk index dc8f8a483..dbcc4ae6f 100644 --- a/contracts/contracts/ccip/pools/lock_release/types.tolk +++ b/contracts/contracts/ccip/pools/lock_release/types.tolk @@ -1,22 +1,23 @@ // SPDX-License-Identifier: BUSL-1.1 import "../../../lib/jetton/jetton_client" -import "../lib/token_pool/types" +import "../lib/token_pool/messages" const LockReleaseTokenPool_CONTRACT_NAME = "link.chain.ton.ccip.LockReleaseTokenPool".literalSlice(); const LockReleaseTokenPool_CONTRACT_VERSION = "0.1.0".literalSlice(); const LockReleaseTokenPool_RELEASE_TRANSFER_VALUE = ton("0.05"); const LockReleaseTokenPool_REPLY_VALUE = ton("0.01"); +const LockReleaseTokenPool_CONTEXT_EXECUTOR_DEPLOY_VALUE = ton("0.02"); -struct LockReleaseTokenPool_PendingRelease { - replyTo: address?; - request: Cell; - out: Cell; - expectedSender: address; +/// @notice Context passed to ContextExecutor for release (mint) operations. +/// Carries the full forward payload for post-release finalization. +struct (0xed696f9b) LockReleaseTokenPool_ReleaseContext { + forwardPayload: Cell; } struct LockReleaseTokenPool_InitConfig { owner: address; jettonClient: JettonClient; + contextExecutorCode: cell; tokenDecimals: uint8; router: address; rmnProxy: address; diff --git a/contracts/contracts/ccip/pools/lock_release_lockbox/contract.tolk b/contracts/contracts/ccip/pools/lock_release_lockbox/contract.tolk index c0bfc460e..64c5218c5 100644 --- a/contracts/contracts/ccip/pools/lock_release_lockbox/contract.tolk +++ b/contracts/contracts/ccip/pools/lock_release_lockbox/contract.tolk @@ -5,8 +5,11 @@ import "@stdlib/lisp-lists" import "../../../lib/utils" import "../../../lib/jetton/messages" +import "../../../lib/jetton/jetton_client" import "../../../lib/jetton/jetton-utils" import "../../common/types" +import "../../executors/context_executor/messages" +import "../../executors/context_executor/types" import "../../rmn_remote/lib" import "../lib/token_pool/entrypoint" import "../lib/token_pool/types" @@ -46,18 +49,23 @@ fun onInternalMessage(in: InMessage): void { val msg = lazy LockReleaseLockboxTokenPool_InMessage.fromSlice(in.body); match (msg) { JettonLockBox_Deposited => { - // Lock flow: lockbox confirmed deposit → restore context → finalize onDeposited(mutate st, in.senderAddress, msg); st.store(); } JettonLockBox_WithdrawFailed => { - // Release flow: lockbox reported withdraw failure → restore context → send failure onWithdrawFailed(mutate st, in.senderAddress, msg); st.store(); } ReturnExcessesBack => { - // Both flows: jetton excesses returned → check which flow this is - onReturnExcessesBack(mutate st, in.senderAddress, msg); + onReturnExcessesBack(st, in.senderAddress, msg); + st.store(); + } + ContextExecutor_Reply => { + onContextExecutorReply(mutate st, in.senderAddress, msg); + st.store(); + } + ContextExecutor_ForwardNotification => { + onContextExecutorForwardNotification(mutate st, in.senderAddress, msg); st.store(); } else => { @@ -66,6 +74,77 @@ fun onInternalMessage(in: InMessage): void { } } +/// @notice Deploys a new ContextExecutor with an Ask message. +/// The Ask carries the release context in forwardPayload. The pool awaits the Reply +/// before sending the operational message to the lockbox, ensuring sequential execution. +/// @notice Deploys a ContextExecutor with an Ask message (empty forwardPayload). +/// The context is already embedded in the CE state from init, so forwardPayload is not needed. +/// @param executor The deterministic CE address to deploy. +/// @param queryId QueryId for correlation between Ask and Reply. +fun deployLockReleaseLockboxContextExecutor( + executor: AutoDeployAddress, + queryId: uint64, +): void { + val deploy = createMessage({ + bounce: BounceMode.NoBounce, + value: LockReleaseLockboxTokenPool_CONTEXT_EXECUTOR_DEPLOY_VALUE, + dest: executor, + body: ContextExecutor_Ask { + queryId: queryId, + forwardPayload: null, + done: false, + }, + }); + deploy.send(SEND_MODE_PAY_FEES_SEPARATELY); +} + +/// @notice Validates a ContextExecutor_Reply by recomputing the expected CE address from +/// the reply fields, then sends the JettonLockBox_Withdraw operational message. +fun onContextExecutorReply( + mutate st: Storage, + sender: address, + msg: ContextExecutor_Reply, +) { + // Validate CE identity: recompute address from reply fields and compare to sender. + val expectedExecutor = ContextExecutor.getAddress( + st.contextExecutorCode, + msg.id, + contract.getAddress(), + msg.context, + msg.forwardFrom, + ); + assert(sender == expectedExecutor.calculateAddress(), LockReleaseLockBoxTokenPool_Error.InvalidContextExecutorReply); + + val context = msg.context.load(); + val operation = context.forwardPayload.load(); + val prepared = lazy operation.prepared.load(); + val requestIn = lazy operation.requestMsg.load().request.load(); + val details = lazy requestIn.transfer.details.load(); + + // Return excesses are the successful withdrawal completion signal. + val extra = JettonLockBox_WithdrawExtra { + sendExcessesTo: sender, + failureContext: context.toCell(), + }.toCell(); + + // Send JettonLockBox_Withdraw to the lockbox. + val withdraw = createMessage({ + bounce: BounceMode.RichBounce, + value: ton("0.1"), + dest: st.lockbox, + body: JettonLockBox_Withdraw { + queryId: msg.queryId, + token: st.poolData.load().jettonClient().masterAddress, + remoteChainSelector: details.remoteChainSelector, + amount: prepared.localAmount, + recipientWallet: details.receiver, + extra, + }, + }); + withdraw.send(SEND_MODE_PAY_FEES_SEPARATELY); +} + +/// @notice Handles bounced lock (deposit) messages: returns jettons and sends failure reply. fun onBouncedMessage(in: InMessageBounced) { val msg = lazy LockReleaseLockboxTokenPool_BouncedMessage.fromSlice(in.bouncedBody.skipBouncedPrefix()); match (msg) { @@ -83,38 +162,26 @@ fun onBouncedMessage(in: InMessageBounced) { * 1. Router sends TokenPool_LockOrBurn + jettons via TransferNotificationForRecipient * 2. TokenPool library processes validation, calls onLockOrBurnTransferContinue * 3. We transfer jettons from our wallet to the lockbox - * 4. Lockbox receives TransferNotificationForRecipient, sends JettonLockBox_Deposited back - * 5. We finalize: emit event + send reply + * 4. Lockbox confirms the deposit directly to the pool + * 5. The confirmation carries the lock context and we finalize */ fun onLockOrBurnTransferContinue( st: Storage, - sender: address, + _sender: address, + _msg: TransferNotificationForRecipient, fwdPayload: TokenPool_LockOrBurnForwardPayload, ): Storage { val requestMsg = lazy fwdPayload.requestMsg.load(); val prepared = lazy fwdPayload.prepared.load(); - assert(!st.pendingLocks.get(requestMsg.queryId).isFound, LockReleaseLockBoxTokenPool_Error.PendingLockAlreadyExists); - - // Store pending lock data for finalization. - st.pendingLocks.set(requestMsg.queryId, LockReleaseLockboxTokenPool_PendingLock { - queryId: requestMsg.queryId, + val context = LockReleaseLockboxTokenPool_LockContext { forwardPayload: fwdPayload.toCell(), - }.toCell()); + }; // Calculate addresses. val jettonClient = st.poolData.load().jettonClient(); - val ownWallet = calcAddressOfJettonWallet( - contract.getAddress(), - jettonClient.masterAddress, - jettonClient.jettonWalletCode, - ); - val lockboxWallet = calcAddressOfJettonWallet( - st.lockbox, - jettonClient.masterAddress, - jettonClient.jettonWalletCode, - ); + val ownWallet = jettonClient.walletAddress(); // Build JettonLockBox_Deposit using generated struct (avoids manual encoding issues). // Navigate: requestMsg (TokenPool_LockOrBurn) -> request (TokenPool_LockOrBurnInV1) -> transfer -> details. @@ -125,6 +192,7 @@ fun onLockOrBurnTransferContinue( token: jettonClient.masterAddress, remoteChainSelector: requestDetails.remoteChainSelector, amount: prepared.destTokenAmount, + context: context.toCell(), }; // Wrap deposit message in forwardPayload cell (flag + ref, same pattern as lockbox uses). @@ -136,6 +204,8 @@ fun onLockOrBurnTransferContinue( .beginParse(); // Transfer jettons from our wallet to the lockbox wallet. + // The CCT JettonWallet treats transferRecipient as an owner address and + // computes the wallet from it, so we pass the lockbox owner (not wallet) address. val transfer = createMessage({ bounce: BounceMode.RichBounce, value: ton("0.1"), @@ -143,9 +213,9 @@ fun onLockOrBurnTransferContinue( body: AskToTransfer { queryId: requestMsg.queryId, jettonAmount: prepared.destTokenAmount, - transferRecipient: lockboxWallet, + transferRecipient: st.lockbox, sendExcessesTo: contract.getAddress(), - customPayload: null, + customPayload: context.toCell(), forwardTonAmount: 0, forwardPayload: forwardPayload, }, @@ -155,54 +225,33 @@ fun onLockOrBurnTransferContinue( return st; } -/* --- JettonLockBox_Deposited callback --- */ +/* --- Bounced AskToTransfer during lock --- */ fun onDeposited( mutate st: Storage, sender: address, msg: JettonLockBox_Deposited, ) { - // Validate sender is our lockbox. assert(sender == st.lockbox, LockReleaseLockBoxTokenPool_Error.UnexpectedLockboxConfirmationSender); - - val pendingEntry = st.pendingLocks.get(msg.queryId); - assert(pendingEntry.isFound, LockReleaseLockBoxTokenPool_Error.PendingLockNotFound); - - val pending = pendingEntry.loadValue().load(); - st.pendingLocks.delete(msg.queryId); - - // Finalize lock flow. + assert(msg.context != null, LockReleaseLockBoxTokenPool_Error.UnexpectedLockboxConfirmationSender); + val context = lazy LockReleaseLockboxTokenPool_LockContext.fromCell(msg.context!); + assert(msg.queryId == context.forwardPayload.load().requestMsg.load().queryId) throw 0xFFFF; var pool = loadPool(st); - pool.onLockOrBurnTransferFinalize(pending.forwardPayload.load()); + pool.onLockOrBurnTransferFinalize(context.forwardPayload.load()); } -/* --- Bounced AskToTransfer during lock --- */ - fun onLockTransferBounced( mutate st: Storage, sender: address, msg: AskToTransfer, ) { - val pendingEntry = st.pendingLocks.get(msg.queryId); - if (!pendingEntry.isFound) { - return; - } - - val pending = pendingEntry.loadValue().load(); - st.pendingLocks.delete(msg.queryId); - - // TODO: try to best-effort return jettons and emit error event - - val fail = createMessage({ - bounce: BounceMode.NoBounce, - value: 0, - dest: createAddressNone() as address, // TODO: pending.replyTo!, - body: TokenPool_LockOrBurnFailure { - queryId: msg.queryId, - errorCode: LockReleaseLockBoxTokenPool_Error.UnexpectedLockBounce as uint16, - }, - }); - fail.send(SEND_MODE_PAY_FEES_SEPARATELY | SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); + assert(st.poolData.load().jettonClient().isWallet(sender)) throw LockReleaseLockBoxTokenPool_Error.UnexpectedLockBounce; + assert(msg.customPayload != null) throw LockReleaseLockBoxTokenPool_Error.UnexpectedLockBounce; + val context = lazy LockReleaseLockboxTokenPool_LockContext.fromCell(msg.customPayload!); + val operation = context.forwardPayload.load(); + returnLockOrBurnTransfer(st, operation); + var pool = loadPool(st); + pool.sendLockOrBurnFailure(operation, LockReleaseLockBoxTokenPool_Error.UnexpectedLockBounce as uint16); } /* === Release (withdraw) flow === */ @@ -210,11 +259,11 @@ fun onLockTransferBounced( * Flow: * 1. Router sends TokenPool_ReleaseOrMint * 2. TokenPool library validates, calls onReleaseOrMintContinue - * 3. We store pending release context + send JettonLockBox_Withdraw(sendExcessesTo=us) to lockbox + * 3. We retain the direct lockbox-failure context and deploy an executor for success * 4. Lockbox sends AskToTransfer to jetton wallet - * 5. Jetton wallet transfers to recipient, sends ReturnExcessesBack to pool - * 6. We restore pending context → finalize release flow - * 7. On error: lockbox sends JettonLockBox_WithdrawFailed → we send TokenPool_ReleaseOrMintFailure + * 5. Jetton wallet transfers to recipient, then sends ReturnExcessesBack to the executor + * 6. The executor forwards context and we finalize the release + * 7. On error, the lockbox sends JettonLockBox_WithdrawFailed directly to the pool */ fun onReleaseOrMintContinue( @@ -225,7 +274,7 @@ fun onReleaseOrMintContinue( ): Storage? { assert(ctx != null, TokenPool_Error.UnsupportedOperation); var st = ctx!; - assert(!st.pendingReleases.get(msg.queryId).isFound, LockReleaseLockBoxTokenPool_Error.PendingReleaseAlreadyExists); + assert(st.canTakeContextExecutorNextId()) throw LockReleaseLockBoxTokenPool_Error.ContextExecutorUnavailable; assert(st.lockbox != createAddressNone(), LockReleaseLockBoxTokenPool_Error.LockboxNotConfigured); val jettonClient = st.poolData.load().jettonClient(); @@ -233,136 +282,117 @@ fun onReleaseOrMintContinue( val request = lazy msg.request.load(); val details = lazy request.transfer.details.load(); - // Calculate the recipient's jetton wallet address. + // The CCT JettonWallet treats transferRecipient as an owner address and + // computes the wallet from it, so we pass the raw receiver (owner) address + // to the lockbox. The lockbox forwards it as transferRecipient in AskToTransfer, + // and the CCT wallet correctly computes the recipient's jetton wallet. + // We still need the computed wallet address for the executor's forwardFrom list + // because ReturnExcessesBack comes from the recipient's wallet. val recipientWallet = calcAddressOfJettonWallet( details.receiver, jettonClient.masterAddress, jettonClient.jettonWalletCode, ); - // Store pending release context keyed by queryId. - st.pendingReleases.set(msg.queryId, LockReleaseLockboxTokenPool_PendingRelease { - queryId: msg.queryId, - replyTo: msg.replyTo.toCell(), - request: request.toCell(), - out: prepared.out.toCell(), - }.toCell()); + val forwardPayload = TokenPool_ReleaseOrMintForwardPayload { + originalSender: sender, + requestMsg: msg.toCell(), + prepared: prepared.toCell(), + }; + val context = LockReleaseLockboxTokenPool_ReleaseContext { + forwardPayload: forwardPayload.toCell(), + }; + val contextExecutorId = st.takeContextExecutorNextId(); + // ReturnExcessesBack comes from the recipient's jetton wallet (which processes + // the InternalTransferStep and sends excesses back to sendExcessesTo). + val executor = ContextExecutor.getAddress( + st.contextExecutorCode, + contextExecutorId, + contract.getAddress(), + context.toCell(), + [recipientWallet], + ); - // Build extra cell with sendExcessesTo=pool (cell ref to avoid overflow). - val extra = JettonLockBox_WithdrawExtra { - sendExcessesTo: contract.getAddress(), - }.toCell(); - - // Send JettonLockBox_Withdraw to the lockbox. - val withdraw = createMessage({ - bounce: BounceMode.RichBounce, - value: ton("0.1"), - dest: st.lockbox, - body: JettonLockBox_Withdraw { - queryId: msg.queryId, - token: jettonClient.masterAddress, - remoteChainSelector: details.remoteChainSelector, - amount: prepared.localAmount, - recipientWallet, - extra, - }, - }); - withdraw.send(SEND_MODE_PAY_FEES_SEPARATELY); + // Deploy CE with Ask — context is already in CE state from init. + // The operational message (JettonLockBox_Withdraw) is sent after CE confirms. + deployLockReleaseLockboxContextExecutor(executor, msg.queryId); return st; } -/* --- ReturnExcessesBack handler --- */ -/* - * For lock flow: just accept TON, do nothing. Completion comes from JettonLockBox_Deposited. - * For release flow: this IS the completion signal. Restore context, finalize release flow. - */ - -/* --- JettonLockBox_WithdrawFailed callback --- */ +fun onReleaseOrMint(st: Storage, _msg: TokenPool_ReleaseOrMint): Storage { + assert(st.canTakeContextExecutorNextId()) throw LockReleaseLockBoxTokenPool_Error.ContextExecutorUnavailable; + return st; +} fun onWithdrawFailed( mutate st: Storage, sender: address, msg: JettonLockBox_WithdrawFailed, ) { - // Validate sender is our lockbox. assert(sender == st.lockbox, LockReleaseLockBoxTokenPool_Error.UnexpectedLockboxConfirmationSender); - - val pendingEntry = st.pendingReleases.get(msg.queryId); - if (!pendingEntry.isFound) { - return; - } - - val pending = pendingEntry.loadValue().load(); - st.pendingReleases.delete(msg.queryId); - - // Send failure notification. - val replyToAddr = pending.replyTo.load(); - if (replyToAddr != null) { - val fail = createMessage({ - bounce: BounceMode.NoBounce, - value: 0, - dest: replyToAddr!, - body: TokenPool_ReleaseOrMintFailure { - queryId: msg.queryId, - errorCode: TokenPool_Error.UnsupportedOperation as uint16, - }, - }); - fail.send(SEND_MODE_PAY_FEES_SEPARATELY | SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); - } + assert(msg.context != null, LockReleaseLockBoxTokenPool_Error.UnexpectedLockboxConfirmationSender); + val context = lazy LockReleaseLockboxTokenPool_ReleaseContext.fromCell(msg.context!); + assert(msg.queryId == context.forwardPayload.load().requestMsg.load().queryId) throw 0xFFFF; + var pool = loadPool(st); + pool.sendReleaseOrMintFailure(context.forwardPayload.load(), TokenPool_Error.UnsupportedOperation as uint16); } -/* --- ReturnExcessesBack handler --- */ -/* - * For lock flow: just accept TON, do nothing. Completion comes from JettonLockBox_Deposited. - * For release flow: this IS the completion signal. Restore context, finalize release flow. - */ - fun onReturnExcessesBack( + _st: Storage, + _sender: address, + _msg: ReturnExcessesBack, +) { + // Lock-transfer excesses are unrelated to completion. Release excesses are + // routed to a Context Executor and never complete directly at the pool. +} + +/// @notice Handles ForwardNotification from ContextExecutor after release (withdraw) completion. +/// Verifies executor identity, then emits event and sends ReleaseOrMintFinished reply. +fun onContextExecutorForwardNotification( mutate st: Storage, sender: address, - msg: ReturnExcessesBack, + msg: ContextExecutor_ForwardNotification, ) { - // Check if this is a release completion (pending release exists). - val releaseEntry = st.pendingReleases.get(msg.queryId); - if (releaseEntry.isFound) { - val pending = releaseEntry.loadValue().load(); - st.pendingReleases.delete(msg.queryId); - - val request = lazy pending.request.load(); - val details = lazy request.transfer.details.load(); - val out = lazy pending.out.load(); - - // Emit release event. - emit(TOPIC_TOKEN_POOL_RELEASED_OR_MINTED, TokenPool_ReleasedOrMinted { - remoteChainSelector: details.remoteChainSelector, - details: TokenPool_ReleasedOrMintedDetails { - token: details.localToken, - sender: contract.getAddress(), - amount: out.destinationAmount, - recipient: details.receiver.toCell(), - }.toCell(), - }); - - // Send completion notification if replyTo is set. - val replyToAddr = pending.replyTo.load(); - if (replyToAddr != null) { - val notify = createMessage({ - bounce: BounceMode.NoBounce, - value: 0, - dest: replyToAddr!, - body: TokenPool_ReleaseOrMintFinished { - queryId: msg.queryId, - out: pending.out, - }, - }); - notify.send(SEND_MODE_PAY_FEES_SEPARATELY | SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); - } - return; - } + val expectedExecutor = ContextExecutor.getAddress( + st.contextExecutorCode, + msg.id, + contract.getAddress(), + msg.context, + msg.forwardFrom, + ); + assert(sender == expectedExecutor.calculateAddress(), TokenPool_Error.Unauthorized); + + val forwarded = msg.message.load(); + val returnExcesses = ReturnExcessesBack.fromCell(forwarded.body); + val operation = msg.context.load().forwardPayload.load(); + assert(returnExcesses.queryId == operation.requestMsg.load().queryId) throw 0xFFFF; + + var pool = loadPool(st); + pool.sendReleaseOrMintFinished(operation); +} - // Otherwise it's from the lock flow (ReturnExcessesBack from jetton wallet after deposit). - // Just accept TON, do nothing. Real completion comes from JettonLockBox_Deposited. +/// @notice Returns jettons to the original sender after a failed lock (deposit) operation. +/// @dev Sends AskToTransfer to the pool's jetton wallet with original sender as recipient. +fun returnLockOrBurnTransfer(st: Storage, forwardPayload: TokenPool_LockOrBurnForwardPayload) { + val requestMsg = lazy forwardPayload.requestMsg.load(); + val request = lazy requestMsg.request.load(); + val details = lazy request.transfer.details.load(); + val returnTransfer = createMessage({ + bounce: BounceMode.NoBounce, + value: 0, + dest: st.poolData.load().jettonClient().walletAddress(), + body: AskToTransfer { + queryId: requestMsg.queryId, + jettonAmount: details.amount, + transferRecipient: details.originalSender, + sendExcessesTo: details.originalSender, + customPayload: null, + forwardTonAmount: 0, + forwardPayload: beginCell().storeBool(false).endCell().beginParse(), + }, + }); + returnTransfer.send(SEND_MODE_PAY_FEES_SEPARATELY); } /* === Pool loader === */ @@ -380,7 +410,7 @@ fun loadPool(st: Storage): TokenPool { preflightCheck: null, onLockOrBurnTransfer: null, onLockOrBurnTransferContinue, - onReleaseOrMint: null, + onReleaseOrMint, validateReleaseOrMint: null, postflightCheck: null, onReleaseOrMintContinue, @@ -420,14 +450,6 @@ get fun offRamp(remoteChainSelector: uint64): address? { return entry.isFound ? entry.loadValue() : null; } -get fun hasPendingLock(queryId: uint64): bool { - return Storage.load().pendingLocks.get(queryId).isFound; -} - -get fun hasPendingRelease(queryId: uint64): bool { - return Storage.load().pendingReleases.get(queryId).isFound; -} - get fun getRMNProxy(): address { return Storage.load().poolData.load().adminConfig.load().rmnProxy; } diff --git a/contracts/contracts/ccip/pools/lock_release_lockbox/errors.tolk b/contracts/contracts/ccip/pools/lock_release_lockbox/errors.tolk index d43cc82e1..a7b36a609 100644 --- a/contracts/contracts/ccip/pools/lock_release_lockbox/errors.tolk +++ b/contracts/contracts/ccip/pools/lock_release_lockbox/errors.tolk @@ -4,12 +4,10 @@ const LockReleaseLockboxTokenPool_FACILITY_NAME = "link.chain.ton.ccip.LockRelea const LockReleaseLockboxTokenPool_FACILITY_ID = 487; // (crc32() % 640) + 10 enum LockReleaseLockBoxTokenPool_Error { - PendingLockAlreadyExists = LockReleaseLockboxTokenPool_FACILITY_ID * 100 - PendingLockNotFound - PendingReleaseAlreadyExists - PendingReleaseNotFound + ContextExecutorUnavailable = LockReleaseLockboxTokenPool_FACILITY_ID * 100 LockboxNotConfigured UnexpectedLockboxConfirmationSender UnexpectedLockBounce + InvalidContextExecutorReply } diff --git a/contracts/contracts/ccip/pools/lock_release_lockbox/messages.tolk b/contracts/contracts/ccip/pools/lock_release_lockbox/messages.tolk index aff7e186a..ff6021c75 100644 --- a/contracts/contracts/ccip/pools/lock_release_lockbox/messages.tolk +++ b/contracts/contracts/ccip/pools/lock_release_lockbox/messages.tolk @@ -1,16 +1,16 @@ // SPDX-License-Identifier: BUSL-1.1 import "../../../lib/jetton/messages" +import "../../executors/context_executor/messages" import "../lockbox/messages" +import "types" -// Messages we receive from lockbox or jetton system: -// - JettonLockBox_Deposited: lockbox confirms deposit (lock flow completion) -// - JettonLockBox_WithdrawFailed: lockbox reports withdraw failure (release error) -// - ReturnExcessesBack: completion signal for both lock and release flows type LockReleaseLockboxTokenPool_InMessage = | ReturnExcessesBack | JettonLockBox_Deposited - | JettonLockBox_WithdrawFailed; + | JettonLockBox_WithdrawFailed + | ContextExecutor_Reply + | ContextExecutor_ForwardNotification; type LockReleaseLockboxTokenPool_OutMessage = | AskToTransfer; diff --git a/contracts/contracts/ccip/pools/lock_release_lockbox/storage.tolk b/contracts/contracts/ccip/pools/lock_release_lockbox/storage.tolk index 279155c04..e3c49b4fe 100644 --- a/contracts/contracts/ccip/pools/lock_release_lockbox/storage.tolk +++ b/contracts/contracts/ccip/pools/lock_release_lockbox/storage.tolk @@ -7,12 +7,31 @@ import "../lib/token_pool/entrypoint" import "types" +/// @notice Pool storage: common pool data, lockbox reference, + ContextExecutor deployment state. struct Storage { poolData: Cell; + /// Address of the JettonLockBox contract used for deposit/withdraw operations. lockbox: address; - // Pending contexts keyed by queryId for async flow completion. - pendingLocks: map> = []; - pendingReleases: map> = []; + /// Compiled code cell of the ContextExecutor contract (deployed per-operation). + contextExecutorCode: cell; + /// Monotonically increasing ID for deterministic ContextExecutor address derivation. + contextExecutorNextId: uint64; +} + +@inline +/// @notice Returns true if the next ContextExecutor ID has not reached max uint64. +fun Storage.canTakeContextExecutorNextId(self): bool { + return self.contextExecutorNextId != 18446744073709551615; +} + +@inline +/// @notice Atomically takes and increments the next ContextExecutor ID. +/// @dev Throws if the ID counter has reached its maximum value. +fun Storage.takeContextExecutorNextId(mutate self): uint64 { + val id = self.contextExecutorNextId; + assert(id != 18446744073709551615) throw 0xFFFF; + self.contextExecutorNextId = id + 1; + return id; } fun Storage.load(): Storage { @@ -54,7 +73,7 @@ fun Storage.init(config: LockReleaseLockboxTokenPool_InitConfig): Storage { tokenTransferFeeConfigs: createEmptyMap(), }.toCell(), lockbox: config.lockbox, - pendingLocks: createEmptyMap(), - pendingReleases: createEmptyMap(), + contextExecutorCode: config.contextExecutorCode, + contextExecutorNextId: 1, }; } diff --git a/contracts/contracts/ccip/pools/lock_release_lockbox/types.tolk b/contracts/contracts/ccip/pools/lock_release_lockbox/types.tolk index 69494081f..5a03812da 100644 --- a/contracts/contracts/ccip/pools/lock_release_lockbox/types.tolk +++ b/contracts/contracts/ccip/pools/lock_release_lockbox/types.tolk @@ -7,25 +7,24 @@ import "../lib/token_pool/messages" const LockReleaseLockboxTokenPool_CONTRACT_NAME = "link.chain.ton.ccip.LockReleaseLockboxTokenPool".literalSlice(); const LockReleaseLockboxTokenPool_CONTRACT_VERSION = "0.1.0".literalSlice(); -// Pending lock context stored by queryId until lockbox confirms deposit. -// On JettonLockBox_Deposited, pool restores this and finalizes the lock flow. -struct LockReleaseLockboxTokenPool_PendingLock { - queryId: uint64; +const LockReleaseLockboxTokenPool_CONTEXT_EXECUTOR_DEPLOY_VALUE = ton("0.02"); + +/// @notice Context passed to ContextExecutor for lock (deposit) operations. +/// Carries the full forward payload for post-lock finalization. +struct (0x60fdb63b) LockReleaseLockboxTokenPool_LockContext { forwardPayload: Cell; } -// Pending release context stored by queryId until ReturnExcessesBack arrives. -// On ReturnExcessesBack, pool restores this and finalizes the release flow. -struct LockReleaseLockboxTokenPool_PendingRelease { - queryId: uint64; - replyTo: Cell; - request: Cell; - out: Cell; +/// @notice Context passed to ContextExecutor for release (withdraw) operations. +/// Carries the full forward payload for post-release finalization. +struct (0x90230477) LockReleaseLockboxTokenPool_ReleaseContext { + forwardPayload: Cell; } struct LockReleaseLockboxTokenPool_InitConfig { owner: address; jettonClient: JettonClient; + contextExecutorCode: cell; tokenDecimals: uint8; router: address; rmnProxy: address; diff --git a/contracts/contracts/ccip/pools/lockbox/JettonLockBox.tolk b/contracts/contracts/ccip/pools/lockbox/JettonLockBox.tolk index 23e9dac27..4772230a4 100644 --- a/contracts/contracts/ccip/pools/lockbox/JettonLockBox.tolk +++ b/contracts/contracts/ccip/pools/lockbox/JettonLockBox.tolk @@ -155,7 +155,6 @@ fun onDepositNotification(mutate st: Storage, sender: address, msg: TransferNoti // Emit deposit event. emit(TOPIC_DEPOSIT, fwdp); - // TODO: needs to include more context/fwdPayload val reply = createMessage({ bounce: BounceMode.NoBounce, value: 0, @@ -165,6 +164,7 @@ fun onDepositNotification(mutate st: Storage, sender: address, msg: TransferNoti token: fwdp.token, remoteChainSelector: fwdp.remoteChainSelector, amount: fwdp.amount, + context: fwdp.context, }, }); reply.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); @@ -193,6 +193,7 @@ fun onWithdraw(mutate st: Storage, sender: address, msg: JettonLockBox_Withdraw) .storeUint(0x17c24007, 32) // JettonLockBox_WithdrawRequest marker .storeUint(msg.queryId, 64) .storeAddress(sender) + .storeMaybeRef(msg.extra != null ? msg.extra!.load().failureContext : null) .endCell(); val fwdPayload = beginCell() @@ -233,7 +234,6 @@ fun onWithdraw(mutate st: Storage, sender: address, msg: JettonLockBox_Withdraw) } /// Handle bounced AskToTransfer (withdraw failed) - send error message to initiator. -/// The pool stores pending releases by queryId and restores context on JettonLockBox_WithdrawFailed. fun onBouncedMessage(in: InMessageBounced) { val st = lazy Storage.load(); @@ -253,6 +253,7 @@ fun onBouncedMessage(in: InMessageBounced) { val marker = bounceData.loadUint(32); val queryId = bounceData.loadUint(64); val withdrawInitiator = bounceData.loadAddress(); + val failureContext = bounceData.loadMaybeRef(); // Send error message to withdraw initiator val errorMsg = createMessage({ @@ -264,6 +265,7 @@ fun onBouncedMessage(in: InMessageBounced) { token: st.minterAddress, amount: msg.jettonAmount, recipientWallet: msg.transferRecipient, + context: failureContext, }, }); errorMsg.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); diff --git a/contracts/contracts/ccip/pools/lockbox/messages.tolk b/contracts/contracts/ccip/pools/lockbox/messages.tolk index 4a0a822da..ec4700e0d 100644 --- a/contracts/contracts/ccip/pools/lockbox/messages.tolk +++ b/contracts/contracts/ccip/pools/lockbox/messages.tolk @@ -11,11 +11,13 @@ struct (0x9e9ec361) JettonLockBox_Deposit { token: address; remoteChainSelector: uint64; amount: coins; + context: cell?; } /// Optional extra fields for JettonLockBox_Withdraw (stored in a cell ref to avoid overflow). struct JettonLockBox_WithdrawExtra { sendExcessesTo: address?; + failureContext: cell?; } /// Withdraws tokens to a specific recipient. @@ -42,6 +44,7 @@ struct (0x6d077f2e) JettonLockBox_Deposited { token: address; remoteChainSelector: uint64; amount: coins; + context: cell?; } /// Contract initialization message. @@ -75,6 +78,7 @@ struct (0x60bae556) JettonLockBox_WithdrawFailed { token: address; amount: coins; recipientWallet: address; + context: cell?; } /// Union of all incoming messages this contract handles. diff --git a/contracts/tests/ccip/ContextExecutor.spec.ts b/contracts/tests/ccip/ContextExecutor.spec.ts new file mode 100644 index 000000000..944b5c3ae --- /dev/null +++ b/contracts/tests/ccip/ContextExecutor.spec.ts @@ -0,0 +1,81 @@ +import '@ton/test-utils' +import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox' +import { Cell, toNano } from '@ton/core' + +import { ContextExecutor } from '../../wrappers/gen/ccip/ContextExecutor' + +describe('ContextExecutor', () => { + let blockchain: Blockchain + let owner: SandboxContract + let forwarder: SandboxContract + let executor: SandboxContract + + beforeEach(async () => { + blockchain = await Blockchain.create() + owner = await blockchain.treasury('owner') + forwarder = await blockchain.treasury('forwarder') + + executor = blockchain.openContract( + ContextExecutor.fromStorage({ + id: 77n, + owner: owner.address, + context: Cell.EMPTY, + forwardFrom: [forwarder.address], + }), + ) + + await executor.sendDeploy(owner.getSender(), toNano('0.05')) + }) + + it('derives distinct addresses for distinct ids with identical context', () => { + const first = ContextExecutor.fromStorage({ + id: 1n, + owner: owner.address, + context: Cell.EMPTY, + forwardFrom: [forwarder.address], + }) + const second = ContextExecutor.fromStorage({ + id: 2n, + owner: owner.address, + context: Cell.EMPTY, + forwardFrom: [forwarder.address], + }) + + expect(first.address.equals(second.address)).toBe(false) + }) + + it('forwards at most one notification from an authorized sender', async () => { + const first = await forwarder.send({ + to: executor.address, + value: toNano('0.02'), + bounce: false, + body: Cell.EMPTY, + }) + + expect(first.transactions).toHaveTransaction({ + from: forwarder.address, + to: executor.address, + success: true, + }) + expect(first.transactions).toHaveTransaction({ + from: executor.address, + to: owner.address, + success: true, + }) + + const second = await forwarder.send({ + to: executor.address, + value: toNano('0.02'), + bounce: false, + body: Cell.EMPTY, + }) + + const repeatedNotifications = second.transactions.filter( + (tx: any) => + tx.inMessage?.info?.src?.equals?.(executor.address) && + tx.inMessage?.info?.dest?.equals?.(owner.address), + ) + + expect(repeatedNotifications).toHaveLength(0) + }) +}) diff --git a/contracts/tests/ccip/pools/BurnMintTokenPool.spec.ts b/contracts/tests/ccip/pools/BurnMintTokenPool.spec.ts index a5dd10446..23f658740 100644 --- a/contracts/tests/ccip/pools/BurnMintTokenPool.spec.ts +++ b/contracts/tests/ccip/pools/BurnMintTokenPool.spec.ts @@ -1,6 +1,6 @@ import '@ton/test-utils' import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox' -import { Address, Cell, Dictionary, toNano } from '@ton/core' +import { Address, beginCell, Cell, toNano } from '@ton/core' import { JettonMinter, JettonWallet } from '../../../wrappers/examples/jetton' import * as cct from '../../../wrappers/gen/ccip/cct/JettonMinter' import { @@ -15,6 +15,7 @@ import { TokenPool_RateLimitConfigPair, TokenPool_ChainUpdate, TokenPool_LockOrBurn, + TokenPool_LockOrBurnFailure, TokenPool_LockOrBurnInV1, TokenPool_LockOrBurnFinished, TokenPool_LockOrBurnForwardPayload, @@ -29,11 +30,39 @@ import { } from '../../../wrappers/gen/ccip/pools/TokenPool' import { TokenPool_LockOrBurnWithdraw } from '../../../wrappers/gen/ccip/pools/BurnMintTokenPool' import { BurnMintTokenPool, JettonClient } from '../../../wrappers/gen/ccip/pools/BurnMintTokenPool' -import { runTokenPoolBehaviorTests, runTokenPoolAsyncHookBehaviorTests } from './TokenPool.behavior' +import { + ContextExecutor, + ContextExecutor_ForwardNotification, + ContextExecutor_InMessageForward, +} from '../../../wrappers/gen/ccip/ContextExecutor' +import { runTokenPoolAsyncHookBehaviorTests, runTokenPoolBehaviorTests } from './TokenPool.behavior' import { MockAdvancedPoolHooks } from '../../../wrappers/gen/ccip/test/MockAdvancedPoolHooks' import * as CrossChainAddressCodec from '../../../wrappers/ccip/common/CrossChainAddressCodec' import { contractCode } from '../../../wrappers/codeLoader' +function buildSpoofedExecutorForwardNotification(senderAddress: Address): Cell { + const forwarded = ContextExecutor_InMessageForward.toCell( + ContextExecutor_InMessageForward.create({ + senderAddress, + valueCoins: 0n, + valueExtra: new Map(), + originalForwardFee: 0n, + createdLt: 0n, + createdAt: 0n, + body: Cell.EMPTY, + }), + ) + + return beginCell() + .storeUint(ContextExecutor_ForwardNotification.PREFIX, 32) + .storeUint(999n, 64) + .storeRef(Cell.EMPTY) + .storeUint(0, 8) + .storeMaybeRef(null) + .storeRef(forwarded) + .endCell() +} + describe('BurnMintTokenPool', () => { let blockchain: Blockchain let deployer: SandboxContract @@ -117,8 +146,8 @@ describe('BurnMintTokenPool', () => { remoteChainConfigs: new Map(), tokenTransferFeeConfigs: new Map(), }), - pendingMints: new Map(), - pendingBurns: new Map(), + contextExecutorCode: ContextExecutor.CodeCell, + contextExecutorNextId: 1n, }, { overrideContractCode: burnMintPoolCode }, ), @@ -308,11 +337,6 @@ describe('BurnMintTokenPool', () => { } }) - it('has no pending burn or mint by default', async () => { - expect(await burnMintPool.getHasPendingBurn(300n)).toBe(false) - expect(await burnMintPool.getHasPendingMint(301n)).toBe(false) - }) - it('rejects claim-minter-admin from non-owner sender', async () => { const result = await burnMintPool.sendBurnMintTokenPoolClaimMinterAdmin( unauthorized.getSender(), @@ -327,7 +351,7 @@ describe('BurnMintTokenPool', () => { }) }) - it('reverts lockOrBurn when caller is not configured on-ramp', async () => { + it('returns transfer inputs that bypass the prevalidated forward path', async () => { const unauthorizedWallet = await userWallet(unauthorized.address) const poolWallet = await userWallet(burnMintPool.address) const result = await unauthorizedWallet.sendTransfer(unauthorized.getSender(), { @@ -365,53 +389,127 @@ describe('BurnMintTokenPool', () => { expect(result.transactions).toHaveTransaction({ from: poolWallet.address, to: burnMintPool.address, - success: false, + success: true, }) + expect(await unauthorizedWallet.getJettonBalance()).toEqual(toNano('2')) + expect(await poolWallet.getJettonBalance()).toEqual(0n) }) - it('reverts lockOrBurn when payload amount does not match transferred amount', async () => { - const onRampWallet = await userWallet(deployer.address) - const poolWallet = await userWallet(burnMintPool.address) - const result = await onRampWallet.sendTransfer(deployer.getSender(), { - value: toNano('2'), - message: { - queryId: 304, - jettonAmount: toNano('2'), - destination: burnMintPool.address, - responseDestination: deployer.address, - customPayload: null, - forwardTonAmount: toNano('0.2'), - forwardPayload: TokenPool_LockOrBurn.toCell( - TokenPool_LockOrBurn.create({ - queryId: 304n, - request: TokenPool_LockOrBurnInV1.create({ - transfer: TokenPool_Transfer.create({ - id: 304n, - details: TokenPool_TransferDetails.create({ - receiver: receiverAddress, - remoteChainSelector, - originalSender: deployer.address, - amount: toNano('1'), - localToken: cctMinter.address, - }), - }), + describe('lockOrBurn transfer input validation', () => { + it('returns tokens and notifies the requester when payload amount does not match transferred amount', async () => { + const onRampWallet = await userWallet(deployer.address) + const poolWallet = await userWallet(burnMintPool.address) + const requestMsg = TokenPool_LockOrBurn.create({ + queryId: 304n, + request: TokenPool_LockOrBurnInV1.create({ + transfer: TokenPool_Transfer.create({ + id: 304n, + details: TokenPool_TransferDetails.create({ + receiver: receiverAddress, + remoteChainSelector, + originalSender: deployer.address, + amount: toNano('1'), + localToken: cctMinter.address, }), - requestedFinalityConfig: 0n, - tokenArgs: null, - replyTo: deployer.address, }), - ), - }, + }), + requestedFinalityConfig: 0n, + tokenArgs: null, + replyTo: deployer.address, + }) + const forwardPayload = TokenPool_LockOrBurnForwardPayload.create({ + originalSender: deployer.address, + requestMsg, + prepared: TokenPool_LockOrBurnPrepared.create({ + feeAmount: 0n, + destTokenAmount: toNano('1'), + out: TokenPool_LockOrBurnOutV1.create({ + destTokenAddress, + destPoolData: Cell.EMPTY, + }), + }), + }) + const result = await onRampWallet.sendTransfer(deployer.getSender(), { + value: toNano('2'), + message: { + queryId: 304, + jettonAmount: toNano('2'), + destination: burnMintPool.address, + responseDestination: deployer.address, + customPayload: null, + forwardTonAmount: toNano('0.2'), + forwardPayload: TokenPool_LockOrBurnForwardPayload.toCell(forwardPayload), + }, + }) + + expect(result.transactions).toHaveTransaction({ + from: poolWallet.address, + to: burnMintPool.address, + success: true, + }) + expect(result.transactions).toHaveTransaction({ + from: burnMintPool.address, + to: poolWallet.address, + success: true, + op: 0x0f8a7ea5, // AskToTransfer + }) + expect(result.transactions).toHaveTransaction({ + from: burnMintPool.address, + to: deployer.address, + // The Treasury test recipient does not implement this callback. Assert + // the emitted message, including that it carries the residual inbound + // value instead of consuming it in the pool. + op: TokenPool_LockOrBurnFailure.PREFIX, + value: (value) => value !== undefined && value > 0n, + body(body) { + if (!body) return false + const failure = TokenPool_LockOrBurnFailure.fromSlice(body.beginParse()) + return failure.queryId === 304n && failure.errorCode === 14920n + }, + }) + expect(await onRampWallet.getJettonBalance()).toEqual(toNano('10')) + expect(await poolWallet.getJettonBalance()).toEqual(0n) }) - expect(result.transactions).toHaveTransaction({ - from: poolWallet.address, - to: burnMintPool.address, - success: false, + it('returns tokens without a callback when transfer forward payload is malformed', async () => { + const deployerWallet = await userWallet(deployer.address) + const poolWallet = await userWallet(burnMintPool.address) + + const result = await deployerWallet.sendTransfer(deployer.getSender(), { + value: toNano('2'), + message: { + queryId: 45, + jettonAmount: toNano('1'), + destination: burnMintPool.address, + responseDestination: deployer.address, + customPayload: null, + forwardTonAmount: toNano('0.2'), + forwardPayload: beginCell().storeUint(0, 32).endCell(), + }, + }) + + expect(result.transactions).toHaveTransaction({ + from: poolWallet.address, + to: burnMintPool.address, + success: true, + }) + expect(result.transactions).toHaveTransaction({ + from: burnMintPool.address, + to: poolWallet.address, + success: true, + op: 0x0f8a7ea5, // AskToTransfer + }) + expect(result.transactions).not.toHaveTransaction({ + from: burnMintPool.address, + to: deployer.address, + op: TokenPool_LockOrBurnFailure.PREFIX, + }) + expect(await deployerWallet.getJettonBalance()).toEqual(toNano('10')) + expect(await poolWallet.getJettonBalance()).toEqual(0n) }) }) - it('burns tokens on lockOrBurn path and clears pending burn on confirmation', async () => { + it('burns tokens on lockOrBurn path and finalizes through the executor notification', async () => { const deployerWallet = await userWallet(deployer.address) const poolWallet = await userWallet(burnMintPool.address) @@ -488,7 +586,7 @@ describe('BurnMintTokenPool', () => { feeAmount: 0n, destTokenAmount: jettonAmount, out: TokenPool_LockOrBurnOutV1.create({ - destTokenAddress: destTokenAddress, + destTokenAddress, destPoolData: Cell.EMPTY, }), }), @@ -510,10 +608,8 @@ describe('BurnMintTokenPool', () => { withdrawBody.forwardPayload, ) - // Jetton wallet sends TransferNotificationForRecipient to pool - // Pool receives notification → validates forward payload → creates pending burn → sends AskToBurn to minter - // Minter receives AskToBurn → burns → sends ReturnExcessesBack to pool - // Pool receives ReturnExcessesBack → clears pending burn → finalizes → TokenPool_LockOrBurnFinished + // Jetton wallet sends TransferNotificationForRecipient to the pool. + // Pool deploys a context executor, asks its own wallet to burn, then finalizes on executor forward notification. const result2 = await deployerWallet.sendTransfer(deployer.getSender(), { value: toNano('0.5'), message: { @@ -527,8 +623,6 @@ describe('BurnMintTokenPool', () => { }, }) - // Verify full async flow completed - expect(await burnMintPool.getHasPendingBurn(queryId)).toBe(false) expect(await poolWallet.getJettonBalance()).toEqual(0n) expect(result2.transactions).toHaveTransaction({ @@ -539,7 +633,7 @@ describe('BurnMintTokenPool', () => { }) }) - it('mints tokens on releaseOrMint path and clears pending mint on confirmation', async () => { + it('mints tokens on releaseOrMint path and finalizes through the executor notification', async () => { const result = await burnMintPool.sendTokenPoolReleaseOrMint( offRamp.getSender(), toNano('0.6'), @@ -556,7 +650,7 @@ describe('BurnMintTokenPool', () => { localToken: cctMinter.address, }), }), - sourcePoolAddress: sourcePoolAddress, + sourcePoolAddress, sourcePoolData: null, offchainTokenData: null, }), @@ -577,8 +671,6 @@ describe('BurnMintTokenPool', () => { success: true, }) - expect(await burnMintPool.getHasPendingMint(22n)).toBe(false) - expect(result.transactions).toHaveTransaction({ from: burnMintPool.address, to: deployer.address, @@ -609,7 +701,7 @@ describe('BurnMintTokenPool', () => { localToken: cctMinter.address, }), }), - sourcePoolAddress: sourcePoolAddress, + sourcePoolAddress, sourcePoolData: null, offchainTokenData: null, }), @@ -630,13 +722,36 @@ describe('BurnMintTokenPool', () => { }) const releaseResponses = result.transactions.filter((tx: any) => { + const body = tx.inMessage?.body + if (!body) { + return false + } + + const slice = body.beginParse() + if (slice.remainingBits < 32) { + return false + } + return ( tx.inMessage?.info?.src?.equals?.(burnMintPool.address) && - tx.inMessage?.body?.beginParse?.().preloadUint?.(32) === - TokenPool_ReleaseOrMintFinished.PREFIX + slice.preloadUint(32) === TokenPool_ReleaseOrMintFinished.PREFIX ) }) expect(releaseResponses.length).toBe(0) - expect(await burnMintPool.getHasPendingMint(305n)).toBe(false) + }) + + it('rejects forged executor forward notifications', async () => { + const forged = await unauthorized.send({ + to: burnMintPool.address, + value: toNano('0.1'), + bounce: false, + body: buildSpoofedExecutorForwardNotification(unauthorized.address), + }) + + expect(forged.transactions).toHaveTransaction({ + from: unauthorized.address, + to: burnMintPool.address, + success: false, + }) }) }) diff --git a/contracts/tests/ccip/pools/JettonLockbox.spec.ts b/contracts/tests/ccip/pools/JettonLockbox.spec.ts index d601d66e7..33eacee38 100644 --- a/contracts/tests/ccip/pools/JettonLockbox.spec.ts +++ b/contracts/tests/ccip/pools/JettonLockbox.spec.ts @@ -196,6 +196,7 @@ describe('JettonLockBox', () => { token: jettonMinter.address, remoteChainSelector, amount, + context: null, }), ) @@ -252,6 +253,7 @@ describe('JettonLockBox', () => { token: jettonMinter.address, remoteChainSelector, amount: 0n, + context: null, }), ) @@ -308,6 +310,7 @@ describe('JettonLockBox', () => { token: jettonMinter.address, remoteChainSelector, amount, + context: null, }), ) @@ -367,6 +370,7 @@ describe('JettonLockBox', () => { token: jettonMinter.address, remoteChainSelector, amount: depositAmount, + context: null, }), ) diff --git a/contracts/tests/ccip/pools/LockReleaseLockboxTokenPool.spec.ts b/contracts/tests/ccip/pools/LockReleaseLockboxTokenPool.spec.ts index 779454b4a..c6288d99a 100644 --- a/contracts/tests/ccip/pools/LockReleaseLockboxTokenPool.spec.ts +++ b/contracts/tests/ccip/pools/LockReleaseLockboxTokenPool.spec.ts @@ -34,6 +34,10 @@ import { JettonLockBox, JettonLockBox_WithdrawExtra, } from '../../../wrappers/gen/ccip/pools/JettonLockBox' +import { + ContextExecutor, + ContextExecutor_ForwardNotification, +} from '../../../wrappers/gen/ccip/ContextExecutor' import { ContractClient as AccessControlClient } from '../../../wrappers/lib/access/AccessControl' import { runTokenPoolBehaviorTests, runTokenPoolAsyncHookBehaviorTests } from './TokenPool.behavior' @@ -166,8 +170,8 @@ describe('LockReleaseLockboxTokenPool', () => { tokenTransferFeeConfigs: new Map(), }), lockbox: jettonLockBox.address, - pendingLocks: new Map(), - pendingReleases: new Map(), + contextExecutorCode: ContextExecutor.CodeCell, + contextExecutorNextId: 1n, }, { overrideContractCode: await contractCode.ccip.local( @@ -282,18 +286,39 @@ describe('LockReleaseLockboxTokenPool', () => { } }) - runTokenPoolBehaviorTests('LockReleaseLockboxTokenPool', async () => ({ - pool, - deployer, - offRamp, - unauthorized: recipient, - recipient, - remoteChainSelector, - onRampAddress: deployer.address, - destTokenAddress, - sourcePoolAddress, - localToken: jettonMinter.address, - })) + const setupTokenPoolBehaviorContext = async () => { + await jettonMinter.sendMint(deployer.getSender(), { + value: toNano('2'), + message: { + queryId: 0n, + destination: jettonLockBox.address, + tonAmount: toNano('0.5'), + jettonAmount: toNano('10'), + from: deployer.address, + responseDestination: deployer.address, + forwardTonAmount: toNano('0.3'), + }, + }) + } + + runTokenPoolBehaviorTests( + 'LockReleaseLockboxTokenPool', + async () => ({ + pool, + deployer, + offRamp, + unauthorized: recipient, + recipient, + remoteChainSelector, + onRampAddress: deployer.address, + destTokenAddress, + sourcePoolAddress, + localToken: jettonMinter.address, + }), + { + setup: setupTokenPoolBehaviorContext, + }, + ) // Async hook behavior tests (TON-TP/6) runTokenPoolAsyncHookBehaviorTests('LockReleaseLockboxTokenPool', async () => { @@ -349,14 +374,6 @@ describe('LockReleaseLockboxTokenPool', () => { expect(await lockReleaseLockboxPool.getToken()).toEqualAddress(jettonMinter.address) expect(await lockReleaseLockboxPool.getTokenDecimals()).toBe(9n) }) - - it('should have no pending lock by default', async () => { - expect(await lockReleaseLockboxPool.getHasPendingLock(999n)).toBe(false) - }) - - it('should have no pending release by default', async () => { - expect(await lockReleaseLockboxPool.getHasPendingRelease(999n)).toBe(false) - }) }) describe('lock flow (jetton transfer -> lockbox custody)', () => { @@ -416,9 +433,6 @@ describe('LockReleaseLockboxTokenPool', () => { to: lockReleaseLockboxPool.address, success: true, }) - - // Pool should have a pending lock - expect(await lockReleaseLockboxPool.getHasPendingLock(100n)).toBe(true) }) it('should store pending lock when forwarded amount matches transfer amount', async () => { @@ -469,8 +483,6 @@ describe('LockReleaseLockboxTokenPool', () => { forwardPayload: TokenPool_LockOrBurnForwardPayload.toCell(forwardPayload), }, }) - - expect(await lockReleaseLockboxPool.getHasPendingLock(101n)).toBe(true) }) }) @@ -523,9 +535,6 @@ describe('LockReleaseLockboxTokenPool', () => { success: true, }) - // Pool should have a pending release - expect(await lockReleaseLockboxPool.getHasPendingRelease(200n)).toBe(true) - // Pool sends JettonLockBox_Withdraw to the lockbox (lockbox will handle the jetton transfer) expect(result.transactions).toHaveTransaction({ from: lockReleaseLockboxPool.address, @@ -535,91 +544,6 @@ describe('LockReleaseLockboxTokenPool', () => { }) }) - it('should reject duplicate release requests with PendingReleaseAlreadyExists', async () => { - // Fund lockbox first - const lockboxWalletAddress = await jettonMinter.getWalletAddress(jettonLockBox.address) - await jettonMinter.sendMint(deployer.getSender(), { - value: toNano('1'), - message: { - queryId: 0n, - destination: lockboxWalletAddress, - tonAmount: toNano('0.05'), - jettonAmount: toNano('50'), - from: deployer.address, - responseDestination: deployer.address, - forwardTonAmount: 0n, - }, - }) - - // First release request - should succeed - const firstResult = await lockReleaseLockboxPool.sendTokenPoolReleaseOrMint( - offRamp.getSender(), - toNano('0.5'), - { - queryId: 220n, - request: TokenPool_ReleaseOrMintInV1.create({ - transfer: TokenPool_Transfer.create({ - id: 220n, - details: TokenPool_TransferDetails.create({ - originalSender: sourcePoolAddress, - remoteChainSelector, - receiver: recipient.address, - amount: toNano('5'), - localToken: jettonMinter.address, - }), - }), - sourcePoolAddress: sourcePoolAddress, - sourcePoolData: null, - offchainTokenData: null, - }), - requestedFinalityConfig: 0n, - replyTo: deployer.address, - }, - ) - - expect(firstResult.transactions).toHaveTransaction({ - to: lockReleaseLockboxPool.address, - success: true, - }) - expect(await lockReleaseLockboxPool.getHasPendingRelease(220n)).toBe(true) - - // Second release with same queryId - should be rejected - const secondResult = await lockReleaseLockboxPool.sendTokenPoolReleaseOrMint( - offRamp.getSender(), - toNano('0.5'), - { - queryId: 220n, - request: TokenPool_ReleaseOrMintInV1.create({ - transfer: TokenPool_Transfer.create({ - id: 220n, - details: TokenPool_TransferDetails.create({ - originalSender: sourcePoolAddress, - remoteChainSelector, - receiver: recipient.address, - amount: toNano('5'), - localToken: jettonMinter.address, - }), - }), - sourcePoolAddress: sourcePoolAddress, - sourcePoolData: null, - offchainTokenData: null, - }), - requestedFinalityConfig: 0n, - replyTo: deployer.address, - }, - ) - - // Exit code 48702 = PendingReleaseAlreadyExists - expect(secondResult.transactions).toHaveTransaction({ - to: lockReleaseLockboxPool.address, - success: false, - exitCode: 48702, - }) - - // Original pending release should still exist - expect(await lockReleaseLockboxPool.getHasPendingRelease(220n)).toBe(true) - }) - it('should reject release when requested amount exceeds lockbox liquidity', async () => { const result = await lockReleaseLockboxPool.sendTokenPoolReleaseOrMint( offRamp.getSender(), @@ -651,7 +575,6 @@ describe('LockReleaseLockboxTokenPool', () => { to: lockReleaseLockboxPool.address, success: false, }) - expect(await lockReleaseLockboxPool.getHasPendingRelease(201n)).toBe(false) }) }) @@ -745,116 +668,6 @@ describe('LockReleaseLockboxTokenPool', () => { op: 0x178d4519, // TransferNotificationForRecipient success: true, }) - - // Pool stores pending lock after receiving TransferNotificationForRecipient - expect(await lockReleaseLockboxPool.getHasPendingLock(300n)).toBe(true) - - // The pool computes the lockbox wallet address and sends AskToTransfer to forward jettons - // In the sandbox, the lockbox wallet is a standard jetton wallet (not JettonLockBox contract). - // It returns excesses but does NOT send JettonLockBox_Deposited back to finalize. - // In production, the JettonLockBox contract would process the deposit and send the callback, - // clearing the pending lock and emitting TokenPool_LockedOrBurned. - }) - - it('should return jettons for transfers without transferInitiator (direct user transfers)', async () => { - // When a user sends jettons directly to the pool wallet (not through on-ramp), - // the transferInitiator will be null (user's own wallet). - // The pool should detect this and return the jettons instead of processing. - // This is tested by verifying that onLockOrBurnTransfer checks transferInitiator != null. - // The TokenPool library handles this - if transferInitiator is null, it calls returnTransfer(). - // In the current test setup, all transfers go through JettonWallet which sets transferInitiator. - // The onLockOrBurnTransfer hook in token_pool.tolk asserts: - // if (msg.transferInitiator != null) { onLockOrBurnTransferContinue(); return; } - // else { returnTransfer(); } - // This prevents rogue transfers from being processed as locks. - }) - - it('should reject duplicate lock requests with PendingLockAlreadyExists', async () => { - // This tests the onLockOrBurnTransferContinue hook directly. - // When a lock request with the same queryId arrives twice, the second should be rejected. - // The contract asserts: !pendingLocks.get(msg.queryId).isFound -> PendingLockAlreadyExists - - const onRampWallet = await userWallet(deployer.address) - const poolWallet = await userWallet(lockReleaseLockboxPool.address) - - const lockOrBurn = TokenPool_LockOrBurn.create({ - queryId: 310n, - request: TokenPool_LockOrBurnInV1.create({ - transfer: TokenPool_Transfer.create({ - id: 310n, - details: TokenPool_TransferDetails.create({ - receiver: receiverAddress, - remoteChainSelector, - originalSender: deployer.address, - amount: toNano('5'), - localToken: jettonMinter.address, - }), - }), - }), - requestedFinalityConfig: 0n, - tokenArgs: null, - replyTo: deployer.address, - }) - - const forwardPayload = TokenPool_LockOrBurnForwardPayload.create({ - originalSender: deployer.address, - requestMsg: lockOrBurn, - prepared: TokenPool_LockOrBurnPrepared.create({ - feeAmount: 0n, - destTokenAmount: toNano('5'), - out: TokenPool_LockOrBurnOutV1.create({ - destTokenAddress: destTokenAddress, - destPoolData: Cell.EMPTY, - }), - }), - }) - - // First lock request - should succeed - const firstResult = await onRampWallet.sendTransfer(deployer.getSender(), { - value: toNano('3'), - message: { - queryId: 310, - jettonAmount: toNano('5'), - destination: lockReleaseLockboxPool.address, - responseDestination: deployer.address, - customPayload: null, - forwardTonAmount: toNano('0.5'), - forwardPayload: TokenPool_LockOrBurnForwardPayload.toCell(forwardPayload), - }, - }) - - expect(firstResult.transactions).toHaveTransaction({ - to: lockReleaseLockboxPool.address, - success: true, - }) - expect(await lockReleaseLockboxPool.getHasPendingLock(310n)).toBe(true) - - // Second lock request with same queryId - should be rejected by onLockOrBurnTransferContinue - // The pool's jetton wallet will forward this back to the pool - // The pool processes it in onLockOrBurnTransfer which checks transferInitiator - // Then calls onLockOrBurnTransferContinue which checks for duplicate queryId - const secondResult = await onRampWallet.sendTransfer(deployer.getSender(), { - value: toNano('3'), - message: { - queryId: 310, - jettonAmount: toNano('5'), - destination: lockReleaseLockboxPool.address, - responseDestination: deployer.address, - customPayload: null, - forwardTonAmount: toNano('0.5'), - forwardPayload: TokenPool_LockOrBurnForwardPayload.toCell(forwardPayload), - }, - }) - - // The pool rejects duplicate with exit code 48700 (PendingLockAlreadyExists) - expect(secondResult.transactions).toHaveTransaction({ - to: lockReleaseLockboxPool.address, - success: false, - exitCode: 48700, - }) - - // Original pending lock should still exist - expect(await lockReleaseLockboxPool.getHasPendingLock(310n)).toBe(true) }) }) @@ -937,18 +750,13 @@ describe('LockReleaseLockboxTokenPool', () => { success: true, }) - // Recipient's wallet sends ReturnExcessesBack to the pool + // ReturnExcessesBack is routed through the context executor before the + // pool finalizes the release and clears the direct failure context. expect(result.transactions).toHaveTransaction({ to: lockReleaseLockboxPool.address, - op: 0xd53276db, // ReturnExcessesBack + op: ContextExecutor_ForwardNotification.PREFIX, success: true, }) - - // Pending release should be cleared (release completed) - expect(await lockReleaseLockboxPool.getHasPendingRelease(400n)).toBe(false) - - // Release flow complete: offRamp -> pool -> lockbox -> lockbox wallet -> TransferNotification -> recipient wallet - // Recipient wallet sends ReturnExcessesBack to pool confirming the transfer. }) it('should handle release flow failure when lockbox has insufficient balance', async () => { @@ -988,7 +796,6 @@ describe('LockReleaseLockboxTokenPool', () => { }) // No pending release should exist (either rejected upfront or cleaned up) - expect(await lockReleaseLockboxPool.getHasPendingRelease(401n)).toBe(false) }) }) diff --git a/contracts/tests/ccip/pools/LockReleaseTokenPool.spec.ts b/contracts/tests/ccip/pools/LockReleaseTokenPool.spec.ts index 9d229b049..ca06af5b2 100644 --- a/contracts/tests/ccip/pools/LockReleaseTokenPool.spec.ts +++ b/contracts/tests/ccip/pools/LockReleaseTokenPool.spec.ts @@ -12,9 +12,14 @@ import { TokenPool_AdminConfig, TokenPool_DynamicConfig, TokenPool_MirroredPolicy, + TokenPool_ReleaseOrMintFailure, TokenPool_ReleaseOrMintFinished, TokenPool_LockOrBurn, + TokenPool_LockOrBurnFailure, + TokenPool_LockOrBurnForwardPayload, TokenPool_LockOrBurnInV1, + TokenPool_LockOrBurnOutV1, + TokenPool_LockOrBurnPrepared, TokenPool_ReleaseOrMintInV1, TokenPool_RateLimitConfigPair, TokenPool_RampUpdate, @@ -29,12 +34,40 @@ import { JettonClient, LockReleaseTokenPool, } from '../../../wrappers/gen/ccip/pools/LockReleaseTokenPool' +import { + ContextExecutor, + ContextExecutor_ForwardNotification, + ContextExecutor_InMessageForward, +} from '../../../wrappers/gen/ccip/ContextExecutor' import * as CrossChainAddressCodec from '../../../wrappers/ccip/common/CrossChainAddressCodec' -import { runTokenPoolBehaviorTests, runTokenPoolAsyncHookBehaviorTests } from './TokenPool.behavior' +import { runTokenPoolAsyncHookBehaviorTests, runTokenPoolBehaviorTests } from './TokenPool.behavior' import { MockAdvancedPoolHooks } from '../../../wrappers/gen/ccip/test/MockAdvancedPoolHooks' import { contractCode } from '../../../wrappers/codeLoader' +function buildSpoofedExecutorForwardNotification(senderAddress: Address): Cell { + const forwarded = ContextExecutor_InMessageForward.toCell( + ContextExecutor_InMessageForward.create({ + senderAddress, + valueCoins: 0n, + valueExtra: new Map(), + originalForwardFee: 0n, + createdLt: 0n, + createdAt: 0n, + body: Cell.EMPTY, + }), + ) + + return beginCell() + .storeUint(ContextExecutor_ForwardNotification.PREFIX, 32) + .storeUint(999n, 64) + .storeRef(Cell.EMPTY) + .storeUint(0, 8) + .storeMaybeRef(null) + .storeRef(forwarded) + .endCell() +} + describe('LockReleaseTokenPool', () => { let blockchain: Blockchain let deployer: SandboxContract @@ -128,7 +161,8 @@ describe('LockReleaseTokenPool', () => { remoteChainConfigs: new Map(), tokenTransferFeeConfigs: new Map(), }), - pendingReleases: new Map(), + contextExecutorCode: ContextExecutor.CodeCell, + contextExecutorNextId: 1n, }, { overrideContractCode: await contractCode.ccip.local('ccip.pools.LockReleaseTokenPool') }, ), @@ -218,18 +252,39 @@ describe('LockReleaseTokenPool', () => { } }) - runTokenPoolBehaviorTests('LockReleaseTokenPool', async () => ({ - pool, - deployer, - offRamp, - unauthorized: recipient, - recipient, - remoteChainSelector, - onRampAddress: deployer.address, - destTokenAddress, - sourcePoolAddress, - localToken: jettonMinter.address, - })) + const setupTokenPoolBehaviorContext = async () => { + await jettonMinter.sendMint(deployer.getSender(), { + value: toNano('1'), + message: { + queryId: 0n, + destination: lockReleasePool.address, + tonAmount: toNano('0.05'), + jettonAmount: toNano('10'), + from: deployer.address, + responseDestination: deployer.address, + forwardTonAmount: 0n, + }, + }) + } + + runTokenPoolBehaviorTests( + 'LockReleaseTokenPool', + async () => ({ + pool, + deployer, + offRamp, + unauthorized: recipient, + recipient, + remoteChainSelector, + onRampAddress: deployer.address, + destTokenAddress, + sourcePoolAddress, + localToken: jettonMinter.address, + }), + { + setup: setupTokenPoolBehaviorContext, + }, + ) // Async hook behavior tests (TON-TP/6) runTokenPoolAsyncHookBehaviorTests('LockReleaseTokenPool', async () => { @@ -274,72 +329,116 @@ describe('LockReleaseTokenPool', () => { } }) - it('has no pending release by default', async () => { - expect(await lockReleasePool.getHasPendingRelease(999n)).toBe(false) - }) - - it('reverts lockOrBurn when forwarded amount does not match transfer amount', async () => { - const onRampWallet = await userWallet(jettonSender.address) - const poolWallet = await userWallet(lockReleasePool.address) - - const result = await jettonSender.sendJettonsExtended(deployer.getSender(), { - value: toNano('2'), - message: { + describe('lockOrBurn transfer input validation', () => { + it('returns tokens and notifies the requester when forwarded amount does not match transfer amount', async () => { + const onRampWallet = await userWallet(jettonSender.address) + const poolWallet = await userWallet(lockReleasePool.address) + const requestMsg = TokenPool_LockOrBurn.create({ queryId: 44n, - amount: toNano('3'), - destination: lockReleasePool.address, - customPayload: beginCell().storeBit(1).endCell(), - forwardTonAmount: toNano('0.2'), - forwardPayload: TokenPool_LockOrBurn.toCell( - TokenPool_LockOrBurn.create({ - queryId: 44n, - request: TokenPool_LockOrBurnInV1.create({ - transfer: TokenPool_Transfer.create({ - id: 44n, - details: TokenPool_TransferDetails.create({ - receiver: receiverAddress, - remoteChainSelector, - originalSender: deployer.address, - amount: toNano('2'), - localToken: jettonMinter.address, - }), - }), + request: TokenPool_LockOrBurnInV1.create({ + transfer: TokenPool_Transfer.create({ + id: 44n, + details: TokenPool_TransferDetails.create({ + receiver: receiverAddress, + remoteChainSelector, + originalSender: deployer.address, + amount: toNano('2'), + localToken: jettonMinter.address, }), - requestedFinalityConfig: 0n, - tokenArgs: null, - replyTo: deployer.address, }), - ), - }, - }) - - expect(result.transactions).toHaveTransaction({ - from: poolWallet.address, - to: lockReleasePool.address, - success: false, - }) - }) - - it('reverts lockOrBurn when forward payload is malformed', async () => { - const onRampWallet = await userWallet(jettonSender.address) - const poolWallet = await userWallet(lockReleasePool.address) - - const result = await jettonSender.sendJettonsExtended(deployer.getSender(), { - value: toNano('2'), - message: { - queryId: 45n, - amount: toNano('1'), - destination: lockReleasePool.address, - customPayload: beginCell().storeBit(1).endCell(), - forwardTonAmount: toNano('0.2'), - forwardPayload: beginCell().storeUint(0, 32).endCell(), - }, + }), + requestedFinalityConfig: 0n, + tokenArgs: null, + replyTo: deployer.address, + }) + const forwardPayload = TokenPool_LockOrBurnForwardPayload.create({ + originalSender: deployer.address, + requestMsg, + prepared: TokenPool_LockOrBurnPrepared.create({ + feeAmount: 0n, + destTokenAmount: toNano('2'), + out: TokenPool_LockOrBurnOutV1.create({ + destTokenAddress, + destPoolData: Cell.EMPTY, + }), + }), + }) + + const result = await jettonSender.sendJettonsExtended(deployer.getSender(), { + value: toNano('2'), + message: { + queryId: 44n, + amount: toNano('3'), + destination: lockReleasePool.address, + customPayload: beginCell().storeBit(1).endCell(), + forwardTonAmount: toNano('0.2'), + forwardPayload: TokenPool_LockOrBurnForwardPayload.toCell(forwardPayload), + }, + }) + + expect(result.transactions).toHaveTransaction({ + from: poolWallet.address, + to: lockReleasePool.address, + success: true, + }) + expect(result.transactions).toHaveTransaction({ + from: lockReleasePool.address, + to: poolWallet.address, + success: true, + op: 0x0f8a7ea5, // AskToTransfer + }) + expect(result.transactions).toHaveTransaction({ + from: lockReleasePool.address, + to: deployer.address, + // The Treasury test recipient does not implement this callback. Assert + // the emitted message, including that it carries the residual inbound + // value instead of consuming it in the pool. + op: TokenPool_LockOrBurnFailure.PREFIX, + value: (value) => value !== undefined && value > 0n, + body(body) { + if (!body) return false + const failure = TokenPool_LockOrBurnFailure.fromSlice(body.beginParse()) + return failure.queryId === 44n && failure.errorCode === 14920n + }, + }) + expect(await onRampWallet.getJettonBalance()).toEqual(toNano('10')) + expect(await poolWallet.getJettonBalance()).toEqual(0n) }) - expect(result.transactions).toHaveTransaction({ - from: poolWallet.address, - to: lockReleasePool.address, - success: false, + it('returns tokens without a callback when forward payload is malformed', async () => { + const onRampWallet = await userWallet(jettonSender.address) + const poolWallet = await userWallet(lockReleasePool.address) + + const result = await jettonSender.sendJettonsExtended(deployer.getSender(), { + value: toNano('2'), + message: { + queryId: 45n, + amount: toNano('1'), + destination: lockReleasePool.address, + customPayload: beginCell().storeBit(1).endCell(), + forwardTonAmount: toNano('0.2'), + forwardPayload: beginCell().storeUint(0, 32).endCell(), + }, + }) + + expect(result.transactions).toHaveTransaction({ + from: poolWallet.address, + to: lockReleasePool.address, + success: true, + }) + expect(result.transactions).toHaveTransaction({ + from: lockReleasePool.address, + to: poolWallet.address, + success: true, + op: 0x0f8a7ea5, // AskToTransfer + }) + expect(result.transactions).not.toHaveTransaction({ + from: lockReleasePool.address, + to: deployer.address, + op: TokenPool_LockOrBurnFailure.PREFIX, + }) + expect(await onRampWallet.getJettonBalance()).toEqual(toNano('10')) + expect(await poolWallet.getJettonBalance()).toEqual(0n) }) }) @@ -360,7 +459,7 @@ describe('LockReleaseTokenPool', () => { localToken: jettonMinter.address, }), }), - sourcePoolAddress: sourcePoolAddress, + sourcePoolAddress, sourcePoolData: null, offchainTokenData: null, }), @@ -374,7 +473,6 @@ describe('LockReleaseTokenPool', () => { to: lockReleasePool.address, success: false, }) - expect(await lockReleasePool.getHasPendingRelease(46n)).toBe(false) }) it('refunds inbound rate-limit capacity when a release bounces (TON-TP/5)', async () => { @@ -401,7 +499,7 @@ describe('LockReleaseTokenPool', () => { localToken: jettonMinter.address, }), }), - sourcePoolAddress: sourcePoolAddress, + sourcePoolAddress, sourcePoolData: null, offchainTokenData: null, }), @@ -416,7 +514,18 @@ describe('LockReleaseTokenPool', () => { inMessageBounced: true, success: true, }) - expect(await lockReleasePool.getHasPendingRelease(77n)).toBe(false) + + expect(result.transactions).toHaveTransaction({ + from: lockReleasePool.address, + to: deployer.address, + success: true, + op: TokenPool_ReleaseOrMintFailure.PREFIX, + body(body) { + if (!body) return false + const failure = TokenPool_ReleaseOrMintFailure.fromSlice(body.beginParse()) + return failure.queryId === 77n + }, + }) // Consumed capacity (5) was refunded: the bucket is restored to its starting balance. const after = await lockReleasePool.getCurrentRateLimiterState(remoteChainSelector, false) @@ -426,6 +535,36 @@ describe('LockReleaseTokenPool', () => { it('locks tokens through a jetton transfer notification and credits the pool wallet', async () => { const onRampWallet = await userWallet(jettonSender.address) const poolWallet = await userWallet(lockReleasePool.address) + const lockOrBurn = TokenPool_LockOrBurn.create({ + queryId: 11n, + request: TokenPool_LockOrBurnInV1.create({ + transfer: TokenPool_Transfer.create({ + id: 11n, + details: TokenPool_TransferDetails.create({ + receiver: receiverAddress, + remoteChainSelector, + originalSender: deployer.address, + amount: toNano('3'), + localToken: jettonMinter.address, + }), + }), + }), + requestedFinalityConfig: 0n, + tokenArgs: null, + replyTo: deployer.address, + }) + const forwardPayload = TokenPool_LockOrBurnForwardPayload.create({ + originalSender: deployer.address, + requestMsg: lockOrBurn, + prepared: TokenPool_LockOrBurnPrepared.create({ + feeAmount: 0n, + destTokenAmount: toNano('3'), + out: TokenPool_LockOrBurnOutV1.create({ + destTokenAddress, + destPoolData: Cell.EMPTY, + }), + }), + }) const result = await jettonSender.sendJettonsExtended(deployer.getSender(), { value: toNano('2'), @@ -435,26 +574,7 @@ describe('LockReleaseTokenPool', () => { destination: lockReleasePool.address, customPayload: beginCell().storeBit(1).endCell(), forwardTonAmount: toNano('0.2'), - forwardPayload: TokenPool_LockOrBurn.toCell( - TokenPool_LockOrBurn.create({ - queryId: 11n, - request: TokenPool_LockOrBurnInV1.create({ - transfer: TokenPool_Transfer.create({ - id: 11n, - details: TokenPool_TransferDetails.create({ - receiver: receiverAddress, - remoteChainSelector, - originalSender: deployer.address, - amount: toNano('3'), - localToken: jettonMinter.address, - }), - }), - }), - requestedFinalityConfig: 0n, - tokenArgs: null, - replyTo: deployer.address, - }), - ), + forwardPayload: TokenPool_LockOrBurnForwardPayload.toCell(forwardPayload), }, }) @@ -467,7 +587,7 @@ describe('LockReleaseTokenPool', () => { expect(await poolWallet.getJettonBalance()).toEqual(toNano('3')) }) - it('releases tokens from pool custody after off-ramp request and clears pending state on confirmation', async () => { + it('releases tokens from pool custody after off-ramp request and finalizes through the executor notification', async () => { const poolWallet = await userWallet(lockReleasePool.address) const recipientWallet = await userWallet(recipient.address) @@ -500,7 +620,7 @@ describe('LockReleaseTokenPool', () => { localToken: jettonMinter.address, }), }), - sourcePoolAddress: sourcePoolAddress, + sourcePoolAddress, sourcePoolData: null, offchainTokenData: null, }), @@ -517,7 +637,6 @@ describe('LockReleaseTokenPool', () => { expect(await recipientWallet.getJettonBalance()).toEqual(toNano('2')) expect(await poolWallet.getJettonBalance()).toEqual(toNano('3')) - expect(await lockReleasePool.getHasPendingRelease(22n)).toBe(false) expect(result.transactions).toHaveTransaction({ from: lockReleasePool.address, @@ -532,15 +651,92 @@ describe('LockReleaseTokenPool', () => { }) }) + it('releases tokens with null replyTo without emitting a response message', async () => { + const poolWallet = await userWallet(lockReleasePool.address) + const recipientWallet = await userWallet(recipient.address) + + await jettonMinter.sendMint(deployer.getSender(), { + value: toNano('1'), + message: { + queryId: 0n, + destination: lockReleasePool.address, + tonAmount: toNano('0.05'), + jettonAmount: toNano('4'), + from: deployer.address, + responseDestination: deployer.address, + forwardTonAmount: 0n, + }, + }) + + const result = await lockReleasePool.sendTokenPoolReleaseOrMint( + offRamp.getSender(), + toNano('0.4'), + { + queryId: 223n, + request: TokenPool_ReleaseOrMintInV1.create({ + transfer: TokenPool_Transfer.create({ + id: 223n, + details: TokenPool_TransferDetails.create({ + originalSender: sourcePoolAddress, + remoteChainSelector, + receiver: recipient.address, + amount: toNano('1'), + localToken: jettonMinter.address, + }), + }), + sourcePoolAddress, + sourcePoolData: null, + offchainTokenData: null, + }), + requestedFinalityConfig: 0n, + replyTo: null, + }, + ) + + expect(await recipientWallet.getJettonBalance()).toEqual(toNano('1')) + expect(await poolWallet.getJettonBalance()).toEqual(toNano('3')) + + const releaseResponses = result.transactions.filter((tx: any) => { + const body = tx.inMessage?.body + if (!body) { + return false + } + + const slice = body.beginParse() + if (slice.remainingBits < 32) { + return false + } + + return ( + tx.inMessage?.info?.src?.equals?.(lockReleasePool.address) && + slice.preloadUint(32) === TokenPool_ReleaseOrMintFinished.PREFIX + ) + }) + expect(releaseResponses.length).toBe(0) + }) + + it('rejects forged executor forward notifications', async () => { + const forged = await recipient.send({ + to: lockReleasePool.address, + value: toNano('0.1'), + bounce: false, + body: buildSpoofedExecutorForwardNotification(recipient.address), + }) + + expect(forged.transactions).toHaveTransaction({ + from: recipient.address, + to: lockReleasePool.address, + success: false, + }) + }) + it('mirrors cursed state locally and blocks release while cursed', async () => { const curseUpdate = await lockReleasePool.sendTokenPoolSetCursedSubjects( deployer.getSender(), toNano('0.2'), { queryId: 901n, - cursedSubjects: CursedSubjects.create({ - data: new Set([remoteChainSelector]), - }), + cursedSubjects: CursedSubjects.create({ data: new Set([remoteChainSelector]) }), }, ) diff --git a/contracts/tests/ccip/pools/TokenPool.behavior.ts b/contracts/tests/ccip/pools/TokenPool.behavior.ts index 979ae8ee2..df382eefa 100644 --- a/contracts/tests/ccip/pools/TokenPool.behavior.ts +++ b/contracts/tests/ccip/pools/TokenPool.behavior.ts @@ -15,6 +15,8 @@ import { TokenPool_RampUpdate, TokenPool_RateLimitConfigPair, TokenPool_ReleaseOrMint, + TokenPool_ReleaseOrMintFailure, + TokenPool_ReleaseOrMintFinished, TokenPool_ReleaseOrMintForwardPayload, TokenPool_ReleaseOrMintInV1, TokenPool_ReleaseOrMintOutV1, @@ -38,6 +40,10 @@ export type TokenPoolBehaviorContext = { localToken: Address } +export type TokenPoolBehaviorHooks = { + setup?: (ctx: TokenPoolBehaviorContext) => Promise +} + function releaseRequest( ctx: TokenPoolBehaviorContext, overrides: Partial = {}, @@ -63,7 +69,15 @@ function releaseRequest( export function runTokenPoolBehaviorTests( name: string, setup: () => Promise, + hooks: TokenPoolBehaviorHooks = {}, ) { + const baseSetup = setup + setup = async () => { + const ctx = await baseSetup() + await hooks.setup?.(ctx) + return ctx + } + describe(`${name} TokenPool behavior`, () => { it('mirrors ramp access and supported chain state after setup', async () => { const ctx = await setup() @@ -124,9 +138,7 @@ export function runTokenPoolBehaviorTests( await ctx.pool.sendTokenPoolSetCursedSubjects(ctx.deployer.getSender(), toNano('0.2'), { queryId: 901n, - cursedSubjects: CursedSubjects.create({ - data: new Set([ctx.remoteChainSelector]), - }), + cursedSubjects: CursedSubjects.create({ data: new Set([ctx.remoteChainSelector]) }), }) expect(await ctx.pool.getVerifyNotCursed(ctx.remoteChainSelector)).toBe(false) @@ -216,9 +228,7 @@ export function runTokenPoolBehaviorTests( toNano('0.2'), { queryId: 904n, - cursedSubjects: CursedSubjects.create({ - data: new Set([ctx.remoteChainSelector]), - }), + cursedSubjects: CursedSubjects.create({ data: new Set([ctx.remoteChainSelector]) }), }, ) @@ -233,17 +243,13 @@ export function runTokenPoolBehaviorTests( const ctx = await setup() await ctx.pool.sendTokenPoolSetCursedSubjects(ctx.deployer.getSender(), toNano('0.2'), { queryId: 901n, - cursedSubjects: CursedSubjects.create({ - data: new Set([ctx.remoteChainSelector]), - }), + cursedSubjects: CursedSubjects.create({ data: new Set([ctx.remoteChainSelector]) }), }) expect(await ctx.pool.getVerifyNotCursed(ctx.remoteChainSelector)).toBe(false) await ctx.pool.sendTokenPoolSetCursedSubjects(ctx.deployer.getSender(), toNano('0.2'), { queryId: 902n, - cursedSubjects: CursedSubjects.create({ - data: new Set(), - }), + cursedSubjects: CursedSubjects.create({ data: new Set() }), }) expect(await ctx.pool.getVerifyNotCursed(ctx.remoteChainSelector)).toBe(true) }) @@ -519,6 +525,88 @@ export function runTokenPoolBehaviorTests( }) expect(await ctx.pool.getIsSupportedChain(ctx.remoteChainSelector)).toBe(true) }) + + it('reuses the same caller queryId safely across repeated release continuations', async () => { + const ctx = await setup() + const amount = toNano('1') + const queryId = 700n + + const repeatedRequest = { + queryId, + request: releaseRequest(ctx, { + transfer: TokenPool_Transfer.create({ + id: queryId, + details: TokenPool_TransferDetails.create({ + originalSender: ctx.sourcePoolAddress, + remoteChainSelector: ctx.remoteChainSelector, + receiver: ctx.recipient.address, + amount, + localToken: ctx.localToken, + }), + }), + }), + requestedFinalityConfig: 0n, + replyTo: ctx.deployer.address, + } + + const first = await ctx.pool.sendTokenPoolReleaseOrMint( + ctx.offRamp.getSender(), + toNano('0.6'), + repeatedRequest, + ) + const second = await ctx.pool.sendTokenPoolReleaseOrMint( + ctx.offRamp.getSender(), + toNano('0.6'), + repeatedRequest, + ) + + const allTransactions = [...first.transactions, ...second.transactions] + const completions = allTransactions.filter((tx: any) => { + const body = tx.inMessage?.body + if (!body) { + return false + } + + const slice = body.beginParse() + if (slice.remainingBits < 32) { + return false + } + + if ( + !tx.inMessage?.info?.src?.equals?.(ctx.pool.address) || + slice.preloadUint(32) !== TokenPool_ReleaseOrMintFinished.PREFIX + ) { + return false + } + + const response = TokenPool_ReleaseOrMintFinished.fromSlice(slice) + return response.queryId === queryId && response.out.destinationAmount === amount + }) + + const failures = allTransactions.filter((tx: any) => { + const body = tx.inMessage?.body + if (!body) { + return false + } + + const slice = body.beginParse() + if (slice.remainingBits < 32) { + return false + } + + if ( + !tx.inMessage?.info?.src?.equals?.(ctx.pool.address) || + slice.preloadUint(32) !== TokenPool_ReleaseOrMintFailure.PREFIX + ) { + return false + } + + return TokenPool_ReleaseOrMintFailure.fromSlice(slice).queryId === queryId + }) + + expect(completions).toHaveLength(2) + expect(failures).toHaveLength(0) + }) }) } @@ -578,12 +666,12 @@ export function runTokenPoolAsyncHookBehaviorTests( originalSender: ctx.deployer.address, requestMsg: TokenPool_LockOrBurn.create({ queryId: 0n, - request: request, + request, requestedFinalityConfig: 0n, tokenArgs: null, replyTo: null, }), - prepared: prepared, + prepared, }) return TokenPool_LockOrBurnForwardPayload.toCell(fwdp) } @@ -606,11 +694,11 @@ export function runTokenPoolAsyncHookBehaviorTests( originalSender: ctx.offRamp.address, requestMsg: TokenPool_ReleaseOrMint.create({ queryId: 0n, - request: request, + request, requestedFinalityConfig: 0n, replyTo: null, }), - prepared: prepared, + prepared, }) return TokenPool_ReleaseOrMintForwardPayload.toCell(fwdp) } @@ -645,7 +733,7 @@ export function runTokenPoolAsyncHookBehaviorTests( const result = await ctx.pool.sendTokenPoolLockOrBurn(ctx.deployer.getSender(), toNano('1'), { queryId: 2n, - request: request, + request, requestedFinalityConfig: 0n, tokenArgs: null, replyTo: ctx.deployer.address, @@ -675,7 +763,7 @@ export function runTokenPoolAsyncHookBehaviorTests( const result = await ctx.pool.sendTokenPoolLockOrBurn(ctx.deployer.getSender(), toNano('1'), { queryId: 1n, - request: request, + request, requestedFinalityConfig: 0n, tokenArgs: null, replyTo: ctx.deployer.address, @@ -715,7 +803,7 @@ export function runTokenPoolAsyncHookBehaviorTests( toNano('1'), { queryId: 2n, - request: request, + request, requestedFinalityConfig: 0n, replyTo: ctx.offRamp.address, }, @@ -748,7 +836,7 @@ export function runTokenPoolAsyncHookBehaviorTests( toNano('1'), { queryId: 1n, - request: request, + request, requestedFinalityConfig: 0n, replyTo: ctx.offRamp.address, }, @@ -773,9 +861,53 @@ export function runTokenPoolAsyncHookBehaviorTests( from: ctx.pool.address, to: ctx.offRamp.address, success: true, + op: TokenPool_ReleaseOrMintFailure.PREFIX, + body(body) { + if (!body) return false + const failure = TokenPool_ReleaseOrMintFailure.fromSlice(body.beginParse()) + return failure.queryId === 1n + }, }) }) + it('does not emit ReleaseOrMintFailure when async postflight fails and replyTo is null', async () => { + const ctx = await setup() + + const request = releaseRequest(ctx) + + const result = await ctx.pool.sendTokenPoolReleaseOrMint( + ctx.offRamp.getSender(), + toNano('1'), + { + queryId: 3n, + request, + requestedFinalityConfig: 0n, + replyTo: null, + }, + ) + + expect(result.transactions).toHaveTransaction({ + from: ctx.hooks.address, + to: ctx.pool.address, + success: true, + }) + + const failures = result.transactions.filter((tx: any) => { + const body = tx.inMessage?.body + if (!body) { + return false + } + + const slice = body.beginParse() + return ( + slice.remainingBits >= 32 && + slice.preloadUint(32) === TokenPool_ReleaseOrMintFailure.PREFIX + ) + }) + + expect(failures).toHaveLength(0) + }) + // === Inline mode (replyTo = null for LockOrBurn) === it('processes LockOrBurn inline when replyTo is null', async () => { @@ -785,7 +917,7 @@ export function runTokenPoolAsyncHookBehaviorTests( const result = await ctx.pool.sendTokenPoolLockOrBurn(ctx.deployer.getSender(), toNano('1'), { queryId: 100n, - request: request, + request, requestedFinalityConfig: 0n, tokenArgs: null, replyTo: null, @@ -808,7 +940,7 @@ export function runTokenPoolAsyncHookBehaviorTests( toNano('1'), { queryId: 100n, - request: request, + request, requestedFinalityConfig: 0n, replyTo: ctx.deployer.address, }, diff --git a/contracts/wrappers/gen/ccip/ContextExecutor.ts b/contracts/wrappers/gen/ccip/ContextExecutor.ts index ea0f2bf6b..e92d721a1 100644 --- a/contracts/wrappers/gen/ccip/ContextExecutor.ts +++ b/contracts/wrappers/gen/ccip/ContextExecutor.ts @@ -258,14 +258,14 @@ export const ContextExecutor_Set = { /** > struct (0xcad4d1d0) ContextExecutor_Ask { > queryId: uint64 - > forwardPayload: cell + > forwardPayload: cell? > done: bool > } */ export interface ContextExecutor_Ask { readonly $: 'ContextExecutor_Ask' queryId: uint64 - forwardPayload: c.Cell + forwardPayload: c.Cell | null done: boolean } @@ -274,7 +274,7 @@ export const ContextExecutor_Ask = { create(args: { queryId?: uint64 - forwardPayload: c.Cell + forwardPayload: c.Cell | null done: boolean }): ContextExecutor_Ask { return { @@ -288,14 +288,16 @@ export const ContextExecutor_Ask = { return { $: 'ContextExecutor_Ask', queryId: s.loadUintBig(64), - forwardPayload: s.loadRef(), + forwardPayload: s.loadBoolean() ? s.loadRef() : null, done: s.loadBoolean(), } }, store(self: ContextExecutor_Ask, b: c.Builder): void { b.storeUint(0xcad4d1d0, 32); b.storeUint(self.queryId, 64); - b.storeRef(self.forwardPayload); + storeTolkNullable(self.forwardPayload, b, + (v,b) => b.storeRef(v) + ); b.storeBit(self.done); }, toCell(self: ContextExecutor_Ask): c.Cell { @@ -309,7 +311,7 @@ export const ContextExecutor_Ask = { > id: uint64 > context: Cell > forwardFrom: array
- > forwardPayload: cell + > forwardPayload: cell? > done: bool > } */ @@ -319,7 +321,7 @@ export interface ContextExecutor_Reply { id: uint64 context: T forwardFrom: array - forwardPayload: c.Cell + forwardPayload: c.Cell | null done: boolean } @@ -331,7 +333,7 @@ export const ContextExecutor_Reply = { id: uint64 context: T forwardFrom: array - forwardPayload: c.Cell + forwardPayload: c.Cell | null done: boolean }): ContextExecutor_Reply { return { @@ -506,7 +508,7 @@ function calculateDeployedAddress(code: c.Cell, data: c.Cell, options: DeployedA } export class ContextExecutor implements c.Contract { - static CodeCell = c.Cell.fromBase64('te6ccgECFAEAA5AAART/APSkE/S88sgLAQIBYgIDAgLOBAUCASAMDQIBIAYHAElO2i7ftsIjJwIW+Ikly5jhAhpFIzb4EkxwWVXwR/2zHg6F8EcIAas+JHyQO1E0NM/+kjUbwAB0wf0BJMhbrOOEgHQ9ASa+khQVW+MJMcAFeYwAegxIm+IWLryidH4kviX+Jj4k3D4OviU+JUqyM7J8AHjAl8EhA8BxwDy9IAgC9wg0NcsIicw92TjAtcsJlamjoSOZmxh0z/U1woAyM+ST5bvFhPLPyfPCz8lzxQkb4hzbVRyIakGjhsByPQAUyG2CFEioSKZU5BvgVj6UgGk5AHJAqHkMDECywf0AMwhzwoAycjPhQgT+lJxzwtuEszJAYMGgEDjBPsAf+CAJCgB2NALIyz/6Uswhb4hzbVRyIakGjhsByPQAUyG2CFEioSKZU2BvgVj6UgGk5AHJAqHkMDEzzwsH9ADJ7VQB/jhfBjKCAKpQXccF8vTTP9RvAAHTB/QFkyBus44Q0PQEmvpIUERvjCPHABTmMOgwIW+IuvKJcMjLP8nIz5JPlu8WFMs/Js8LPyLPFCFviHNtVHIhqQaOGwHI9ABTIbYIUSKhIplTYG+BWPpSAaTkAckCoeQwMQLLB/QAE8zPgckLAOwwVHqYU6nwAo5mBsj6UlAF+gIT9AAB+gLLP8sfzMnIz5FW0ErmJc8LPyPPFCJviHNtVHIhqQaOGwHI9ABTIbYIUSKhIplTcG+BWPpSAaTkAckCoeQwMQLLB/QAzMnIz4UIUkD6UnHPC27MyYBA+wB/4Gxh0McAACbIz4UIFPpScc8LbhPMyYBA+wB/AgEgDg8Abbzsh2omhpn/0kGOoYt4AA6YP6AkmQt1nHCQDoegJNfSQoKrfGEmOACvMYAPQYkTfEGYFdeUTowCAnIQEQIBIBITAF6pX40I2xpbmsuY2hhaW4udG9uLmNjaXAuQ29udGV4dEV4ZWN1dG9ygi1MC4xLjCABsqFrtRNDTPzH6SDHUMW8AAdMH9ASTIW6zjhIB0PQEmvpIUFVvjCTHABXmMAHoMSJviFi68onRAG20o72omhpn5j9JGoYt4AA6YP6AkmQt1nHCQDoegJNfSQoKrfGEmOACvMYAPQYkTfEGYFdeUTowAG20AX2omhpn5j9JBjqN4AA6YP6AkmQt1nHCQDoegJNfSQoKrfGEmOACvMYAPQYkTfEGYFdeUTow'); + static CodeCell = c.Cell.fromBase64('te6ccgECFQEAA5kAART/APSkE/S88sgLAQIBYgIDAgLOBAUCASANDgIBIAYHAElO2i7ftsIjJwIW+Ikly5jhAhpFIzb4EkxwWVXwR/2zHg6F8EcIAas+JHyQO1E0NM/+kjUbwAB0wf0BJMhbrOOEgHQ9ASa+khQVW+MJMcAFeYwAegxIm+IWLryidH4kviX+Jj4k3D4OviU+JUqyM7J8AHjAl8EhA8BxwDy9IAgDRwg0NcsIicw92TjAtcsJlamjoTjAjBUephTqfAC4wJsYdDHAIAkKCwB2NALIyz/6Uswhb4hzbVRyIakGjhsByPQAUyG2CFEioSKZU2BvgVj6UgGk5AHJAqHkMDEzzwsH9ADJ7VQB/jhfBjKCAKpQXccF8vTTP9RvAAHTB/QFkyBus44Q0PQEmvpIUERvjCPHABTmMOgwIW+IuvKJcMjLP8nIz5JPlu8WFMs/Js8LPyLPFCFviHNtVHIhqQaOGwHI9ABTIbYIUSKhIplTYG+BWPpSAaTkAckCoeQwMQLLB/QAE/QAz4EMANBsYdM/9ATXCgDIz5JPlu8WE8s/J88LPyXPFCRviHNtVHIhqQaOGwHI9ABTIbYIUSKhIplTkG+BWPpSAaTkAckCoeQwMQLLB/QA9AAhzwoAycjPhQgT+lJxzwtuEszJAYMGgEDjBPsAfwDWbwAHyPpSUAb6AhT0AFj6Ass/yx/MycjPkVbQSuYmzws/JM8UI2+Ic21UciGpBo4bAcj0AFMhtghRIqEimVOAb4FY+lIBpOQByQKh5DAxNc8LBxP0ABLMycjPhQhSQPpScc8LbszJgED7AH8AKMnIz4UIFPpScc8LbhPMyYBA+wB/AgEgDxAAbbzsh2omhpn/0kGOoYt4AA6YP6AkmQt1nHCQDoegJNfSQoKrfGEmOACvMYAPQYkTfEGYFdeUTowCAnIREgIBIBMUAF6pX40I2xpbmsuY2hhaW4udG9uLmNjaXAuQ29udGV4dEV4ZWN1dG9ygi1MC4xLjCABsqFrtRNDTPzH6SDHUMW8AAdMH9ASTIW6zjhIB0PQEmvpIUFVvjCTHABXmMAHoMSJviFi68onRAG20o72omhpn5j9JGoYt4AA6YP6AkmQt1nHCQDoegJNfSQoKrfGEmOACvMYAPQYkTfEGYFdeUTowAG20AX2omhpn5j9JBjqN4AA6YP6AkmQt1nHCQDoegJNfSQoKrfGEmOACvMYAPQYkTfEGYFdeUTow'); static Errors = { 'ContextExecutor_Error.OnlyCallableByOwner': 43600, @@ -566,7 +568,7 @@ export class ContextExecutor implements c.Contract { static createCellOfContextExecutorAsk(body: { queryId?: uint64 - forwardPayload: c.Cell + forwardPayload: c.Cell | null done: boolean }) { return ContextExecutor_Ask.toCell(ContextExecutor_Ask.create(body)); @@ -611,7 +613,7 @@ export class ContextExecutor implements c.Contract { async sendContextExecutorAsk(provider: ContractProvider, via: Sender, msgValue: coins, body: { queryId?: uint64 - forwardPayload: c.Cell + forwardPayload: c.Cell | null done: boolean }, extraOptions?: ExtraSendOptions) { return provider.internal(via, { diff --git a/contracts/wrappers/gen/ccip/MockTokenPool.ts b/contracts/wrappers/gen/ccip/MockTokenPool.ts index 9c2f6a890..5ca98e43e 100644 --- a/contracts/wrappers/gen/ccip/MockTokenPool.ts +++ b/contracts/wrappers/gen/ccip/MockTokenPool.ts @@ -4273,7 +4273,7 @@ function calculateDeployedAddress(code: c.Cell, data: c.Cell, options: DeployedA } export class MockTokenPool implements c.Contract { - static CodeCell = c.Cell.fromBase64('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'); + static CodeCell = c.Cell.fromBase64('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'); static Errors = { 'Common_Error.CrossChainAddressOutOfRange': 5, @@ -4291,6 +4291,8 @@ export class MockTokenPool implements c.Contract { 'TokenPool_Error.InvalidRemotePoolForChain': 14912, 'TokenPool_Error.InvalidRemoteChainDecimals': 14913, 'TokenPool_Error.OverflowDetected': 14914, + 'TokenPool_Error.MissingTransferInitiator': 14919, + 'TokenPool_Error.AmountMismatch': 14920, 'TokenPool_Error.InvalidRequestedFinality': 14921, 'RateLimiter_Error.BucketOverfilled': 16300, 'RateLimiter_Error.TokenMaxCapacityExceeded': 16301, diff --git a/contracts/wrappers/gen/ccip/pools/BurnMintTokenPool.ts b/contracts/wrappers/gen/ccip/pools/BurnMintTokenPool.ts index e78f49fab..d0afe013c 100644 --- a/contracts/wrappers/gen/ccip/pools/BurnMintTokenPool.ts +++ b/contracts/wrappers/gen/ccip/pools/BurnMintTokenPool.ts @@ -11,6 +11,8 @@ import { beginCell, ContractProvider, Sender, SendMode } from '@ton/core'; type RemainingBitsAndRefs = c.Slice +type array = T[] + // TypeScript wrappers flatten a TVM linked list `[1 [2 [3 null]]]` to `[1 2 3]` type lisp_list = T[] @@ -106,6 +108,35 @@ function storeTolkNullable(v: T | null, b: c.Builder, storeFn_T: StoreCallbac } } +function storeArrayOf(v: array, b: c.Builder, storeFn_T: StoreCallback): void { + // the compiler stores array in chunks; in TypeScript, for simplicity, store "1 elem = 1 ref" + let tail = null as c.Cell | null; + for (let i = 0; i < v.length; ++i) { + let chunkB = beginCell().storeMaybeRef(tail); + storeFn_T(v[v.length - 1 - i], chunkB); + tail = chunkB.endCell(); + } + b.storeUint(v.length, 8); + b.storeMaybeRef(tail); +} + +function loadArrayOf(s: c.Slice, loadFn_T: LoadCallback): array { + let len = s.loadUint(8); + let head = s.loadMaybeRef(); + let outArr = [] as array; + while (head != null) { + let s = head.beginParse(); + head = s.loadMaybeRef(); + while (s.remainingBits || s.remainingRefs) { + outArr.push(loadFn_T(s)); + } + } + if (len !== outArr.length) { + throw new Error(`mismatch array binary data: expected ${len} elements, got ${outArr.length}`); + } + return outArr; +} + function storeLispListOf(v: lisp_list, b: c.Builder, storeFn_T: StoreCallback): void { let tail = c.Cell.EMPTY; for (let i = 0; i < v.length; ++i) { @@ -278,6 +309,8 @@ function invokeCustomUnpackFromSlice(typeName: string, s: c.Slice): T { type coins = bigint +type int32 = bigint + type uint8 = bigint type uint16 = bigint type uint32 = bigint @@ -286,6 +319,34 @@ type uint120 = bigint type uint128 = bigint type uint256 = bigint +type varuint32 = bigint + +/** + > type ExtraCurrenciesMap = map + */ +export type ExtraCurrenciesMap = Map + +export const ExtraCurrenciesMap = { + fromSlice(s: c.Slice): ExtraCurrenciesMap { + return dictToMap(c.Dictionary.load(c.Dictionary.Keys.BigInt(32), createDictionaryValue( + (s) => s.loadVarUintBig(5), + (v,b) => b.storeVarUint(v, 5) + ), s)); + }, + store(self: ExtraCurrenciesMap, b: c.Builder): void { + b.storeDict(mapToDict(self, c.Dictionary.Keys.BigInt(32), createDictionaryValue( + (s) => s.loadVarUintBig(5), + (v,b) => b.storeVarUint(v, 5) + )), c.Dictionary.Keys.BigInt(32), createDictionaryValue( + (s) => s.loadVarUintBig(5), + (v,b) => b.storeVarUint(v, 5) + )); + }, + toCell(self: ExtraCurrenciesMap): c.Cell { + return makeCellFrom(self, ExtraCurrenciesMap.store); + } +} + /** > type SnakedCell = cell */ @@ -658,44 +719,6 @@ export const InternalTransferStep = { } } -/** - > struct (0xd53276db) ReturnExcessesBack { - > queryId: uint64 - > } - */ -export interface ReturnExcessesBack { - readonly $: 'ReturnExcessesBack' - queryId: uint64 -} - -export const ReturnExcessesBack = { - PREFIX: 0xd53276db, - - create(args: { - queryId?: uint64 - }): ReturnExcessesBack { - return { - $: 'ReturnExcessesBack', - ...args, - queryId: args.queryId ?? 0n - } - }, - fromSlice(s: c.Slice): ReturnExcessesBack { - loadAndCheckPrefix32(s, 0xd53276db, 'ReturnExcessesBack'); - return { - $: 'ReturnExcessesBack', - queryId: s.loadUintBig(64), - } - }, - store(self: ReturnExcessesBack, b: c.Builder): void { - b.storeUint(0xd53276db, 32); - b.storeUint(self.queryId, 64); - }, - toCell(self: ReturnExcessesBack): c.Cell { - return makeCellFrom(self, ReturnExcessesBack.store); - } -} - /** > struct (0x595f07bc) AskToBurn { > queryId: uint64 @@ -842,6 +865,44 @@ export const ClaimMinterAdmin = { } } +/** + > struct JettonClient { + > masterAddress: address + > jettonWalletCode: cell + > } + */ +export interface JettonClient { + readonly $: 'JettonClient' + masterAddress: c.Address + jettonWalletCode: c.Cell +} + +export const JettonClient = { + create(args: { + masterAddress: c.Address + jettonWalletCode: c.Cell + }): JettonClient { + return { + $: 'JettonClient', + ...args + } + }, + fromSlice(s: c.Slice): JettonClient { + return { + $: 'JettonClient', + masterAddress: s.loadAddress(), + jettonWalletCode: s.loadRef(), + } + }, + store(self: JettonClient, b: c.Builder): void { + b.storeAddress(self.masterAddress); + b.storeRef(self.jettonWalletCode); + }, + toCell(self: JettonClient): c.Cell { + return makeCellFrom(self, JettonClient.store); + } +} + /** > type CrossChainAddress = slice */ @@ -859,6 +920,190 @@ export const CrossChainAddress = { } } +/** + > struct (0xcad4d1d0) ContextExecutor_Ask { + > queryId: uint64 + > forwardPayload: cell? + > done: bool + > } + */ +export interface ContextExecutor_Ask { + readonly $: 'ContextExecutor_Ask' + queryId: uint64 + forwardPayload: c.Cell | null + done: boolean +} + +export const ContextExecutor_Ask = { + PREFIX: 0xcad4d1d0, + + create(args: { + queryId?: uint64 + forwardPayload: c.Cell | null + done: boolean + }): ContextExecutor_Ask { + return { + $: 'ContextExecutor_Ask', + ...args, + queryId: args.queryId ?? 0n + } + }, + fromSlice(s: c.Slice): ContextExecutor_Ask { + loadAndCheckPrefix32(s, 0xcad4d1d0, 'ContextExecutor_Ask'); + return { + $: 'ContextExecutor_Ask', + queryId: s.loadUintBig(64), + forwardPayload: s.loadBoolean() ? s.loadRef() : null, + done: s.loadBoolean(), + } + }, + store(self: ContextExecutor_Ask, b: c.Builder): void { + b.storeUint(0xcad4d1d0, 32); + b.storeUint(self.queryId, 64); + storeTolkNullable(self.forwardPayload, b, + (v,b) => b.storeRef(v) + ); + b.storeBit(self.done); + }, + toCell(self: ContextExecutor_Ask): c.Cell { + return makeCellFrom(self, ContextExecutor_Ask.store); + } +} + +/** + > struct (0x93e5bbc5) ContextExecutor_Reply { + > queryId: uint64 + > id: uint64 + > context: Cell + > forwardFrom: array
+ > forwardPayload: cell? + > done: bool + > } + */ +export interface ContextExecutor_Reply { + readonly $: 'ContextExecutor_Reply' + queryId: uint64 + id: uint64 + context: T + forwardFrom: array + forwardPayload: c.Cell | null + done: boolean +} + +export const ContextExecutor_Reply = { + PREFIX: 0x93e5bbc5, + + create(args: { + queryId?: uint64 + id: uint64 + context: T + forwardFrom: array + forwardPayload: c.Cell | null + done: boolean + }): ContextExecutor_Reply { + return { + $: 'ContextExecutor_Reply', + ...args, + queryId: args.queryId ?? 0n + } + }, +} + +/** + > struct (0x55b412b9) ContextExecutor_ForwardNotification { + > id: uint64 + > context: Cell + > forwardFrom: array
+ > message: Cell + > } + */ +export interface ContextExecutor_ForwardNotification { + readonly $: 'ContextExecutor_ForwardNotification' + id: uint64 + context: T + forwardFrom: array + message: ContextExecutor_InMessageForward +} + +export const ContextExecutor_ForwardNotification = { + PREFIX: 0x55b412b9, + + create(args: { + id: uint64 + context: T + forwardFrom: array + message: ContextExecutor_InMessageForward + }): ContextExecutor_ForwardNotification { + return { + $: 'ContextExecutor_ForwardNotification', + ...args + } + }, +} + +/** + > struct ContextExecutor_InMessageForward { + > senderAddress: address + > valueCoins: coins + > valueExtra: ExtraCurrenciesMap + > originalForwardFee: coins + > createdLt: uint64 + > createdAt: uint32 + > body: cell + > } + */ +export interface ContextExecutor_InMessageForward { + readonly $: 'ContextExecutor_InMessageForward' + senderAddress: c.Address + valueCoins: coins + valueExtra: ExtraCurrenciesMap + originalForwardFee: coins + createdLt: uint64 + createdAt: uint32 + body: c.Cell +} + +export const ContextExecutor_InMessageForward = { + create(args: { + senderAddress: c.Address + valueCoins: coins + valueExtra: ExtraCurrenciesMap + originalForwardFee: coins + createdLt: uint64 + createdAt: uint32 + body: c.Cell + }): ContextExecutor_InMessageForward { + return { + $: 'ContextExecutor_InMessageForward', + ...args + } + }, + fromSlice(s: c.Slice): ContextExecutor_InMessageForward { + return { + $: 'ContextExecutor_InMessageForward', + senderAddress: s.loadAddress(), + valueCoins: s.loadCoins(), + valueExtra: ExtraCurrenciesMap.fromSlice(s), + originalForwardFee: s.loadCoins(), + createdLt: s.loadUintBig(64), + createdAt: s.loadUintBig(32), + body: s.loadRef(), + } + }, + store(self: ContextExecutor_InMessageForward, b: c.Builder): void { + b.storeAddress(self.senderAddress); + b.storeCoins(self.valueCoins); + ExtraCurrenciesMap.store(self.valueExtra, b); + b.storeCoins(self.originalForwardFee); + b.storeUint(self.createdLt, 64); + b.storeUint(self.createdAt, 32); + b.storeRef(self.body); + }, + toCell(self: ContextExecutor_InMessageForward): c.Cell { + return makeCellFrom(self, ContextExecutor_InMessageForward.store); + } +} + /** > struct CursedSubjects { > data: map @@ -4432,110 +4677,131 @@ export const BurnMintTokenPool_ClaimMinterAdmin = { } /** - > struct BurnMintTokenPool_PendingBurn { - > forwardPayload: TokenPool_LockOrBurnForwardPayload - > expectedSender: address + > struct (0xba302a47) BurnMintTokenPool_BurnContext { + > wallet: address + > forwardPayload: Cell > } */ -export interface BurnMintTokenPool_PendingBurn { - readonly $: 'BurnMintTokenPool_PendingBurn' +export interface BurnMintTokenPool_BurnContext { + readonly $: 'BurnMintTokenPool_BurnContext' + wallet: c.Address forwardPayload: TokenPool_LockOrBurnForwardPayload - expectedSender: c.Address } -export const BurnMintTokenPool_PendingBurn = { +export const BurnMintTokenPool_BurnContext = { + PREFIX: 0xba302a47, + create(args: { + wallet: c.Address forwardPayload: TokenPool_LockOrBurnForwardPayload - expectedSender: c.Address - }): BurnMintTokenPool_PendingBurn { + }): BurnMintTokenPool_BurnContext { return { - $: 'BurnMintTokenPool_PendingBurn', + $: 'BurnMintTokenPool_BurnContext', ...args } }, - fromSlice(s: c.Slice): BurnMintTokenPool_PendingBurn { + fromSlice(s: c.Slice): BurnMintTokenPool_BurnContext { + loadAndCheckPrefix32(s, 0xba302a47, 'BurnMintTokenPool_BurnContext'); return { - $: 'BurnMintTokenPool_PendingBurn', - forwardPayload: TokenPool_LockOrBurnForwardPayload.fromSlice(s), - expectedSender: s.loadAddress(), + $: 'BurnMintTokenPool_BurnContext', + wallet: s.loadAddress(), + forwardPayload: loadCellRef(s, TokenPool_LockOrBurnForwardPayload.fromSlice), } }, - store(self: BurnMintTokenPool_PendingBurn, b: c.Builder): void { - TokenPool_LockOrBurnForwardPayload.store(self.forwardPayload, b); - b.storeAddress(self.expectedSender); + store(self: BurnMintTokenPool_BurnContext, b: c.Builder): void { + b.storeUint(0xba302a47, 32); + b.storeAddress(self.wallet); + storeCellRef(self.forwardPayload, b, TokenPool_LockOrBurnForwardPayload.store); }, - toCell(self: BurnMintTokenPool_PendingBurn): c.Cell { - return makeCellFrom(self, BurnMintTokenPool_PendingBurn.store); + toCell(self: BurnMintTokenPool_BurnContext): c.Cell { + return makeCellFrom(self, BurnMintTokenPool_BurnContext.store); } } /** - > struct BurnMintTokenPool_PendingMint { - > replyTo: address? - > request: Cell - > out: Cell - > expectedSender: address + > struct (0xb3d52361) BurnMintTokenPool_MintContext { + > forwardPayload: Cell > } */ -export interface BurnMintTokenPool_PendingMint { - readonly $: 'BurnMintTokenPool_PendingMint' - replyTo: c.Address | null - request: TokenPool_ReleaseOrMintInV1 - out: TokenPool_ReleaseOrMintOutV1 - expectedSender: c.Address +export interface BurnMintTokenPool_MintContext { + readonly $: 'BurnMintTokenPool_MintContext' + forwardPayload: TokenPool_ReleaseOrMintForwardPayload } -export const BurnMintTokenPool_PendingMint = { +export const BurnMintTokenPool_MintContext = { + PREFIX: 0xb3d52361, + create(args: { - replyTo: c.Address | null - request: TokenPool_ReleaseOrMintInV1 - out: TokenPool_ReleaseOrMintOutV1 - expectedSender: c.Address - }): BurnMintTokenPool_PendingMint { + forwardPayload: TokenPool_ReleaseOrMintForwardPayload + }): BurnMintTokenPool_MintContext { return { - $: 'BurnMintTokenPool_PendingMint', + $: 'BurnMintTokenPool_MintContext', ...args } }, - fromSlice(s: c.Slice): BurnMintTokenPool_PendingMint { + fromSlice(s: c.Slice): BurnMintTokenPool_MintContext { + loadAndCheckPrefix32(s, 0xb3d52361, 'BurnMintTokenPool_MintContext'); return { - $: 'BurnMintTokenPool_PendingMint', - replyTo: s.loadMaybeAddress(), - request: loadCellRef(s, TokenPool_ReleaseOrMintInV1.fromSlice), - out: loadCellRef(s, TokenPool_ReleaseOrMintOutV1.fromSlice), - expectedSender: s.loadAddress(), + $: 'BurnMintTokenPool_MintContext', + forwardPayload: loadCellRef(s, TokenPool_ReleaseOrMintForwardPayload.fromSlice), } }, - store(self: BurnMintTokenPool_PendingMint, b: c.Builder): void { - b.storeAddress(self.replyTo); - storeCellRef(self.request, b, TokenPool_ReleaseOrMintInV1.store); - storeCellRef(self.out, b, TokenPool_ReleaseOrMintOutV1.store); - b.storeAddress(self.expectedSender); + store(self: BurnMintTokenPool_MintContext, b: c.Builder): void { + b.storeUint(0xb3d52361, 32); + storeCellRef(self.forwardPayload, b, TokenPool_ReleaseOrMintForwardPayload.store); }, - toCell(self: BurnMintTokenPool_PendingMint): c.Cell { - return makeCellFrom(self, BurnMintTokenPool_PendingMint.store); + toCell(self: BurnMintTokenPool_MintContext): c.Cell { + return makeCellFrom(self, BurnMintTokenPool_MintContext.store); + } +} + +/** + > type BurnMintTokenPool_ContextExecutorContext = BurnMintTokenPool_BurnContext | BurnMintTokenPool_MintContext + */ +export type BurnMintTokenPool_ContextExecutorContext = + | BurnMintTokenPool_BurnContext + | BurnMintTokenPool_MintContext + +export const BurnMintTokenPool_ContextExecutorContext = { + fromSlice(s: c.Slice): BurnMintTokenPool_ContextExecutorContext { + return lookupPrefix(s, 0xba302a47, 32) ? BurnMintTokenPool_BurnContext.fromSlice(s) : + lookupPrefix(s, 0xb3d52361, 32) ? BurnMintTokenPool_MintContext.fromSlice(s) : + throwNonePrefixMatch('BurnMintTokenPool_ContextExecutorContext'); + }, + store(self: BurnMintTokenPool_ContextExecutorContext, b: c.Builder): void { + switch (self.$) { + case 'BurnMintTokenPool_BurnContext': + BurnMintTokenPool_BurnContext.store(self, b); + break; + case 'BurnMintTokenPool_MintContext': + BurnMintTokenPool_MintContext.store(self, b); + break; + } + }, + toCell(self: BurnMintTokenPool_ContextExecutorContext): c.Cell { + return makeCellFrom(self, BurnMintTokenPool_ContextExecutorContext.store); } } /** > struct Storage { > poolData: Cell - > pendingBurns: map> - > pendingMints: map> + > contextExecutorCode: cell + > contextExecutorNextId: uint64 > } */ export interface Storage { readonly $: 'Storage' poolData: TokenPool_Data - pendingBurns: Map /* = [] as map> */ - pendingMints: Map /* = [] as map> */ + contextExecutorCode: c.Cell + contextExecutorNextId: uint64 } export const Storage = { create(args: { poolData: TokenPool_Data - pendingBurns: Map /* = [] as map> */ - pendingMints: Map /* = [] as map> */ + contextExecutorCode: c.Cell + contextExecutorNextId: uint64 }): Storage { return { $: 'Storage', @@ -4546,76 +4812,20 @@ export const Storage = { return { $: 'Storage', poolData: loadCellRef(s, TokenPool_Data.fromSlice), - pendingBurns: dictToMap(c.Dictionary.load(c.Dictionary.Keys.BigUint(64), createDictionaryValue( - (s) => loadCellRef(s, BurnMintTokenPool_PendingBurn.fromSlice), - (v,b) => storeCellRef(v, b, BurnMintTokenPool_PendingBurn.store) - ), s)), - pendingMints: dictToMap(c.Dictionary.load(c.Dictionary.Keys.BigUint(64), createDictionaryValue( - (s) => loadCellRef(s, BurnMintTokenPool_PendingMint.fromSlice), - (v,b) => storeCellRef(v, b, BurnMintTokenPool_PendingMint.store) - ), s)), + contextExecutorCode: s.loadRef(), + contextExecutorNextId: s.loadUintBig(64), } }, store(self: Storage, b: c.Builder): void { storeCellRef(self.poolData, b, TokenPool_Data.store); - b.storeDict(mapToDict(self.pendingBurns, c.Dictionary.Keys.BigUint(64), createDictionaryValue( - (s) => loadCellRef(s, BurnMintTokenPool_PendingBurn.fromSlice), - (v,b) => storeCellRef(v, b, BurnMintTokenPool_PendingBurn.store) - )), c.Dictionary.Keys.BigUint(64), createDictionaryValue( - (s) => loadCellRef(s, BurnMintTokenPool_PendingBurn.fromSlice), - (v,b) => storeCellRef(v, b, BurnMintTokenPool_PendingBurn.store) - )); - b.storeDict(mapToDict(self.pendingMints, c.Dictionary.Keys.BigUint(64), createDictionaryValue( - (s) => loadCellRef(s, BurnMintTokenPool_PendingMint.fromSlice), - (v,b) => storeCellRef(v, b, BurnMintTokenPool_PendingMint.store) - )), c.Dictionary.Keys.BigUint(64), createDictionaryValue( - (s) => loadCellRef(s, BurnMintTokenPool_PendingMint.fromSlice), - (v,b) => storeCellRef(v, b, BurnMintTokenPool_PendingMint.store) - )); + b.storeRef(self.contextExecutorCode); + b.storeUint(self.contextExecutorNextId, 64); }, toCell(self: Storage): c.Cell { return makeCellFrom(self, Storage.store); } } -/** - > struct JettonClient { - > masterAddress: address - > jettonWalletCode: cell - > } - */ -export interface JettonClient { - readonly $: 'JettonClient' - masterAddress: c.Address - jettonWalletCode: c.Cell -} - -export const JettonClient = { - create(args: { - masterAddress: c.Address - jettonWalletCode: c.Cell - }): JettonClient { - return { - $: 'JettonClient', - ...args - } - }, - fromSlice(s: c.Slice): JettonClient { - return { - $: 'JettonClient', - masterAddress: s.loadAddress(), - jettonWalletCode: s.loadRef(), - } - }, - store(self: JettonClient, b: c.Builder): void { - b.storeAddress(self.masterAddress); - b.storeRef(self.jettonWalletCode); - }, - toCell(self: JettonClient): c.Cell { - return makeCellFrom(self, JettonClient.store); - } -} - /** > struct RateLimiter_Config { > isEnabled: bool @@ -4751,7 +4961,7 @@ function calculateDeployedAddress(code: c.Cell, data: c.Cell, options: DeployedA } export class BurnMintTokenPool implements c.Contract { - static CodeCell = c.Cell.fromBase64('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'); + static CodeCell = c.Cell.fromBase64('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'); static Errors = { 'Common_Error.CrossChainAddressOutOfRange': 5, @@ -4770,15 +4980,16 @@ export class BurnMintTokenPool implements c.Contract { 'TokenPool_Error.InvalidRemoteChainDecimals': 14913, 'TokenPool_Error.OverflowDetected': 14914, 'TokenPool_Error.UnsupportedOperation': 14917, + 'TokenPool_Error.MissingTransferInitiator': 14919, + 'TokenPool_Error.AmountMismatch': 14920, 'TokenPool_Error.InvalidRequestedFinality': 14921, 'RateLimiter_Error.BucketOverfilled': 16300, 'RateLimiter_Error.TokenMaxCapacityExceeded': 16301, 'RateLimiter_Error.TokenRateLimitReached': 16302, - 'BurnMintTokenPool_Error.PendingBurnAlreadyExists': 41200, - 'BurnMintTokenPool_Error.PendingMintAlreadyExists': 41202, - 'BurnMintTokenPool_Error.PendingMintNotFound': 41203, - 'BurnMintTokenPool_Error.UnexpectedBurnConfirmationSender': 41204, - 'BurnMintTokenPool_Error.UnexpectedMintConfirmationSender': 41205, + 'BurnMintTokenPool_Error.UnexpectedBurnBounce': 41200, + 'BurnMintTokenPool_Error.UnexpectedMintBounce': 41201, + 'BurnMintTokenPool_Error.ContextExecutorUnavailable': 41202, + 'BurnMintTokenPool_Error.InvalidContextExecutorReply': 41203, 'Ownable2Step_Error.OnlyCallableByOwner': 49800, 'Ownable2Step_Error.CannotTransferToSelf': 49801, 'Ownable2Step_Error.MustBeProposedOwner': 49802, @@ -4809,8 +5020,8 @@ export class BurnMintTokenPool implements c.Contract { static fromStorage(emptyStorage: { poolData: TokenPool_Data - pendingBurns: Map /* = [] as map> */ - pendingMints: Map /* = [] as map> */ + contextExecutorCode: c.Cell + contextExecutorNextId: uint64 }, deployedOptions?: DeployedAddrOptions) { const initialState = { code: deployedOptions?.overrideContractCode ?? BurnMintTokenPool.CodeCell, @@ -4965,10 +5176,44 @@ export class BurnMintTokenPool implements c.Contract { return BurnMintTokenPool_ClaimMinterAdmin.toCell(BurnMintTokenPool_ClaimMinterAdmin.create(body)); } - static createCellOfReturnExcessesBack(body: { + static createCellOfContextExecutorReplyBurnMintTokenPoolContextExecutorContext_(body: { queryId?: uint64 + id: uint64 + context: BurnMintTokenPool_ContextExecutorContext + forwardFrom: array + forwardPayload: c.Cell | null + done: boolean + }) { + return makeCellFrom>(ContextExecutor_Reply.create(body), + (v,b) => { b.storeUint(0x93e5bbc5, 32); + b.storeUint(v.queryId, 64); + b.storeUint(v.id, 64); + storeCellRef(v.context, b, BurnMintTokenPool_ContextExecutorContext.store); + storeArrayOf(v.forwardFrom, b, + (v,b) => b.storeAddress(v) + ); + storeTolkNullable(v.forwardPayload, b, + (v,b) => b.storeRef(v) + ); + b.storeBit(v.done); } + ); + } + + static createCellOfContextExecutorForwardNotificationBurnMintTokenPoolContextExecutorContext_(body: { + id: uint64 + context: BurnMintTokenPool_ContextExecutorContext + forwardFrom: array + message: ContextExecutor_InMessageForward }) { - return ReturnExcessesBack.toCell(ReturnExcessesBack.create(body)); + return makeCellFrom>(ContextExecutor_ForwardNotification.create(body), + (v,b) => { b.storeUint(0x55b412b9, 32); + b.storeUint(v.id, 64); + storeCellRef(v.context, b, BurnMintTokenPool_ContextExecutorContext.store); + storeArrayOf(v.forwardFrom, b, + (v,b) => b.storeAddress(v) + ); + storeCellRef(v.message, b, ContextExecutor_InMessageForward.store); } + ); } async sendDeploy(provider: ContractProvider, via: Sender, msgValue: coins, extraOptions?: ExtraSendOptions) { @@ -5208,12 +5453,50 @@ export class BurnMintTokenPool implements c.Contract { }); } - async sendReturnExcessesBack(provider: ContractProvider, via: Sender, msgValue: coins, body: { + async sendContextExecutorReplyBurnMintTokenPoolContextExecutorContext_(provider: ContractProvider, via: Sender, msgValue: coins, body: { queryId?: uint64 + id: uint64 + context: BurnMintTokenPool_ContextExecutorContext + forwardFrom: array + forwardPayload: c.Cell | null + done: boolean }, extraOptions?: ExtraSendOptions) { return provider.internal(via, { value: msgValue, - body: ReturnExcessesBack.toCell(ReturnExcessesBack.create(body)), + body: makeCellFrom>(ContextExecutor_Reply.create(body), + (v,b) => { b.storeUint(0x93e5bbc5, 32); + b.storeUint(v.queryId, 64); + b.storeUint(v.id, 64); + storeCellRef(v.context, b, BurnMintTokenPool_ContextExecutorContext.store); + storeArrayOf(v.forwardFrom, b, + (v,b) => b.storeAddress(v) + ); + storeTolkNullable(v.forwardPayload, b, + (v,b) => b.storeRef(v) + ); + b.storeBit(v.done); } + ), + ...extraOptions + }); + } + + async sendContextExecutorForwardNotificationBurnMintTokenPoolContextExecutorContext_(provider: ContractProvider, via: Sender, msgValue: coins, body: { + id: uint64 + context: BurnMintTokenPool_ContextExecutorContext + forwardFrom: array + message: ContextExecutor_InMessageForward + }, extraOptions?: ExtraSendOptions) { + return provider.internal(via, { + value: msgValue, + body: makeCellFrom>(ContextExecutor_ForwardNotification.create(body), + (v,b) => { b.storeUint(0x55b412b9, 32); + b.storeUint(v.id, 64); + storeCellRef(v.context, b, BurnMintTokenPool_ContextExecutorContext.store); + storeArrayOf(v.forwardFrom, b, + (v,b) => b.storeAddress(v) + ); + storeCellRef(v.message, b, ContextExecutor_InMessageForward.store); } + ), ...extraOptions }); } @@ -5264,20 +5547,6 @@ export class BurnMintTokenPool implements c.Contract { ); } - async getHasPendingBurn(provider: ContractProvider, queryId: uint64): Promise { - const r = StackReader.fromGetMethod(1, await provider.get('hasPendingBurn', [ - { type: 'int', value: queryId }, - ])); - return r.readBoolean(); - } - - async getHasPendingMint(provider: ContractProvider, queryId: uint64): Promise { - const r = StackReader.fromGetMethod(1, await provider.get('hasPendingMint', [ - { type: 'int', value: queryId }, - ])); - return r.readBoolean(); - } - async getRMNProxy(provider: ContractProvider): Promise { const r = StackReader.fromGetMethod(1, await provider.get('getRMNProxy', [])); return r.readSlice().loadAddress(); diff --git a/contracts/wrappers/gen/ccip/pools/JettonLockBox.ts b/contracts/wrappers/gen/ccip/pools/JettonLockBox.ts index 813b1986b..c63a1ed77 100644 --- a/contracts/wrappers/gen/ccip/pools/JettonLockBox.ts +++ b/contracts/wrappers/gen/ccip/pools/JettonLockBox.ts @@ -386,6 +386,7 @@ export const Storage = { > token: address > remoteChainSelector: uint64 > amount: coins + > context: cell? > } */ export interface JettonLockBox_Deposit { @@ -394,6 +395,7 @@ export interface JettonLockBox_Deposit { token: c.Address remoteChainSelector: uint64 amount: coins + context: c.Cell | null } export const JettonLockBox_Deposit = { @@ -404,6 +406,7 @@ export const JettonLockBox_Deposit = { token: c.Address remoteChainSelector: uint64 amount: coins + context: c.Cell | null }): JettonLockBox_Deposit { return { $: 'JettonLockBox_Deposit', @@ -419,6 +422,7 @@ export const JettonLockBox_Deposit = { token: s.loadAddress(), remoteChainSelector: s.loadUintBig(64), amount: s.loadCoins(), + context: s.loadBoolean() ? s.loadRef() : null, } }, store(self: JettonLockBox_Deposit, b: c.Builder): void { @@ -427,6 +431,9 @@ export const JettonLockBox_Deposit = { b.storeAddress(self.token); b.storeUint(self.remoteChainSelector, 64); b.storeCoins(self.amount); + storeTolkNullable(self.context, b, + (v,b) => b.storeRef(v) + ); }, toCell(self: JettonLockBox_Deposit): c.Cell { return makeCellFrom(self, JettonLockBox_Deposit.store); @@ -436,16 +443,19 @@ export const JettonLockBox_Deposit = { /** > struct JettonLockBox_WithdrawExtra { > sendExcessesTo: address? + > failureContext: cell? > } */ export interface JettonLockBox_WithdrawExtra { readonly $: 'JettonLockBox_WithdrawExtra' sendExcessesTo: c.Address | null + failureContext: c.Cell | null } export const JettonLockBox_WithdrawExtra = { create(args: { sendExcessesTo: c.Address | null + failureContext: c.Cell | null }): JettonLockBox_WithdrawExtra { return { $: 'JettonLockBox_WithdrawExtra', @@ -456,10 +466,14 @@ export const JettonLockBox_WithdrawExtra = { return { $: 'JettonLockBox_WithdrawExtra', sendExcessesTo: s.loadMaybeAddress(), + failureContext: s.loadBoolean() ? s.loadRef() : null, } }, store(self: JettonLockBox_WithdrawExtra, b: c.Builder): void { b.storeAddress(self.sendExcessesTo); + storeTolkNullable(self.failureContext, b, + (v,b) => b.storeRef(v) + ); }, toCell(self: JettonLockBox_WithdrawExtra): c.Cell { return makeCellFrom(self, JettonLockBox_WithdrawExtra.store); @@ -537,6 +551,7 @@ export const JettonLockBox_Withdraw = { > token: address > remoteChainSelector: uint64 > amount: coins + > context: cell? > } */ export interface JettonLockBox_Deposited { @@ -545,6 +560,7 @@ export interface JettonLockBox_Deposited { token: c.Address remoteChainSelector: uint64 amount: coins + context: c.Cell | null } export const JettonLockBox_Deposited = { @@ -555,6 +571,7 @@ export const JettonLockBox_Deposited = { token: c.Address remoteChainSelector: uint64 amount: coins + context: c.Cell | null }): JettonLockBox_Deposited { return { $: 'JettonLockBox_Deposited', @@ -570,6 +587,7 @@ export const JettonLockBox_Deposited = { token: s.loadAddress(), remoteChainSelector: s.loadUintBig(64), amount: s.loadCoins(), + context: s.loadBoolean() ? s.loadRef() : null, } }, store(self: JettonLockBox_Deposited, b: c.Builder): void { @@ -578,6 +596,9 @@ export const JettonLockBox_Deposited = { b.storeAddress(self.token); b.storeUint(self.remoteChainSelector, 64); b.storeCoins(self.amount); + storeTolkNullable(self.context, b, + (v,b) => b.storeRef(v) + ); }, toCell(self: JettonLockBox_Deposited): c.Cell { return makeCellFrom(self, JettonLockBox_Deposited.store); @@ -696,6 +717,7 @@ export const JettonLockBox_Initialized = { > token: address > amount: coins > recipientWallet: address + > context: cell? > } */ export interface JettonLockBox_WithdrawFailed { @@ -704,6 +726,7 @@ export interface JettonLockBox_WithdrawFailed { token: c.Address amount: coins recipientWallet: c.Address + context: c.Cell | null } export const JettonLockBox_WithdrawFailed = { @@ -714,6 +737,7 @@ export const JettonLockBox_WithdrawFailed = { token: c.Address amount: coins recipientWallet: c.Address + context: c.Cell | null }): JettonLockBox_WithdrawFailed { return { $: 'JettonLockBox_WithdrawFailed', @@ -729,6 +753,7 @@ export const JettonLockBox_WithdrawFailed = { token: s.loadAddress(), amount: s.loadCoins(), recipientWallet: s.loadAddress(), + context: s.loadBoolean() ? s.loadRef() : null, } }, store(self: JettonLockBox_WithdrawFailed, b: c.Builder): void { @@ -737,6 +762,9 @@ export const JettonLockBox_WithdrawFailed = { b.storeAddress(self.token); b.storeCoins(self.amount); b.storeAddress(self.recipientWallet); + storeTolkNullable(self.context, b, + (v,b) => b.storeRef(v) + ); }, toCell(self: JettonLockBox_WithdrawFailed): c.Cell { return makeCellFrom(self, JettonLockBox_WithdrawFailed.store); @@ -782,7 +810,7 @@ function calculateDeployedAddress(code: c.Cell, data: c.Cell, options: DeployedA } export class JettonLockBox implements c.Contract { - static CodeCell = c.Cell.fromBase64('te6ccgECQwEACbgAART/APSkE/S88sgLAQIBYgIDAgLLGBkCASAEBQIBIAYHAgEgEBECAUgICQIBSA4PAgEgCgsAW7BX40IWxpbmsuY2hhaW4udG9uLmNjaXAuSmV0dG9uTG9ja0JveIItTAuMS4wiAAN6wLdqJoaZ+Y/SQY/SgY+gK2rDa2tqwBuAD4BcACASAMDQAWq5HtRNDTPzH6SDAAHKh27UTQ0z8x+kgx+lAwABuyH7tRNDTPzH6SDDHBYAA3sFH7UTQ0z8x+kgx+lAx9AVtWG1tbVgDcAHwDYAIBIBITAgEgFhcAN7aC3aiaGmfmP0kGP0oGPoCgTa2rTa2rTgBeAPACAUgUFQA3rpt2omhpn5j9JBj9KBj6ArasNra2rAG4APgHwAA3rY72omhpn5j9JBj9KBj6ArasNra2rAG4APgIwAA3tK2dqJoaZ+Y/SQY/SgY+gKBNratNratOAF4B0AA3tEq9qJoaZ+Y/SQY/SgY+gKBNratNratOAF4CEAIBIBobAgHOQUICASAcHQIBIDAxAgEgHh8CASAoKQT1Ttou37+JGS8AXg7UTQ0z/6SPpQ9ATRJNcsI5sWhOSPUdcsJoMuGDSOxNcsJ/03dcyOODBtbW1tcCX4kviXLBA4EDcQNhA1EDTwCWxRjhExNALIyz/6UvpU9ADJ7VTbMeAwhA8FxwAV8vQD4w0D4w1VAuMNA8jLPxL6UoICEiIwBfQgwAGeMPgo+kQwgXUwAfg2qwDgwAOd+Cj6RDCBdTAB+DaqAOD4KPpEMIF1MAH4NoAf4zNAHTP/pI+kj6UDD4koIA88MGbhby9IIA88KLAiTHBbPy9IIA88KLAiPHBbPy9FMhbW1tbXBUUOFTAYMH9A5voZsx1NHQ0//TP/QE0Y4ZMHAgbXDIy/9wzws/UhD0AMlARYMH9BdBM+JwyMv/Ess/9ADJAVYQUAODB/QXPytxJAH6NQTTP/pI0z/6APpI9AX4koIA88QobrPy9CptbW1tcIIQoncdBCfwBoIA88AkwgDy9IIA88GLAiTHBbPy9MjPkF8JAB4nzws/+lLJyM+DzM9QggDzwihus/L0IW6zjhEh0PpQ0W6zlSHQ+lDRkvgo4pL4KOJtyM+QPin6liknACg1BNM/+gD6UPiSEHgQZxBWVTDwBAAO+lT0AMntVAL88ALIz5L1+i86Ess/cM8L/xLL/3DPC//JyM+FCFLg+lJY+gJxzwtqzMlx+wCCEKJ3HQRR7lMBgwf0Dm+hmzHU0dDT/9M/9ATRjhkwcCBtcMjL/3DPCz9SEPQAyUBFgwf0F0Ez4nDIy/8Syz/0AMkBVhBQA4MH9Bc/K3HwAsiJJSYACL1+i84A4M8WEss/ghCidx0Ezwv/Esv/cM8L/8nIz4UIUuD6Ulj6AnHPC2rMyXH7ACdus1QQjOMEJxBuGHAsVE4wUrDwCF8GgBCCCvrwgAH7AsjPk6fTjEYWyz8U+lIS+lL6UsnIz4UIFPpScc8LbhPMyYBA+wAAuM8LPyb6AlJQ+lIS+lT0AM+EIM7JyM+FiFKA+lLPhBBz+gJxzwtlzMmAUPsAyM+TQZcMGhbLPxT6UhLLPwH6AvpS9ADJyM+PGAAEghAtDIGDzwv3cc8LYczJcPsAAgEgKisCASAuLwL1DOCAPPEJm6z8vQkbW1tbXCCEKJ3HQQn8AaCAPPAAsIAEvL0ggDzwiVus/L0ggDzwlE1xwUT8vT0BCFumDEgxwCSMG3gktHQ4iBukVvg1ywk9PYbDPK/0z/6SNM/+gDRyM+SensNhiTPCz9SMPpSIs8LPyH6AsnIic8WgLC0AxTtRNAB0x8xAdM/MfpIMAHXLCB8U/UsjkfTPzH6APpI+lAx9AH6ADHTAAHCAJJfBOHXTNDTHzHTP/pIMMjPkYLrlVoSyz8U+lJY+gL6UsnIz4UIEvpScc8LbszJgED7AODyP4AAFxgABAGyCEJ6ew2HPC/dxzwthzMlw+wDIz5G0Hfy6FMs/EvpSyz8B+gLJyM+FCBL6UnHPC27MyYBA+wAALRVUVN28AeRW+DI+lLL/89QggC5KPLxgAD8bFICgwf0Dm+hkltw4dTR0IEBQNch9AWBAQv0Cm+hMYAIBIDIzAgEgOjsCASA0NQIBIDg5AfcJcMAlSdus8MAkXDil1R5QlNK2kDeUaJTAYMH9A5voZsx1NHQ0//TP/QE0Y4ZMHAgbXDIy/9wzws/UhD0AMlARYMH9BdBM+JTQIEBC/QKb6ExlhA3Xwc2cODIz4NSUoEBC/RBAaQCyMv/Ess/9ADJUjKDB/QXcfACVHJCgNgHnI7w7aLt+zFUd2VUd2V/UYfwCgHXLCSuaqB8jlPXLCS02G3MjiHTP9P/+kgwVHqYVHqYJ/ALVGuwVGuwVGuwKvAGQQTwDDCOJtcsIcopYjSVXwNw2zHh0z/T//pIMFMDxwWWggC5KfLw4UEE8Aww4uMNf9iA3ANInxwWRNI4syM+TPPKg3ijPCz8mzwv/UiD6UlIQ+lLJyM+FCBb6UiP6AnHPC2oVzMlx+wDicFRNE+MEyM+TPPKg3hfLPxTL/xP6UvpSycjPhQgT+lJQA/oCcc8LaszJB5KAQJFx4hf7AH8AQtM/0//6SDBUephUepgn8AtUa7BUa7BUa7Aq8AZBBPAIMAAvDMzNQTDAJUhbrPDAJFw4pRAM9ox4GwxgACcbFEBgwf0Dm+hkjBw4dTR0NcL/4AIBIDw9AgEgP0AB9QlwwCVJm6zwwCRcOKXVHlCU0naQN5RolMBgwf0Dm+hmzHU0dDT/9M/9ATRjhkwcCBtcMjL/3DPCz9SEPQAyUBFgwf0F0Ez4lNAgQEL9ApvoTGWEDdfBzZw4VJAgQEL9FkwAaUCyMv/Ess/9ADJUjKDB/QXcfACVHJCJ4D4ALxsUQGDB/QOb6GZ1NHQ0/8x1ws/kjBw4oADQxwWRNI4syM+SZD+HHijPCz8mzwv/UiD6UlIQ+lLJyM+FCBb6UiP6AnHPC2oVzMlx+wDicFRNE+MEyM+SZD+HHhfLPxTL/xP6UvpSycjPhQgT+lJQA/oCcc8LaszJB5KAQJFx4hf7AH8AlxsUgKDB/QOb6GOPNTR0NP/MdM/9AVSIr6SW23gcCGBAQv0gm+lMppTJLmTIcMAkXDinTEigQEL9HRvpTICpALobCIykjBt35JbbeKAATxsUQGDB/QOb6GOGNTR0NP/MdM/9AUBkjBt4YEBC/SCb6UwMZIwbeKAATRsUgKDB/QOb6GOF9TR0IEBQNch9AWBAQv0dG+lbBKSMG3hkltt4oAAxGxRAYMH9A5voZrU0dCBAUDXIfQFkjBt4oA=='); + static CodeCell = c.Cell.fromBase64('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'); static Errors = { 'AccessControl_Error.UnauthorizedAccount': 47400, diff --git a/contracts/wrappers/gen/ccip/pools/LockReleaseLockboxTokenPool.ts b/contracts/wrappers/gen/ccip/pools/LockReleaseLockboxTokenPool.ts index 98eabe50c..da204fcd6 100644 --- a/contracts/wrappers/gen/ccip/pools/LockReleaseLockboxTokenPool.ts +++ b/contracts/wrappers/gen/ccip/pools/LockReleaseLockboxTokenPool.ts @@ -11,6 +11,8 @@ import { beginCell, ContractProvider, Sender, SendMode } from '@ton/core'; type RemainingBitsAndRefs = c.Slice +type array = T[] + // TypeScript wrappers flatten a TVM linked list `[1 [2 [3 null]]]` to `[1 2 3]` type lisp_list = T[] @@ -106,6 +108,35 @@ function storeTolkNullable(v: T | null, b: c.Builder, storeFn_T: StoreCallbac } } +function storeArrayOf(v: array, b: c.Builder, storeFn_T: StoreCallback): void { + // the compiler stores array in chunks; in TypeScript, for simplicity, store "1 elem = 1 ref" + let tail = null as c.Cell | null; + for (let i = 0; i < v.length; ++i) { + let chunkB = beginCell().storeMaybeRef(tail); + storeFn_T(v[v.length - 1 - i], chunkB); + tail = chunkB.endCell(); + } + b.storeUint(v.length, 8); + b.storeMaybeRef(tail); +} + +function loadArrayOf(s: c.Slice, loadFn_T: LoadCallback): array { + let len = s.loadUint(8); + let head = s.loadMaybeRef(); + let outArr = [] as array; + while (head != null) { + let s = head.beginParse(); + head = s.loadMaybeRef(); + while (s.remainingBits || s.remainingRefs) { + outArr.push(loadFn_T(s)); + } + } + if (len !== outArr.length) { + throw new Error(`mismatch array binary data: expected ${len} elements, got ${outArr.length}`); + } + return outArr; +} + function storeLispListOf(v: lisp_list, b: c.Builder, storeFn_T: StoreCallback): void { let tail = c.Cell.EMPTY; for (let i = 0; i < v.length; ++i) { @@ -278,6 +309,8 @@ function invokeCustomUnpackFromSlice(typeName: string, s: c.Slice): T { type coins = bigint +type int32 = bigint + type uint8 = bigint type uint16 = bigint type uint32 = bigint @@ -286,6 +319,34 @@ type uint120 = bigint type uint128 = bigint type uint256 = bigint +type varuint32 = bigint + +/** + > type ExtraCurrenciesMap = map + */ +export type ExtraCurrenciesMap = Map + +export const ExtraCurrenciesMap = { + fromSlice(s: c.Slice): ExtraCurrenciesMap { + return dictToMap(c.Dictionary.load(c.Dictionary.Keys.BigInt(32), createDictionaryValue( + (s) => s.loadVarUintBig(5), + (v,b) => b.storeVarUint(v, 5) + ), s)); + }, + store(self: ExtraCurrenciesMap, b: c.Builder): void { + b.storeDict(mapToDict(self, c.Dictionary.Keys.BigInt(32), createDictionaryValue( + (s) => s.loadVarUintBig(5), + (v,b) => b.storeVarUint(v, 5) + )), c.Dictionary.Keys.BigInt(32), createDictionaryValue( + (s) => s.loadVarUintBig(5), + (v,b) => b.storeVarUint(v, 5) + )); + }, + toCell(self: ExtraCurrenciesMap): c.Cell { + return makeCellFrom(self, ExtraCurrenciesMap.store); + } +} + /** > type SnakedCell = cell */ @@ -511,6 +572,44 @@ export const ReturnExcessesBack = { } } +/** + > struct JettonClient { + > masterAddress: address + > jettonWalletCode: cell + > } + */ +export interface JettonClient { + readonly $: 'JettonClient' + masterAddress: c.Address + jettonWalletCode: c.Cell +} + +export const JettonClient = { + create(args: { + masterAddress: c.Address + jettonWalletCode: c.Cell + }): JettonClient { + return { + $: 'JettonClient', + ...args + } + }, + fromSlice(s: c.Slice): JettonClient { + return { + $: 'JettonClient', + masterAddress: s.loadAddress(), + jettonWalletCode: s.loadRef(), + } + }, + store(self: JettonClient, b: c.Builder): void { + b.storeAddress(self.masterAddress); + b.storeRef(self.jettonWalletCode); + }, + toCell(self: JettonClient): c.Cell { + return makeCellFrom(self, JettonClient.store); + } +} + /** > type CrossChainAddress = slice */ @@ -528,6 +627,190 @@ export const CrossChainAddress = { } } +/** + > struct (0xcad4d1d0) ContextExecutor_Ask { + > queryId: uint64 + > forwardPayload: cell? + > done: bool + > } + */ +export interface ContextExecutor_Ask { + readonly $: 'ContextExecutor_Ask' + queryId: uint64 + forwardPayload: c.Cell | null + done: boolean +} + +export const ContextExecutor_Ask = { + PREFIX: 0xcad4d1d0, + + create(args: { + queryId?: uint64 + forwardPayload: c.Cell | null + done: boolean + }): ContextExecutor_Ask { + return { + $: 'ContextExecutor_Ask', + ...args, + queryId: args.queryId ?? 0n + } + }, + fromSlice(s: c.Slice): ContextExecutor_Ask { + loadAndCheckPrefix32(s, 0xcad4d1d0, 'ContextExecutor_Ask'); + return { + $: 'ContextExecutor_Ask', + queryId: s.loadUintBig(64), + forwardPayload: s.loadBoolean() ? s.loadRef() : null, + done: s.loadBoolean(), + } + }, + store(self: ContextExecutor_Ask, b: c.Builder): void { + b.storeUint(0xcad4d1d0, 32); + b.storeUint(self.queryId, 64); + storeTolkNullable(self.forwardPayload, b, + (v,b) => b.storeRef(v) + ); + b.storeBit(self.done); + }, + toCell(self: ContextExecutor_Ask): c.Cell { + return makeCellFrom(self, ContextExecutor_Ask.store); + } +} + +/** + > struct (0x93e5bbc5) ContextExecutor_Reply { + > queryId: uint64 + > id: uint64 + > context: Cell + > forwardFrom: array
+ > forwardPayload: cell? + > done: bool + > } + */ +export interface ContextExecutor_Reply { + readonly $: 'ContextExecutor_Reply' + queryId: uint64 + id: uint64 + context: T + forwardFrom: array + forwardPayload: c.Cell | null + done: boolean +} + +export const ContextExecutor_Reply = { + PREFIX: 0x93e5bbc5, + + create(args: { + queryId?: uint64 + id: uint64 + context: T + forwardFrom: array + forwardPayload: c.Cell | null + done: boolean + }): ContextExecutor_Reply { + return { + $: 'ContextExecutor_Reply', + ...args, + queryId: args.queryId ?? 0n + } + }, +} + +/** + > struct (0x55b412b9) ContextExecutor_ForwardNotification { + > id: uint64 + > context: Cell + > forwardFrom: array
+ > message: Cell + > } + */ +export interface ContextExecutor_ForwardNotification { + readonly $: 'ContextExecutor_ForwardNotification' + id: uint64 + context: T + forwardFrom: array + message: ContextExecutor_InMessageForward +} + +export const ContextExecutor_ForwardNotification = { + PREFIX: 0x55b412b9, + + create(args: { + id: uint64 + context: T + forwardFrom: array + message: ContextExecutor_InMessageForward + }): ContextExecutor_ForwardNotification { + return { + $: 'ContextExecutor_ForwardNotification', + ...args + } + }, +} + +/** + > struct ContextExecutor_InMessageForward { + > senderAddress: address + > valueCoins: coins + > valueExtra: ExtraCurrenciesMap + > originalForwardFee: coins + > createdLt: uint64 + > createdAt: uint32 + > body: cell + > } + */ +export interface ContextExecutor_InMessageForward { + readonly $: 'ContextExecutor_InMessageForward' + senderAddress: c.Address + valueCoins: coins + valueExtra: ExtraCurrenciesMap + originalForwardFee: coins + createdLt: uint64 + createdAt: uint32 + body: c.Cell +} + +export const ContextExecutor_InMessageForward = { + create(args: { + senderAddress: c.Address + valueCoins: coins + valueExtra: ExtraCurrenciesMap + originalForwardFee: coins + createdLt: uint64 + createdAt: uint32 + body: c.Cell + }): ContextExecutor_InMessageForward { + return { + $: 'ContextExecutor_InMessageForward', + ...args + } + }, + fromSlice(s: c.Slice): ContextExecutor_InMessageForward { + return { + $: 'ContextExecutor_InMessageForward', + senderAddress: s.loadAddress(), + valueCoins: s.loadCoins(), + valueExtra: ExtraCurrenciesMap.fromSlice(s), + originalForwardFee: s.loadCoins(), + createdLt: s.loadUintBig(64), + createdAt: s.loadUintBig(32), + body: s.loadRef(), + } + }, + store(self: ContextExecutor_InMessageForward, b: c.Builder): void { + b.storeAddress(self.senderAddress); + b.storeCoins(self.valueCoins); + ExtraCurrenciesMap.store(self.valueExtra, b); + b.storeCoins(self.originalForwardFee); + b.storeUint(self.createdLt, 64); + b.storeUint(self.createdAt, 32); + b.storeRef(self.body); + }, + toCell(self: ContextExecutor_InMessageForward): c.Cell { + return makeCellFrom(self, ContextExecutor_InMessageForward.store); + } +} + /** > struct CursedSubjects { > data: map @@ -4063,94 +4346,39 @@ export const TokenPool_RampAccessUpdated = { } /** - > struct LockReleaseLockboxTokenPool_PendingLock { - > queryId: uint64 - > forwardPayload: Cell + > struct (0x90230477) LockReleaseLockboxTokenPool_ReleaseContext { + > forwardPayload: Cell > } */ -export interface LockReleaseLockboxTokenPool_PendingLock { - readonly $: 'LockReleaseLockboxTokenPool_PendingLock' - queryId: uint64 - forwardPayload: TokenPool_LockOrBurnForwardPayload -} - -export const LockReleaseLockboxTokenPool_PendingLock = { - create(args: { - queryId?: uint64 - forwardPayload: TokenPool_LockOrBurnForwardPayload - }): LockReleaseLockboxTokenPool_PendingLock { - return { - $: 'LockReleaseLockboxTokenPool_PendingLock', - ...args, - queryId: args.queryId ?? 0n - } - }, - fromSlice(s: c.Slice): LockReleaseLockboxTokenPool_PendingLock { - return { - $: 'LockReleaseLockboxTokenPool_PendingLock', - queryId: s.loadUintBig(64), - forwardPayload: loadCellRef(s, TokenPool_LockOrBurnForwardPayload.fromSlice), - } - }, - store(self: LockReleaseLockboxTokenPool_PendingLock, b: c.Builder): void { - b.storeUint(self.queryId, 64); - storeCellRef(self.forwardPayload, b, TokenPool_LockOrBurnForwardPayload.store); - }, - toCell(self: LockReleaseLockboxTokenPool_PendingLock): c.Cell { - return makeCellFrom(self, LockReleaseLockboxTokenPool_PendingLock.store); - } +export interface LockReleaseLockboxTokenPool_ReleaseContext { + readonly $: 'LockReleaseLockboxTokenPool_ReleaseContext' + forwardPayload: TokenPool_ReleaseOrMintForwardPayload } -/** - > struct LockReleaseLockboxTokenPool_PendingRelease { - > queryId: uint64 - > replyTo: Cell - > request: Cell - > out: Cell - > } - */ -export interface LockReleaseLockboxTokenPool_PendingRelease { - readonly $: 'LockReleaseLockboxTokenPool_PendingRelease' - queryId: uint64 - replyTo: c.Address | null - request: TokenPool_ReleaseOrMintInV1 - out: TokenPool_ReleaseOrMintOutV1 -} +export const LockReleaseLockboxTokenPool_ReleaseContext = { + PREFIX: 0x90230477, -export const LockReleaseLockboxTokenPool_PendingRelease = { create(args: { - queryId?: uint64 - replyTo: c.Address | null - request: TokenPool_ReleaseOrMintInV1 - out: TokenPool_ReleaseOrMintOutV1 - }): LockReleaseLockboxTokenPool_PendingRelease { + forwardPayload: TokenPool_ReleaseOrMintForwardPayload + }): LockReleaseLockboxTokenPool_ReleaseContext { return { - $: 'LockReleaseLockboxTokenPool_PendingRelease', - ...args, - queryId: args.queryId ?? 0n + $: 'LockReleaseLockboxTokenPool_ReleaseContext', + ...args } }, - fromSlice(s: c.Slice): LockReleaseLockboxTokenPool_PendingRelease { + fromSlice(s: c.Slice): LockReleaseLockboxTokenPool_ReleaseContext { + loadAndCheckPrefix32(s, 0x90230477, 'LockReleaseLockboxTokenPool_ReleaseContext'); return { - $: 'LockReleaseLockboxTokenPool_PendingRelease', - queryId: s.loadUintBig(64), - replyTo: loadCellRef(s, - (s) => s.loadMaybeAddress() - ), - request: loadCellRef(s, TokenPool_ReleaseOrMintInV1.fromSlice), - out: loadCellRef(s, TokenPool_ReleaseOrMintOutV1.fromSlice), + $: 'LockReleaseLockboxTokenPool_ReleaseContext', + forwardPayload: loadCellRef(s, TokenPool_ReleaseOrMintForwardPayload.fromSlice), } }, - store(self: LockReleaseLockboxTokenPool_PendingRelease, b: c.Builder): void { - b.storeUint(self.queryId, 64); - storeCellRef(self.replyTo, b, - (v,b) => b.storeAddress(v) - ); - storeCellRef(self.request, b, TokenPool_ReleaseOrMintInV1.store); - storeCellRef(self.out, b, TokenPool_ReleaseOrMintOutV1.store); + store(self: LockReleaseLockboxTokenPool_ReleaseContext, b: c.Builder): void { + b.storeUint(0x90230477, 32); + storeCellRef(self.forwardPayload, b, TokenPool_ReleaseOrMintForwardPayload.store); }, - toCell(self: LockReleaseLockboxTokenPool_PendingRelease): c.Cell { - return makeCellFrom(self, LockReleaseLockboxTokenPool_PendingRelease.store); + toCell(self: LockReleaseLockboxTokenPool_ReleaseContext): c.Cell { + return makeCellFrom(self, LockReleaseLockboxTokenPool_ReleaseContext.store); } } @@ -4158,24 +4386,24 @@ export const LockReleaseLockboxTokenPool_PendingRelease = { > struct Storage { > poolData: Cell > lockbox: address - > pendingLocks: map> - > pendingReleases: map> + > contextExecutorCode: cell + > contextExecutorNextId: uint64 > } */ export interface Storage { readonly $: 'Storage' poolData: TokenPool_Data lockbox: c.Address - pendingLocks: Map /* = [] as map> */ - pendingReleases: Map /* = [] as map> */ + contextExecutorCode: c.Cell + contextExecutorNextId: uint64 } export const Storage = { create(args: { poolData: TokenPool_Data lockbox: c.Address - pendingLocks: Map /* = [] as map> */ - pendingReleases: Map /* = [] as map> */ + contextExecutorCode: c.Cell + contextExecutorNextId: uint64 }): Storage { return { $: 'Storage', @@ -4187,33 +4415,15 @@ export const Storage = { $: 'Storage', poolData: loadCellRef(s, TokenPool_Data.fromSlice), lockbox: s.loadAddress(), - pendingLocks: dictToMap(c.Dictionary.load(c.Dictionary.Keys.BigUint(64), createDictionaryValue( - (s) => loadCellRef(s, LockReleaseLockboxTokenPool_PendingLock.fromSlice), - (v,b) => storeCellRef(v, b, LockReleaseLockboxTokenPool_PendingLock.store) - ), s)), - pendingReleases: dictToMap(c.Dictionary.load(c.Dictionary.Keys.BigUint(64), createDictionaryValue( - (s) => loadCellRef(s, LockReleaseLockboxTokenPool_PendingRelease.fromSlice), - (v,b) => storeCellRef(v, b, LockReleaseLockboxTokenPool_PendingRelease.store) - ), s)), + contextExecutorCode: s.loadRef(), + contextExecutorNextId: s.loadUintBig(64), } }, store(self: Storage, b: c.Builder): void { storeCellRef(self.poolData, b, TokenPool_Data.store); b.storeAddress(self.lockbox); - b.storeDict(mapToDict(self.pendingLocks, c.Dictionary.Keys.BigUint(64), createDictionaryValue( - (s) => loadCellRef(s, LockReleaseLockboxTokenPool_PendingLock.fromSlice), - (v,b) => storeCellRef(v, b, LockReleaseLockboxTokenPool_PendingLock.store) - )), c.Dictionary.Keys.BigUint(64), createDictionaryValue( - (s) => loadCellRef(s, LockReleaseLockboxTokenPool_PendingLock.fromSlice), - (v,b) => storeCellRef(v, b, LockReleaseLockboxTokenPool_PendingLock.store) - )); - b.storeDict(mapToDict(self.pendingReleases, c.Dictionary.Keys.BigUint(64), createDictionaryValue( - (s) => loadCellRef(s, LockReleaseLockboxTokenPool_PendingRelease.fromSlice), - (v,b) => storeCellRef(v, b, LockReleaseLockboxTokenPool_PendingRelease.store) - )), c.Dictionary.Keys.BigUint(64), createDictionaryValue( - (s) => loadCellRef(s, LockReleaseLockboxTokenPool_PendingRelease.fromSlice), - (v,b) => storeCellRef(v, b, LockReleaseLockboxTokenPool_PendingRelease.store) - )); + b.storeRef(self.contextExecutorCode); + b.storeUint(self.contextExecutorNextId, 64); }, toCell(self: Storage): c.Cell { return makeCellFrom(self, Storage.store); @@ -4223,16 +4433,19 @@ export const Storage = { /** > struct JettonLockBox_WithdrawExtra { > sendExcessesTo: address? + > failureContext: cell? > } */ export interface JettonLockBox_WithdrawExtra { readonly $: 'JettonLockBox_WithdrawExtra' sendExcessesTo: c.Address | null + failureContext: c.Cell | null } export const JettonLockBox_WithdrawExtra = { create(args: { sendExcessesTo: c.Address | null + failureContext: c.Cell | null }): JettonLockBox_WithdrawExtra { return { $: 'JettonLockBox_WithdrawExtra', @@ -4243,10 +4456,14 @@ export const JettonLockBox_WithdrawExtra = { return { $: 'JettonLockBox_WithdrawExtra', sendExcessesTo: s.loadMaybeAddress(), + failureContext: s.loadBoolean() ? s.loadRef() : null, } }, store(self: JettonLockBox_WithdrawExtra, b: c.Builder): void { b.storeAddress(self.sendExcessesTo); + storeTolkNullable(self.failureContext, b, + (v,b) => b.storeRef(v) + ); }, toCell(self: JettonLockBox_WithdrawExtra): c.Cell { return makeCellFrom(self, JettonLockBox_WithdrawExtra.store); @@ -4324,6 +4541,7 @@ export const JettonLockBox_Withdraw = { > token: address > remoteChainSelector: uint64 > amount: coins + > context: cell? > } */ export interface JettonLockBox_Deposited { @@ -4332,6 +4550,7 @@ export interface JettonLockBox_Deposited { token: c.Address remoteChainSelector: uint64 amount: coins + context: c.Cell | null } export const JettonLockBox_Deposited = { @@ -4342,6 +4561,7 @@ export const JettonLockBox_Deposited = { token: c.Address remoteChainSelector: uint64 amount: coins + context: c.Cell | null }): JettonLockBox_Deposited { return { $: 'JettonLockBox_Deposited', @@ -4357,6 +4577,7 @@ export const JettonLockBox_Deposited = { token: s.loadAddress(), remoteChainSelector: s.loadUintBig(64), amount: s.loadCoins(), + context: s.loadBoolean() ? s.loadRef() : null, } }, store(self: JettonLockBox_Deposited, b: c.Builder): void { @@ -4365,6 +4586,9 @@ export const JettonLockBox_Deposited = { b.storeAddress(self.token); b.storeUint(self.remoteChainSelector, 64); b.storeCoins(self.amount); + storeTolkNullable(self.context, b, + (v,b) => b.storeRef(v) + ); }, toCell(self: JettonLockBox_Deposited): c.Cell { return makeCellFrom(self, JettonLockBox_Deposited.store); @@ -4377,6 +4601,7 @@ export const JettonLockBox_Deposited = { > token: address > amount: coins > recipientWallet: address + > context: cell? > } */ export interface JettonLockBox_WithdrawFailed { @@ -4385,6 +4610,7 @@ export interface JettonLockBox_WithdrawFailed { token: c.Address amount: coins recipientWallet: c.Address + context: c.Cell | null } export const JettonLockBox_WithdrawFailed = { @@ -4395,6 +4621,7 @@ export const JettonLockBox_WithdrawFailed = { token: c.Address amount: coins recipientWallet: c.Address + context: c.Cell | null }): JettonLockBox_WithdrawFailed { return { $: 'JettonLockBox_WithdrawFailed', @@ -4410,6 +4637,7 @@ export const JettonLockBox_WithdrawFailed = { token: s.loadAddress(), amount: s.loadCoins(), recipientWallet: s.loadAddress(), + context: s.loadBoolean() ? s.loadRef() : null, } }, store(self: JettonLockBox_WithdrawFailed, b: c.Builder): void { @@ -4418,50 +4646,15 @@ export const JettonLockBox_WithdrawFailed = { b.storeAddress(self.token); b.storeCoins(self.amount); b.storeAddress(self.recipientWallet); + storeTolkNullable(self.context, b, + (v,b) => b.storeRef(v) + ); }, toCell(self: JettonLockBox_WithdrawFailed): c.Cell { return makeCellFrom(self, JettonLockBox_WithdrawFailed.store); } } -/** - > struct JettonClient { - > masterAddress: address - > jettonWalletCode: cell - > } - */ -export interface JettonClient { - readonly $: 'JettonClient' - masterAddress: c.Address - jettonWalletCode: c.Cell -} - -export const JettonClient = { - create(args: { - masterAddress: c.Address - jettonWalletCode: c.Cell - }): JettonClient { - return { - $: 'JettonClient', - ...args - } - }, - fromSlice(s: c.Slice): JettonClient { - return { - $: 'JettonClient', - masterAddress: s.loadAddress(), - jettonWalletCode: s.loadRef(), - } - }, - store(self: JettonClient, b: c.Builder): void { - b.storeAddress(self.masterAddress); - b.storeRef(self.jettonWalletCode); - }, - toCell(self: JettonClient): c.Cell { - return makeCellFrom(self, JettonClient.store); - } -} - /** > struct Ownable2Step { > owner: address @@ -4719,7 +4912,7 @@ function calculateDeployedAddress(code: c.Cell, data: c.Cell, options: DeployedA } export class LockReleaseLockboxTokenPool implements c.Contract { - static CodeCell = c.Cell.fromBase64('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'); + static CodeCell = c.Cell.fromBase64('te6ccgECzwEAMgUAART/APSkE/S88sgLAQIBYgIDAgLLBAUCASClpgIBIAYHAgEgcHECASAUFQIBIAgJAgEgCgsCASA1NgIBIAwNAgEgEBEAKQhkVvhgTpJIZQCusMAk2whcOLy9IAL1GwzMzMi0CLQAsj6UhTMEszJJtDU1DHTBzH0BDH0BDHR0NQx+kgx1DH6SNdM+CgiyM+EAhL6UvpSyQHIz4TQzMz5FsjPigBAy//PUATXLCfT7SIk8r/TP9dM0NP/MddM0AT6ADH6ADAE1ws/yM+Rg/bY7iTPFMlTJciJgDg8ACJ6ew2EAvs8WEss/FfpSEss/UAP6AhL0AMnIz4PMz1CCEAX14QD4KMjPkYP22O4VzMnIz5A+KfqWFMs/UAX6AlKA+lIT+lT0AM+EIM7JyM+FiBP6UgH6As+Bc/oCcc8LZczJcfsAAfUgTpFCcMAGfL0ggC+PCmEP73y9IIAvj2LAizHBbPy9CvQ1NQx0wcx9AQx9AQx0dDUMfpIMdQx+kjXTCbQ0/8x10zQ+kgwyM+EAvpSEvpSyQHIz4TQzMz5FsjPigBAy//PUMjPkNR9344nzws/FswUyx8S+lTJAcjLH1iASABkXwSCAL48IYQ/vfL0gAfz6AlAF+gLJA8j6UhTMEszJhA8khD+98vQjpPgoyM+SQIwR3hPMyQRvAQXIyz8S+lITzCNviHNtVHIhqQaOGwHI9ABTIbYIUSKhIplTgG+BWPpSAaTkAckCoeQwMTXPCwcT9ADJI4IJMS0AbcjPiQgBU0PIz4TQzMz5Fs8L/1gTAEL6AoEAjM8LcBLMEszPkytTR0ITyz8S9ADPgclx+wCBAIYCASAWFwIBIC4vAvdPiRjo7THzHXLCB8U/Us4wLyP+DtRNDU+kjU0z/RI9DU1NMH9AT0BNGBAIZtbW2S8AsAbW2S8AoAbW1tbZLwCQCBAIdWFlYWVhZWFviS+JcFERMFBBESBAMREQMCERACEF8QThA9ECwQWxBKEDkQKBBXEEYQNRAkVh3wDIGBkCASAsLQL+7UTQ1PpI1NM/0QTTPzH6ADH6SDH6UDH0BfiSggC+PyXQ1NQx0wcx9AQx9AQx0dDUMfpIMdQx+kjXTPgoyM+EAvpSEvpSyQHIz4TQzMz5FsjPigBAy//PUBLHBfL0ggC+PyFus/L00NcsIwftsdzyv9dM0PpIMdTUMdEg0InXJxobBNxXEl8NBI4rPDwBwwCTNjY2mTA4OBBHXiMQNOICyMzMywcT9AD0AMnIzPpSEszLP8ntVOBfCSTXLCNoO/l04wLXLCMF1yq04wLXLCapk7bcnTA0AsjM+lLMyz/J7VTg1ywkny3eLOMC1ywiraCVzB0eHyAACPp9pEQB/vK/0z/XTNDT/zHXTNDTPzH6SPoAMCbQ1NQx0wcx9AQx9AQx0dDUMfpIMdQx+kjXTPgoyM+EAvpSEvpSyQHIz4TQzMz5FsjPigBAy//PUG2LBMjPkD4p+pYWyz9QA/oCUjD6UhP6VPQAz4QgEs7JyM+FCBL6UnHPC27MyXH7ACMcAKLQ1DHUMdMHMfQEMfQEMdHQ1ywn0+0iJPK/0z/UMdMfMfQB+lAwIG6RW44ayM+FCPpSghA0dupyzwuOyz/Pivj+yYBB+wDiAsjM+lLMyz/J7VQB/jUE0z/6SDHTPzH6ADH0BfiSggC+PlEVxwXy9IIAvj4hbrPy9NDXLCMH7bHc8r/XTIQPIdD6SDHU1DHR0NcsJ9PtIiTyv9M/1DHTHzH0BDH6UDHRE7oS8vQj0NQx1DHTBzH0BDH0BDHR0PpI1NTRAdAB0AHXLCfT7SIk8r/TP9QhAf41BNM/+kgx+gAx+kgx9AX4koIAvj5RFccF8vSCAL4+IW6z8vTQ1ywkgRgjvPK/10yEDyHQ+kgx1NQx0dDXLCGo+78c8r/TP9Qx0x8x+lAx0RO6EvL0I9DUMdQx0wcx9AQx9AQx0dD6SDHU1DHR0NcsIaj7vxzyv9M/0x8x+lAwIwH+NQTTP9M/1G8AAdMH9AWTIG6zjhDQ9ASa+khQRG+MI8cAFOYw6DAhb4i68on4kvgoBMjLPxT6UiLPFCFviHNtVHIhqQaOGwHI9ABTIbYIUSKhIplTYG+BWPpSAaTkAckCoeQwMTPPCwf0AMlSQIIAvkACyM+E0MzM+RbIz4oAQCQBFuMCXwWEDwHHAPL0JwH+0x8x9AH6UDAB0NP/MdTR0NQx0z/6SDH6ADH6SNEE+gAx+gDU10wGyPpSF/pSIfoCycjPjxgABIIQN91vbs8L93DPC2ETyz8SzMlw+wAhbpJfBY4hBMjME8zJyM+FiBP6UoIQ9DKk488Ljss/zAH6AsmAQPsA4gLIzPpSzMs/ySIABO1UAFggbpFbjhrIz4UI+lKCEO8Ms27PC47LP8+I6RbJgEH7AOICyMz6UszLP8ntVAL+y//PUCPHBfL00NcsJIEYI7zyv9TRIND6SDHU1NHQAdDXLCGo+78c8r/TPzHU0x8x+lAx0dDT/zHXTNDIz5JAjBHeE8zJA8j6VBP0AMkC0x8x+gAwAfpI1ws/ghAF9eEAKNDU1DHTBzH0BDH0BDHR0NQx+kgx1DH6SDDIic8WFyUmAAjQZcMGAGbLPxb6Uss/WPoC+lL0AMnIz4WIUkD6Ulj6As+Bc/oCcc8LZczJcfsAAsjM+lLMyz/J7VQC/jUE0z/UbwAB0wf0BJMhbrOOEgHQ9ASa+khQVW+MJMcAFeYwAegxIm+IWLryiddM+JL4KAXIyz8V+lIjzxQib4hzbVRyIakGjhsByPQAUyG2CFEioSKZU3BvgVj6UgGk5AHJAqHkMDE0zwsHEvQAyVJAgTo+AsjPhNDMzPkWyIkoKQADgBAB/s8Wy//PUBTHBRPy9AHQ+kgx+gAx9AQx+gAx0z8x0x8x1NHQ1ywmqZO23PK/0z/RAdDXLCSBGCO88r/U0dD6SDHU1NGEDyLQ1ywhqPu/HPK/0z/UMdMfMfpQMdEUuhPy9CTQ1DHUMdMHMfQEMfQEMdHQ1ywhqPu/HPK/0z/U0x8qAf4x+lAwAdDT/zHXTNAD0AP6SNM/0/8x+kgwBdMfMfoAMfoAMPgoA8j6UskGyPpSE/pSIvoCFczJyM+PGAAEghDpwAyXzwv3cM8LYRXLPxTMyXD7ACBukl8Djh7IUAP6AsnIz4UIE/pSghDg6IL1zwuOyz/MyYBB+wDiAsjM+lLMKwAKyz/J7VQAVwhbpJbcOCCaQAAAAAAAAAAAAAAAAAAASKDBvQOb6Exklt/4AGDBvQOb6ExgAGkIMJNkzBwceBxepMiwgCOICJxsMABnIT/IqkEIb7yhGaoAd4CqwAgwgCUUSCoAt4C6DAxcIAIBIDAxAgEgMzQBqTtou371ywnkNvtDI5E1ywnzxTyVJRbcNsx4YIAwoojbrPy9CGCAMKKBMcFE/L0IG0D1ws/iwIByMs/FfpSEvpSycjPhyAUznHPC2ETzMlw+wDjDX+AyAI8MCOzkX+VIMAAwwDikTDgU1L4IyehEGheNBA3SIBSgPAHNVFluZWBP63y8OBTNLmOEF8FIJWBP67y8OEwgT+u8vDgUDShUDSAAZmwS0z/6SDCCAMKIUTTHBRPy9IIAwolTI8cFs/L0IYsCyM+HIM5wzwthEss/EvpSyXD7AABZDAjs5F/lSDAAMMA4pEw4FNS+CMnoRBoXjQQN0iAUoDwBzVQRaBQBbYIREQDgADUII4VNly8lYE/rPLw4FFSqKAUtgj4I1BE4FuACASA3OAIBIGVmAT07aLt+yDXLCfT7SIknmwh0z/U0x/0BPpQMPAN4w5/gOQHxCbDAJUrbrPDAJFw4o4jVxMJERYJCBEVCAcRFAcGERMGVQQREirapAMREANP7YEAhg3gERYRHBEWERURGxEVERQRGhEUERMRGRETERIRGBESERERFxERERARHBEQDxEbDw4RGg4NERkNDBEYDAsRFwsKERwKCREbCYGIB/tcsIEeX/byOdGwx0z8x10zQ+kjU1NEB0AHQAdcsJ9PtIiTyv9M/1NMf9AT6UDAF+gD6ANTXTMjPk+n2kRIozws/F8wVyx8T9ABSYPpUychY+gJY+gISzBLMycjPk56NQQYTyz8U+lITzBLMycjPhYgS+lJxzwtuzMmAQPsA4w46Av7XLCU2/TEcjvTXLCObFoTkjhIx0z/6APpQVQPxgBKAHoAX2ziO1mwS1ywhqPu/HJsx0z/U0x/6UDDwFY6+1ywk8VNbNI4zbCHTPzHXTND6SNTU0QHQAdAB1ywhqPu/HPK/0z/U0x/6UDAE0x/6APoAMBBWEEUQNPAb4w7i4uMNOzwD8tcsIQ847DyPbmwh0z8x10zQ+kgx1NTRIdDXLCGo+78c8r/TPzHU1wsfAdDT/zHXTNAC0AL6SDHXCz8C0x8x+gAwAeMP0NcsIaj7vxzyv9M/0x8x+lAwIG6RW44ayM+FCPpSghDvDLNuzwuOyz/PiOkWyYBB+wDi4w49Pj8C3mwx0z8x10zQ+kgx1NTRIdDXLCfT7SIk8r/TPzHU1wsfAdDT/zHXTNAC0ALXCz8C+gAx+gAwAeMP0NcsJ9PtIiTyv9M/1DHTHzH0AfpQMCBukVuOGsjPhQj6UoIQNHbqcs8Ljss/z4jpFsmAQfsA4l1eAv6BOjgiVhiAQPQOb6ES8vTU9ATU1NEg0NTU0dDTd9M/0gDTd9N30SKOOF8GAdDU1NHQ03fTP9IA03fTd9FWItDUMfpIMdQx+kgwUqDwBgTIy3cTyz/KAMt3y3fJAcjMzMkB4w0DyMwS9ADMzFIiERiAQPRDVhnQ1DH6SDHUMfpIQEEB9oE6OCJWGIBA9A5voRLy9NT0BNTU0QHQ1NTR0NN30z/SANN303fRViLQ1DH6SDHUMfpIMFKg8AYEyMt3E8s/ygDLd8t3yQHIzMzJA8jMEvQAEszMUiIRGIBA9ENWGdDUMfpIMdQx+kgwAsjLPxL6UgERFvoCycjPjxgABEIDKNcsIre56byPCdcsIL4SFuTjD+MNQ0RFAFg2ViLQ1DH6SDHUMfpIMBBFEDRBMFQmoPAGBMjLdxPLP8oAy3fLd8kByMzMyQBMMALIyz8S+lIBERb6AsnIz48YAASCEDR//HzPC/dxzwthzMlw+wAAJIIQdAmtj88L93HPC2HMyXD7AAH8MdM/0z/XTFYa0NT6SDHUMfpIMdQx0x8x+lAx0dD6SPpQMdEkggDCiALHBfL0gTo4IlYZgED0Dm+hMfL0gTo4IlYZgED0Dm+hEvL01PQE1NTRgTo3JdDTByHBQfKFAaoC1xjR10nDAPL0JPkAgTo/UxSDB/QOb6Exs/L0VEUURgPy1ywiE1xmJI9u1ywmu4lAhI7j1ywh4oUc3I5YMdM/1wsfERnQ1PpI1PpI1NMfMfpQ0SXQ+kj6UDHRKIIAwogCxwXy9FYeBsjMFfpSE8z6UswSyx/6VMnIz4UIE/pSghBCanE7zwuOyz8BERgByx/JgED7AOMO4w3jDUdISQL+MdM/1NdMVhrQ1PpIMdQx+kgx1DHTHzH6UDHR0PpI+lAx0SSCAMKIAscF8vQB0JQgxwCzjj8g10sBkTCbgTS8AcAB8vTXTNDi0z9SEBEZgED0W4E6OAHy9MjPjxgABIIQJ5CCi88L93DPC2ESyz/JcPsAERfoMNCUIMcAs4roMFhZAKSDB/QXyM+PGAAEghC/DRq2zwv3cM8LYSbPCz8lzxTJcPsAA8jME/QAEszMUiIRGYBA9EPIz4UIFPpSghASzEmFzwuOEss/yz8BERUBzMmAQPsAA/TXLCH65Pq8j2/XLCJ/FpNkjuQx0z/XTFYZ0NT6SDHU+kgx1DHTHzH6UDHR0PpI+lD6UNED0PpI+lAx0ZLwHABUJHDsTySBOj4DxwWSMH+U2gHDAOLy9NCUIMcAs4roMMjPhQgS+lKCEN17DHHPC47LP8mAQPsA4w7jDUpLTADmMdM/+kj6UPpQMBEb0NT6SNQx+kjU0x/6UNEl0PpI+lAx0SqCAMKIAscF8vQnyPpSUnD6VFYhAfpUyQbIzBX6UhXMEvpSzBLLH/pUycjPktzXjDIUyz8S+lL6VAERGQH6VMnIz4UIEvpScc8LbszJgED7AAH6MdM/0z/XTFYa0NT6SDHUMfpIMdQx0x8x+lAx0dD6SPpQMdEkggDCiALHBfL0gTo4IlYZgED0Dm+hMfL0gTo4IlYZgED0Dm+hEvL01PQE1NTRJPkAUAODB/RbgTpAAfL0A8jME/QAEszMUiIRGYBA9EPIz48YAASCELwUx+hXAvwg10sBkTCbgTS8AcAB8vTXTNDi0z/SANTUgTo4JVYcgED0Dm+hEvL01PQE1NTRJ44/AdDUMdQx0SXQ0gDTd9N30fgjIsjLd8s/E8oAy3fLd8kl0NIA03fTd9H4IyLIy3fLPxPKAMt3y3fJAcjMzMkB4w0DyMwS9ADMzFJSERxNTgPy1ywhhQ6PvI9u1ywnGDsl9I7j1ywkyU2yFI5YMdM/+kgwERnQ1PpIMdT6SNTTH/pQ0SXQ+kj6UDHRKIIAwogCxwXy9FYeBsjMFvpSFMwS+lLMyx/6VMnIz4UIE/pSghDl0IsuzwuOyz8BERgB+lLJgED7AOMO4w3jDU9QUQCuMdM/+lAwERnQ1PpI1PpI1NMf+lAx0SXQ+kj6UDHRKIIAwogCxwXy9FYeBsjMFfpSE8z6UszLH/pUycjPhQgT+lKCEDyGnYDPC47LPwERGAH6VMmAQPsAAHrQ1DHUMdEl0NIA03fTd9H4IyLIy3fLPxPKAMt3y3fJJdDSANN303fR+CMiyMt3yz8TygDLd8t3yQHIzMzJAEqAQPRDyM+PGAAEghD/nb92zwv3cM8LYRXLPxPKAMzMyXD7ABEWAf7XLCTtJtBMjk8wVhjQ1PpI1PpI1NMf+lDRBtD6SPpQ0UEJKPAEjig3Vx4RHcj6UhX6VMnIzBL6UswS+lIBERkBzMsfAREXAfpUyREWf9sx4BB4XwjHANsx4THTP/QFVhnQ1PpI1DH6SDHUMdMfMfpQMdEB0PpI+lAx0SSBOj4CUgHQMdM/10xWGdDU+kgx1DH6SDHUMdMfMfpQMdHQ+kj6UDHRI4IAwogCxwXy9BEY0PQE9AT0BNERGtCUIMcAs4roMAHI9AD0AAERGAH0AMnIz4UIEvpSghDX9cVjzwuOAREXAcs/yYBA+wBTAv4x0z/U10xWGtDU+kgx1DH6SDHUMdMfMfpQMdHQ+kj6UDHRJIIAwogCxwXy9AHQlCDHALOK6DDQlCDHALOOOiDXSwGRMJuBNLwBwAHy9NdM0OLTP1IQEReAQPRbMMjPjxgABIIQ1kbH0c8L93DPC2ESyz/JcPsAERXoMMjPhQgSVFUAhMcFkjF/llJCxwXDAOLy9BEY0PQE9AT0BDHRVhkCyPQA9AD0AMnIz4UIE/pSghAVgAFhzwuOyz8BERcB9ADJgED7AADUINdLAZEwm4E0vAHAAfL010zQ4tM/+lD6UCJul1I2gED0WzCbIsj6UlQgR4BA9EPiIW6XUjWAQPRbMJshyPpSVCBGgED0Q+IDyMs/EvpU+lTJyM+PGAAEghCcWruVzwv3cc8LYczJcPsAWAH+INdLAZEwm4E0vAHAAfL010zQ4tM/0x/TH/oA+gDTD9MP0gCBOjgpViGAQPQOb6Ex8vSBOjUi8vSBOjQkgScQufL0gTo0I4EnELny9IE6NSjCAPL0J8jLHyfPCx8m+gIl+gIkzwsPI88LDyLPCgBSkhEggED0QwfIyx8Wyx9QBFYAJPpSghAoy8xkzwuOyz/JgED7AABU+gJY+gLLD8sPygDJyM+PGAAEghD75h8Vzwv3cM8LYRPLPxLMyXD7ABEWAGTPC/dwzwthIs8LP1YYzxTJcPsAyM+FCBT6UoIQ4XvzzM8LjhLLP8s/AREVAczJgED7AAH+INdLAZEwm4E0vAHAAfL010zQ4tM/1NTUgTo3I9DTByHBQfKFAaoC1xjR10nDAPL0gTo7JVYcgED0Dm+hMbPy9AHQ1NTRbQLQ0gDTd9N30fgjIsjLd8s/E8oAy3fLd8kB0NIA03fTd9H4IyLIy3fLPxPKAMt3y3fJAcjMzMn4I1oALsjPhQgS+lKCEK14M9fPC47LP8mAQPsAAbRwyMt3yz9wzwvwyfgjcMjLd8s/cM8L8MkByMzMySQG0JQgxwCziugwBcjMEvQAzBPMUjIRGoBA9EPIz48YAASCEO03xLzPC/dwzwthE8s/AREYAczJcPsAERZbAf4g10sBkTCbgTS8AcAB8vTXTNDi0wchwUHyhQGqAtcYyCLXSSCpOALyRasCIMFB8oXPCwcSzsmBOjch0NMHIcFB8oUBqgLXGNHXScMA8vQg+QCBOj9TFoMH9A5voTGz8vRUQRaDB/QXyM+PGAAEghC/DRq2zwv3cM8LYSnPCz8VXAAKzMlw+wAC/IE6OCJWGIBA9A5voRLy9NT0BNTU0SDQ1NTRAdDTd9M/0gDTd9N30SKOOF8GAdDU1NEB0NN30z/SANN303fRViLQ1DH6SDHUMfpIMFKg8AYEyMt3E8s/ygDLd8t3ycjMzMkB4w0DyMwS9ADMzFIiERiAQPRDVhnQ1DH6SDHUMV9gAfaBOjgiVhiAQPQOb6ES8vTU9ATU1NEB0NTU0QHQ03fTP9IA03fTd9FWItDUMfpIMdQx+kgwUqDwBgTIy3cTyz/KAMt3y3fJyMzMyQPIzBL0ABLMzFIiERiAQPRDVhnQ1DH6SDHUMfpIMALIyz8S+lIBERb6AsnIz48YAARhAFY2ViLQ1DH6SDHUMfpIMBBFEDRBMFQmoPAGBMjLdxPLP8oAy3fLd8nIzMzJAFD6SDACyMs/EvpSAREW+gLJyM+PGAAEghAUH34szwv3cc8LYczJcPsAACSCEDDrq9vPC/dxzwthzMlw+wAB+ggRGggHERkHBhEYBgURFwUEERwEAxEbAwIRGgIBERkBERhWF1YdVh1WHVYdVh3wDgSOOF8EVxdXF1cXVxdXF1cXERARFhEQDxEVDw4RFA4NERMNDBESDAsREQsKERAKEJ8QjhB9EGxVVVUE4MjPk+n2kRJWIc8LPwERIAHMYwH8AREeAcsfAREcAfQAVhoB+lTJyAERHPoCAREc+gIBERwBzAERGgHMycjPk56NQQYBERwByz8BERYB+lIBERcBzAERGQHMycjPhYgBERUB+lJxzwtuAREUAczJgED7ABEQERYREA8RFQ8OERQODRETDQwREgwLERELChEQChCfZAAkEI4QfRBsEFsQShA5SBZFVQcDAfUJsMAlSpus8MAkXDijjFXEwkRFgkIERUIBxEUBwYREwZVBBESVhbaqQgRFQgHERQHBhETBgUREgWBAIYRElVA4CPQ0//U0SDQ0z/6SDH6APpIMIE6PVYi0NQx+kgx1DH6SDBYxwXy9IE6OSJWH4BA9A5voTHy9FYgViCBnAGsUM1fCzg4ODkFwwCVI26zwwCRcOKYNgVQREYW2mDgXwUygTo6AdD0BDH0BDH0BNFY8AKz8vSAB/FYgViBWIFYgViBWIFYgViBWIFYgViBWIFYgViBWIFYgViBWIFYgViBWIFYY8A9WH9D0BPQEMfQEMdFSIIBA9A5voZP6SNGSMG3igTo+IW6z8vSBOj5RG8cF8vQqwwCWVhZus8MAkXDijhBWG1YbVhtWG1YbU+ZWHdpw3lYgAmgD/lYgAlYgAlYgAlYgAlYgAlYgAlYgAlYgAlYgAlYgAlYgAlYgAlYgAlYgAlYgAlYgAlYgAlYgAlYgAlYgAlYgAlYgAgERGgERGVYd8BBRIqEl4w+BOjhRIYBA9A5voRPy9AHU9AQx1DHUMdFWHcjL/8nIz5Pp9pESKc8LPyjPFCdpamsC/lYf0NQx+kgx1DH6SDHUMdMf+lAx0VJg8AiBOjgiVh6AQPQOb6ES8vTU9ATU1NEg0NTU0QHQ03fTP9IA03fTd9EijjhfBgHQ1NTRAdDTd9M/0gDTd9N30VYo0NQx+kgx1DH6SDBSoPAFBMjLdxPLP8oAy3fLd8nIzMzJAeMNA8hsbQH+gTo4IlYegED0Dm+hEvL01PQE1NTRAdDU1NEB0NN30z/SANN303fRVijQ1DH6SDHUMfpIMFKg8AUEyMt3E8s/ygDLd8t3ycjMzMkDyMwS9AASzMxSIhEegED0Q1Yf0NQx+kgx1DH6SDAiyMs/+lJWHfoCycjPjxgABIIQz1BZ/G4B/s8LH1Jg9ABSUPpUycgl+gJWH/oCI88UIs8UyStWIwlWIwlWI1GYViMJViMJViMJViMJViMJViMJViMJViMJViMJViMJViMJViMJViMJViMJViMJViMJViMJViMJViMJCBEjCAcRIgcGESEGBREgBQIRIAIBESIBESHwEQMRGANvAFY2VijQ1DH6SDHUMfpIMBBFEDRBMFQmoPAFBMjLdxPLP8oAy3fLd8nIzMzJAHzMEvQAzMxSIhEegED0Q1Yf0NQx+kgx1DH6SDAiyMs/+lJWHfoCycjPjxgABIIQ66SMC88L93HPC2HMyXD7AAAYzwv3cc8LYczJcPsAAARDNAIBIHJzAgEglpcCASB0dQIBIISFAgEgdncCASB5egCfFcSVxBfDzU1WzMzAtDTP/pIMfoAMAKAQPQOb6GTXwNw4dMfMdMfMfoAMfoAMdMP0w/SANGTXwRw4QORMJEy4oE6NCKBJxC58vSogScQqQSABoQ4OTk6Ojo6Ojo6Ojo/Pz8/A8MAlSpus8MAkXDijhI+EI0QfBBrEFoQSVAHBgUE2tHgbFU1NQfQ1DH6SDHUMfpIMdQx0x8x+lDRIG7jA18IcIHgAliTQ+gAx+gDUMdQx0fgoBMj6UhPMFczJyM+RBKdEJhTLPxfMFcsfE/QAUAT6AhL6UvQAycjPhYgS+lLPhBBz+gJxzwtlzMmAQPsAfwH1CbDAJUobrPDAJFw4o4lMFcSCBEVCAcRFAcGERMGBRESBVUDERFWE9qUAxEQA0/tgQCGDeCBOj5WHdDUMfpIMdQx+kjXTPgoyM+EAvpSEvpSyQHIz4TQzMz5FsjPigBAy//PUCfHBfL0VhxWHFYcVhxWHFYcVhxWHFYcgewL3CjDAJUpbrPDAJFw4o4kVxULERgLChEXCgkRFgkIERUIVQYRFFYV2sQDERADT+2BAIYN4GwzMzMC0AHQAdcsJ9PtIiTyv9M/1NMfMfQB+lAwAdDT/zHU0dDUMdM/+kgx+gAx+kjRBPoAMfoA1NdMBsj6Uhf6UiH6AsnIiYIKDAv5WHFYcVhxWHFYcVhxWHFYcVhxWHFYcVhxWHFYcVhxWHFYcVhxWHI5MW1YbBVYbBVYbBVYbBVYbBVYbBVYbBVYbBVYbBVYbBVYbBVYbBVYbBVYbBVYbBVYbBVYbBVYbBVYbBVYbBVYbBVYbBVYbRRVQRAPwFO3juoAcf+0Riu1BfH0D/CH0BCFumDEgxwCSMG3gktHQ4vpI1NTRVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BVh8BER9WHYrt47qAHn/tEYrtQe3xAX5/gAAK7fEB8v8B/jFWHgVWHgVWHgVWHgVWHgVWHgVWHgVWHgVWHgVWHgVWHgVWHgVWHgVWHgVWHgVWHgVWHgVWHgVWHgVWHgVWHgVWHgVWHgUEER4EAxEdAxEcWfAU+CdvEFMBvJGikltw4vgvoHL7AgHQ1ywn0+0iJPK/0z/UMdMfMfQB+lAwIG6BAGAg0NcsJ9PtIiTyv9dM0NP/MddM0NM/MfpIMfoAMIE6SFEXuvL0gTpHJW6z8vRY8BMABPL/AD6SXwOOGcjPhQj6UoIQNHbqcs8Ljss/yw/JgQCC+wDiAAXGAAEAgs8WghA33W9uzwv3cM8LYRPLPxLMyXD7ACFukl8FjiEEyMwTzMnIz4WIE/pSghD0MqTjzwuOyz/MAfoCyYBA+wDiAgEghocCASCKiwD3DBsxGyENDSBOj4F0NQx+kgx1DH6SNdM+CjIz4QC+lIS+lLJAcjPhNDMzPkWyM+KAEDL/89QIscFFfL0IW6SXwTgghAF9eEAbYsEyM+QPin6lhfLP1AF+gJSMPpSE/pUE/QAz4QgE87JyM+FCBL6Ulj6AnHPC2rMyXP7AIAHxCXDAJUubrPDAJFw4o4vVxIDERUDAhEUAgEREwERElYUVhRWFFYUVhHahAMRFQMCERQCARETARESgQCGERLeERYRGxEWERURGhEVERQRGREUERMRGBETERIRFxESERERGxERERARGhEQDxEZDw4RGA4NERcNDBEbDIIgB/AsRGgsKERkKCREYCQgRFwgHERsHBhEaBgURGQUEERgEAxEXAwIRGwIBERoBERlWGFYYVh1WHVYd8BYDjjRfA1cXVxdXF1cXVxcREREWEREREBEVERAPERQPDhETDg0REg0MEREMCxEQCxCvEJ4QjVVH4BEZER4RGREYER0RGIkArhEXERwRFxEWERsRFhEVERoRFREUERkRFBETERgRExESERcREhERERYREREQERUREA8RFA8OERMODRESDQwREQwLERALEK8QnhCNEHwQaxBaEEkQOFjwGwH3CXDAJUtbrPDAJFw4o4wVxIIERUIBxEUBwYREwYFERIFVQMRESzamAcRFAcGERMGBRESBQQREQSBAIYREVUw4CLQ0/8x1NT0BPQEMdEC0PpIMdM/1DHT//pI0YE6PVYh0NQx+kgx1DH6SDBYxwXy9IE6OSJWHoBA9A5voYIwAUxXElcQXw81NVtsE4BA9A5voZJbcOHUMfQE1DHUMdEB+QABgwf0Dm+hMYAL+MfL0Vh9WH1YfVh9WH1YfVh9WH1YfVh9WH1YfVh9WH1YfVh9WH1YfVh9WH1YfVh9WH1YY8A9WHtD0BDH0BPQEMdFSIIBA9A5voZP6SNGSMG3igTo+IW6z8vSBOj5RGscF8vQpwwCWVhRus8MAkXDi4wBWH1YfVh9WH1YfVh9WH42OACBWGlYaVhpWGlYaU9ZWG9pwAf5WH1YfVh9WH1YfVh9WH1YfVh9WH1YfVh9WH1YfVh9WH1YYgTpAERvwFxPy9FYeAVYeAVYeAVYeAVYeAVYeAVYeAVYeAVYeAVYeAVYeAVYeAVYeAVYeAVYeAVYeAVYeAVYeAVYeAVYeAVYeAVYeAVYeAREZ8BiS8BkAElYfAlYfjwP+AlYfAlYfAlYfAlYfAlYfAlYfAlYfAlYfAlYfAlYfAlYfAlYfAlYfAlYfAlYfAlYfAlYfAlYfAlYfAlYfAlYfWYAZf+0R2gEgwQCRf5YghHe8wwDilYE6QvLw4CPjDyJWGsjPkNR9344nzws/Js8UJc8LH1JA+lTJIsjLHyL6ApCRkgL+Vh3Q1DH6SDHUMfpIMdQx0x/6UDHRUkDwCIE6OCJWHIBA9A5voRLy9NT0BNTU0SDQ1NTR0NN30z/SANN303fRIo44XwYB0NTU0dDTd9M/0gDTd9N30VYm0NQx+kgx1DH6SDBSoPAFBMjLdxPLP8oAy3fLd8kByMzMyQHjDQPIzJOUAfSBOjgiVhyAQPQOb6ES8vTU9ATU1NEB0NTU0dDTd9M/0gDTd9N30VYm0NQx+kgx1DH6SDBSoPAFBMjLdxPLP8oAy3fLd8kByMzMyQPIzBL0ABLMzFIiERyAQPRDVh3Q1DH6SDHUMfpIMALIyz8S+lJWGvoCycjPjxgABJUA1FYd+gLJKVYhCFYhCFYhUYAIViEIViEIViEIViEIViEIViEIViEIViEIViEIViEIViEIViEIViEIViEIViEIViEIViEIViEIViEIBxEhBwYRIAYFER8FBBEgBAMRHwMCESEC8BoCERcCQTMAWDZWJtDUMfpIMdQx+kgwEEUQNEEwVCag8AUEyMt3E8s/ygDLd8t3yQHIzMzJAHwS9ADMzFIiERyAQPRDVh3Q1DH6SDHUMfpIMALIyz8S+lJWGvoCycjPjxgABIIQy8QOW88L93HPC2HMyXD7AAAkghCLsl+ozwv3cc8LYczJcPsAAgEgmJkCASCgoQIBIJqbAgEgnJ0AYxXEF8PNl8EMiBukTDgMdCBOkEh10mDB7qXIddKwADDAJFw4vL00//RgTpBIYQHu/L0gAIEVxBfDzZfBDJTIbqSbCHgUyG8nQKi8AOVgTpC8vDgqQTgAqHwA5WBOkLy8OCBOkIhmYT/IqkEI77DAJF/4vL0qIAGXDc4ODg4ODg5OTk5OT4+Pj4BwwCVLG6zwwCRcOKfPRB8EGsQWhBJEDhVFdrB4GxjNgbQ1DH6SDHUMfpIMdQx0x8x+lDRIG7jA18HcIJ4B9wowwCVLm6zwwCRcOKOIQwRGQwLERgLChEXCgkRFgkIERUILtrVBBERBAMREANP7eAyMwPQ0/8x1NQx9AQx9AQx0dD6SNM/1DHT/zH6SNECyPpSyQLI+lIX+lJQA/oCEszJyM+PGAAEghDpwAyXzwv3cM8LYRXLPxTMyXCCfAJAl0NMfMfoA+gAx0fgoA8j6UhjMFszJyM+RwPCtYhXLPxPMUAX6AhTLHxP6UhL0AMnIz4WIEvpSz4QQc/oCcc8LZczJgED7AH8ATvsAIm6SXwOOHcgB+gLJyM+FiBP6UoIQ4OiC9c8Ljss/zMmAQPsA4gIBIKKjAK9BNfA1cSVxBfDzU1WzMD0NQx+kgx1DH6SDHUMdMf+lAx0VIQ8AhZgED0Dm+hlltwVHAAcOHTH9Mf+gD6ANMP0w/SANGXXwdwVHAAcOEGlDAxA3/gMTQDf4AB0MTIgbrOUxwXDAJJbcOKAB9xXElcQXw81NVuBOjg0gED0Dm+hE/L0AdQx9AQx1NTRAo5XMNDU1NEB0NN30z/SANN303fR+CNQBKEjqBSgI7YI+CMByMt3yz/KABLLd8t3yQHQ03fTP9IA03fTd9H4I1AEoSOoFKAjtgj4IwHIy3fLP8oAEst3y3fJ4DGCkAKzQ1NTRAdDTd9M/0gDTd9N30fgjUAShI6gUoCO2CPgjAcjLd8s/ygASy3fLd8kB0NN30z/SANN303fR+CNQBKEjqBSgI7YI+CMByMt3yz/KABLLd8t3yQIBIKeoAgEgw8QCASCpqgIBILe4AgEgq6wCASC1tgIBIK2uAgEgs7QCAWavsAIBSLGyAF+gX7UTQ1PpIMdQx0z8x0dDU1DHTBzH0BDH0BDHR0NQx+kgx1DH6SDHUMdMf+lAx0YAdaHHtRNDU+kgx1DHTPzHRINAx1DHUMdMHMfQE9AQx0W0hgED0hm+lMpEBnVICbwJREoBA9HxvpTLoMDGAGmlddqJoan0kGOoY6Z+Y6OhqGOppg5j6Ahj6Ahjo6HoCGPoCegIY6MAgegc30Mn9JGjJGDbxQBPpyPaiaGp9JBjqGOmfmOjoamoY6YOY+gIY+gIY6OhqGP0kGOoY/SQYQB3rK/Ghe2NLc1lzG0MLS3Fzo3txcxsbS4FyY3sbWpMrYysLmypjextbE3vCo3tbK3KDe3tkEWpgXGJcYRAAMWt2HaiaGp9JGppn+iR6GpqaYP6AnoCaMCAQza2tsl4BYA2tsl4BQA2tra2yXgEgAiIiIwIiIiICIuIiAeIiweHCIqHBoiKBocIiYcGiIkGhwiIhwaIiAaIZ6qVwIBDqpB4CEAAO7NqO1E0NT6SDHUMdM/MdHQ1DHUMdMH9AQx9AQx0YAC7sB87UTQ1PpI1NM/0SPQ1NTTB/QE9ATRgQCGbW1tkvALAG1tkvAKAG1tbW2S8AkAERERFxERERARFhEQDxEVDw4RFA4NERMNDxESDw4REQ4NERANEM5VG4EAhwLwF4AIBILm6AgEgwcICASC7vABlsnG7UTQ1PpIMdQx0z8x0SDQMdTUMdMHMfQEMfQEMdHQ1DH6SDHUMfpIMdQx0x8x+lDRgAgEgvb4CAWK/wAD4qoftRNDU+kjU0z/RI9DU1NMH9AT0BNGBAIZtbW2S8AsAbW2S8AoAbW1tbZLwCQAREREbEREREBEaERAPERkPDhEYDg0RFw0RFREWERURFBEVERQRExEUERMREhETERIMERIMCxERCwoREAoQnxCOEH0QbFVVgQCHVVDwHgBuqR3tRNDU+kgx1DHTPzHR0NTUMdMHMfQEMfQEMdHQ1PpIMdQx+kgx1DHTHzH6UDHR0PpI+lAx0QBXofu1E0NT6SDHUMdM/MdEg0DHU1DHTBzH0BDH0BDHR0NQx+kgx1DH6SDDHBYAV6B/tRNDU+kgx1DHTPzHRINAx1NQx0wcx9AQx9AQx0dDUMddM0PpI+lD6UNGALuyXrtRNDU+kjU0z/RI9DU1NMH9AT0BNGBAIZtbW2S8AsAbW2S8AoAbW1tbZLwCQAREREXEREREBEWERAPERUPDhEUDg0REw0PERIPDhERDg0REA0QzlUbgQCHAvAdgAGuy4btRNDU+kgx1DHTPzHR0NQx1NMHMfQEMfQEMdHQ9AT0BDH0BDHRgED0Dm+hk/pI0ZIwbeKACASDFxgIBIMfIAGG0fb2omhqfSQY6hjpn5jo6GpqGOmDmPoCGPoCGOjoahj9JGoY/SQY6hjpj5j9KBjowAIm33P2omhqfSQY6hjpn5jokGgY6hjqGOmDmPoCGPoCaMAgegc30McJaY/pj/0AfQBph+mH6QBowIBEzJg2tra2tra2uHFACASDJygIBIMvMAKWxG3tRNDU+kgx1DHTPzHRINAx1DHUMdMHMfQE9AQx0YE6OFmAQPQOb6ES8vTUMfQE1DHUMdFtIYMH9IZvpZCeAdTRWG8CURKDB/R8b6XoECNfA4AAfsop7UTQ1DH6SNQx0z8x0YAIBas3OAEmyHHtRNDU+kgx1DHTPzHR0NQx1DHTBzH0BPQEMdGAQPQOb6ExgAGmlX9qJoan0kGOoY6Z+Y6JBoGOoY6hjpg5j6AnoCGOjAnRwswCB6BzfQiXl6anoCGOoY6hjowBVpwPaiaGp9JBjqGOmfmOjoahjqaYOY+gIY+gIY6Oh6Ahj6Ahj6AmiA+AFZw=='); static Errors = { 'Common_Error.CrossChainAddressOutOfRange': 5, @@ -4738,15 +4931,17 @@ export class LockReleaseLockboxTokenPool implements c.Contract { 'TokenPool_Error.InvalidRemoteChainDecimals': 14913, 'TokenPool_Error.OverflowDetected': 14914, 'TokenPool_Error.UnsupportedOperation': 14917, + 'TokenPool_Error.MissingTransferInitiator': 14919, + 'TokenPool_Error.AmountMismatch': 14920, 'TokenPool_Error.InvalidRequestedFinality': 14921, 'RateLimiter_Error.BucketOverfilled': 16300, 'RateLimiter_Error.TokenMaxCapacityExceeded': 16301, 'RateLimiter_Error.TokenRateLimitReached': 16302, - 'LockReleaseLockBoxTokenPool_Error.PendingLockAlreadyExists': 48700, - 'LockReleaseLockBoxTokenPool_Error.PendingLockNotFound': 48701, - 'LockReleaseLockBoxTokenPool_Error.PendingReleaseAlreadyExists': 48702, - 'LockReleaseLockBoxTokenPool_Error.LockboxNotConfigured': 48704, - 'LockReleaseLockBoxTokenPool_Error.UnexpectedLockboxConfirmationSender': 48705, + 'LockReleaseLockBoxTokenPool_Error.ContextExecutorUnavailable': 48700, + 'LockReleaseLockBoxTokenPool_Error.LockboxNotConfigured': 48701, + 'LockReleaseLockBoxTokenPool_Error.UnexpectedLockboxConfirmationSender': 48702, + 'LockReleaseLockBoxTokenPool_Error.UnexpectedLockBounce': 48703, + 'LockReleaseLockBoxTokenPool_Error.InvalidContextExecutorReply': 48704, 'Ownable2Step_Error.OnlyCallableByOwner': 49800, 'Ownable2Step_Error.CannotTransferToSelf': 49801, 'Ownable2Step_Error.MustBeProposedOwner': 49802, @@ -4778,8 +4973,8 @@ export class LockReleaseLockboxTokenPool implements c.Contract { static fromStorage(emptyStorage: { poolData: TokenPool_Data lockbox: c.Address - pendingLocks: Map /* = [] as map> */ - pendingReleases: Map /* = [] as map> */ + contextExecutorCode: c.Cell + contextExecutorNextId: uint64 }, deployedOptions?: DeployedAddrOptions) { const initialState = { code: deployedOptions?.overrideContractCode ?? LockReleaseLockboxTokenPool.CodeCell, @@ -4939,6 +5134,7 @@ export class LockReleaseLockboxTokenPool implements c.Contract { token: c.Address remoteChainSelector: uint64 amount: coins + context: c.Cell | null }) { return JettonLockBox_Deposited.toCell(JettonLockBox_Deposited.create(body)); } @@ -4948,10 +5144,51 @@ export class LockReleaseLockboxTokenPool implements c.Contract { token: c.Address amount: coins recipientWallet: c.Address + context: c.Cell | null }) { return JettonLockBox_WithdrawFailed.toCell(JettonLockBox_WithdrawFailed.create(body)); } + static createCellOfContextExecutorReplyLockReleaseLockboxTokenPoolReleaseContext_(body: { + queryId?: uint64 + id: uint64 + context: LockReleaseLockboxTokenPool_ReleaseContext + forwardFrom: array + forwardPayload: c.Cell | null + done: boolean + }) { + return makeCellFrom>(ContextExecutor_Reply.create(body), + (v,b) => { b.storeUint(0x93e5bbc5, 32); + b.storeUint(v.queryId, 64); + b.storeUint(v.id, 64); + storeCellRef(v.context, b, LockReleaseLockboxTokenPool_ReleaseContext.store); + storeArrayOf(v.forwardFrom, b, + (v,b) => b.storeAddress(v) + ); + storeTolkNullable(v.forwardPayload, b, + (v,b) => b.storeRef(v) + ); + b.storeBit(v.done); } + ); + } + + static createCellOfContextExecutorForwardNotificationLockReleaseLockboxTokenPoolReleaseContext_(body: { + id: uint64 + context: LockReleaseLockboxTokenPool_ReleaseContext + forwardFrom: array + message: ContextExecutor_InMessageForward + }) { + return makeCellFrom>(ContextExecutor_ForwardNotification.create(body), + (v,b) => { b.storeUint(0x55b412b9, 32); + b.storeUint(v.id, 64); + storeCellRef(v.context, b, LockReleaseLockboxTokenPool_ReleaseContext.store); + storeArrayOf(v.forwardFrom, b, + (v,b) => b.storeAddress(v) + ); + storeCellRef(v.message, b, ContextExecutor_InMessageForward.store); } + ); + } + async sendDeploy(provider: ContractProvider, via: Sender, msgValue: coins, extraOptions?: ExtraSendOptions) { return provider.internal(via, { value: msgValue, @@ -5194,6 +5431,7 @@ export class LockReleaseLockboxTokenPool implements c.Contract { token: c.Address remoteChainSelector: uint64 amount: coins + context: c.Cell | null }, extraOptions?: ExtraSendOptions) { return provider.internal(via, { value: msgValue, @@ -5207,6 +5445,7 @@ export class LockReleaseLockboxTokenPool implements c.Contract { token: c.Address amount: coins recipientWallet: c.Address + context: c.Cell | null }, extraOptions?: ExtraSendOptions) { return provider.internal(via, { value: msgValue, @@ -5215,6 +5454,54 @@ export class LockReleaseLockboxTokenPool implements c.Contract { }); } + async sendContextExecutorReplyLockReleaseLockboxTokenPoolReleaseContext_(provider: ContractProvider, via: Sender, msgValue: coins, body: { + queryId?: uint64 + id: uint64 + context: LockReleaseLockboxTokenPool_ReleaseContext + forwardFrom: array + forwardPayload: c.Cell | null + done: boolean + }, extraOptions?: ExtraSendOptions) { + return provider.internal(via, { + value: msgValue, + body: makeCellFrom>(ContextExecutor_Reply.create(body), + (v,b) => { b.storeUint(0x93e5bbc5, 32); + b.storeUint(v.queryId, 64); + b.storeUint(v.id, 64); + storeCellRef(v.context, b, LockReleaseLockboxTokenPool_ReleaseContext.store); + storeArrayOf(v.forwardFrom, b, + (v,b) => b.storeAddress(v) + ); + storeTolkNullable(v.forwardPayload, b, + (v,b) => b.storeRef(v) + ); + b.storeBit(v.done); } + ), + ...extraOptions + }); + } + + async sendContextExecutorForwardNotificationLockReleaseLockboxTokenPoolReleaseContext_(provider: ContractProvider, via: Sender, msgValue: coins, body: { + id: uint64 + context: LockReleaseLockboxTokenPool_ReleaseContext + forwardFrom: array + message: ContextExecutor_InMessageForward + }, extraOptions?: ExtraSendOptions) { + return provider.internal(via, { + value: msgValue, + body: makeCellFrom>(ContextExecutor_ForwardNotification.create(body), + (v,b) => { b.storeUint(0x55b412b9, 32); + b.storeUint(v.id, 64); + storeCellRef(v.context, b, LockReleaseLockboxTokenPool_ReleaseContext.store); + storeArrayOf(v.forwardFrom, b, + (v,b) => b.storeAddress(v) + ); + storeCellRef(v.message, b, ContextExecutor_InMessageForward.store); } + ), + ...extraOptions + }); + } + async getTypeAndVersion(provider: ContractProvider): Promise<[ c.Slice, c.Slice, @@ -5266,20 +5553,6 @@ export class LockReleaseLockboxTokenPool implements c.Contract { ); } - async getHasPendingLock(provider: ContractProvider, queryId: uint64): Promise { - const r = StackReader.fromGetMethod(1, await provider.get('hasPendingLock', [ - { type: 'int', value: queryId }, - ])); - return r.readBoolean(); - } - - async getHasPendingRelease(provider: ContractProvider, queryId: uint64): Promise { - const r = StackReader.fromGetMethod(1, await provider.get('hasPendingRelease', [ - { type: 'int', value: queryId }, - ])); - return r.readBoolean(); - } - async getRMNProxy(provider: ContractProvider): Promise { const r = StackReader.fromGetMethod(1, await provider.get('getRMNProxy', [])); return r.readSlice().loadAddress(); diff --git a/contracts/wrappers/gen/ccip/pools/LockReleaseTokenPool.ts b/contracts/wrappers/gen/ccip/pools/LockReleaseTokenPool.ts index 064ab24b4..975c2a28f 100644 --- a/contracts/wrappers/gen/ccip/pools/LockReleaseTokenPool.ts +++ b/contracts/wrappers/gen/ccip/pools/LockReleaseTokenPool.ts @@ -11,6 +11,8 @@ import { beginCell, ContractProvider, Sender, SendMode } from '@ton/core'; type RemainingBitsAndRefs = c.Slice +type array = T[] + // TypeScript wrappers flatten a TVM linked list `[1 [2 [3 null]]]` to `[1 2 3]` type lisp_list = T[] @@ -106,6 +108,35 @@ function storeTolkNullable(v: T | null, b: c.Builder, storeFn_T: StoreCallbac } } +function storeArrayOf(v: array, b: c.Builder, storeFn_T: StoreCallback): void { + // the compiler stores array in chunks; in TypeScript, for simplicity, store "1 elem = 1 ref" + let tail = null as c.Cell | null; + for (let i = 0; i < v.length; ++i) { + let chunkB = beginCell().storeMaybeRef(tail); + storeFn_T(v[v.length - 1 - i], chunkB); + tail = chunkB.endCell(); + } + b.storeUint(v.length, 8); + b.storeMaybeRef(tail); +} + +function loadArrayOf(s: c.Slice, loadFn_T: LoadCallback): array { + let len = s.loadUint(8); + let head = s.loadMaybeRef(); + let outArr = [] as array; + while (head != null) { + let s = head.beginParse(); + head = s.loadMaybeRef(); + while (s.remainingBits || s.remainingRefs) { + outArr.push(loadFn_T(s)); + } + } + if (len !== outArr.length) { + throw new Error(`mismatch array binary data: expected ${len} elements, got ${outArr.length}`); + } + return outArr; +} + function storeLispListOf(v: lisp_list, b: c.Builder, storeFn_T: StoreCallback): void { let tail = c.Cell.EMPTY; for (let i = 0; i < v.length; ++i) { @@ -278,6 +309,8 @@ function invokeCustomUnpackFromSlice(typeName: string, s: c.Slice): T { type coins = bigint +type int32 = bigint + type uint8 = bigint type uint16 = bigint type uint32 = bigint @@ -286,6 +319,34 @@ type uint120 = bigint type uint128 = bigint type uint256 = bigint +type varuint32 = bigint + +/** + > type ExtraCurrenciesMap = map + */ +export type ExtraCurrenciesMap = Map + +export const ExtraCurrenciesMap = { + fromSlice(s: c.Slice): ExtraCurrenciesMap { + return dictToMap(c.Dictionary.load(c.Dictionary.Keys.BigInt(32), createDictionaryValue( + (s) => s.loadVarUintBig(5), + (v,b) => b.storeVarUint(v, 5) + ), s)); + }, + store(self: ExtraCurrenciesMap, b: c.Builder): void { + b.storeDict(mapToDict(self, c.Dictionary.Keys.BigInt(32), createDictionaryValue( + (s) => s.loadVarUintBig(5), + (v,b) => b.storeVarUint(v, 5) + )), c.Dictionary.Keys.BigInt(32), createDictionaryValue( + (s) => s.loadVarUintBig(5), + (v,b) => b.storeVarUint(v, 5) + )); + }, + toCell(self: ExtraCurrenciesMap): c.Cell { + return makeCellFrom(self, ExtraCurrenciesMap.store); + } +} + /** > type SnakedCell = cell */ @@ -595,44 +656,6 @@ export const TransferNotificationForRecipient = { } } -/** - > struct (0xd53276db) ReturnExcessesBack { - > queryId: uint64 - > } - */ -export interface ReturnExcessesBack { - readonly $: 'ReturnExcessesBack' - queryId: uint64 -} - -export const ReturnExcessesBack = { - PREFIX: 0xd53276db, - - create(args: { - queryId?: uint64 - }): ReturnExcessesBack { - return { - $: 'ReturnExcessesBack', - ...args, - queryId: args.queryId ?? 0n - } - }, - fromSlice(s: c.Slice): ReturnExcessesBack { - loadAndCheckPrefix32(s, 0xd53276db, 'ReturnExcessesBack'); - return { - $: 'ReturnExcessesBack', - queryId: s.loadUintBig(64), - } - }, - store(self: ReturnExcessesBack, b: c.Builder): void { - b.storeUint(0xd53276db, 32); - b.storeUint(self.queryId, 64); - }, - toCell(self: ReturnExcessesBack): c.Cell { - return makeCellFrom(self, ReturnExcessesBack.store); - } -} - /** > struct JettonClient { > masterAddress: address @@ -688,6 +711,190 @@ export const CrossChainAddress = { } } +/** + > struct (0xcad4d1d0) ContextExecutor_Ask { + > queryId: uint64 + > forwardPayload: cell? + > done: bool + > } + */ +export interface ContextExecutor_Ask { + readonly $: 'ContextExecutor_Ask' + queryId: uint64 + forwardPayload: c.Cell | null + done: boolean +} + +export const ContextExecutor_Ask = { + PREFIX: 0xcad4d1d0, + + create(args: { + queryId?: uint64 + forwardPayload: c.Cell | null + done: boolean + }): ContextExecutor_Ask { + return { + $: 'ContextExecutor_Ask', + ...args, + queryId: args.queryId ?? 0n + } + }, + fromSlice(s: c.Slice): ContextExecutor_Ask { + loadAndCheckPrefix32(s, 0xcad4d1d0, 'ContextExecutor_Ask'); + return { + $: 'ContextExecutor_Ask', + queryId: s.loadUintBig(64), + forwardPayload: s.loadBoolean() ? s.loadRef() : null, + done: s.loadBoolean(), + } + }, + store(self: ContextExecutor_Ask, b: c.Builder): void { + b.storeUint(0xcad4d1d0, 32); + b.storeUint(self.queryId, 64); + storeTolkNullable(self.forwardPayload, b, + (v,b) => b.storeRef(v) + ); + b.storeBit(self.done); + }, + toCell(self: ContextExecutor_Ask): c.Cell { + return makeCellFrom(self, ContextExecutor_Ask.store); + } +} + +/** + > struct (0x93e5bbc5) ContextExecutor_Reply { + > queryId: uint64 + > id: uint64 + > context: Cell + > forwardFrom: array
+ > forwardPayload: cell? + > done: bool + > } + */ +export interface ContextExecutor_Reply { + readonly $: 'ContextExecutor_Reply' + queryId: uint64 + id: uint64 + context: T + forwardFrom: array + forwardPayload: c.Cell | null + done: boolean +} + +export const ContextExecutor_Reply = { + PREFIX: 0x93e5bbc5, + + create(args: { + queryId?: uint64 + id: uint64 + context: T + forwardFrom: array + forwardPayload: c.Cell | null + done: boolean + }): ContextExecutor_Reply { + return { + $: 'ContextExecutor_Reply', + ...args, + queryId: args.queryId ?? 0n + } + }, +} + +/** + > struct (0x55b412b9) ContextExecutor_ForwardNotification { + > id: uint64 + > context: Cell + > forwardFrom: array
+ > message: Cell + > } + */ +export interface ContextExecutor_ForwardNotification { + readonly $: 'ContextExecutor_ForwardNotification' + id: uint64 + context: T + forwardFrom: array + message: ContextExecutor_InMessageForward +} + +export const ContextExecutor_ForwardNotification = { + PREFIX: 0x55b412b9, + + create(args: { + id: uint64 + context: T + forwardFrom: array + message: ContextExecutor_InMessageForward + }): ContextExecutor_ForwardNotification { + return { + $: 'ContextExecutor_ForwardNotification', + ...args + } + }, +} + +/** + > struct ContextExecutor_InMessageForward { + > senderAddress: address + > valueCoins: coins + > valueExtra: ExtraCurrenciesMap + > originalForwardFee: coins + > createdLt: uint64 + > createdAt: uint32 + > body: cell + > } + */ +export interface ContextExecutor_InMessageForward { + readonly $: 'ContextExecutor_InMessageForward' + senderAddress: c.Address + valueCoins: coins + valueExtra: ExtraCurrenciesMap + originalForwardFee: coins + createdLt: uint64 + createdAt: uint32 + body: c.Cell +} + +export const ContextExecutor_InMessageForward = { + create(args: { + senderAddress: c.Address + valueCoins: coins + valueExtra: ExtraCurrenciesMap + originalForwardFee: coins + createdLt: uint64 + createdAt: uint32 + body: c.Cell + }): ContextExecutor_InMessageForward { + return { + $: 'ContextExecutor_InMessageForward', + ...args + } + }, + fromSlice(s: c.Slice): ContextExecutor_InMessageForward { + return { + $: 'ContextExecutor_InMessageForward', + senderAddress: s.loadAddress(), + valueCoins: s.loadCoins(), + valueExtra: ExtraCurrenciesMap.fromSlice(s), + originalForwardFee: s.loadCoins(), + createdLt: s.loadUintBig(64), + createdAt: s.loadUintBig(32), + body: s.loadRef(), + } + }, + store(self: ContextExecutor_InMessageForward, b: c.Builder): void { + b.storeAddress(self.senderAddress); + b.storeCoins(self.valueCoins); + ExtraCurrenciesMap.store(self.valueExtra, b); + b.storeCoins(self.originalForwardFee); + b.storeUint(self.createdLt, 64); + b.storeUint(self.createdAt, 32); + b.storeRef(self.body); + }, + toCell(self: ContextExecutor_InMessageForward): c.Cell { + return makeCellFrom(self, ContextExecutor_InMessageForward.store); + } +} + /** > struct CursedSubjects { > data: map @@ -4223,86 +4430,61 @@ export const TokenPool_RampAccessUpdated = { } /** - > type LockReleaseTokenPool_InMessage = ReturnExcessesBack - */ -export type LockReleaseTokenPool_InMessage = ReturnExcessesBack - -export const LockReleaseTokenPool_InMessage = { - fromSlice(s: c.Slice): LockReleaseTokenPool_InMessage { - return ReturnExcessesBack.fromSlice(s); - }, - store(self: LockReleaseTokenPool_InMessage, b: c.Builder): void { - ReturnExcessesBack.store(self, b); - }, - toCell(self: LockReleaseTokenPool_InMessage): c.Cell { - return makeCellFrom(self, LockReleaseTokenPool_InMessage.store); - } -} - -/** - > struct LockReleaseTokenPool_PendingRelease { - > replyTo: address? - > request: Cell - > out: Cell - > expectedSender: address + > struct (0xed696f9b) LockReleaseTokenPool_ReleaseContext { + > forwardPayload: Cell > } */ -export interface LockReleaseTokenPool_PendingRelease { - readonly $: 'LockReleaseTokenPool_PendingRelease' - replyTo: c.Address | null - request: TokenPool_ReleaseOrMintInV1 - out: TokenPool_ReleaseOrMintOutV1 - expectedSender: c.Address +export interface LockReleaseTokenPool_ReleaseContext { + readonly $: 'LockReleaseTokenPool_ReleaseContext' + forwardPayload: TokenPool_ReleaseOrMintForwardPayload } -export const LockReleaseTokenPool_PendingRelease = { +export const LockReleaseTokenPool_ReleaseContext = { + PREFIX: 0xed696f9b, + create(args: { - replyTo: c.Address | null - request: TokenPool_ReleaseOrMintInV1 - out: TokenPool_ReleaseOrMintOutV1 - expectedSender: c.Address - }): LockReleaseTokenPool_PendingRelease { + forwardPayload: TokenPool_ReleaseOrMintForwardPayload + }): LockReleaseTokenPool_ReleaseContext { return { - $: 'LockReleaseTokenPool_PendingRelease', + $: 'LockReleaseTokenPool_ReleaseContext', ...args } }, - fromSlice(s: c.Slice): LockReleaseTokenPool_PendingRelease { + fromSlice(s: c.Slice): LockReleaseTokenPool_ReleaseContext { + loadAndCheckPrefix32(s, 0xed696f9b, 'LockReleaseTokenPool_ReleaseContext'); return { - $: 'LockReleaseTokenPool_PendingRelease', - replyTo: s.loadMaybeAddress(), - request: loadCellRef(s, TokenPool_ReleaseOrMintInV1.fromSlice), - out: loadCellRef(s, TokenPool_ReleaseOrMintOutV1.fromSlice), - expectedSender: s.loadAddress(), + $: 'LockReleaseTokenPool_ReleaseContext', + forwardPayload: loadCellRef(s, TokenPool_ReleaseOrMintForwardPayload.fromSlice), } }, - store(self: LockReleaseTokenPool_PendingRelease, b: c.Builder): void { - b.storeAddress(self.replyTo); - storeCellRef(self.request, b, TokenPool_ReleaseOrMintInV1.store); - storeCellRef(self.out, b, TokenPool_ReleaseOrMintOutV1.store); - b.storeAddress(self.expectedSender); + store(self: LockReleaseTokenPool_ReleaseContext, b: c.Builder): void { + b.storeUint(0xed696f9b, 32); + storeCellRef(self.forwardPayload, b, TokenPool_ReleaseOrMintForwardPayload.store); }, - toCell(self: LockReleaseTokenPool_PendingRelease): c.Cell { - return makeCellFrom(self, LockReleaseTokenPool_PendingRelease.store); + toCell(self: LockReleaseTokenPool_ReleaseContext): c.Cell { + return makeCellFrom(self, LockReleaseTokenPool_ReleaseContext.store); } } /** > struct Storage { > poolData: Cell - > pendingReleases: map> + > contextExecutorCode: cell + > contextExecutorNextId: uint64 > } */ export interface Storage { readonly $: 'Storage' poolData: TokenPool_Data - pendingReleases: Map /* = [] as map> */ + contextExecutorCode: c.Cell + contextExecutorNextId: uint64 } export const Storage = { create(args: { poolData: TokenPool_Data - pendingReleases: Map /* = [] as map> */ + contextExecutorCode: c.Cell + contextExecutorNextId: uint64 }): Storage { return { $: 'Storage', @@ -4313,21 +4495,14 @@ export const Storage = { return { $: 'Storage', poolData: loadCellRef(s, TokenPool_Data.fromSlice), - pendingReleases: dictToMap(c.Dictionary.load(c.Dictionary.Keys.BigUint(64), createDictionaryValue( - (s) => loadCellRef(s, LockReleaseTokenPool_PendingRelease.fromSlice), - (v,b) => storeCellRef(v, b, LockReleaseTokenPool_PendingRelease.store) - ), s)), + contextExecutorCode: s.loadRef(), + contextExecutorNextId: s.loadUintBig(64), } }, store(self: Storage, b: c.Builder): void { storeCellRef(self.poolData, b, TokenPool_Data.store); - b.storeDict(mapToDict(self.pendingReleases, c.Dictionary.Keys.BigUint(64), createDictionaryValue( - (s) => loadCellRef(s, LockReleaseTokenPool_PendingRelease.fromSlice), - (v,b) => storeCellRef(v, b, LockReleaseTokenPool_PendingRelease.store) - )), c.Dictionary.Keys.BigUint(64), createDictionaryValue( - (s) => loadCellRef(s, LockReleaseTokenPool_PendingRelease.fromSlice), - (v,b) => storeCellRef(v, b, LockReleaseTokenPool_PendingRelease.store) - )); + b.storeRef(self.contextExecutorCode); + b.storeUint(self.contextExecutorNextId, 64); }, toCell(self: Storage): c.Cell { return makeCellFrom(self, Storage.store); @@ -4469,7 +4644,7 @@ function calculateDeployedAddress(code: c.Cell, data: c.Cell, options: DeployedA } export class LockReleaseTokenPool implements c.Contract { - static CodeCell = c.Cell.fromBase64('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'); + static CodeCell = c.Cell.fromBase64('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'); static Errors = { 'Common_Error.CrossChainAddressOutOfRange': 5, @@ -4488,14 +4663,15 @@ export class LockReleaseTokenPool implements c.Contract { 'TokenPool_Error.InvalidRemoteChainDecimals': 14913, 'TokenPool_Error.OverflowDetected': 14914, 'TokenPool_Error.UnsupportedOperation': 14917, + 'TokenPool_Error.MissingTransferInitiator': 14919, + 'TokenPool_Error.AmountMismatch': 14920, 'TokenPool_Error.InvalidRequestedFinality': 14921, 'RateLimiter_Error.BucketOverfilled': 16300, 'RateLimiter_Error.TokenMaxCapacityExceeded': 16301, 'RateLimiter_Error.TokenRateLimitReached': 16302, - 'Error.PendingReleaseAlreadyExists': 26300, - 'Error.PendingReleaseNotFound': 26301, - 'Error.UnexpectedReleaseConfirmationSender': 26302, - 'Error.UnexpectedReleaseBounce': 26303, + 'Error.UnexpectedReleaseBounce': 26300, + 'Error.ContextExecutorUnavailable': 26301, + 'Error.InvalidContextExecutorReply': 26302, 'Ownable2Step_Error.OnlyCallableByOwner': 49800, 'Ownable2Step_Error.CannotTransferToSelf': 49801, 'Ownable2Step_Error.MustBeProposedOwner': 49802, @@ -4526,7 +4702,8 @@ export class LockReleaseTokenPool implements c.Contract { static fromStorage(emptyStorage: { poolData: TokenPool_Data - pendingReleases: Map /* = [] as map> */ + contextExecutorCode: c.Cell + contextExecutorNextId: uint64 }, deployedOptions?: DeployedAddrOptions) { const initialState = { code: deployedOptions?.overrideContractCode ?? LockReleaseTokenPool.CodeCell, @@ -4675,8 +4852,44 @@ export class LockReleaseTokenPool implements c.Contract { return TokenPool_SetCursedSubjects.toCell(TokenPool_SetCursedSubjects.create(body)); } - static createCellOfLockReleaseTokenPoolInMessage(body: LockReleaseTokenPool_InMessage) { - return LockReleaseTokenPool_InMessage.toCell(body); + static createCellOfContextExecutorReplyLockReleaseTokenPoolReleaseContext_(body: { + queryId?: uint64 + id: uint64 + context: LockReleaseTokenPool_ReleaseContext + forwardFrom: array + forwardPayload: c.Cell | null + done: boolean + }) { + return makeCellFrom>(ContextExecutor_Reply.create(body), + (v,b) => { b.storeUint(0x93e5bbc5, 32); + b.storeUint(v.queryId, 64); + b.storeUint(v.id, 64); + storeCellRef(v.context, b, LockReleaseTokenPool_ReleaseContext.store); + storeArrayOf(v.forwardFrom, b, + (v,b) => b.storeAddress(v) + ); + storeTolkNullable(v.forwardPayload, b, + (v,b) => b.storeRef(v) + ); + b.storeBit(v.done); } + ); + } + + static createCellOfContextExecutorForwardNotificationLockReleaseTokenPoolReleaseContext_(body: { + id: uint64 + context: LockReleaseTokenPool_ReleaseContext + forwardFrom: array + message: ContextExecutor_InMessageForward + }) { + return makeCellFrom>(ContextExecutor_ForwardNotification.create(body), + (v,b) => { b.storeUint(0x55b412b9, 32); + b.storeUint(v.id, 64); + storeCellRef(v.context, b, LockReleaseTokenPool_ReleaseContext.store); + storeArrayOf(v.forwardFrom, b, + (v,b) => b.storeAddress(v) + ); + storeCellRef(v.message, b, ContextExecutor_InMessageForward.store); } + ); } async sendDeploy(provider: ContractProvider, via: Sender, msgValue: coins, extraOptions?: ExtraSendOptions) { @@ -4906,10 +5119,50 @@ export class LockReleaseTokenPool implements c.Contract { }); } - async sendLockReleaseTokenPoolInMessage(provider: ContractProvider, via: Sender, msgValue: coins, body: LockReleaseTokenPool_InMessage, extraOptions?: ExtraSendOptions) { + async sendContextExecutorReplyLockReleaseTokenPoolReleaseContext_(provider: ContractProvider, via: Sender, msgValue: coins, body: { + queryId?: uint64 + id: uint64 + context: LockReleaseTokenPool_ReleaseContext + forwardFrom: array + forwardPayload: c.Cell | null + done: boolean + }, extraOptions?: ExtraSendOptions) { + return provider.internal(via, { + value: msgValue, + body: makeCellFrom>(ContextExecutor_Reply.create(body), + (v,b) => { b.storeUint(0x93e5bbc5, 32); + b.storeUint(v.queryId, 64); + b.storeUint(v.id, 64); + storeCellRef(v.context, b, LockReleaseTokenPool_ReleaseContext.store); + storeArrayOf(v.forwardFrom, b, + (v,b) => b.storeAddress(v) + ); + storeTolkNullable(v.forwardPayload, b, + (v,b) => b.storeRef(v) + ); + b.storeBit(v.done); } + ), + ...extraOptions + }); + } + + async sendContextExecutorForwardNotificationLockReleaseTokenPoolReleaseContext_(provider: ContractProvider, via: Sender, msgValue: coins, body: { + id: uint64 + context: LockReleaseTokenPool_ReleaseContext + forwardFrom: array + message: ContextExecutor_InMessageForward + }, extraOptions?: ExtraSendOptions) { return provider.internal(via, { value: msgValue, - body: LockReleaseTokenPool_InMessage.toCell(body), + body: makeCellFrom>(ContextExecutor_ForwardNotification.create(body), + (v,b) => { b.storeUint(0x55b412b9, 32); + b.storeUint(v.id, 64); + storeCellRef(v.context, b, LockReleaseTokenPool_ReleaseContext.store); + storeArrayOf(v.forwardFrom, b, + (v,b) => b.storeAddress(v) + ); + storeCellRef(v.message, b, ContextExecutor_InMessageForward.store); } + ), ...extraOptions }); } @@ -4960,13 +5213,6 @@ export class LockReleaseTokenPool implements c.Contract { ); } - async getHasPendingRelease(provider: ContractProvider, queryId: uint64): Promise { - const r = StackReader.fromGetMethod(1, await provider.get('hasPendingRelease', [ - { type: 'int', value: queryId }, - ])); - return r.readBoolean(); - } - async getRMNProxy(provider: ContractProvider): Promise { const r = StackReader.fromGetMethod(1, await provider.get('getRMNProxy', [])); return r.readSlice().loadAddress(); diff --git a/pkg/ccip/bindings/contextexecutor/exitcode_string.go b/pkg/ccip/bindings/contextexecutor/exitcode_string.go new file mode 100644 index 000000000..706846c19 --- /dev/null +++ b/pkg/ccip/bindings/contextexecutor/exitcode_string.go @@ -0,0 +1,24 @@ +// Code generated by "stringer -type=ExitCode -trimprefix=ExitCode -output=exitcode_string.go"; DO NOT EDIT. + +package contextexecutor + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ExitCodeOnlyCallableByOwner-26000] +} + +const _ExitCode_name = "OnlyCallableByOwner" + +var _ExitCode_index = [...]uint8{0, 19} + +func (i ExitCode) String() string { + idx := int(i) - 26000 + if i < 26000 || idx >= len(_ExitCode_index)-1 { + return "ExitCode(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _ExitCode_name[_ExitCode_index[idx]:_ExitCode_index[idx+1]] +} diff --git a/pkg/ccip/bindings/contextexecutor/types.go b/pkg/ccip/bindings/contextexecutor/types.go new file mode 100644 index 000000000..742954956 --- /dev/null +++ b/pkg/ccip/bindings/contextexecutor/types.go @@ -0,0 +1,122 @@ +package contextexecutor + +import ( + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/smartcontractkit/chainlink-ton/cciplib/ton/tvm" +) + +// --- Constants --- + +const ( + // FacilityID is derived from crc32("link.chain.ton.lib.executor.ContextExecutor") % 640 + 10. + FacilityID = 260 + ErrorCodeBase = FacilityID * 100 +) + +// --- Data types --- + +// Data represents the ContextExecutor contract storage. +// C = context type (generic, encoded as a cell). +// Note: array
in Tolk uses a custom encoding (not standard TLB). +// We encode/decode the entire array as a cell ref for safety. +type Data struct { + ID uint64 `tlb:"## 64"` + Owner *address.Address `tlb:"addr"` + Context *cell.Cell `tlb:"^"` + ForwardFrom *cell.Cell `tlb:"^"` // array
- Tolk uses custom encoding +} + +// --- Messages (incoming) --- + +// Set initializes the executor with context and forwardFrom addresses. +// Only callable by the owner. +// Opcode: 0x44e61eec +type Set struct { + _ tlb.Magic `tlb:"#44e61eec" json:"-"` //nolint:revive // (opcode) should stay uninitialized + QueryID uint64 `tlb:"## 64"` + Context *cell.Cell `tlb:"^"` + ForwardFrom *cell.Cell `tlb:"^"` // array
+} + +// Ask queries the executor for context and forward payload. +// Opcode: 0xcad4d1d0 +type Ask struct { + _ tlb.Magic `tlb:"#cad4d1d0" json:"-"` //nolint:revive // (opcode) should stay uninitialized + QueryID uint64 `tlb:"## 64"` + ForwardPayload *cell.Cell `tlb:"^"` + Done bool `tlb:"bool"` +} + +// --- Messages (outgoing) --- + +// Reply is sent in response to Set or Ask messages. +// Opcode: 0x93e5bbc5 +type Reply struct { + _ tlb.Magic `tlb:"#93e5bbc5" json:"-"` //nolint:revive // (opcode) should stay uninitialized + QueryID uint64 `tlb:"## 64"` + ID uint64 `tlb:"## 64"` + Context *cell.Cell `tlb:"^"` + ForwardFrom *cell.Cell `tlb:"^"` // array
+ ForwardPayload *cell.Cell `tlb:"^"` + Done bool `tlb:"bool"` +} + +// ForwardNotification is sent by the executor to the owner when a message +// arrives from a registered forwardFrom address. The executor clears its +// forwardFrom list before sending this notification (one-shot semantics). +// +// Opcode: 0x55b412b9 +type ForwardNotification struct { + _ tlb.Magic `tlb:"#55b412b9" json:"-"` //nolint:revive // (opcode) should stay uninitialized + ID uint64 `tlb:"## 64"` + Context *cell.Cell `tlb:"^"` + ForwardFrom *cell.Cell `tlb:"^"` // array
(captured before clearing) + Message *cell.Cell `tlb:"^"` // Cell +} + +// --- InMessageForward --- + +// InMessageForward represents an internal message received by the executor. +// This mirrors the Tolk struct ContextExecutor_InMessageForward which is +// a synthetic struct (not a TLB message with an opcode). +// We encode it as a raw cell since it follows InternalMsgAddress encoding, +// not a standard TLB struct. +type InMessageForward struct { + SenderAddress *address.Address `tlb:"addr"` + ValueCoins tlb.Coins `tlb:"int 257"` + ValueExtra *cell.Cell `tlb:"^"` // ExtraCurrenciesMap + OriginalForwardFee tlb.Coins `tlb:"int 257"` + CreatedLT uint64 `tlb:"## 64"` + CreatedAt uint32 `tlb:"## 32"` + Body *cell.Cell `tlb:"^"` +} + +// --- Exit Codes --- + +// ExitCode represents a ContextExecutor-specific error code. +type ExitCode tvm.ExitCode + +//go:generate go run golang.org/x/tools/cmd/stringer@v0.38.0 -type=ExitCode -trimprefix=ExitCode -output=exitcode_string.go + +const ( + ExitCodeOnlyCallableByOwner ExitCode = iota + FacilityID*100 +) + +// New converts an ExitCode to a tvm.ExitCode. +func (e ExitCode) New() tvm.ExitCode { + return tvm.ExitCode(e) +} + +// --- TLB Registry --- + +var TLBs = tvm.MustNewTLBMap([]any{ + // Incoming + Set{}, + Ask{}, + // Outgoing + Reply{}, + ForwardNotification{}, +}).MustWithStorageType(Data{}) diff --git a/pkg/ccip/bindings/tokenpool/burnmint/exitcode_string.go b/pkg/ccip/bindings/tokenpool/burnmint/exitcode_string.go index 812c4f9f0..83a94d5dc 100644 --- a/pkg/ccip/bindings/tokenpool/burnmint/exitcode_string.go +++ b/pkg/ccip/bindings/tokenpool/burnmint/exitcode_string.go @@ -8,17 +8,14 @@ func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} - _ = x[ExitCodePendingBurnAlreadyExists-41200] - _ = x[ExitCodePendingBurnNotFound-41201] - _ = x[ExitCodePendingMintAlreadyExists-41202] - _ = x[ExitCodePendingMintNotFound-41203] - _ = x[ExitCodeUnexpectedBurnConfirmationSender-41204] - _ = x[ExitCodeUnexpectedMintConfirmationSender-41205] + _ = x[ExitCodeUnexpectedBurnBounce-41200] + _ = x[ExitCodeUnexpectedMintBounce-41201] + _ = x[ExitCodeContextExecutorUnavailable-41202] } -const _ExitCode_name = "PendingBurnAlreadyExistsPendingBurnNotFoundPendingMintAlreadyExistsPendingMintNotFoundUnexpectedBurnConfirmationSenderUnexpectedMintConfirmationSender" +const _ExitCode_name = "UnexpectedBurnBounceUnexpectedMintBounceContextExecutorUnavailable" -var _ExitCode_index = [...]uint8{0, 24, 43, 67, 86, 118, 150} +var _ExitCode_index = [...]uint8{0, 20, 40, 66} func (i ExitCode) String() string { idx := int(i) - 41200 diff --git a/pkg/ccip/bindings/tokenpool/burnmint/reader.go b/pkg/ccip/bindings/tokenpool/burnmint/reader.go index e6b4de284..9f0a24bc6 100644 --- a/pkg/ccip/bindings/tokenpool/burnmint/reader.go +++ b/pkg/ccip/bindings/tokenpool/burnmint/reader.go @@ -1,46 +1,12 @@ package burnmint import ( - "fmt" - "math/big" - - "github.com/xssnick/tonutils-go/ton" - "github.com/smartcontractkit/chainlink-ton/cciplib/ccip/bindings/common" - "github.com/smartcontractkit/chainlink-ton/cciplib/ton/tvm" "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/tokenpool" ) // --- Getters from burn_mint/contract.tolk --- -// GetHasPendingBurn checks if there is a pending burn operation for the given query ID. -// -// On-chain: get fun hasPendingBurn(queryId: uint64): bool -var GetHasPendingBurn = tvm.Getter[uint64, bool]{ - Name: "hasPendingBurn", - Decoder: tvm.NewResultDecoder(func(r *ton.ExecutionResult) (bool, error) { - v, err := r.Int(0) - if err != nil { - return false, fmt.Errorf("error getting Int(0) - hasPendingBurn: %w", err) - } - return v.Cmp(big.NewInt(0)) != 0, nil - }), -} - -// GetHasPendingMint checks if there is a pending mint operation for the given query ID. -// -// On-chain: get fun hasPendingMint(queryId: uint64): bool -var GetHasPendingMint = tvm.Getter[uint64, bool]{ - Name: "hasPendingMint", - Decoder: tvm.NewResultDecoder(func(r *ton.ExecutionResult) (bool, error) { - v, err := r.Int(0) - if err != nil { - return false, fmt.Errorf("error getting Int(0) - hasPendingMint: %w", err) - } - return v.Cmp(big.NewInt(0)) != 0, nil - }), -} - // GetTypeAndVersion gets the contract type and version. // // On-chain: get fun typeAndVersion(): (slice, slice) diff --git a/pkg/ccip/bindings/tokenpool/burnmint/types.go b/pkg/ccip/bindings/tokenpool/burnmint/types.go index 5c035f9ab..4fff6ffb0 100644 --- a/pkg/ccip/bindings/tokenpool/burnmint/types.go +++ b/pkg/ccip/bindings/tokenpool/burnmint/types.go @@ -5,29 +5,38 @@ import ( "github.com/xssnick/tonutils-go/tlb" "github.com/xssnick/tonutils-go/tvm/cell" - "github.com/smartcontractkit/chainlink-ton/cciplib/ton/tlbe" "github.com/smartcontractkit/chainlink-ton/cciplib/ton/tvm" "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/tokenpool" ) // --- Constants --- +const ( + // ClaimAdminValue is the TON value for claiming minter admin. + ClaimAdminValue = 50000000 // 0.05 TON in nanotons + // BurnValue is the TON value for burn operations. + BurnValue = 50000000 // 0.05 TON in nanotons + // MintValue is the TON value for mint operations. + MintValue = 100000000 // 0.1 TON in nanotons + // ContextExecutorDeployValue is the TON value for deploying a ContextExecutor. + ContextExecutorDeployValue = 20000000 // 0.02 TON in nanotons +) + // --- Data types --- -// PendingBurn represents a pending burn operation. -// Corresponds to BurnMintTokenPool_PendingBurn in the Tolk contract. -type PendingBurn struct { - ForwardPayload tokenpool.LockOrBurnForwardPayload `tlb:"."` - ExpectedSender *address.Address `tlb:"addr"` +// BurnContext represents the context for a burn operation managed by ContextExecutor. +// Corresponds to BurnMintTokenPool_BurnContext in the Tolk contract (opcode: 0xba302a47). +type BurnContext struct { + _ tlb.Magic `tlb:"#ba302a47" json:"-"` //nolint:revive // (opcode) should stay uninitialized + Wallet *address.Address `tlb:"addr"` + ForwardPayload tokenpool.LockOrBurnForwardPayload `tlb:"^"` } -// PendingMint represents a pending mint operation. -// Corresponds to BurnMintTokenPool_PendingMint in the Tolk contract. -type PendingMint struct { - ReplyTo *address.Address `tlb:"addr"` - Request tokenpool.ReleaseOrMintInV1 `tlb:"^"` - Out tokenpool.ReleaseOrMintOutV1 `tlb:"^"` - ExpectedSender *address.Address `tlb:"addr"` +// MintContext represents the context for a mint operation managed by ContextExecutor. +// Corresponds to BurnMintTokenPool_MintContext in the Tolk contract (opcode: 0xb3d52361). +type MintContext struct { + _ tlb.Magic `tlb:"#b3d52361" json:"-"` //nolint:revive // (opcode) should stay uninitialized + ForwardPayload tokenpool.ReleaseOrMintForwardPayload `tlb:"^"` } // --- Messages (incoming) --- @@ -48,12 +57,11 @@ type ReturnExcessesBack struct { // --- Storage --- // Storage represents the BurnMintTokenPool contract storage. -// Extends the common TokenPool storage with burn/mint specific state. +// Matches Tolk: struct Storage { poolData: Cell; contextExecutorCode: cell; contextExecutorNextId: uint64; } type Storage struct { - PoolData tokenpool.Storage `tlb:"^"` - JettonClient *cell.Cell `tlb:"^"` // Cell - TODO: define JettonClient type in common/jetton bindings - PendingBurns *tlbe.Dict[uint64, *cell.Cell] `tlb:"dict 64"` // map> - PendingMints *tlbe.Dict[uint64, *cell.Cell] `tlb:"dict 64"` // map> + PoolData tokenpool.Storage `tlb:"^"` + ContextExecutorCode *cell.Cell `tlb:"^"` // Code cell for ContextExecutor deployment + ContextExecutorNextID uint64 `tlb:"## 64"` // Monotonically increasing ID for deterministic executor addresses } // --- Exit Codes --- @@ -65,12 +73,9 @@ type ExitCode tvm.ExitCode //go:generate go run golang.org/x/tools/cmd/stringer@v0.38.0 -type=ExitCode -trimprefix=ExitCode -output=exitcode_string.go const ( - ExitCodePendingBurnAlreadyExists ExitCode = iota + 41200 // Facility ID 412 * 100 - ExitCodePendingBurnNotFound - ExitCodePendingMintAlreadyExists - ExitCodePendingMintNotFound - ExitCodeUnexpectedBurnConfirmationSender - ExitCodeUnexpectedMintConfirmationSender + ExitCodeUnexpectedBurnBounce ExitCode = iota + 41200 // Facility ID 412 * 100 + ExitCodeUnexpectedMintBounce + ExitCodeContextExecutorUnavailable ) // New converts an ExitCode to a tvm.ExitCode. @@ -84,6 +89,9 @@ var TLBs = tvm.MustNewTLBMap([]any{ // Incoming ClaimMinterAdmin{}, ReturnExcessesBack{}, + // Context types + BurnContext{}, + MintContext{}, }).MustWithStorageType(Storage{}) // --- Standard interface --- diff --git a/pkg/ccip/bindings/tokenpool/lockbox/types.go b/pkg/ccip/bindings/tokenpool/lockbox/types.go index 36ca6d93c..3a218ff8a 100644 --- a/pkg/ccip/bindings/tokenpool/lockbox/types.go +++ b/pkg/ccip/bindings/tokenpool/lockbox/types.go @@ -5,6 +5,7 @@ import ( "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/tvm/cell" "github.com/smartcontractkit/chainlink-ton/cciplib/ton/tvm" "github.com/smartcontractkit/chainlink-ton/pkg/bindings/lib/access/rbac" @@ -19,14 +20,16 @@ const OperatorRole = 1735955001 // crc32("OPERATOR_ROLE") type Deposit struct { _ tlb.Magic `tlb:"#9e9ec361" json:"-"` //nolint:revive // (opcode) should stay uninitialized QueryID uint64 `tlb:"## 64"` - Token *address.Address `tlb:"addr"` // The address of the token to deposit. - RemoteChainSelector uint64 `tlb:"## 64"` // The chain selector of the remote chain. - Amount *big.Int `tlb:"## 256"` // The amount of tokens to deposit. + Token *address.Address `tlb:"addr"` // The address of the token to deposit. + RemoteChainSelector uint64 `tlb:"## 64"` // The chain selector of the remote chain. + Amount *big.Int `tlb:"## 256"` // The amount of tokens to deposit. + Context *cell.Cell `tlb:"maybe ^"` // Optional context carried through the deposit flow. } // WithdrawExtra holds optional extra fields for Withdraw. type WithdrawExtra struct { SendExcessesTo *address.Address `tlb:"addr"` + FailureContext *cell.Cell `tlb:"maybe ^"` // Optional context carried through withdrawal failure chain. } // Withdraws tokens to a specific recipient. @@ -43,9 +46,10 @@ type Withdraw struct { type Deposited struct { _ tlb.Magic `tlb:"#6d077f2e" json:"-"` //nolint:revive // (opcode) should stay uninitialized QueryID uint64 `tlb:"## 64"` - Token *address.Address `tlb:"addr"` // The token address. - RemoteChainSelector uint64 `tlb:"## 64"` // The chain selector of the remote chain. - Amount *big.Int `tlb:"## 256"` // The amount of tokens deposited. + Token *address.Address `tlb:"addr"` // The token address. + RemoteChainSelector uint64 `tlb:"## 64"` // The chain selector of the remote chain. + Amount *big.Int `tlb:"## 256"` // The amount of tokens deposited. + Context *cell.Cell `tlb:"maybe ^"` // Optional context carried through the deposit flow. } // Init initializes the lockbox with a jetton minter/wallet and admin. @@ -71,9 +75,10 @@ type Initialized struct { type WithdrawFailed struct { _ tlb.Magic `tlb:"#60bae556" json:"-"` //nolint:revive // (opcode) should stay uninitialized QueryID uint64 `tlb:"## 64"` - Token *address.Address `tlb:"addr"` // The token address. - Amount *big.Int `tlb:"## 256"` // The amount that failed to withdraw. - RecipientWallet *address.Address `tlb:"addr"` // The jetton wallet address of the intended recipient. + Token *address.Address `tlb:"addr"` // The token address. + Amount *big.Int `tlb:"## 256"` // The amount that failed to withdraw. + RecipientWallet *address.Address `tlb:"addr"` // The jetton wallet address of the intended recipient. + Context *cell.Cell `tlb:"maybe ^"` // Optional context carried through the failure chain. } // ExitCode represents a JettonLockBox-specific error code. diff --git a/pkg/ccip/bindings/tokenpool/lockrelease/exitcode_string.go b/pkg/ccip/bindings/tokenpool/lockrelease/exitcode_string.go index 4f24700a1..cf9a2a5d2 100644 --- a/pkg/ccip/bindings/tokenpool/lockrelease/exitcode_string.go +++ b/pkg/ccip/bindings/tokenpool/lockrelease/exitcode_string.go @@ -8,15 +8,13 @@ func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} - _ = x[ExitCodePendingReleaseAlreadyExists-26300] - _ = x[ExitCodePendingReleaseNotFound-26301] - _ = x[ExitCodeUnexpectedReleaseConfirmationSender-26302] - _ = x[ExitCodeUnexpectedReleaseBounce-26303] + _ = x[ExitCodeUnexpectedReleaseBounce-26300] + _ = x[ExitCodeContextExecutorUnavailable-26301] } -const _ExitCode_name = "PendingReleaseAlreadyExistsPendingReleaseNotFoundUnexpectedReleaseConfirmationSenderUnexpectedReleaseBounce" +const _ExitCode_name = "UnexpectedReleaseBounceContextExecutorUnavailable" -var _ExitCode_index = [...]uint8{0, 27, 49, 84, 107} +var _ExitCode_index = [...]uint8{0, 23, 49} func (i ExitCode) String() string { idx := int(i) - 26300 diff --git a/pkg/ccip/bindings/tokenpool/lockrelease/reader.go b/pkg/ccip/bindings/tokenpool/lockrelease/reader.go index 0d1281b4d..e2fd103f8 100644 --- a/pkg/ccip/bindings/tokenpool/lockrelease/reader.go +++ b/pkg/ccip/bindings/tokenpool/lockrelease/reader.go @@ -2,7 +2,6 @@ package lockrelease import ( "fmt" - "math/big" "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/ton" @@ -14,25 +13,9 @@ import ( // --- Getters from lock_release/contract.tolk --- -// GetHasPendingRelease checks if there is a pending release operation for the given query ID. -// -// On-chain: get fun hasPendingRelease(queryId: uint64): bool -var GetHasPendingRelease = tvm.Getter[uint64, bool]{ - Name: "hasPendingRelease", - Decoder: tvm.NewResultDecoder(func(r *ton.ExecutionResult) (bool, error) { - v, err := r.Int(0) - if err != nil { - return false, fmt.Errorf("error getting Int(0) - hasPendingRelease: %w", err) - } - return v.Cmp(big.NewInt(0)) != 0, nil - }), -} - // GetLockbox gets the lockbox address (for lock_release variant with lockbox support). // -// On-chain: matching EVM getLockBox() -> address -// Note: The base lock_release contract may not expose this, but the lock_release_lockbox variant does. -// This is provided here for consistency with the EVM LockReleaseTokenPool spec. +// On-chain: get fun lockbox(): address var GetLockbox = tvm.NewNoArgsGetter(tvm.NoArgsOpts[*address.Address]{ Name: "lockbox", Decoder: tvm.NewResultDecoder(func(r *ton.ExecutionResult) (*address.Address, error) { diff --git a/pkg/ccip/bindings/tokenpool/lockrelease/types.go b/pkg/ccip/bindings/tokenpool/lockrelease/types.go index cbe1565d4..22813003c 100644 --- a/pkg/ccip/bindings/tokenpool/lockrelease/types.go +++ b/pkg/ccip/bindings/tokenpool/lockrelease/types.go @@ -1,11 +1,9 @@ package lockrelease import ( - "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/tlb" "github.com/xssnick/tonutils-go/tvm/cell" - "github.com/smartcontractkit/chainlink-ton/cciplib/ton/tlbe" "github.com/smartcontractkit/chainlink-ton/cciplib/ton/tvm" "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/tokenpool" ) @@ -20,17 +18,17 @@ const ( ReleaseTransferValue = 50000000 // 0.05 TON in nanotons // ReplyValue is the TON value for reply messages. ReplyValue = 10000000 // 0.01 TON in nanotons + // ContextExecutorDeployValue is the TON value for deploying a ContextExecutor. + ContextExecutorDeployValue = 20000000 // 0.02 TON in nanotons ) // --- Data types --- -// PendingRelease represents a pending release operation. -// Corresponds to LockReleaseTokenPool_PendingRelease in the Tolk contract. -type PendingRelease struct { - ReplyTo *address.Address `tlb:"addr"` - Request *tokenpool.ReleaseOrMintInV1 `tlb:"^"` - Out *tokenpool.ReleaseOrMintOutV1 `tlb:"^"` - ExpectedSender *address.Address `tlb:"addr"` +// ReleaseContext represents the context for a release operation managed by ContextExecutor. +// Corresponds to LockReleaseTokenPool_ReleaseContext in the Tolk contract (opcode: 0xed696f9b). +type ReleaseContext struct { + _ tlb.Magic `tlb:"#ed696f9b" json:"-"` //nolint:revive // (opcode) should stay uninitialized + ForwardPayload tokenpool.ReleaseOrMintForwardPayload `tlb:"^"` } // --- Messages (incoming) --- @@ -45,11 +43,11 @@ type ReturnExcessesBack struct { // --- Storage --- // Storage represents the LockReleaseTokenPool contract storage. -// Extends the common TokenPool storage with lock/release specific state. +// Matches Tolk: struct Storage { poolData: Cell; contextExecutorCode: cell; contextExecutorNextId: uint64; } type Storage struct { - PoolData tokenpool.Storage `tlb:"^"` - JettonClient *cell.Cell `tlb:"^"` // Cell - TODO: define JettonClient type in common/jetton bindings - PendingReleases *tlbe.Dict[uint64, *cell.Cell] `tlb:"dict 64"` // map> + PoolData tokenpool.Storage `tlb:"^"` + ContextExecutorCode *cell.Cell `tlb:"^"` // Code cell for ContextExecutor deployment + ContextExecutorNextID uint64 `tlb:"## 64"` // Monotonically increasing ID for deterministic executor addresses } // --- Exit Codes --- @@ -61,10 +59,8 @@ type ExitCode tvm.ExitCode //go:generate go run golang.org/x/tools/cmd/stringer@v0.38.0 -type=ExitCode -trimprefix=ExitCode -output=exitcode_string.go const ( - ExitCodePendingReleaseAlreadyExists ExitCode = iota + 26300 // Facility ID 263 * 100 - ExitCodePendingReleaseNotFound - ExitCodeUnexpectedReleaseConfirmationSender - ExitCodeUnexpectedReleaseBounce + ExitCodeUnexpectedReleaseBounce ExitCode = iota + 26300 // Facility ID 263 * 100 + ExitCodeContextExecutorUnavailable ) // New converts an ExitCode to a tvm.ExitCode. @@ -77,6 +73,8 @@ func (e ExitCode) New() tvm.ExitCode { var TLBs = tvm.MustNewTLBMap([]any{ // Incoming ReturnExcessesBack{}, + // Context types + ReleaseContext{}, }).MustWithStorageType(Storage{}) // --- Standard interface --- diff --git a/pkg/ccip/bindings/tokenpool/lockreleaselockbox/exitcode_string.go b/pkg/ccip/bindings/tokenpool/lockreleaselockbox/exitcode_string.go new file mode 100644 index 000000000..db318c5e7 --- /dev/null +++ b/pkg/ccip/bindings/tokenpool/lockreleaselockbox/exitcode_string.go @@ -0,0 +1,27 @@ +// Code generated by "stringer -type=ExitCode -trimprefix=ExitCode -output=exitcode_string.go"; DO NOT EDIT. + +package lockreleaselockbox + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ExitCodeContextExecutorUnavailable-48700] + _ = x[ExitCodeLockboxNotConfigured-48701] + _ = x[ExitCodeUnexpectedLockboxConfirmationSender-48702] + _ = x[ExitCodeUnexpectedLockBounce-48703] +} + +const _ExitCode_name = "ContextExecutorUnavailableLockboxNotConfiguredUnexpectedLockboxConfirmationSenderUnexpectedLockBounce" + +var _ExitCode_index = [...]uint8{0, 26, 46, 81, 101} + +func (i ExitCode) String() string { + idx := int(i) - 48700 + if i < 48700 || idx >= len(_ExitCode_index)-1 { + return "ExitCode(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _ExitCode_name[_ExitCode_index[idx]:_ExitCode_index[idx+1]] +} diff --git a/pkg/ccip/bindings/tokenpool/lockreleaselockbox/reader.go b/pkg/ccip/bindings/tokenpool/lockreleaselockbox/reader.go index e009e3dea..c270fc735 100644 --- a/pkg/ccip/bindings/tokenpool/lockreleaselockbox/reader.go +++ b/pkg/ccip/bindings/tokenpool/lockreleaselockbox/reader.go @@ -2,7 +2,6 @@ package lockreleaselockbox import ( "fmt" - "math/big" "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/ton" @@ -19,34 +18,6 @@ import ( // --- Variant-specific getters --- -// GetHasPendingLock checks if there is a pending lock operation for the given query ID. -// -// On-chain: get fun hasPendingLock(queryId: uint64): bool -var GetHasPendingLock = tvm.Getter[uint64, bool]{ - Name: "hasPendingLock", - Decoder: tvm.NewResultDecoder(func(r *ton.ExecutionResult) (bool, error) { - v, err := r.Int(0) - if err != nil { - return false, fmt.Errorf("error getting Int(0) - hasPendingLock: %w", err) - } - return v.Cmp(big.NewInt(0)) != 0, nil - }), -} - -// GetHasPendingRelease checks if there is a pending release operation for the given query ID. -// -// On-chain: get fun hasPendingRelease(queryId: uint64): bool -var GetHasPendingRelease = tvm.Getter[uint64, bool]{ - Name: "hasPendingRelease", - Decoder: tvm.NewResultDecoder(func(r *ton.ExecutionResult) (bool, error) { - v, err := r.Int(0) - if err != nil { - return false, fmt.Errorf("error getting Int(0) - hasPendingRelease: %w", err) - } - return v.Cmp(big.NewInt(0)) != 0, nil - }), -} - // GetLockbox gets the JettonLockBox address used by this token pool. // // On-chain: get fun lockbox(): address diff --git a/pkg/ccip/bindings/tokenpool/lockreleaselockbox/types.go b/pkg/ccip/bindings/tokenpool/lockreleaselockbox/types.go new file mode 100644 index 000000000..5e91b6d48 --- /dev/null +++ b/pkg/ccip/bindings/tokenpool/lockreleaselockbox/types.go @@ -0,0 +1,96 @@ +package lockreleaselockbox + +import ( + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/smartcontractkit/chainlink-ton/cciplib/ton/tvm" + "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/tokenpool" +) + +// --- Constants --- + +const ( + // ContextExecutorDeployValue is the TON value for deploying a ContextExecutor. + // Matches Tolk: LockReleaseLockboxTokenPool_CONTEXT_EXECUTOR_DEPLOY_VALUE = ton("0.02") + ContextExecutorDeployValue = 20000000 // 0.02 TON in nanotons +) + +// --- Data types --- + +// LockContext represents the context for a lock operation managed by ContextExecutor. +// Corresponds to LockReleaseLockboxTokenPool_LockContext (opcode: 0x60fdb63b). +type LockContext struct { + _ tlb.Magic `tlb:"#60fdb63b" json:"-"` //nolint:revive // (opcode) should stay uninitialized + ForwardPayload tokenpool.LockOrBurnForwardPayload `tlb:"^"` +} + +// ReleaseContext represents the context for a release operation managed by ContextExecutor. +// Corresponds to LockReleaseLockboxTokenPool_ReleaseContext (opcode: 0x90230477). +type ReleaseContext struct { + _ tlb.Magic `tlb:"#90230477" json:"-"` //nolint:revive // (opcode) should stay uninitialized + ForwardPayload tokenpool.ReleaseOrMintForwardPayload `tlb:"^"` +} + +// --- Messages (incoming) --- + +// ReturnExcessesBack is sent by the jetton wallet after a transfer operation. +// TODO: move to shared jetton bindings package, shared opcode 0xd53276db +type ReturnExcessesBack struct { + _ tlb.Magic `tlb:"#d53276db" json:"-"` //nolint:revive // (opcode) should stay uninitialized + QueryID uint64 `tlb:"## 64"` +} + +// --- Storage --- + +// Storage represents the LockReleaseLockboxTokenPool contract storage. +// Matches Tolk: struct Storage { poolData: Cell; lockbox: address; contextExecutorCode: cell; contextExecutorNextId: uint64; } +type Storage struct { + PoolData tokenpool.Storage `tlb:"^"` + Lockbox *address.Address `tlb:"addr"` // JettonLockBox address + ContextExecutorCode *cell.Cell `tlb:"^"` // Code cell for ContextExecutor deployment + ContextExecutorNextID uint64 `tlb:"## 64"` // Monotonically increasing ID for deterministic executor addresses +} + +// --- Exit Codes --- + +// ExitCode represents a LockReleaseLockboxTokenPool-specific error code. +// FACILITY_ID = 487, base error = 48700. +type ExitCode tvm.ExitCode + +//go:generate go run golang.org/x/tools/cmd/stringer@v0.38.0 -type=ExitCode -trimprefix=ExitCode -output=exitcode_string.go + +const ( + ExitCodeContextExecutorUnavailable ExitCode = iota + 48700 // Facility ID 487 * 100 + ExitCodeLockboxNotConfigured + ExitCodeUnexpectedLockboxConfirmationSender + ExitCodeUnexpectedLockBounce +) + +// New converts an ExitCode to a tvm.ExitCode. +func (e ExitCode) New() tvm.ExitCode { + return tvm.ExitCode(e) +} + +// --- TLB Registry --- + +var TLBs = tvm.MustNewTLBMap([]any{ + // Incoming + ReturnExcessesBack{}, + // Context types + LockContext{}, + ReleaseContext{}, +}).MustWithStorageType(Storage{}) + +// --- Standard interface --- + +// Re-import common types from the parent tokenpool package for convenience. +type ( + LockOrBurn = tokenpool.LockOrBurn + ReleaseOrMint = tokenpool.ReleaseOrMint + LockOrBurnTransferDetails = tokenpool.LockOrBurnTransferDetails + ReleaseOrMintTransferDetails = tokenpool.ReleaseOrMintTransferDetails + LockOrBurnForwardPayload = tokenpool.LockOrBurnForwardPayload + ChainSelector = tokenpool.ChainSelector +)