-
Notifications
You must be signed in to change notification settings - Fork 15
Support TON → Solana sends in ccip-cli #211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
d7bb432
7fceedd
eb45a9a
11302f7
009d83b
89482ba
f17ceb5
2f3e794
4072c7f
eeef3e0
7645c87
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,19 @@ | ||
| import { type Cell, beginCell, toNano } from '@ton/core' | ||
| import { type TonClient, Address } from '@ton/ton' | ||
| import { zeroPadValue } from 'ethers' | ||
| import { toBigInt, zeroPadValue } from 'ethers' | ||
|
|
||
| import type { UnsignedTONTx } from './types.ts' | ||
| import { CCIPError, CCIPErrorCode, CCIPExtraArgsInvalidError } from '../errors/index.ts' | ||
| import { type ExtraArgs, EVMExtraArgsV2Tag } from '../extra-args.ts' | ||
| import { | ||
| type ExtraArgs, | ||
| type SVMExtraArgsV1, | ||
| type SuiExtraArgsV1, | ||
| EVMExtraArgsV2Tag, | ||
| SVMExtraArgsV1Tag, | ||
| SuiExtraArgsV1Tag, | ||
| } from '../extra-args.ts' | ||
| import { type AnyMessage, type WithLogger, ChainFamily } from '../types.ts' | ||
| import { bigIntReplacer, bytesToBuffer, getDataBytes } from '../utils.ts' | ||
| import { bigIntReplacer, bytesToBuffer, getAddressBytes } from '../utils.ts' | ||
|
|
||
| /** Opcode for Router ccipSend operation */ | ||
| export const CCIP_SEND_OPCODE = 0x31768d95 | ||
|
|
@@ -45,18 +52,51 @@ function encodeTokenAmounts( | |
| return builder.endCell() | ||
| } | ||
|
|
||
| /** | ||
| * Checks if extraArgs is SVMExtraArgsV1 format. | ||
| */ | ||
| function isSVMExtraArgs(extraArgs: ExtraArgs): extraArgs is SVMExtraArgsV1 { | ||
| return 'computeUnits' in extraArgs | ||
| } | ||
|
|
||
| /** | ||
| * Checks if extraArgs is SuiExtraArgsV1 format. | ||
| */ | ||
| function isSuiExtraArgs(extraArgs: ExtraArgs): extraArgs is SuiExtraArgsV1 { | ||
| return 'receiverObjectIds' in extraArgs | ||
| } | ||
|
|
||
| /** | ||
| * Encodes extraArgs as a Cell. | ||
| * | ||
| * Supports three formats based on the destination chain: | ||
| * - GenericExtraArgsV2 (EVMExtraArgsV2) for EVM/TON/Aptos destinations | ||
| * - SVMExtraArgsV1 for Solana destinations | ||
| * - SuiExtraArgsV1 for Sui destinations | ||
| * | ||
| * @param extraArgs - Extra arguments for CCIP message | ||
| * @returns Cell encoding the extra arguments | ||
| * @throws {@link CCIPExtraArgsInvalidError} if extraArgs format is invalid | ||
| */ | ||
| export function encodeExtraArgsCell(extraArgs: ExtraArgs): Cell { | ||
| if (isSVMExtraArgs(extraArgs)) { | ||
| return encodeSVMExtraArgsCell(extraArgs) | ||
| } | ||
| if (isSuiExtraArgs(extraArgs)) { | ||
| return encodeSuiExtraArgsCell(extraArgs) | ||
| } | ||
| return encodeEVMExtraArgsCell(extraArgs) | ||
| } | ||
|
|
||
| /** | ||
| * Encodes extraArgs as a Cell using the GenericExtraArgsV2 (EVMExtraArgsV2) format. | ||
| * | ||
| * Format per chainlink-ton TL-B: | ||
| * - tag: 32-bit opcode (0x181dcf10) | ||
| * - gasLimit: Maybe<uint256> (1 bit flag + 256 bits if present) | ||
| * - allowOutOfOrderExecution: 1 bit | ||
| * @param extraArgs - Extra arguments for CCIP message | ||
| * @returns Cell encoding the extra arguments | ||
| * @throws {@link CCIPExtraArgsInvalidError} if `extraArgs` contains fields other than `gasLimit` and `allowOutOfOrderExecution` | ||
| */ | ||
| export function encodeExtraArgsCell(extraArgs: ExtraArgs): Cell { | ||
| function encodeEVMExtraArgsCell(extraArgs: ExtraArgs): Cell { | ||
| if ( | ||
| Object.keys(extraArgs).filter((k) => k !== '_tag').length !== 2 || | ||
| !('gasLimit' in extraArgs && 'allowOutOfOrderExecution' in extraArgs) | ||
|
|
@@ -75,6 +115,81 @@ export function encodeExtraArgsCell(extraArgs: ExtraArgs): Cell { | |
| return builder.endCell() | ||
| } | ||
|
|
||
| /** | ||
| * Encodes extraArgs as a Cell using the SVMExtraArgsV1 format. | ||
| * | ||
| * Format per chainlink-ton TL-B: | ||
| * - tag: 32-bit opcode (0x1f3b3aba) | ||
| * - computeUnits: uint32 | ||
| * - accountIsWritableBitmap: uint64 | ||
| * - allowOutOfOrderExecution: bool | ||
| * - tokenReceiver: uint256 | ||
| * - accounts: SnakedCell<uint256> | ||
| */ | ||
|
|
||
| function encodeSVMExtraArgsCell(extraArgs: SVMExtraArgsV1): Cell { | ||
| // Encode accounts as a snaked cell of uint256 values | ||
| let accountsCell = beginCell().endCell() | ||
| if (extraArgs.accounts.length > 0) { | ||
| const accountBuilder = beginCell() | ||
| for (const account of extraArgs.accounts) { | ||
| accountBuilder.storeUint(toBigInt(getAddressBytes(account)), 256) | ||
| } | ||
| accountsCell = accountBuilder.endCell() | ||
| } | ||
|
krebernisak marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Encode tokenReceiver as uint256 | ||
| const tokenReceiver = extraArgs.tokenReceiver | ||
| ? toBigInt(getAddressBytes(extraArgs.tokenReceiver)) | ||
| : 0n | ||
|
|
||
| const builder = beginCell() | ||
| .storeUint(Number(SVMExtraArgsV1Tag), 32) // 0x1f3b3aba | ||
| .storeUint(Number(extraArgs.computeUnits), 32) | ||
| .storeUint(extraArgs.accountIsWritableBitmap, 64) | ||
| .storeBit(extraArgs.allowOutOfOrderExecution) | ||
| .storeUint(tokenReceiver, 256) // uint256 | ||
| .storeRef(accountsCell) // SnakedCell<uint256> | ||
|
|
||
| return builder.endCell() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we please test But in ccip-o11y, we'll need to serialize those to store in ccip-o11y-db, and it'd be good to have a consistent way to serialize them here (we may eventually pull ccip-sdk into ccip-o11y, and then we'll need this). Also, the serialized extraArgs coming from ccip-o11y db through CCIP-API for these will be decoded. So far for EVM, our idea was to store only the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, Andre! I've pushed a commit: eeef3e0 Please review and let's discuss how to resolve this cc @Farber98
This is simple for EVM (small args) but not straightforwards for SVM/SUI extra args which have lists stored as nested cells, and might fail on other future args where more data is required. Cell is a tree structure and has a special BOC encoding b/c of it. This clashes with the idea that chain-specific extraArgs encodings should start with a standard prefix. Then there's also the current observability implementation which just reads first cell bits, which currently drops data for SVM/SUI (data encoded as more than one root cell): https://github.com/smartcontractkit/ccip-o11y/blob/main/ccip/ton/processor/ton_event_decode.go#L266 |
||
| } | ||
|
|
||
| /** | ||
| * Encodes extraArgs as a Cell using the SuiExtraArgsV1 format. | ||
| * | ||
| * Format per chainlink-ton TL-B: | ||
| * - tag: 32-bit opcode (0x21ea4ca9) | ||
| * - gasLimit: uint256 | ||
| * - allowOutOfOrderExecution: bool | ||
| * - tokenReceiver: uint256 | ||
| * - receiverObjectIds: SnakedCell<uint256> | ||
| */ | ||
| function encodeSuiExtraArgsCell(extraArgs: SuiExtraArgsV1): Cell { | ||
| // Encode receiverObjectIds as a snaked cell of uint256 values | ||
| let objectIdsCell = beginCell().endCell() | ||
| if (extraArgs.receiverObjectIds.length > 0) { | ||
| const objectIdsBuilder = beginCell() | ||
| for (const objectId of extraArgs.receiverObjectIds) { | ||
| objectIdsBuilder.storeUint(toBigInt(getAddressBytes(objectId)), 256) | ||
| } | ||
| objectIdsCell = objectIdsBuilder.endCell() | ||
| } | ||
|
|
||
| // Encode tokenReceiver as uint256 | ||
| const tokenReceiver = extraArgs.tokenReceiver | ||
| ? toBigInt(getAddressBytes(extraArgs.tokenReceiver)) | ||
| : 0n | ||
|
|
||
| const builder = beginCell() | ||
| .storeUint(Number(SuiExtraArgsV1Tag), 32) // 0x21ea4ca9 | ||
| .storeUint(extraArgs.gasLimit, 256) | ||
| .storeBit(extraArgs.allowOutOfOrderExecution) | ||
| .storeUint(tokenReceiver, 256) // uint256 | ||
| .storeRef(objectIdsCell) // SnakedCell<uint256> | ||
|
|
||
| return builder.endCell() | ||
| } | ||
|
|
||
| /** | ||
| * Builds the Router ccipSend message cell. | ||
| * | ||
|
|
@@ -92,8 +207,11 @@ export function buildCcipSendCell( | |
| feeTokenAddress: Address | null = null, | ||
| queryId = 0n, | ||
| ): Cell { | ||
| // Get receiver bytes and pad to 32 bytes for cross-chain encoding | ||
| const paddedReceiver = bytesToBuffer(zeroPadValue(getDataBytes(message.receiver), 32)) | ||
| // Get receiver bytes — use getAddressBytes to handle hex, base58 (Solana), TON raw formats | ||
| const receiverBytes = getAddressBytes(message.receiver) | ||
| const paddedReceiver = bytesToBuffer( | ||
| receiverBytes.length <= 32 ? zeroPadValue(receiverBytes, 32) : receiverBytes, | ||
|
krebernisak marked this conversation as resolved.
Outdated
|
||
| ) | ||
|
|
||
| // Data cell (ref 0) | ||
| const dataCell = beginCell() | ||
|
|
@@ -162,8 +280,11 @@ export async function getFee( | |
| } | ||
|
|
||
| // Build stack parameters for validatedFee call | ||
| const paddedReceiver = bytesToBuffer(zeroPadValue(getDataBytes(message.receiver), 32)) | ||
| const receiverSlice = beginCell().storeBuffer(paddedReceiver).endCell() | ||
| const feeReceiverBytes = getAddressBytes(message.receiver) | ||
| const paddedFeeReceiver = bytesToBuffer( | ||
| feeReceiverBytes.length <= 32 ? zeroPadValue(feeReceiverBytes, 32) : feeReceiverBytes, | ||
| ) | ||
| const receiverSlice = beginCell().storeBuffer(paddedFeeReceiver).endCell() | ||
| const dataCell = beginCell() | ||
| .storeBuffer(bytesToBuffer(message.data || '0x')) | ||
| .endCell() | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.