Skip to content
143 changes: 132 additions & 11 deletions ccip-sdk/src/ton/send.ts
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
Expand Down Expand Up @@ -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)) {
Comment thread
krebernisak marked this conversation as resolved.
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)
Expand All @@ -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()
}
Comment thread
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()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please test TONChain.encodeExtraArgs() with these, and decodeExtraArgs its result?
The encode method isn't actively used right now, as we build the cells directly here from the ExtraArgs object when we need to ccipSend below, so we use the Cell result directly, which should be fine.

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 bits content of the cell in serialized form (i.e. NOT the whole serialized cell), so it can begin with the tag, for consistency with all other chain families. But we need to make sure this also works for these snaked cells.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

So far for EVM, our idea was to store only the bits content of the cell in serialized form (i.e. NOT the whole serialized cell)

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.
*
Expand All @@ -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,
Comment thread
krebernisak marked this conversation as resolved.
Outdated
)

// Data cell (ref 0)
const dataCell = beginCell()
Expand Down Expand Up @@ -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()
Expand Down
Loading