damm-sdk 1.4.30 → 1.4.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Pure byte-level decoder for CCTP V2 cross-chain messages.
3
+ *
4
+ * Use when you need to verify or inspect the contents of a CCTP V2 message
5
+ * without trusting the pre-decoded fields from iris-api (defense in depth),
6
+ * or when working with a raw message blob produced by a source-chain tx.
7
+ *
8
+ * Message format reference:
9
+ * https://developers.circle.com/stablecoins/message-format
10
+ *
11
+ * The outer header (148 bytes) wraps a variable-length body. For USDC burn
12
+ * transfers the body is a `BurnMessage` V2 — the only body shape this
13
+ * module supports (other body shapes are non-USDC payloads that cli-damm
14
+ * does not currently finalize).
15
+ */
16
+
17
+ import { getAddress, isHex, size as hexSize, sliceHex, hexToBigInt, hexToNumber } from "viem";
18
+ import type { Address } from "viem";
19
+ import type { HexString } from "../../types";
20
+
21
+ /** Version byte at the start of a CCTP V2 header. */
22
+ export const CCTP_V2_HEADER_VERSION = 1 as const;
23
+
24
+ /** Version byte at the start of a CCTP V2 BurnMessage body. */
25
+ export const CCTP_V2_BURN_BODY_VERSION = 1 as const;
26
+
27
+ /** Total byte length of the fixed-size CCTP V2 header. */
28
+ const HEADER_BYTE_LENGTH = 148;
29
+
30
+ /** Byte length of the fixed-size portion of a BurnMessage V2 body (before `hookData`). */
31
+ const BURN_BODY_FIXED_BYTE_LENGTH = 228;
32
+
33
+ // ─── Byte offsets within the header ──────────────────────────────────────
34
+ const OFF_VERSION = 0;
35
+ const OFF_SOURCE_DOMAIN = 4;
36
+ const OFF_DEST_DOMAIN = 8;
37
+ const OFF_NONCE = 12;
38
+ const OFF_SENDER = 44;
39
+ const OFF_RECIPIENT = 76;
40
+ const OFF_DESTINATION_CALLER = 108;
41
+ const OFF_MIN_FINALITY = 140;
42
+ const OFF_FINALITY_EXECUTED = 144;
43
+ const OFF_BODY = 148;
44
+
45
+ // ─── Byte offsets within a BurnMessage V2 body ───────────────────────────
46
+ const BODY_OFF_VERSION = 0;
47
+ const BODY_OFF_BURN_TOKEN = 4;
48
+ const BODY_OFF_MINT_RECIPIENT = 36;
49
+ const BODY_OFF_AMOUNT = 68;
50
+ const BODY_OFF_MESSAGE_SENDER = 100;
51
+ const BODY_OFF_MAX_FEE = 132;
52
+ const BODY_OFF_FEE_EXECUTED = 164;
53
+ const BODY_OFF_EXPIRATION_BLOCK = 196;
54
+ const BODY_OFF_HOOK_DATA = 228;
55
+
56
+ /**
57
+ * Decoded CCTP V2 header. All bytes32 fields are preserved as 0x-prefixed
58
+ * hex strings (not narrowed to addresses) except `sender` and `recipient`,
59
+ * which Circle guarantees are address-padded bytes32 values.
60
+ */
61
+ export type CctpV2Header = Readonly<{
62
+ version: number;
63
+ sourceDomain: number;
64
+ destinationDomain: number;
65
+ nonce: HexString;
66
+ sender: Address;
67
+ recipient: Address;
68
+ destinationCaller: HexString;
69
+ minFinalityThreshold: number;
70
+ finalityThresholdExecuted: number;
71
+ messageBody: HexString;
72
+ }>;
73
+
74
+ /**
75
+ * Decoded CCTP V2 BurnMessage body — the message-body shape used for USDC
76
+ * bridging. `hookData` is empty (`"0x"`) for standard transfers.
77
+ */
78
+ export type CctpV2BurnMessageBody = Readonly<{
79
+ version: number;
80
+ burnToken: Address;
81
+ mintRecipient: Address;
82
+ amount: bigint;
83
+ messageSender: Address;
84
+ maxFee: bigint;
85
+ feeExecuted: bigint;
86
+ expirationBlock: bigint;
87
+ hookData: HexString;
88
+ }>;
89
+
90
+ /** Header + burn-body decoded together in one pass. */
91
+ export type CctpV2Message = Readonly<
92
+ CctpV2Header & {
93
+ decodedBody: CctpV2BurnMessageBody;
94
+ }
95
+ >;
96
+
97
+ const requireHex = (value: unknown): HexString => {
98
+ if (typeof value !== "string" || !isHex(value)) {
99
+ throw new Error(`CCTP decoder: expected a 0x-prefixed hex string, got ${typeof value}`);
100
+ }
101
+ return value as HexString;
102
+ };
103
+
104
+ /**
105
+ * Decode the outer CCTP V2 header and return it alongside the embedded
106
+ * `messageBody`. Does not inspect the body contents — use {@link decodeCctpV2BurnBody}
107
+ * for that.
108
+ *
109
+ * @throws if `message` is shorter than the 148-byte header
110
+ * @throws if the header version byte is not {@link CCTP_V2_HEADER_VERSION}
111
+ */
112
+ export const decodeCctpV2Header = (message: HexString): CctpV2Header => {
113
+ const bytes = requireHex(message);
114
+ const length = hexSize(bytes);
115
+ if (length < HEADER_BYTE_LENGTH) {
116
+ throw new Error(`CCTP decoder: header requires ${HEADER_BYTE_LENGTH} bytes, got ${length}`);
117
+ }
118
+
119
+ const version = hexToNumber(sliceHex(bytes, OFF_VERSION, OFF_SOURCE_DOMAIN));
120
+ if (version !== CCTP_V2_HEADER_VERSION) {
121
+ throw new Error(`CCTP decoder: unknown header version ${version} (expected ${CCTP_V2_HEADER_VERSION})`);
122
+ }
123
+
124
+ // The sender and recipient are 32-byte slots holding left-padded 20-byte
125
+ // addresses. Slice the last 20 bytes (offset +12) to recover them.
126
+ const senderPadded = sliceHex(bytes, OFF_SENDER, OFF_RECIPIENT);
127
+ const recipientPadded = sliceHex(bytes, OFF_RECIPIENT, OFF_DESTINATION_CALLER);
128
+ const sender = getAddress(`0x${senderPadded.slice(2 + 24)}`);
129
+ const recipient = getAddress(`0x${recipientPadded.slice(2 + 24)}`);
130
+
131
+ return Object.freeze({
132
+ version,
133
+ sourceDomain: hexToNumber(sliceHex(bytes, OFF_SOURCE_DOMAIN, OFF_DEST_DOMAIN)),
134
+ destinationDomain: hexToNumber(sliceHex(bytes, OFF_DEST_DOMAIN, OFF_NONCE)),
135
+ nonce: sliceHex(bytes, OFF_NONCE, OFF_SENDER),
136
+ sender,
137
+ recipient,
138
+ destinationCaller: sliceHex(bytes, OFF_DESTINATION_CALLER, OFF_MIN_FINALITY),
139
+ minFinalityThreshold: hexToNumber(sliceHex(bytes, OFF_MIN_FINALITY, OFF_FINALITY_EXECUTED)),
140
+ finalityThresholdExecuted: hexToNumber(sliceHex(bytes, OFF_FINALITY_EXECUTED, OFF_BODY)),
141
+ messageBody: sliceHex(bytes, OFF_BODY),
142
+ });
143
+ };
144
+
145
+ /**
146
+ * Decode a CCTP V2 BurnMessage body.
147
+ *
148
+ * @throws if `body` is shorter than the 228-byte fixed portion
149
+ * @throws if the body version byte is not {@link CCTP_V2_BURN_BODY_VERSION}
150
+ */
151
+ export const decodeCctpV2BurnBody = (body: HexString): CctpV2BurnMessageBody => {
152
+ const bytes = requireHex(body);
153
+ const length = hexSize(bytes);
154
+ if (length < BURN_BODY_FIXED_BYTE_LENGTH) {
155
+ throw new Error(`CCTP decoder: burn body requires ${BURN_BODY_FIXED_BYTE_LENGTH} bytes, got ${length}`);
156
+ }
157
+
158
+ const version = hexToNumber(sliceHex(bytes, BODY_OFF_VERSION, BODY_OFF_BURN_TOKEN));
159
+ if (version !== CCTP_V2_BURN_BODY_VERSION) {
160
+ throw new Error(`CCTP decoder: unknown burn body version ${version} (expected ${CCTP_V2_BURN_BODY_VERSION})`);
161
+ }
162
+
163
+ const burnTokenPadded = sliceHex(bytes, BODY_OFF_BURN_TOKEN, BODY_OFF_MINT_RECIPIENT);
164
+ const mintRecipientPadded = sliceHex(bytes, BODY_OFF_MINT_RECIPIENT, BODY_OFF_AMOUNT);
165
+ const messageSenderPadded = sliceHex(bytes, BODY_OFF_MESSAGE_SENDER, BODY_OFF_MAX_FEE);
166
+
167
+ return Object.freeze({
168
+ version,
169
+ burnToken: getAddress(`0x${burnTokenPadded.slice(2 + 24)}`),
170
+ mintRecipient: getAddress(`0x${mintRecipientPadded.slice(2 + 24)}`),
171
+ amount: hexToBigInt(sliceHex(bytes, BODY_OFF_AMOUNT, BODY_OFF_MESSAGE_SENDER)),
172
+ messageSender: getAddress(`0x${messageSenderPadded.slice(2 + 24)}`),
173
+ maxFee: hexToBigInt(sliceHex(bytes, BODY_OFF_MAX_FEE, BODY_OFF_FEE_EXECUTED)),
174
+ feeExecuted: hexToBigInt(sliceHex(bytes, BODY_OFF_FEE_EXECUTED, BODY_OFF_EXPIRATION_BLOCK)),
175
+ expirationBlock: hexToBigInt(sliceHex(bytes, BODY_OFF_EXPIRATION_BLOCK, BODY_OFF_HOOK_DATA)),
176
+ hookData: length > BURN_BODY_FIXED_BYTE_LENGTH ? sliceHex(bytes, BODY_OFF_HOOK_DATA) : "0x",
177
+ });
178
+ };
179
+
180
+ /**
181
+ * Convenience: decode the outer header and the embedded burn body in a
182
+ * single call. Throws the same errors as {@link decodeCctpV2Header} and
183
+ * {@link decodeCctpV2BurnBody}.
184
+ */
185
+ export const decodeCctpV2Message = (message: HexString): CctpV2Message => {
186
+ const header = decodeCctpV2Header(message);
187
+ const decodedBody = decodeCctpV2BurnBody(header.messageBody);
188
+ return Object.freeze({ ...header, decodedBody });
189
+ };
@@ -1,3 +1,5 @@
1
1
  export { default as CctpTokenMessengerV2Abi } from "./token.messenger.v2.abi";
2
2
  export { default as CctpMessageTransmitterV2Abi } from "./message.transmitter.v2.abi";
3
3
  export * from "./cctp.v2";
4
+ export * from "./cctp.message.decoder";
5
+ export * from "./cctp.iris.api";
@@ -370,7 +370,8 @@
370
370
  "tbtc": "0x6c84a8f1c29108F47a79964b5Fe888D4f4D0dE40",
371
371
  "op": "0x4200000000000000000000000000000000000042",
372
372
  "usde": "0x5d3a1ff2b6bab83b63cd9ad0787074081a52ef34",
373
- "weeth": "0x5A7fACB970D094B6C7FF1df0eA68D99E6e73CBFF"
373
+ "weeth": "0x5A7fACB970D094B6C7FF1df0eA68D99E6e73CBFF",
374
+ "wsteth": "0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb"
374
375
  },
375
376
  "gnosisSafe": {
376
377
  "v1_4_1": {