Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 5 additions & 1 deletion packages/btcindexer/src/btcindexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Service } from "@cloudflare/workers-types";
import type { WorkerEntrypoint } from "cloudflare:workers";
import type { SuiIndexerRpc } from "@gonative-cc/sui-indexer/rpc-interface";
import { logError, logger } from "@gonative-cc/lib/logger";
import { getMnemonic } from "@gonative-cc/lib/secrets";
import { OP_RETURN } from "./opcodes";
import { BitcoinMerkleTree } from "./bitcoin-merkle-tree";
import { SuiClient, type SuiClientI } from "./sui_client";
Expand Down Expand Up @@ -54,7 +55,10 @@ export async function indexerFromEnv(env: Env): Promise<Indexer> {
throw new Error("Invalid MAX_NBTC_MINT_TX_RETRIES in config. Must be a number >= 0.");
}

const mnemonic = await env.NBTC_MINTING_SIGNER_MNEMONIC.get();
const mnemonic = await getMnemonic(env.NBTC_MINTING_SIGNER_MNEMONIC);
if (!mnemonic) {
throw new Error("Failed to retrieve mnemonic");
}
const suiClients = new Map<SuiNet, SuiClient>();
for (const p of packageConfigs) {
if (!suiClients.has(p.sui_network))
Expand Down
23 changes: 23 additions & 0 deletions packages/lib/src/secrets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { logger } from "./logger";

interface SecretStore {
get(): Promise<string>;
}

/**
* Retrieves the mnemonic from the secrets store with proper error handling.
* Returns the mnemonic string or null if not found/failed.
*/
export async function getMnemonic(secret: SecretStore): Promise<string | null> {
try {
const mnemonic = await secret.get();
if (!mnemonic) {
logger.error({ msg: "Missing NBTC_MINTING_SIGNER_MNEMONIC" });
return null;
}
return mnemonic;
} catch (error) {
logger.error({ msg: "Failed to retrieve NBTC_MINTING_SIGNER_MNEMONIC", error });
return null;
}
}
67 changes: 60 additions & 7 deletions packages/sui-indexer/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ import {
} from "./models";
import { logger } from "@gonative-cc/lib/logger";
import { fromBase64 } from "@mysten/sui/utils";
import type { SuiClient } from "./redeem-sui-client";

export class SuiEventHandler {
private storage: D1Storage;
private setupId?: number;
private suiClient?: SuiClient;

constructor(storage: D1Storage, setupId?: number) {
constructor(storage: D1Storage, setupId?: number, suiClient?: SuiClient) {
this.storage = storage;
this.setupId = setupId;
this.suiClient = suiClient;
}

public async handleEvents(events: SuiEventNode[]) {
Expand Down Expand Up @@ -121,14 +124,64 @@ 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, // true if it's Ika future transaction signature type
signature_length: data.signature.length,
txDigest: e.txDigest,
});

// IKA coordinator is shared across protocols, so we only process sign IDs that match our redeems.
// The final signature is recorded via SignatureRecordedEvent from nbtc.move (handled above).
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.suiClient) {
logger.warn({ msg: "No SuiClient available to record signature", sign_id: signId });
return;
}

await this.suiClient.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.warn({
msg: "Rejected sign ID not found in our redeems, ignoring",
sign_id: signId,
});
return;
}

logger.debug({
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);
}
}
46 changes: 25 additions & 21 deletions packages/sui-indexer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import type { NetworkConfig } from "./models";
import { Processor } from "./processor";
import { D1Storage } from "./storage";
import { logError, logger } from "@gonative-cc/lib/logger";
import { getMnemonic } from "@gonative-cc/lib/secrets";
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 +27,26 @@ export default {
const storage = new D1Storage(env.DB);
const activeNetworks = await storage.getActiveNetworks();

const mnemonic = await getMnemonic(env.NBTC_MINTING_SIGNER_MNEMONIC);
if (!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 +70,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 +83,14 @@ 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 suiClient = suiClients.get(netCfg.name);
const p = new Processor(netCfg, storage, client, suiClient);

const nbtcPkgs = await storage.getActiveNbtcPkgs(netCfg.name);
if (nbtcPkgs.length > 0) {
Expand All @@ -98,23 +114,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;
}
18 changes: 15 additions & 3 deletions packages/sui-indexer/src/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,24 @@ 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";

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

constructor(netCfg: NetworkConfig, storage: D1Storage, eventFetcher: EventFetcher) {
suiClient?: SuiClient;

constructor(
netCfg: NetworkConfig,
storage: D1Storage,
eventFetcher: EventFetcher,
suiClient?: SuiClient,
) {
this.netCfg = netCfg;
this.storage = storage;
this.eventFetcher = eventFetcher;
this.suiClient = suiClient;
}

// TODO: Refactor pollAllNbtcEvents and pollIkaEvents into a single generic pollEvents function
Expand Down Expand Up @@ -113,7 +121,11 @@ export class Processor {
});

if (result.events.length > 0) {
const handler = new SuiEventHandler(this.storage);
const handler = new SuiEventHandler(
this.storage,
undefined,
this.suiClient,
);
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
Loading