Skip to content

Commit 9813c3d

Browse files
committed
feat: don't process sanctioned BTC and SUI address
Signed-off-by: Ravindra Meena <rmeena840@gmail.com>
1 parent e2e5203 commit 9813c3d

File tree

7 files changed

+216
-19
lines changed

7 files changed

+216
-19
lines changed

packages/btcindexer/db/migrations/0001_initial_schema.sql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,11 @@ CREATE TABLE IF NOT EXISTS presign_objects (
124124
) STRICT;
125125

126126
CREATE INDEX IF NOT EXISTS presign_objects_sui_network_created_at ON presign_objects(sui_network, created_at);
127+
128+
CREATE TABLE IF NOT EXISTS SanctionedCryptoAddresses (
129+
wallet_address VARCHAR(255) PRIMARY KEY,
130+
address_type VARCHAR(3) NOT NULL,
131+
CONSTRAINT chk_address_type CHECK (address_type IN ('BTC', 'SUI'))
132+
);
133+
134+
CREATE INDEX idx_type ON SanctionedCryptoAddresses(address_type);

packages/btcindexer/src/btcindexer.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,13 @@ import type {
3434
NbtcPkgCfg,
3535
NbtcDepositAddrsMap,
3636
} from "./models";
37-
import { MintTxStatus, InsertBlockStatus } from "./models";
37+
import { MintTxStatus, InsertBlockStatus, btcNetworkCfg } from "./models";
3838
import type { Electrs } from "./electrs";
3939
import { ElectrsService, ELECTRS_URLS_BY_NETWORK } from "./electrs";
4040
import { fetchNbtcAddresses, fetchPackageConfigs, type Storage } from "./storage";
4141
import { CFStorage } from "./cf-storage";
4242
import type { PutNbtcTxResponse } from "./rpc-interface";
4343

44-
const btcNetworkCfg: Record<BtcNet, Network> = {
45-
[BtcNet.MAINNET]: networks.bitcoin,
46-
[BtcNet.TESTNET]: networks.testnet,
47-
[BtcNet.REGTEST]: networks.regtest,
48-
[BtcNet.SIGNET]: networks.testnet,
49-
};
50-
5144
interface ConfirmingTxCandidate<T> {
5245
id: string | number;
5346
blockHeight: number;
@@ -76,7 +69,7 @@ export async function indexerFromEnv(env: Env): Promise<Indexer> {
7669
const suiClients = new Map<SuiNet, SuiClient>();
7770
for (const p of packageConfigs) {
7871
if (!suiClients.has(p.sui_network))
79-
suiClients.set(p.sui_network, new SuiClient(p, mnemonic));
72+
suiClients.set(p.sui_network, new SuiClient(p, mnemonic, env.DB));
8073
}
8174

8275
const electrsClients = new Map<BtcNet, ElectrsService>();

packages/btcindexer/src/index.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { logError, logger } from "@gonative-cc/lib/logger";
1111
import HttpRouter from "./router";
1212
import { type BlockQueueRecord } from "@gonative-cc/lib/nbtc";
1313
import { processBlockBatch } from "./queue-handler";
14+
import { processSanctionedAddress } from "./sanction";
1415

1516
// Export RPC entrypoints for service bindings
1617
export { RPC } from "./rpc";
@@ -73,12 +74,25 @@ export default {
7374

7475
// the scheduled handler is invoked at the interval set in our wrangler.jsonc's
7576
// [[triggers]] configuration.
76-
async scheduled(_event: ScheduledController, env: Env, _ctx): Promise<void> {
77+
async scheduled(event: ScheduledController, env: Env, _ctx): Promise<void> {
7778
logger.debug({ msg: "Cron job starting" });
7879
try {
79-
const indexer = await indexerFromEnv(env);
80-
await indexer.updateConfirmationsAndFinalize();
81-
await indexer.processFinalizedTransactions();
80+
const trigger = event.cron;
81+
switch (trigger) {
82+
case "0 1 * * *":
83+
await processSanctionedAddress(env);
84+
break;
85+
case "* * * * *":
86+
{
87+
const indexer = await indexerFromEnv(env);
88+
await indexer.updateConfirmationsAndFinalize();
89+
await indexer.processFinalizedTransactions();
90+
}
91+
break;
92+
default:
93+
logger.info({ msg: "Unknown schedule triggered" });
94+
}
95+
8296
logger.info({ msg: "Cron job finished successfully" });
8397
} catch (e) {
8498
logError({ msg: "Cron job failed", method: "scheduled" }, e);

packages/btcindexer/src/models.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Transaction } from "bitcoinjs-lib";
1+
import { networks, Transaction, type Network } from "bitcoinjs-lib";
22
import { BitcoinTxStatus, BtcNet, type BlockQueueRecord } from "@gonative-cc/lib/nbtc";
33
import type { NbtcPkg, SuiNet } from "@gonative-cc/lib/nsui";
44

@@ -195,3 +195,10 @@ export const enum InsertBlockStatus {
195195
}
196196

197197
export type InsertBlockResult = InsertBlockStatus;
198+
199+
export const btcNetworkCfg: Record<BtcNet, Network> = {
200+
[BtcNet.MAINNET]: networks.bitcoin,
201+
[BtcNet.TESTNET]: networks.testnet,
202+
[BtcNet.REGTEST]: networks.regtest,
203+
[BtcNet.SIGNET]: networks.testnet,
204+
};
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { logError, logger } from "@gonative-cc/lib/logger";
2+
import { StringDecoder } from "string_decoder";
3+
4+
interface WalletEntity {
5+
properties: {
6+
currency: string[];
7+
publicKey: string[];
8+
};
9+
}
10+
11+
interface SanctionEntity {
12+
properties: {
13+
cryptoWallets?: WalletEntity[];
14+
};
15+
}
16+
17+
const URL = "https://data.opensanctions.org/datasets/latest/us_ofac_sdn/targets.nested.json";
18+
19+
export async function processSanctionedAddress(env: Env) {
20+
try {
21+
logger.debug({ msg: "Downloading and processing..." });
22+
23+
const response = await fetch(URL);
24+
if (!response.body) throw new Error("No response body");
25+
26+
const btcAddresses: string[] = [];
27+
const suiAddresses: string[] = [];
28+
const decoder = new StringDecoder("utf8");
29+
let buffer = "";
30+
31+
// Helper to process a single entity line
32+
const processLine = (line: string) => {
33+
if (!line.trim()) return;
34+
const entity: SanctionEntity = JSON.parse(line);
35+
36+
if (entity.properties?.cryptoWallets) {
37+
for (const wallet of entity.properties.cryptoWallets) {
38+
const currency = wallet.properties.currency?.[0]?.toUpperCase();
39+
const address = wallet.properties.publicKey?.[0];
40+
41+
if (!currency || !address) continue;
42+
43+
if (currency === "XBT") btcAddresses.push(address);
44+
if (currency === "SUI") suiAddresses.push(address);
45+
}
46+
}
47+
};
48+
49+
// Stream processing
50+
const reader = response.body.getReader();
51+
52+
while (true) {
53+
const { done, value } = await reader.read();
54+
if (done) break;
55+
56+
buffer += decoder.write(Buffer.from(value));
57+
const lines = buffer.split("\n");
58+
buffer = lines.pop() || "";
59+
60+
for (const line of lines) {
61+
processLine(line);
62+
}
63+
}
64+
65+
// Handle leftover buffer
66+
if (buffer.trim()) processLine(buffer);
67+
68+
logger.debug({
69+
msg: `Found ${btcAddresses.length} BTC and ${suiAddresses.length} SUI addresses.`,
70+
});
71+
72+
// --- Database Transaction (Batch) ---
73+
74+
const statements: D1PreparedStatement[] = [
75+
env.DB.prepare("DELETE FROM SanctionedCryptoAddresses"),
76+
];
77+
78+
// Add BTC inserts
79+
btcAddresses.forEach((addr) => {
80+
statements.push(
81+
env.DB.prepare(
82+
"INSERT OR IGNORE INTO SanctionedCryptoAddresses (wallet_address, address_type) VALUES (?, ?)",
83+
).bind(addr, "BTC"),
84+
);
85+
});
86+
87+
// Add SUI inserts
88+
suiAddresses.forEach((addr) => {
89+
statements.push(
90+
env.DB.prepare(
91+
"INSERT INTO SanctionedCryptoAddresses (wallet_address, address_type) VALUES (?, ?)",
92+
).bind(addr, "SUI"),
93+
);
94+
});
95+
96+
await env.DB.batch(statements);
97+
98+
logger.debug({
99+
msg: `"Database updated successfully.`,
100+
});
101+
} catch (err) {
102+
const error = err as Error;
103+
logError({
104+
method: "processSanctionedAddress",
105+
msg: error.message || "Error processing sanctioned address",
106+
});
107+
}
108+
}

packages/btcindexer/src/sui_client.ts

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import { SuiClient as Client, getFullnodeUrl } from "@mysten/sui/client";
33
import type { Signer } from "@mysten/sui/cryptography";
44
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
55
import { Transaction as SuiTransaction } from "@mysten/sui/transactions";
6-
import type { MintBatchArg, NbtcPkgCfg, SuiTxDigest } from "./models";
6+
import { btcNetworkCfg, type MintBatchArg, type NbtcPkgCfg, type SuiTxDigest } from "./models";
77
import { logError, logger } from "@gonative-cc/lib/logger";
88
import { nBTCContractModule } from "@gonative-cc/nbtc";
9+
import { Transaction, payments, script } from "bitcoinjs-lib";
10+
import type { D1Database } from "@cloudflare/workers-types";
911

1012
const LC_MODULE = "light_client";
1113

@@ -18,20 +20,22 @@ export interface SuiClientI {
1820

1921
export type SuiClientConstructor = (config: NbtcPkgCfg) => SuiClientI;
2022

21-
export function NewSuiClient(mnemonic: string): SuiClientConstructor {
22-
return (config: NbtcPkgCfg) => new SuiClient(config, mnemonic);
23+
export function NewSuiClient(mnemonic: string, db: D1Database): SuiClientConstructor {
24+
return (config: NbtcPkgCfg) => new SuiClient(config, mnemonic, db);
2325
}
2426

2527
export class SuiClient implements SuiClientI {
2628
private client: Client;
2729
private signer: Signer;
2830
private config: NbtcPkgCfg;
31+
private db: D1Database;
2932

30-
constructor(config: NbtcPkgCfg, mnemonic: string) {
33+
constructor(config: NbtcPkgCfg, mnemonic: string, db?: D1Database) {
3134
this.config = config;
3235
this.client = new Client({ url: getFullnodeUrl(config.sui_network) });
3336
// TODO: instead of mnemonic, let's use the Signer interface in the config
3437
this.signer = Ed25519Keypair.deriveKeypair(mnemonic);
38+
this.db = db!;
3539
logger.debug({
3640
msg: "Sui Client Initialized",
3741
suiSignerAddress: this.signer.getPublicKey().toSuiAddress(),
@@ -129,6 +133,17 @@ export class SuiClient implements SuiClientI {
129133
const proofLittleEndian = args.proof.proofPath.map((p) => Array.from(p));
130134
const txBytes = Array.from(args.tx.toBuffer());
131135

136+
// Extract sender address and check sanctions
137+
const senderAddress = await this.extractSenderAddress(args.tx);
138+
if (senderAddress && (await this.isSanctioned(senderAddress))) {
139+
logger.error({
140+
msg: "Sanctioned address detected, skipping mint",
141+
txId: args.tx.getId(),
142+
senderAddress,
143+
});
144+
continue;
145+
}
146+
132147
tx.add(
133148
nBTCContractModule.mint({
134149
package: this.config.nbtc_pkg,
@@ -168,6 +183,58 @@ export class SuiClient implements SuiClientI {
168183
return [success, result.digest];
169184
}
170185

186+
private async extractSenderAddress(tx: Transaction): Promise<string | null> {
187+
try {
188+
if (tx.ins.length === 0) return null;
189+
190+
const firstInput = tx.ins[0];
191+
const network = btcNetworkCfg[this.config.btc_network];
192+
193+
if (firstInput && firstInput.witness && firstInput.witness.length >= 2) {
194+
const pubKey = firstInput.witness[1];
195+
const { address } = payments.p2wpkh({
196+
pubkey: pubKey,
197+
network: network,
198+
});
199+
return address || null;
200+
}
201+
202+
const scriptSig = firstInput?.script;
203+
if (scriptSig && scriptSig.length >= 65) {
204+
const chunks = script.decompile(scriptSig);
205+
if (!chunks) return null;
206+
207+
const pubKey = chunks[chunks.length - 1] as Buffer;
208+
209+
const { address } = payments.p2pkh({
210+
pubkey: pubKey,
211+
network: network,
212+
});
213+
return address || null;
214+
}
215+
216+
return null;
217+
} catch (e) {
218+
console.error("Failed to extract sender address", e);
219+
return null;
220+
}
221+
}
222+
223+
private async isSanctioned(btcAddress: string): Promise<boolean> {
224+
try {
225+
const result = await this.db
226+
.prepare(
227+
"SELECT 1 FROM SanctionedCryptoAddresses WHERE wallet_address = ? AND address_type = 'BTC'",
228+
)
229+
.bind(btcAddress)
230+
.first();
231+
return result !== null;
232+
} catch (e) {
233+
logError({ method: "isSanctioned", msg: "Failed to check sanctions", btcAddress }, e);
234+
return false;
235+
}
236+
}
237+
171238
/**
172239
* Wrapper for mintNbtcBatch that catches pre-submission errors.
173240
* Returns:

packages/btcindexer/wrangler.jsonc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
},
1313
"triggers": {
1414
// every one minute
15-
"crons": ["* * * * *"],
15+
"crons": ["* * * * *", "0 1 * * *"],
1616
},
1717
"compatibility_flags": ["nodejs_compat"],
1818
/**

0 commit comments

Comments
 (0)