Skip to content

Commit 90cede0

Browse files
authored
implement Chain.buildMessageForDest and allow partial extraArgs (#102)
* implement Chain.populateDefaultMessageForDest and allow partial extraArgs this function populates a partial message (`RequestMessage` type is exported and replaces stricter `AnyMesssage`) with default extraArgs and massage other parameters; it allows sending a simple token transfer by just calling: `chain.sendMessage({router, dest, message: { receiver, tokenAmounts: [{ token, amount}] }})` * bump to v0.94, add changelog * chore: bump deps, selectors * address comments, rename buildMessageForDest
1 parent 6684849 commit 90cede0

File tree

17 files changed

+1218
-484
lines changed

17 files changed

+1218
-484
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.94.0]
11+
1012
- SDK: Browser compatibility - explicit `buffer` dependency and imports for cross-platform support
1113
- CI: Added `publint` and `@arethetypeswrong/cli` validation for package exports
1214
- ESLint: `import/no-nodejs-modules` rule prevents Node.js-only imports in SDK
1315
- Docs: Cross-Platform Portability guidelines in CONTRIBUTING.md
16+
- SDK: Populate default extraArgs for getFee, sendMessage methods, requiring minimal parameters to use these methods
1417

15-
## [0.93.0] - 2025-12-26 - Pre-release
18+
## [0.93.0] - 2025-12-31 - Pre-release
1619

1720
- SDK: `CCIPAPIClient` and `Chain.getLaneLatency()` for querying lane delivery times via CCIP API
1821
- CLI: `lane-latency <source> <dest>` command; `--no-api` flag for decentralized mode

ccip-cli/package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@chainlink/ccip-cli",
3-
"version": "0.93.0",
3+
"version": "0.94.0",
44
"description": "CCIP Command Line Interface, based on @chainlink/ccip-sdk",
55
"author": "Chainlink devs",
66
"license": "MIT",
@@ -32,34 +32,34 @@
3232
],
3333
"devDependencies": {
3434
"@eslint/js": "^9.39.2",
35-
"@types/node": "25.0.3",
35+
"@types/node": "25.0.6",
3636
"@types/yargs": "17.0.35",
3737
"eslint": "^9.39.2",
3838
"eslint-config-prettier": "10.1.8",
3939
"eslint-import-resolver-typescript": "4.4.4",
4040
"eslint-plugin-import": "^2.32.0",
41-
"eslint-plugin-jsdoc": "^61.5.0",
41+
"eslint-plugin-jsdoc": "^62.0.0",
4242
"eslint-plugin-prettier": "^5.5.4",
4343
"eslint-plugin-tsdoc": "^0.5.0",
4444
"prettier": "^3.7.4",
4545
"tsx": "4.21.0",
4646
"typescript": "5.9.3",
47-
"typescript-eslint": "8.51.0"
47+
"typescript-eslint": "8.52.0"
4848
},
4949
"dependencies": {
50-
"@aptos-labs/ts-sdk": "^5.2.0",
51-
"@chainlink/ccip-sdk": "^0.93.0",
50+
"@aptos-labs/ts-sdk": "^5.2.1",
51+
"@chainlink/ccip-sdk": "^0.94.0",
5252
"@coral-xyz/anchor": "^0.29.0",
5353
"@ethers-ext/signer-ledger": "^6.0.0-beta.1",
54-
"@inquirer/prompts": "8.1.0",
54+
"@inquirer/prompts": "8.2.0",
5555
"@ledgerhq/hw-app-aptos": "^6.34.11",
5656
"@ledgerhq/hw-app-solana": "^7.6.2",
5757
"@ledgerhq/hw-transport-node-hid": "^6.29.16",
5858
"@solana/web3.js": "^1.98.4",
5959
"@ton-community/ton-ledger": "^7.3.0",
6060
"bs58": "^6.0.0",
6161
"ethers": "6.16.0",
62-
"type-fest": "^5.3.1",
62+
"type-fest": "^5.4.0",
6363
"yargs": "18.0.0"
6464
},
6565
"overrides": {

ccip-cli/src/commands/send.ts

Lines changed: 23 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {
2-
type AnyMessage,
32
type CCIPVersion,
43
type ChainStatic,
54
type EVMChain,
65
type ExtraArgs,
6+
type RequestMessage,
77
CCIPArgumentInvalidError,
88
CCIPChainFamilyUnsupportedError,
99
CCIPTokenNotFoundError,
@@ -13,7 +13,7 @@ import {
1313
networkInfo,
1414
sourceToDestTokenAmounts,
1515
} from '@chainlink/ccip-sdk/src/index.ts'
16-
import { type BytesLike, dataLength, formatUnits, toUtf8Bytes } from 'ethers'
16+
import { type BytesLike, formatUnits, toUtf8Bytes } from 'ethers'
1717
import type { Argv } from 'yargs'
1818

1919
import type { GlobalOpts } from '../index.ts'
@@ -67,7 +67,6 @@ export const builder = (yargs: Argv) =>
6767
type: 'number',
6868
describe:
6969
'Gas limit for receiver callback execution; defaults to default configured on ramps',
70-
default: 0,
7170
},
7271
'estimate-gas-limit': {
7372
type: 'number',
@@ -105,10 +104,11 @@ export const builder = (yargs: Argv) =>
105104
describe: "Address of the Solana tokenReceiver (if different than program's receiver)",
106105
},
107106
account: {
107+
alias: 'receiver-object-id',
108108
type: 'array',
109109
string: true,
110110
describe:
111-
'List of accounts needed by Solana receiver program; append `=rw` to specify account as writable; can be specified multiple times',
111+
'List of accounts needed by Solana receiver program, or receiverObjectIds needed by Sui; On Solana, append `=rw` to specify account as writable; can be specified multiple times',
112112
example: 'requiredPdaAddress=rw',
113113
},
114114
'only-get-fee': {
@@ -159,54 +159,33 @@ async function sendMessage(
159159
const getChain = fetchChainsFromRpcs(ctx, argv)
160160
const source = await getChain(sourceNetwork.name)
161161

162-
let data: BytesLike
162+
let data: BytesLike | undefined
163163
if (argv.data) {
164164
try {
165165
data = getDataBytes(argv.data)
166166
} catch (_) {
167167
data = toUtf8Bytes(argv.data)
168168
}
169-
} else {
170-
data = '0x'
171169
}
172170

173171
const tokenAmounts: { token: string; amount: bigint }[] = argv.transferTokens?.length
174172
? await parseTokenAmounts(source, argv.transferTokens)
175173
: []
176174

177175
let receiver = argv.receiver
178-
let tokenReceiver
179176
let accounts,
180177
accountIsWritableBitmap = 0n
181178
if (destNetwork.family === ChainFamily.Solana) {
182-
if (argv.tokenReceiver) tokenReceiver = argv.tokenReceiver
183-
else if (!tokenAmounts.length) {
184-
tokenReceiver = '11111111111111111111111111111111'
185-
} else if (!dataLength(data)) {
186-
// sending tokens without data, i.e. not for a receiver contract
187-
tokenReceiver = receiver
188-
receiver = '11111111111111111111111111111111'
189-
} else {
190-
throw new CCIPArgumentInvalidError(
191-
'token-receiver',
192-
'required when sending tokens with data to Solana',
193-
)
194-
}
195-
196-
if (argv.account) {
179+
// parse accounts with or without `=rw` suffix
180+
if (argv.account?.length) {
197181
accounts = argv.account.map((account, i) => {
198182
if (account.endsWith('=rw')) {
199183
accountIsWritableBitmap |= 1n << BigInt(i)
200184
account = account.substring(0, account.length - 3)
201185
}
202186
return account
203187
})
204-
} else accounts = [] as string[]
205-
} else if (argv.tokenReceiver || argv.account?.length) {
206-
throw new CCIPArgumentInvalidError(
207-
'token-receiver/account',
208-
'only valid for Solana destination',
209-
)
188+
}
210189
}
211190

212191
let walletAddress, wallet
@@ -244,7 +223,7 @@ async function sendMessage(
244223
message: {
245224
sender: walletAddress,
246225
receiver,
247-
data,
226+
data: data || '0x',
248227
tokenAmounts: destTokenAmounts,
249228
},
250229
})
@@ -253,17 +232,19 @@ async function sendMessage(
253232
if (argv.onlyEstimate) return
254233
}
255234

256-
// `--allow-out-of-order-exec` forces EVMExtraArgsV2, which shouldn't work on v1.2 lanes;
257-
// otherwise, fallsback to EVMExtraArgsV1 (compatible with v1.2 & v1.5)
235+
// builds a catch-all extraArgs object, which can be massaged by
236+
// [[Chain.buildMessageForDest]] to create suitable extraArgs with defaults if needed
258237
const extraArgs = {
259-
...(argv.allowOutOfOrderExec != null || destNetwork.family !== ChainFamily.EVM
260-
? { allowOutOfOrderExecution: !!argv.allowOutOfOrderExec }
261-
: {}),
262-
...(destNetwork.family === ChainFamily.Solana
263-
? { computeUnits: BigInt(argv.gasLimit) }
264-
: { gasLimit: BigInt(argv.gasLimit) }),
265-
...(tokenReceiver ? { tokenReceiver } : {}),
266-
...(accounts ? { accounts, accountIsWritableBitmap } : {}),
238+
...(argv.allowOutOfOrderExec != null && {
239+
allowOutOfOrderExecution: !!argv.allowOutOfOrderExec,
240+
}),
241+
...(argv.gasLimit == null
242+
? {}
243+
: destNetwork.family === ChainFamily.Solana
244+
? { computeUnits: BigInt(argv.gasLimit) }
245+
: { gasLimit: BigInt(argv.gasLimit) }),
246+
...(!!argv.tokenReceiver && { tokenReceiver: argv.tokenReceiver }),
247+
...(!!accounts && { accounts, accountIsWritableBitmap }), // accounts also used as Sui receiverObjectIds
267248
}
268249

269250
let feeToken, feeTokenInfo
@@ -287,7 +268,7 @@ async function sendMessage(
287268
feeTokenInfo = await source.getTokenInfo(nativeToken)
288269
}
289270

290-
const message: AnyMessage = {
271+
const message: RequestMessage = {
291272
receiver,
292273
data,
293274
extraArgs: extraArgs as ExtraArgs,
@@ -322,9 +303,7 @@ async function sendMessage(
322303
})
323304
logger.info(
324305
'🚀 Sending message to',
325-
tokenReceiver && tokenReceiver !== '11111111111111111111111111111111'
326-
? tokenReceiver
327-
: receiver,
306+
receiver,
328307
'@',
329308
destNetwork.name,
330309
', tx =>',

ccip-cli/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { Format } from './commands/index.ts'
1111
util.inspect.defaultOptions.depth = 6 // print down to tokenAmounts in requests
1212
// generate:nofail
1313
// `const VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'`
14-
const VERSION = '0.93.0-883a2a0'
14+
const VERSION = '0.94.0-a836308'
1515
// generate:end
1616

1717
const globalOpts = {

ccip-sdk/package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@chainlink/ccip-sdk",
3-
"version": "0.93.0",
3+
"version": "0.94.0",
44
"description": "SDK/Library to interact with CCIP",
55
"author": "Chainlink devs",
66
"license": "MIT",
@@ -51,38 +51,38 @@
5151
"@depay/web3-mock": "^15.3.0",
5252
"@eslint/js": "^9.39.2",
5353
"@types/bn.js": "^5.2.0",
54-
"@types/node": "25.0.3",
54+
"@types/node": "25.0.6",
5555
"@types/yargs": "17.0.35",
5656
"eslint": "^9.39.2",
5757
"eslint-config-prettier": "10.1.8",
5858
"eslint-import-resolver-typescript": "4.4.4",
5959
"eslint-plugin-import": "^2.32.0",
60-
"eslint-plugin-jsdoc": "^61.5.0",
60+
"eslint-plugin-jsdoc": "^62.0.0",
6161
"eslint-plugin-prettier": "^5.5.4",
6262
"eslint-plugin-tsdoc": "^0.5.0",
6363
"ethers-abitype": "1.0.3",
6464
"prettier": "^3.7.4",
6565
"typescript": "5.9.3",
66-
"typescript-eslint": "8.51.0",
67-
"viem": "^2.43.3"
66+
"typescript-eslint": "8.52.0",
67+
"viem": "^2.44.1"
6868
},
6969
"dependencies": {
70-
"@aptos-labs/ts-sdk": "^5.2.0",
70+
"@aptos-labs/ts-sdk": "^5.2.1",
7171
"buffer": "^6.0.3",
7272
"@coral-xyz/anchor": "^0.29.0",
7373
"@mysten/bcs": "^1.9.2",
7474
"@mysten/sui": "^1.45.2",
7575
"@solana/spl-token": "0.4.14",
7676
"@solana/web3.js": "^1.98.4",
77-
"@ton/core": "0.62.0",
77+
"@ton/core": "0.62.1",
7878
"@ton/ton": "^16.1.0",
7979
"abitype": "1.2.3",
8080
"bn.js": "^5.2.2",
8181
"borsh": "^2.0.0",
8282
"bs58": "^6.0.0",
8383
"ethers": "6.16.0",
8484
"micro-memoize": "^5.1.1",
85-
"type-fest": "^5.3.1",
85+
"type-fest": "^5.4.0",
8686
"yaml": "2.8.2"
8787
},
8888
"overrides": {

ccip-sdk/src/aptos/index.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
zeroPadValue,
2020
} from 'ethers'
2121
import { memoize } from 'micro-memoize'
22-
import type { PickDeep, SetRequired } from 'type-fest'
22+
import type { PickDeep } from 'type-fest'
2323

2424
import {
2525
type ChainContext,
@@ -88,7 +88,13 @@ import { getAptosLeafHasher } from './hasher.ts'
8888
import { getUserTxByVersion, getVersionTimestamp, streamAptosLogs } from './logs.ts'
8989
import { getTokenInfo } from './token.ts'
9090
import type { CCIPMessage_V1_6_EVM } from '../evm/messages.ts'
91-
import { decodeMessage, getMessageById, getMessagesInBatch, getMessagesInTx } from '../requests.ts'
91+
import {
92+
buildMessageForDest,
93+
decodeMessage,
94+
getMessageById,
95+
getMessagesInBatch,
96+
getMessagesInTx,
97+
} from '../requests.ts'
9298
export type { UnsignedAptosTx }
9399

94100
/**
@@ -530,21 +536,29 @@ export class AptosChain extends Chain<typeof ChainFamily.Aptos> {
530536
destChainSelector,
531537
message,
532538
}: Parameters<Chain['getFee']>[0]): Promise<bigint> {
533-
return getFee(this.provider, router, destChainSelector, message)
539+
const populatedMessage = buildMessageForDest(message, networkInfo(destChainSelector).family)
540+
return getFee(this.provider, router, destChainSelector, populatedMessage)
534541
}
535542

536543
/** {@inheritDoc Chain.generateUnsignedSendMessage} */
537544
async generateUnsignedSendMessage(
538545
opts: Parameters<Chain['generateUnsignedSendMessage']>[0],
539546
): Promise<UnsignedAptosTx> {
540-
const { sender, router, destChainSelector, message } = opts
541-
if (!message.fee) message.fee = await this.getFee(opts)
547+
const { sender, router, destChainSelector } = opts
548+
const populatedMessage = buildMessageForDest(
549+
opts.message,
550+
networkInfo(destChainSelector).family,
551+
)
552+
const message = {
553+
...populatedMessage,
554+
fee: opts.message.fee ?? (await this.getFee({ ...opts, message: populatedMessage })),
555+
}
542556
const tx = await generateUnsignedCcipSend(
543557
this.provider,
544558
sender,
545559
router,
546560
destChainSelector,
547-
message as SetRequired<typeof message, 'fee'>,
561+
message,
548562
opts,
549563
)
550564
return {

ccip-sdk/src/aptos/send.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ function messageArgs(
2323
] {
2424
// Prepare the message structure for the view call
2525
const receiver = getBytes(zeroPadValue(getDataBytes(message.receiver), 32))
26-
const data = getDataBytes(message.data)
26+
const data = getDataBytes(message.data || '0x')
2727

2828
// Get the native token to use as fee token if not specified
2929
const feeToken = message.feeToken || DEFAULT_FEE_TOKEN

0 commit comments

Comments
 (0)