Skip to content

Commit 95c331e

Browse files
authored
feat(sealevel-sdk): svm sdk cross collateral token (#8384)
1 parent 3cf5394 commit 95c331e

16 files changed

Lines changed: 977 additions & 15 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hyperlane-xyz/sealevel-sdk': minor
3+
---
4+
5+
Added cross-collateral token support to the SVM SDK, including create, read, and update operations for cross-collateral warp routes.

.github/workflows/test-sdk-e2e.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ jobs:
171171
- native-token
172172
- synthetic-token
173173
- collateral-token
174+
- cross-collateral-token
174175
- provider
175176
- read-token
176177
include:

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

typescript/svm-sdk/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"chai": "^4.4.1",
5959
"chai-as-promised": "catalog:",
6060
"mocha": "^10.4.0",
61+
"mocha-steps": "catalog:",
6162
"oxfmt": "catalog:",
6263
"sinon": "catalog:",
6364
"testcontainers": "catalog:",
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { decodeAccountData } from '../codecs/account-data.js';
2+
import type { ByteCursor } from '../codecs/binary.js';
3+
import { decodeMapU32VecH256 } from '../codecs/shared.js';
4+
5+
export interface CrossCollateralStateData {
6+
bump: number;
7+
dispatchAuthorityBump: number;
8+
localDomain: number;
9+
enrolledRouters: Map<number, Uint8Array[]>;
10+
}
11+
12+
export function decodeCrossCollateralStateAccount(
13+
raw: Uint8Array,
14+
): CrossCollateralStateData | null {
15+
const wrapped = decodeAccountData(raw, decodeCrossCollateralStateInner);
16+
return wrapped.data;
17+
}
18+
19+
function decodeCrossCollateralStateInner(
20+
cursor: ByteCursor,
21+
): CrossCollateralStateData {
22+
const bump = cursor.readU8();
23+
const dispatchAuthorityBump = cursor.readU8();
24+
const localDomain = cursor.readU32LE();
25+
const enrolledRouters = decodeMapU32VecH256(cursor);
26+
return { bump, dispatchAuthorityBump, localDomain, enrolledRouters };
27+
}

typescript/svm-sdk/src/codecs/shared.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,23 @@ export function decodeMapU32H256(cursor: ByteCursor): Map<number, H256> {
369369
return entries;
370370
}
371371

372+
export function decodeMapU32VecH256(
373+
cursor: ByteCursor,
374+
): Map<number, Uint8Array[]> {
375+
const mapLen = cursor.readU32LE();
376+
const entries = new Map<number, Uint8Array[]>();
377+
for (let i = 0; i < mapLen; i += 1) {
378+
const key = cursor.readU32LE();
379+
const setLen = cursor.readU32LE();
380+
const set: Uint8Array[] = [];
381+
for (let j = 0; j < setLen; j += 1) {
382+
set.push(cursor.readBytes(32));
383+
}
384+
entries.set(key, set);
385+
}
386+
return entries;
387+
}
388+
372389
export function decodeMapU32GasOracle(
373390
cursor: ByteCursor,
374391
): Map<number, GasOracle> {

typescript/svm-sdk/src/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ export {
9797
deriveValidatorAnnouncePda,
9898
deriveValidatorStorageLocationsPda,
9999
deriveReplayProtectionPda,
100+
deriveCrossCollateralStatePda,
101+
deriveCrossCollateralDispatchAuthorityPda,
100102
} from './pda.js';
101103

102104
// Account decoders
@@ -119,3 +121,16 @@ export {
119121
decodeValidatorAnnounceAccount,
120122
decodeValidatorStorageLocationsAccount,
121123
} from './core/validator-announce-query.js';
124+
export { decodeCrossCollateralStateAccount } from './accounts/cross-collateral-token.js';
125+
126+
// Cross-collateral instruction builders
127+
export {
128+
getCrossCollateralInitInstruction,
129+
getSetCrossCollateralRoutersInstruction,
130+
} from './instructions/cross-collateral-token.js';
131+
132+
// Cross-collateral warp token reader/writer
133+
export {
134+
SvmCrossCollateralTokenReader,
135+
SvmCrossCollateralTokenWriter,
136+
} from './warp/cross-collateral-token.js';
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import type {
2+
Address,
3+
Instruction,
4+
ReadonlyUint8Array,
5+
TransactionSigner,
6+
} from '@solana/kit';
7+
8+
import { concatBytes, u8, u32le, vec } from '../codecs/binary.js';
9+
import {
10+
encodeH256,
11+
encodeRemoteRouterConfig,
12+
type H256,
13+
type RemoteRouterConfig,
14+
} from '../codecs/shared.js';
15+
import { SYSTEM_PROGRAM_ADDRESS } from '../constants.js';
16+
import {
17+
deriveCrossCollateralDispatchAuthorityPda,
18+
deriveCrossCollateralStatePda,
19+
deriveHyperlaneTokenPda,
20+
} from '../pda.js';
21+
import {
22+
getTokenInitInstruction,
23+
type TokenInitInstructionData,
24+
} from './token.js';
25+
import {
26+
buildInstruction,
27+
type InstructionAccountMeta,
28+
readonlyAccount,
29+
writableAccount,
30+
writableSignerAddress,
31+
} from './utils.js';
32+
33+
// Cross-collateral plugin discriminator [2; 8], distinct from the base
34+
// token program discriminator [1; 8] (PROGRAM_INSTRUCTION_DISCRIMINATOR).
35+
const CC_INSTRUCTION_DISCRIMINATOR = new Uint8Array([2, 2, 2, 2, 2, 2, 2, 2]);
36+
37+
export enum CrossCollateralInstructionKind {
38+
SetCrossCollateralRouters = 0,
39+
TransferRemoteTo = 1,
40+
HandleLocal = 2,
41+
HandleLocalAccountMetas = 3,
42+
}
43+
44+
export type CrossCollateralRouterUpdate =
45+
| { kind: 'add'; config: { domain: number; router: H256 } }
46+
| { kind: 'remove'; config: RemoteRouterConfig };
47+
48+
function encodeCrossCollateralRouterUpdate(
49+
update: CrossCollateralRouterUpdate,
50+
): ReadonlyUint8Array {
51+
if (update.kind === 'add') {
52+
return concatBytes(
53+
u8(0),
54+
u32le(update.config.domain),
55+
encodeH256(update.config.router),
56+
);
57+
}
58+
59+
return concatBytes(u8(1), encodeRemoteRouterConfig(update.config));
60+
}
61+
62+
export async function getCrossCollateralInitInstruction(
63+
programAddress: Address,
64+
payer: TransactionSigner,
65+
init: TokenInitInstructionData,
66+
pluginAccounts: InstructionAccountMeta[],
67+
mailboxOutboxPda: Address,
68+
): Promise<Instruction> {
69+
const { address: ccStatePda } =
70+
await deriveCrossCollateralStatePda(programAddress);
71+
const { address: ccDispatchAuthority } =
72+
await deriveCrossCollateralDispatchAuthorityPda(programAddress);
73+
74+
return getTokenInitInstruction(programAddress, payer, init, [
75+
...pluginAccounts,
76+
writableAccount(ccStatePda),
77+
writableAccount(ccDispatchAuthority),
78+
readonlyAccount(mailboxOutboxPda),
79+
]);
80+
}
81+
82+
export async function getSetCrossCollateralRoutersInstruction(
83+
programAddress: Address,
84+
owner: Address,
85+
updates: CrossCollateralRouterUpdate[],
86+
): Promise<Instruction> {
87+
const { address: ccStatePda } =
88+
await deriveCrossCollateralStatePda(programAddress);
89+
const { address: tokenPda } = await deriveHyperlaneTokenPda(programAddress);
90+
91+
return buildInstruction(
92+
programAddress,
93+
[
94+
readonlyAccount(SYSTEM_PROGRAM_ADDRESS),
95+
writableAccount(ccStatePda),
96+
readonlyAccount(tokenPda),
97+
writableSignerAddress(owner),
98+
],
99+
concatBytes(
100+
CC_INSTRUCTION_DISCRIMINATOR,
101+
u8(CrossCollateralInstructionKind.SetCrossCollateralRouters),
102+
vec(updates, encodeCrossCollateralRouterUpdate),
103+
),
104+
);
105+
}

typescript/svm-sdk/src/instructions/token.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ export async function getTokenSetDestinationGasConfigsInstruction(
359359
);
360360
}
361361

362-
function encodeTokenInit(value: TokenInitInstructionData): Uint8Array {
362+
export function encodeTokenInit(value: TokenInitInstructionData): Uint8Array {
363363
const normalized: TokenInitCodecValue = {
364364
mailbox: value.mailbox,
365365
interchainSecurityModule: value.interchainSecurityModule,

typescript/svm-sdk/src/pda.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,23 @@ export async function deriveEscrowPda(
221221
utf8.encode('escrow'),
222222
]);
223223
}
224+
225+
export async function deriveCrossCollateralStatePda(
226+
programAddress: Address,
227+
): Promise<PdaWithBump> {
228+
return derive(programAddress, [
229+
utf8.encode('hyperlane_token'),
230+
utf8.encode('-'),
231+
utf8.encode('cross_collateral'),
232+
]);
233+
}
234+
235+
export async function deriveCrossCollateralDispatchAuthorityPda(
236+
programAddress: Address,
237+
): Promise<PdaWithBump> {
238+
return derive(programAddress, [
239+
utf8.encode('hyperlane_cc'),
240+
utf8.encode('-'),
241+
utf8.encode('dispatch_authority'),
242+
]);
243+
}

0 commit comments

Comments
 (0)