Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 69 additions & 7 deletions packages/sui-indexer/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,18 @@ import {
} from "./models";
import { logger } from "@gonative-cc/lib/logger";
import { fromBase64 } from "@mysten/sui/utils";
import type { SuiClient } from "./redeem-sui-client";
import type { SuiNet } from "@gonative-cc/lib/nsui";

export class SuiEventHandler {
private storage: D1Storage;
private setupId?: number;
private suiClients?: Map<SuiNet, SuiClient>;

constructor(storage: D1Storage, setupId?: number) {
constructor(storage: D1Storage, setupId?: number, suiClients?: Map<SuiNet, SuiClient>) {
this.storage = storage;
this.setupId = setupId;
this.suiClients = suiClients;
}

public async handleEvents(events: SuiEventNode[]) {
Expand Down Expand Up @@ -121,14 +125,72 @@ export class SuiEventHandler {
}

private async handleCompletedSign(e: SuiEventNode) {
const { sign_id } = e.json as CompletedSignEventRaw;
logger.info({ msg: "IKA sign completed", type: e.type, signId: sign_id });
//TODO: will handle the sign in the redeem-service in next PR
const data = e.json as CompletedSignEventRaw;
const signId = data.sign_id as string;
logger.info({
msg: "Ika signature completed",
sign_id: signId,
is_future_sign: data.is_future_sign,
signature_length: data.signature.length,
txDigest: e.txDigest,
});

const redeemInfo = await this.storage.getRedeemInfoBySignId(signId);
if (!redeemInfo) {
logger.debug({ msg: "Sign ID not found in our redeems, ignoring", sign_id: signId });
return;
}

if (!this.suiClients) {
logger.warn({ msg: "No SuiClients available to record signature", sign_id: signId });
return;
}

const client = this.suiClients.get(redeemInfo.sui_network);
if (!client) {
logger.warn({
msg: "No SuiClient for network",
network: redeemInfo.sui_network,
sign_id: signId,
});
return;
}

await client.validateSignature(
redeemInfo.redeem_id,
redeemInfo.input_index,
signId,
redeemInfo.nbtc_pkg,
redeemInfo.nbtc_contract,
);
await this.storage.markRedeemInputVerified(redeemInfo.redeem_id, redeemInfo.utxo_id);

logger.info({
msg: "Recorded Ika signature",
redeem_id: redeemInfo.redeem_id,
utxo_id: redeemInfo.utxo_id,
sign_id: signId,
});
}

private async handleRejectedSign(e: SuiEventNode) {
const { sign_id } = e.json as RejectedSignEventRaw;
logger.warn({ msg: "IKA sign rejected", type: e.type, signId: sign_id });
//TODO: will handle the sign in the redeem-service in next PR
const data = e.json as RejectedSignEventRaw;
const signId = data.sign_id as string;
const redeemInfo = await this.storage.getRedeemInfoBySignId(signId);
if (!redeemInfo) {
logger.debug({
msg: "Rejected sign ID not found in our redeems, ignoring",
sign_id: signId,
});
return;
}

logger.warn({
msg: "Ika signature rejected, clearing sign_id for retry",
sign_id: signId,
redeem_id: redeemInfo.redeem_id,
utxo_id: redeemInfo.utxo_id,
});
await this.storage.clearRedeemInputSignId(redeemInfo.redeem_id, redeemInfo.utxo_id);
}
}
53 changes: 32 additions & 21 deletions packages/sui-indexer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Processor } from "./processor";
import { D1Storage } from "./storage";
import { logError, logger } from "@gonative-cc/lib/logger";
import { RedeemService } from "./redeem-service";
import { createSuiClients } from "./redeem-sui-client";
import { createSuiClients, type SuiClient } from "./redeem-sui-client";
import type { Service } from "@cloudflare/workers-types";
import type { WorkerEntrypoint } from "cloudflare:workers";
import type { BtcIndexerRpc } from "@gonative-cc/btcindexer/rpc-interface";
Expand All @@ -26,18 +26,35 @@ export default {
const storage = new D1Storage(env.DB);
const activeNetworks = await storage.getActiveNetworks();

let mnemonic: string;
try {
mnemonic = (await env.NBTC_MINTING_SIGNER_MNEMONIC.get()) || "";
} catch (error) {
logger.error({ msg: "Failed to retrieve NBTC_MINTING_SIGNER_MNEMONIC", error });
return;
}
if (!mnemonic) {
logger.error({ msg: "Missing NBTC_MINTING_SIGNER_MNEMONIC" });
return;
}
const suiClients = await createSuiClients(activeNetworks, mnemonic);

// Run both indexer and redeem solver tasks in parallel
const results = await Promise.allSettled([
runSuiIndexer(storage, env, activeNetworks),
runRedeemSolver(storage, env, activeNetworks),
runSuiIndexer(storage, activeNetworks, suiClients),
runRedeemSolver(storage, env, suiClients),
]);

// Check for any rejected promises and log errors
reportErrors(results, "scheduled", "Scheduled task error", ["SuiIndexer", "RedeemSolver"]);
},
} satisfies ExportedHandler<Env>;

async function runSuiIndexer(storage: D1Storage, env: Env, activeNetworks: SuiNet[]) {
async function runSuiIndexer(
storage: D1Storage,
activeNetworks: SuiNet[],
suiClients: Map<SuiNet, SuiClient>,
) {
if (activeNetworks.length === 0) {
logger.info({ msg: "No active packages/networks found in database." });
return;
Expand All @@ -61,7 +78,9 @@ async function runSuiIndexer(storage: D1Storage, env: Env, activeNetworks: SuiNe
networks: networksToProcess.map((n) => n.name),
});

const networkJobs = networksToProcess.map((netCfg) => poolAndProcessEvents(netCfg, storage));
const networkJobs = networksToProcess.map((netCfg) =>
poolAndProcessEvents(netCfg, storage, suiClients),
);
const results = await Promise.allSettled(networkJobs);
reportErrors(
results,
Expand All @@ -72,9 +91,13 @@ async function runSuiIndexer(storage: D1Storage, env: Env, activeNetworks: SuiNe
);
}

async function poolAndProcessEvents(netCfg: NetworkConfig, storage: D1Storage) {
async function poolAndProcessEvents(
netCfg: NetworkConfig,
storage: D1Storage,
suiClients: Map<SuiNet, SuiClient>,
) {
const client = new SuiGraphQLClient(netCfg.url);
const p = new Processor(netCfg, storage, client);
const p = new Processor(netCfg, storage, client, suiClients);

const nbtcPkgs = await storage.getActiveNbtcPkgs(netCfg.name);
if (nbtcPkgs.length > 0) {
Expand All @@ -97,23 +120,11 @@ async function poolAndProcessEvents(netCfg: NetworkConfig, storage: D1Storage) {
}
}

async function runRedeemSolver(storage: D1Storage, env: Env, activeNetworks: SuiNet[]) {
async function runRedeemSolver(storage: D1Storage, env: Env, suiClients: Map<SuiNet, SuiClient>) {
logger.info({ msg: "Running scheduled redeem solver task..." });
let mnemonic: string;
try {
mnemonic = (await env.NBTC_MINTING_SIGNER_MNEMONIC.get()) || "";
} catch (error) {
logger.error({ msg: "Failed to retrieve NBTC_MINTING_SIGNER_MNEMONIC", error });
return;
}
if (!mnemonic) {
logger.error({ msg: "Missing NBTC_MINTING_SIGNER_MNEMONIC" });
return;
}
const clients = await createSuiClients(activeNetworks, mnemonic);
const service = new RedeemService(
storage,
clients,
suiClients,
env.BtcIndexer as unknown as Service<BtcIndexerRpc & WorkerEntrypoint>,
env.UTXO_LOCK_TIME,
env.REDEEM_DURATION_MS,
Expand Down
9 changes: 9 additions & 0 deletions packages/sui-indexer/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,12 @@ export interface IkaCursorUpdate {

export type CompletedSignEventRaw = typeof CoordinatorInnerModule.CompletedSignEvent.$inferInput;
export type RejectedSignEventRaw = typeof CoordinatorInnerModule.RejectedSignEvent.$inferInput;

export interface RedeemSignInfo {
redeem_id: number;
utxo_id: number;
input_index: number;
nbtc_pkg: string;
nbtc_contract: string;
sui_network: SuiNet;
}
19 changes: 16 additions & 3 deletions packages/sui-indexer/src/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,25 @@ import { D1Storage } from "./storage";
import { logError, logger } from "@gonative-cc/lib/logger";
import { SuiEventHandler } from "./handler";
import type { EventFetcher } from "./graphql-client";
import type { SuiClient } from "./redeem-sui-client";
import type { SuiNet } from "@gonative-cc/lib/nsui";

export class Processor {
netCfg: NetworkConfig;
storage: D1Storage;
eventFetcher: EventFetcher;

constructor(netCfg: NetworkConfig, storage: D1Storage, eventFetcher: EventFetcher) {
suiClients?: Map<SuiNet, SuiClient>;

constructor(
netCfg: NetworkConfig,
storage: D1Storage,
eventFetcher: EventFetcher,
suiClients?: Map<SuiNet, SuiClient>,
) {
this.netCfg = netCfg;
this.storage = storage;
this.eventFetcher = eventFetcher;
this.suiClients = suiClients;
}

// poll Nbtc events by multiple package ids
Expand Down Expand Up @@ -111,7 +120,11 @@ export class Processor {
});

if (result.events.length > 0) {
const handler = new SuiEventHandler(this.storage);
const handler = new SuiEventHandler(
this.storage,
undefined,
this.suiClients,
);
await handler.handleEvents(result.events);
}

Expand Down
41 changes: 3 additions & 38 deletions packages/sui-indexer/src/redeem-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export class RedeemService {
// NOTE: here we are processing only 50 redeems every minute (every cron), we are not
// looping thought all the solved redeems to avoid cloudflare timeout, since we are
// already waiting for ika to sign, when calling ikaSdk.getPresignInParicularState
// Signature verification (recordIkaSig) has been moved to the event indexer handler,
// which reacts to IKA CompletedSignEvent / RejectedSignEvent.
const solved = await this.storage.getSolvedRedeems();
if (solved.length === 0) return;

Expand Down Expand Up @@ -116,18 +118,14 @@ export class RedeemService {
try {
if (!input.sign_id) {
await this.requestIkaSig(client, req, input);
} else if (input.sign_id && !input.verified) {
// TODO: this should be triggered when getting the event from ika
await this.recordIkaSig(client, req, input);
}
} catch (e) {
logError(
{
msg: "Failed to process input",
msg: "Failed to request signature for input",
method: "processSolvedRedeem",
redeemId: req.redeem_id,
utxoId: input.utxo_id,
step: !input.sign_id ? "request_signature" : "verify_signature",
},
e,
);
Expand Down Expand Up @@ -260,39 +258,6 @@ export class RedeemService {
}
}

private async recordIkaSig(
client: SuiClient,
req: RedeemRequestWithInputs,
input: RedeemInput,
) {
logger.info({
msg: "Verifying signature for input",
redeemId: req.redeem_id,
utxoId: input.utxo_id,
inputIdx: input.input_index,
signId: input.sign_id,
});

if (!input.sign_id) {
throw new Error("Input signature ID is missing");
}

await client.validateSignature(
req.redeem_id,
input.input_index,
input.sign_id,
req.nbtc_pkg,
req.nbtc_contract,
);

await this.storage.markRedeemInputVerified(req.redeem_id, input.utxo_id);
logger.info({
msg: "Signature verified",
redeemId: req.redeem_id,
utxoId: input.utxo_id,
});
}

private getSuiClient(suiNet: SuiNet): SuiClient {
const c = this.clients.get(suiNet);
if (c === undefined) throw new Error("No SuiClient for the sui network = " + suiNet);
Expand Down
32 changes: 31 additions & 1 deletion packages/sui-indexer/src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type RedeemRequestResp,
type Utxo,
type IkaCursorUpdate,
type RedeemSignInfo,
} from "./models";
import { toSuiNet, type SuiNet } from "@gonative-cc/lib/nsui";
import { address, networks } from "bitcoinjs-lib";
Expand Down Expand Up @@ -388,6 +389,35 @@ export class D1Storage {
await this.db.batch([updateSolution, updateRequest]);
}

async clearRedeemInputSignId(redeemId: number, utxoId: number): Promise<void> {
await this.db
.prepare(
`UPDATE nbtc_redeem_solutions SET sign_id = NULL WHERE redeem_id = ? AND utxo_id = ?`,
)
.bind(redeemId, utxoId)
.run();
}

async getRedeemInfoBySignId(signId: string): Promise<RedeemSignInfo | null> {
const query = `
SELECT s.redeem_id, s.utxo_id, s.input_index, p.nbtc_pkg, p.nbtc_contract, p.sui_network
FROM nbtc_redeem_solutions s
JOIN nbtc_redeem_requests r ON s.redeem_id = r.redeem_id
JOIN setups p ON r.setup_id = p.id
WHERE s.sign_id = ?
`;
const result = await this.db.prepare(query).bind(signId).first<{
redeem_id: number;
utxo_id: number;
input_index: number;
nbtc_pkg: string;
nbtc_contract: string;
sui_network: string;
}>();
if (!result) return null;
return { ...result, sui_network: toSuiNet(result.sui_network) };
}

async markRedeemSolved(redeemId: number): Promise<void> {
try {
await this.db
Expand Down Expand Up @@ -540,7 +570,7 @@ export class D1Storage {
FROM nbtc_redeem_requests r
JOIN setups p ON r.setup_id = p.id
WHERE r.status = ?
AND EXISTS (SELECT 1 FROM nbtc_redeem_solutions s WHERE s.redeem_id = r.redeem_id AND (s.sign_id IS NULL OR s.verified = 0))
AND EXISTS (SELECT 1 FROM nbtc_redeem_solutions s WHERE s.redeem_id = r.redeem_id AND s.sign_id IS NULL)
ORDER BY r.created_at ASC
LIMIT 50;
`;
Expand Down