Skip to content

Commit 81d2f42

Browse files
authored
feat: track in-progress set max reserve
* chore: update schema * chore: extend vaultService * chore: add syncManager tracking * chore: fix type networknames * chore: sync manager only in v3_1 * chore: improved typing * fix: message decoding for single version registry * feat: track inprogress set max reserve * chore: init vault with max reserve possible
1 parent e388657 commit 81d2f42

File tree

8 files changed

+237
-6
lines changed

8 files changed

+237
-6
lines changed

ponder.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export const contractsV3_1 = decorateDeploymentContracts(
8282
"ShareClassManager",
8383
"Spoke",
8484
"VaultRegistry",
85+
"SyncManager",
8586
] as const,
8687
{
8788
vaultV3_1: {

ponder.schema.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export const VaultKinds = ["Async", "Sync", "SyncDepositAsyncRedeem"] as const;
179179
export const VaultKind = onchainEnum("vault_kind", VaultKinds);
180180
export const VaultStatuses = ["LinkInProgress", "UnlinkInProgress", "Linked", "Unlinked"] as const;
181181
export const VaultStatus = onchainEnum("vault_status", VaultStatuses);
182-
export const VaultCrosschainInProgressTypes = [`Deploy`, `Link`, `Unlink`] as const;
182+
export const VaultCrosschainInProgressTypes = [`Deploy`, `Link`, `Unlink`, `MaxReserve`] as const;
183183
export const VaultCrosschainInProgress = onchainEnum(
184184
"vault_crosschain_in_progress",
185185
VaultCrosschainInProgressTypes
@@ -196,7 +196,9 @@ const VaultColumns = (t: PgColumnsBuilders) => ({
196196
assetAddress: t.hex(),
197197
factory: t.text(),
198198
manager: t.text(),
199+
maxReserve: t.bigint().default(0n),
199200
crosschainInProgress: VaultCrosschainInProgress("vault_crosschain_in_progress"),
201+
crosschainInProgressValue: t.bigint(),
200202
...defaultColumns(t),
201203
});
202204
export const Vault = onchainTable("vault", VaultColumns, (t) => ({

src/chains.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ type ExtractNetworkNamesFromKeys<K> = K extends keyof typeof networkNames
4141
? (typeof networkNames)[K]
4242
: never;
4343

44-
export type NetworkNames<V> = V extends RegistryVersions
44+
export type NetworkNames<V extends RegistryVersions> = V extends RegistryVersions
4545
? ExtractNetworkNamesFromKeys<RegistryChainsKeys<V>>
4646
: never;
4747

src/handlers/hubHandlers.ts

Lines changed: 179 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
AccountService,
99
centrifugeIdFromAssetId,
1010
VaultService,
11+
AssetService,
1112
} from "../services";
1213
import { VaultCrosschainInProgressTypes } from "ponder:schema";
1314

@@ -132,6 +133,33 @@ multiMapper("hub:UpdateVault", async ({ event, context }) => {
132133
await vault.setCrosschainInProgress(vaultUpdateKind).save(event);
133134
});
134135

136+
multiMapper("hub:UpdateContract", async ({ event, context }) => {
137+
logEvent(event, context, "hub:UpdateContract");
138+
const { centrifugeId: destCentrifugeId, poolId, scId: tokenId, payload } = event.args;
139+
140+
const decoded = decodeUpdateContract(payload);
141+
if (!decoded || !decoded.payload)
142+
return serviceError(`Invalid update contract payload: ${payload}`);
143+
144+
if (decoded.kind === "MaxReserve" && "maxReserve" in decoded.payload) {
145+
const { assetId, maxReserve } = decoded.payload as { assetId: bigint; maxReserve: bigint };
146+
const asset = await AssetService.get(context, { id: assetId });
147+
if (!asset)
148+
return serviceError(`Asset not found for assetId ${assetId}. Cannot update vault maxReserve`);
149+
const assetAddress = asset.read().address as `0x${string}`;
150+
if (!assetAddress) return serviceError(`Asset has no address for assetId ${assetId}`);
151+
152+
const vault = (await VaultService.get(context, {
153+
centrifugeId: destCentrifugeId.toString(),
154+
poolId,
155+
tokenId,
156+
assetAddress,
157+
})) as VaultService | null;
158+
if (!vault) return serviceError(`Vault not found. Cannot update maxReserve`);
159+
await vault.setMaxReserve(maxReserve).setCrosschainInProgress().save(event);
160+
}
161+
});
162+
135163
enum RestrictionType {
136164
"Invalid",
137165
"Member",
@@ -148,7 +176,7 @@ function decodeUpdateRestriction(
148176
payload: `0x${string}`
149177
):
150178
| [
151-
restrictionType: RestrictionType.Member | RestrictionType.Freeze | RestrictionType.Unfreeze,
179+
restrictionType: (typeof RestrictionType)[keyof typeof RestrictionType],
152180
accountAddress: `0x${string}`,
153181
validUntil: Date | null,
154182
]
@@ -172,3 +200,153 @@ function decodeUpdateRestriction(
172200
return null;
173201
}
174202
}
203+
204+
// ---------------------------------------------------------------------------
205+
// UpdateContract: single-pass decoding (SyncManager, OnOfframpManager, BaseTransferHook)
206+
// ---------------------------------------------------------------------------
207+
208+
/** ABI word size. Solidity abi.encode uses 32-byte words; uint8/uint128/bool are right-aligned. */
209+
const WORD_SIZE = 32;
210+
/** Start offset of word N (0-based). */
211+
const word = (n: number) => n * WORD_SIZE;
212+
/** Decode a value from the 32-byte ABI word at wordIndex using the given decoder. */
213+
function decodeAtWord<T>(buffer: Buffer, wordIndex: number, decoder: (chunk: Buffer) => T): T {
214+
const start = wordIndex * WORD_SIZE;
215+
return decoder(buffer.subarray(start, start + WORD_SIZE));
216+
}
217+
/** Decoder: right-aligned uint8 in a 32-byte word (bytes 31). */
218+
const decodeUint8InWord = (chunk: Buffer): number => chunk.readUInt8(WORD_SIZE - 1);
219+
/** Decoder: right-aligned uint128 in a 32-byte word (bytes 16–31). */
220+
const decodeUint128InWord = (chunk: Buffer): bigint =>
221+
chunk.readBigUInt64BE(16) | (chunk.readBigUInt64BE(24) << 64n);
222+
/** Decoder: bytes32 as 20-byte address (right-padded, bytes 12–31). */
223+
const decodeBytes32Address = (chunk: Buffer): `0x${string}` =>
224+
`0x${chunk.subarray(12, 32).toString("hex")}` as `0x${string}`;
225+
/** Payload kind (uint8) in UpdateContract; matches SyncManager / OnOfframpManager / BaseTransferHook TrustedCall. */
226+
enum UpdateContractPayloadKind {
227+
"Valuation", // SyncManager.Valuation | OnOfframpManager.Onramp | BaseTransferHook.UpdateHookManager
228+
"MaxReserve", // SyncManager.MaxReserve | OnOfframpManager.Relayer
229+
"Offramp", // OnOfframpManager.Offramp
230+
"Withdraw", // OnOfframpManager.Withdraw
231+
}
232+
233+
/** All possible decoded payloads from protocol UpdateContract, by kind. Payload is null when shape is unrecognized. */
234+
export type DecodedUpdateContract =
235+
| {
236+
kind: "Valuation";
237+
payload:
238+
| { valuation: `0x${string}` } // SyncManager
239+
| { assetId: bigint; isEnabled: boolean } // OnOfframpManager.Onramp
240+
| { manager: `0x${string}`; canManage: boolean } // BaseTransferHook.UpdateHookManager
241+
| null;
242+
}
243+
| {
244+
kind: "MaxReserve";
245+
payload:
246+
| { assetId: bigint; maxReserve: bigint } // SyncManager
247+
| { relayerAddress: `0x${string}`; isEnabled: boolean } // OnOfframpManager.Relayer
248+
| null;
249+
}
250+
| {
251+
kind: "Offramp";
252+
payload:
253+
| { assetId: bigint; receiverAddress: `0x${string}`; isEnabled: boolean } // OnOfframpManager.Offramp
254+
| null;
255+
}
256+
| {
257+
kind: "Withdraw";
258+
payload:
259+
| { assetId: bigint; amount: bigint; receiverAddress: `0x${string}` } // OnOfframpManager.Withdraw
260+
| null;
261+
};
262+
263+
/** True if word at index looks like a right-aligned uint128 (bytes 0–15 zero). */
264+
function isWordZeroPaddedUint128(b: Buffer, wordIndex: number): boolean {
265+
const start = wordIndex * WORD_SIZE;
266+
for (let i = 0; i < 16; i++) if (b[start + i] !== 0) return false;
267+
return true;
268+
}
269+
270+
/**
271+
* Decodes the update contract payload into its parameters.
272+
* Does not resolve target address; decoding is based only on payload kind and shape.
273+
* @param payload - The payload to decode.
274+
* @returns The decoded parameters.
275+
*/
276+
export function decodeUpdateContract(payload: `0x${string}`): DecodedUpdateContract | null {
277+
const b = Buffer.from(payload.slice(2), "hex");
278+
if (b.length < WORD_SIZE) return null;
279+
const kindValue = decodeAtWord(b, 0, decodeUint8InWord);
280+
const kind = Object.keys(UpdateContractPayloadKind)[kindValue] as
281+
| keyof typeof UpdateContractPayloadKind
282+
| undefined;
283+
if (kind === undefined) return null;
284+
285+
const result: DecodedUpdateContract = {
286+
kind,
287+
payload: null,
288+
};
289+
290+
// ABI layout: each slot is 32 bytes; uint8/uint128/bool right-aligned.
291+
switch (kind) {
292+
case "Valuation":
293+
// (uint8, bytes32) = 2 words — SyncManager only
294+
if (b.length === word(2)) {
295+
result.payload = {
296+
valuation: decodeAtWord(b, 1, decodeBytes32Address),
297+
};
298+
}
299+
// (uint8, uint128, bool) = 3 words (Onramp) | (uint8, bytes32, bool) = 3 words (UpdateHookManager)
300+
if (b.length === word(3) && isWordZeroPaddedUint128(b, 1)) {
301+
result.payload = {
302+
assetId: decodeAtWord(b, 1, decodeUint128InWord),
303+
isEnabled: decodeAtWord(b, 2, decodeUint8InWord) !== 0,
304+
};
305+
}
306+
if (b.length === word(3) && !isWordZeroPaddedUint128(b, 1)) {
307+
result.payload = {
308+
manager: decodeAtWord(b, 1, decodeBytes32Address),
309+
canManage: decodeAtWord(b, 2, decodeUint8InWord) !== 0,
310+
};
311+
}
312+
break;
313+
case "MaxReserve":
314+
// (uint8, uint128, uint128) = 3 words (SyncManager) | (uint8, bytes32, bool) = 3 words (Relayer)
315+
if (b.length === word(3)) {
316+
if (isWordZeroPaddedUint128(b, 1) && isWordZeroPaddedUint128(b, 2)) {
317+
result.payload = {
318+
assetId: decodeAtWord(b, 1, decodeUint128InWord),
319+
maxReserve: decodeAtWord(b, 2, decodeUint128InWord),
320+
};
321+
} else {
322+
result.payload = {
323+
relayerAddress: decodeAtWord(b, 1, decodeBytes32Address),
324+
isEnabled: decodeAtWord(b, 2, decodeUint8InWord) !== 0,
325+
};
326+
}
327+
}
328+
break;
329+
case "Offramp":
330+
// (uint8, uint128, bytes32, bool) = 4 words
331+
if (b.length === word(4)) {
332+
result.payload = {
333+
assetId: decodeAtWord(b, 1, decodeUint128InWord),
334+
receiverAddress: decodeAtWord(b, 2, decodeBytes32Address),
335+
isEnabled: decodeAtWord(b, 3, decodeUint8InWord) !== 0,
336+
};
337+
}
338+
break;
339+
case "Withdraw":
340+
// (uint8, uint128, uint128, bytes32) = 4 words
341+
if (b.length === word(4)) {
342+
result.payload = {
343+
assetId: decodeAtWord(b, 1, decodeUint128InWord),
344+
amount: decodeAtWord(b, 2, decodeUint128InWord),
345+
receiverAddress: decodeAtWord(b, 3, decodeBytes32Address),
346+
};
347+
}
348+
break;
349+
}
350+
351+
return result;
352+
}

src/handlers/vaultHandlers.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { multiMapper } from "../helpers/multiMapper";
2-
import { logEvent, serviceError } from "../helpers/logger";
2+
import { logEvent, serviceError, serviceLog } from "../helpers/logger";
33
import {
44
AccountService,
55
AssetService,
@@ -588,3 +588,26 @@ function getSharePrice(
588588
if (sharesAmount === 0n) return null;
589589
return (assetsAmount * 10n ** BigInt(18 - assetDecimals + shareDecimals)) / sharesAmount;
590590
}
591+
592+
multiMapper("syncManager:SetMaxReserve", async ({ event, context }) => {
593+
logEvent(event, context, "syncManager:SetMaxReserve");
594+
const centrifugeId = await BlockchainService.getCentrifugeId(context);
595+
const {
596+
poolId,
597+
scId: tokenId,
598+
asset: assetAddress,
599+
tokenId: _assetTokenId,
600+
maxReserve,
601+
} = event.args;
602+
603+
const vault = (await VaultService.get(context, {
604+
centrifugeId,
605+
poolId,
606+
tokenId,
607+
assetAddress,
608+
})) as VaultService | null;
609+
if (!vault)
610+
return serviceLog(`Vault not found. Cannot retrieve vault. Maybe it's not deployed yet?`);
611+
612+
await vault.setMaxReserve(maxReserve).setCrosschainInProgress().save(event);
613+
});

src/handlers/vaultRegistryHandlers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export async function deployVault({
6464
isActive: true,
6565
status: "Unlinked",
6666
crosschainInProgress: null,
67+
maxReserve: 2n ** 128n - 1n,
6768
},
6869
event
6970
)) as VaultService;

src/helpers/formatter.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,13 @@ export function formatBigIntToDecimal(value: bigint, decimals: number = 18): str
1818

1919
return `${integerStr}.${remainderStr}`;
2020
}
21+
22+
/**
23+
* Formats a bytes32 value as a 0x address.
24+
*
25+
* @param value - The bytes32 value to format
26+
* @returns The formatted 0x address
27+
*/
28+
export function formatBytes32ToAddress(value: `0x${string}`): `0x${string}` {
29+
return value.substring(0, 42).toLowerCase() as `0x${string}`;
30+
}

src/services/VaultService.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,26 @@ export class VaultService extends mixinCommonStatics(Service<typeof Vault>, Vaul
4040
* @returns The service instance for method chaining
4141
*/
4242
public setCrosschainInProgress(
43-
crosschainInProgress?: (typeof VaultCrosschainInProgressTypes)[number]
43+
crosschainInProgress?: (typeof VaultCrosschainInProgressTypes)[number],
44+
crosschainInProgressValue?: bigint
4445
) {
4546
this.data.crosschainInProgress = crosschainInProgress ?? null;
46-
serviceLog(`Setting crosschainInProgress to ${crosschainInProgress}`);
47+
this.data.crosschainInProgressValue = crosschainInProgressValue ?? null;
48+
serviceLog(
49+
`Setting crosschainInProgress to ${crosschainInProgress} with value ${crosschainInProgressValue}`
50+
);
51+
return this;
52+
}
53+
54+
/**
55+
* Sets the max reserve for the vault.
56+
*
57+
* @param maxReserve - The value to set for maxReserve
58+
* @returns The service instance for method chaining
59+
*/
60+
public setMaxReserve(maxReserve: bigint) {
61+
this.data.maxReserve = maxReserve;
62+
serviceLog(`Setting maxReserve to ${maxReserve}`);
4763
return this;
4864
}
4965
}

0 commit comments

Comments
 (0)