Skip to content

Commit

Permalink
feat: UtuProvider tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Th0rgal committed Oct 23, 2024
1 parent 4cddce1 commit 768ef11
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 175 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ jobs:
run: bun run build

- name: Run tests
env:
BITCOIN_RPC_URL: ${{ secrets.BITCOIN_RPC_URL }}
BITCOIN_RPC_USERNAME: ${{ secrets.BITCOIN_RPC_USERNAME }}
BITCOIN_RPC_PASSWORD: ${{ secrets.BITCOIN_RPC_PASSWORD }}
run: bun run test
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"dependencies": {
"@types/jest": "^29.5.13",
"cross-fetch": "^4.0.0",
"dotenv": "^16.4.5",
"jest": "^29.7.0",
"ts-jest": "^29.2.5"
},
Expand Down
6 changes: 0 additions & 6 deletions src/BitcoinTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,3 @@ export interface RawTransaction {
time: number;
blocktime: number;
}

export interface BlockHeightProof {
blockHeader: BlockHeader;
rawCoinbaseTx: string;
merkleProof: string[];
}
58 changes: 57 additions & 1 deletion src/UtuProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BitcoinProvider } from "./BitcoinProvider";
import { BlockHeightProof } from "@/BitcoinTypes";
import { BlockHeightProof, RegisterBlocksTx } from "@/UtuTypes";
import { BlockHeader } from "./BitcoinTypes";

export interface UtuProviderResult {
inclusionProof: string;
Expand Down Expand Up @@ -61,4 +62,59 @@ export class UtuProvider {
merkleProof: leftMerkleBranch,
};
}

async getRegisterBlocksTx(blocks: string[]): Promise<RegisterBlocksTx> {
const blockHeaders = await Promise.all(
blocks.map((block) => this.bitcoinProvider.getBlockHeader(block))
);

return {
contractAddress: "0x...", // Replace with actual contract address
selector: "0x...", // Replace with actual selector
calldata: [
"0x" + blocks.length.toString(16),
...blockHeaders.flatMap((header) => this.serializeBlockHeader(header)),
],
};
}

private serializeBlockHeader(blockHeader: BlockHeader): string[] {
// Ensure all fields are present
const requiredFields = [
"version",
"previousblockhash",
"merkleroot",
"time",
"bits",
"nonce",
];

for (const field of requiredFields) {
if (!(field in blockHeader)) {
throw new Error(`Missing required field: ${field}`);
}
}

// Helper function to convert to little-endian hex
const toLittleEndianHex = (num: number): string => {
return num
.toString(16)
.padStart(8, "0")
.match(/.{2}/g)!
.reverse()
.join("");
};

// Serialize each field
const serialized = [
"0x" + toLittleEndianHex(blockHeader.version),
"0x" + blockHeader.previousblockhash,
"0x" + blockHeader.merkleroot,
"0x" + toLittleEndianHex(blockHeader.time),
"0x" + blockHeader.bits,
"0x" + toLittleEndianHex(blockHeader.nonce),
];

return serialized;
}
}
13 changes: 13 additions & 0 deletions src/UtuTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { BlockHeader } from "@/BitcoinTypes";

export interface BlockHeightProof {
blockHeader: BlockHeader;
rawCoinbaseTx: string;
merkleProof: string[];
}

export interface RegisterBlocksTx {
contractAddress: string;
selector: string;
calldata: string[];
}
183 changes: 15 additions & 168 deletions src/tests/BitcoinRpcProvider.test.ts
Original file line number Diff line number Diff line change
@@ -1,210 +1,57 @@
import { config } from "dotenv";
import { BitcoinRpcProvider } from "@/BitcoinRpcProvider";
import fetch from "cross-fetch";

jest.mock("cross-fetch");
config();

describe("BitcoinRpcProvider", () => {
let provider: BitcoinRpcProvider;

beforeEach(() => {
provider = new BitcoinRpcProvider({
url: "http://example-bitcoin-rpc.com",
username: "satoshi",
password: "fakepassword123",
url: process.env.BITCOIN_RPC_URL || "",
username: process.env.BITCOIN_RPC_USERNAME || "",
password: process.env.BITCOIN_RPC_PASSWORD || "",
});

// Add this mock reset
(fetch as jest.MockedFunction<typeof fetch>).mockReset();
});

it("should get block header", async () => {
const knownBlockHash =
"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"; // Genesis block hash

// Update the mock to correctly simulate the RPC response
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValueOnce({
json: jest.fn().mockResolvedValue({
result: {
hash: knownBlockHash,
confirmations: 866714,
height: 0,
version: 1,
versionHex: "00000001",
merkleroot:
"4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
time: 1231006505,
mediantime: 1231006505,
nonce: 2083236893,
bits: "1d00ffff",
difficulty: 1,
chainwork:
"0000000000000000000000000000000000000000000000000000000100010001",
nTx: 1,
nextblockhash:
"00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048",
},
}),
} as any);

const result = await provider.getBlockHeader(knownBlockHash);
expect(result).toBeDefined();
expect(result.hash).toBe(knownBlockHash);
expect(result.confirmations).toBe(866714);
expect(result.height).toBe(0);
expect(result.version).toBe(1);
expect(result.versionHex).toBe("00000001");
expect(result.merkleroot).toBe(
"4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"
);
expect(result.time).toBe(1231006505);
expect(result.mediantime).toBe(1231006505);
expect(result.nonce).toBe(2083236893);
expect(result.bits).toBe("1d00ffff");
expect(result.difficulty).toBe(1);
expect(result.chainwork).toBe(
"0000000000000000000000000000000000000000000000000000000100010001"
);
expect(result.nTx).toBe(1);
expect(result.nextblockhash).toBe(
"00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048"
);
});

it("should get block hash", async () => {
const mockBlockHash =
"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f";
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValueOnce({
json: jest.fn().mockResolvedValue({ result: mockBlockHash }),
} as any);

const result = await provider.getBlockHash(0);
expect(result).toBe(mockBlockHash);
expect(result).toBe(
"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
);
});

it("should get tx out proof", async () => {
const mockProof =
"02000000b64ea7b7615283a01d9d6019f5bfbd694d2a534ca87a7d07aa0100000000000045ff55adc8d6bc183e9abfa7bdfef3e7b942d786dcd116e776ead8238451a238ac204f516e81021ad0f8cd016400000008e5af83540c63d08a8f3ea70dc9b0aff130e5f69b0cf8d3a96f512fdaa501360feface67ef372ccc368143e9ea96c5939d1be2cb1eddd710808a8d36e311f3b26ee09b877d17d3628466e765dd123411dd12e8bdde192236a1dbd85be6ddd7df83ed791db9b7c5e2f990e2cd9d2a921c72b327a6097c05e73d38dca50795f1ce39a9f995341847bc1bab40c70115f3addf7d8bcc2372922ccf8e404e63db683800cb80f65a64ff99d3519b116c79bfaf3ee68c723344660f5177a92ebff184da4839c7d6351941e1facc6d64a327e4445ac8d5d0e07d1977ecc587623fcabbbd136857a60c637fb60938cbc6885b33bbd16568d8997ca3b38ab4e04bd7b54620b02ff00";
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValueOnce({
json: jest.fn().mockResolvedValue({ result: mockProof }),
} as any);

const txids = [
"0f3601a5da2f516fa9d3f80c9bf6e530f1afb0c90da73e8f8ad0630c5483afe5",
];
const result = await provider.getTxOutProof(txids);
expect(result).toBe(mockProof);
expect(result).toBeDefined();
expect(typeof result).toBe("string");
});

it("should get raw transaction", async () => {
const mockRawTx = {
txid: "0f3601a5da2f516fa9d3f80c9bf6e530f1afb0c90da73e8f8ad0630c5483afe5",
hash: "0f3601a5da2f516fa9d3f80c9bf6e530f1afb0c90da73e8f8ad0630c5483afe5",
version: 1,
size: 124,
vsize: 124,
weight: 496,
locktime: 0,
vin: [
{
coinbase:
"03fc7903062f503253482f04ac204f510858029a11000003550d3363646164312f736c7573682f",
sequence: 0,
},
],
vout: [
{
value: 25.0626,
n: 0,
scriptPubKey: {
asm: "OP_DUP OP_HASH160 e285a29e0704004d4e95dbb7c57a98563d9fb2eb OP_EQUALVERIFY OP_CHECKSIG",
desc: "addr(1MejoVXRvsmwyDpTpkw3VJ82NsjjT8SyEw)#5zfhcaw2",
hex: "76a914e285a29e0704004d4e95dbb7c57a98563d9fb2eb88ac",
address: "1MejoVXRvsmwyDpTpkw3VJ82NsjjT8SyEw",
type: "pubkeyhash",
},
},
],
hex: "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff2703fc7903062f503253482f04ac204f510858029a11000003550d3363646164312f736c7573682f0000000001207e6295000000001976a914e285a29e0704004d4e95dbb7c57a98563d9fb2eb88ac00000000",
blockhash:
"00000000000000d0dfd4c9d588d325dce4f32c1b31b7c0064cba7025a9b9adcc",
confirmations: 639041,
time: 1364140204,
blocktime: 1364140204,
};
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValueOnce({
json: jest.fn().mockResolvedValue({ result: mockRawTx }),
} as any);

const txid =
"0f3601a5da2f516fa9d3f80c9bf6e530f1afb0c90da73e8f8ad0630c5483afe5";
const result = await provider.getRawTransaction(txid);
expect(result).toEqual(mockRawTx);
expect(result).toBeDefined();
expect(result.txid).toBe(txid);
expect(result.vin[0].coinbase).toBe(
"03fc7903062f503253482f04ac204f510858029a11000003550d3363646164312f736c7573682f"
);
expect(result.vout[0].value).toBe(25.0626);
expect(result.vout[0].scriptPubKey.address).toBe(
"1MejoVXRvsmwyDpTpkw3VJ82NsjjT8SyEw"
);
});

it("should get block", async () => {
const mockBlockHash =
const blockHash =
"00000000000000d0dfd4c9d588d325dce4f32c1b31b7c0064cba7025a9b9adcc";
const mockBlockData = {
hash: "00000000000000d0dfd4c9d588d325dce4f32c1b31b7c0064cba7025a9b9adcc",
confirmations: 639148,
height: 227836,
version: 2,
versionHex: "00000002",
merkleroot:
"38a2518423d8ea76e716d1dc86d742b9e7f3febda7bf9a3e18bcd6c8ad55ff45",
time: 1364140204,
mediantime: 1364138296,
nonce: 30275792,
bits: "1a02816e",
difficulty: 6695826.282596251,
chainwork:
"000000000000000000000000000000000000000000000030f64e660f4b573ba8",
nTx: 100,
previousblockhash:
"00000000000001aa077d7aa84c532a4d69bdbff519609d1da0835261b7a74eb6",
nextblockhash:
"000000000000002579bc6db5a836a81d3a217b549721a0ef1facdf8f069ce0cb",
strippedsize: 39628,
size: 39628,
weight: 158512,
tx: [
"0f3601a5da2f516fa9d3f80c9bf6e530f1afb0c90da73e8f8ad0630c5483afe5",
// ... other transaction IDs ...
"13aa4bb9a1664275a481766b7fb9ea07c7e60b1a8adb5bdff08db8eccc614e53",
],
};

(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValueOnce({
json: jest.fn().mockResolvedValue({ result: mockBlockData }),
} as any);

const result = await provider.getBlock(mockBlockHash);
expect(result).toEqual(mockBlockData);
expect(result.hash).toBe(mockBlockHash);
expect(result.height).toBe(227836);
expect(result.version).toBe(2);
expect(result.merkleroot).toBe(
"38a2518423d8ea76e716d1dc86d742b9e7f3febda7bf9a3e18bcd6c8ad55ff45"
);
expect(result.time).toBe(1364140204);
expect(result.nonce).toBe(30275792);
expect(result.bits).toBe("1a02816e");
expect(result.difficulty).toBe(6695826.282596251);
expect(result.nTx).toBe(100);
expect(result.tx).toHaveLength(2);
expect(result.tx[0]).toBe(
"0f3601a5da2f516fa9d3f80c9bf6e530f1afb0c90da73e8f8ad0630c5483afe5"
);
expect(result.tx[1]).toBe(
"13aa4bb9a1664275a481766b7fb9ea07c7e60b1a8adb5bdff08db8eccc614e53"
);
const result = await provider.getBlock(blockHash);
expect(result).toBeDefined();
expect(result.hash).toBe(blockHash);
});
});
44 changes: 44 additions & 0 deletions src/tests/UtuProvider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { config } from "dotenv";
import { UtuProvider } from "@/UtuProvider";
import { BitcoinRpcProvider } from "@/BitcoinRpcProvider";

config();

describe("UtuProvider", () => {
let utuProvider: UtuProvider;
let bitcoinProvider: BitcoinRpcProvider;

beforeAll(() => {
bitcoinProvider = new BitcoinRpcProvider({
url: process.env.BITCOIN_RPC_URL || "",
username: process.env.BITCOIN_RPC_USERNAME || "",
password: process.env.BITCOIN_RPC_PASSWORD || "",
});
utuProvider = new UtuProvider(bitcoinProvider);
});

it("should get block height proof for block 800000", async () => {
const height = 800000;
const proof = await utuProvider.getBlockHeightProof(height);

expect(proof.blockHeader).toBeDefined();
expect(proof.rawCoinbaseTx).toBe(
"010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff1a0300350c0120130909092009092009102cda1492140000000000ffffffff02c09911260000000017a914c3f8f898ae5cab4f4c1d597ecb0f3a81a9b146c3870000000000000000266a24aa21a9ed9fbe517a588ccaca585a868f3cf19cb6897e3c26f3351361fb28ac8509e69a7e0120000000000000000000000000000000000000000000000000000000000000000000000000"
);
expect(proof.merkleProof).toEqual([
"b75ca3106ed100521aa50e3ec267a06431c6319538898b25e1b757a5736f5fb4",
"d41f5de48325e79070ccd3a23005f7a3b405f3ce1faa4df09f6d71770497e9d5",
"e966899d07c2e59033c073820b2f37a11532c1d11184373c4e558d65dac475e0",
"9f43ef264af1c3a4678d2bf5e60cddbd87b97618b1c80bd2b8a7f9b7f3baca68",
"4befb427613b7021015030bf67472af6c76f680fadc90bc4c267a9e5804d8948",
"bf61e05d4675710220c0b8dd669dcac9a1cbc3edb7ac64fc50410da9228333d5",
"c88892d93e8110f2ec82c41ac30e6a3c8dfe8cf062fefb4b5c09ee754d7ce42c",
"d4e7722bda133364a17b82990b16c3eb62f4a47d6aaae1c16bb0553806fcd3df",
"2cbc00355a2debbb8b90dd60ab0dd520699b40e4e4ad90d546864a6e4c5087f8",
"f2a33c753e9894eea7728206d927e830e946c4e13706275df14362398538e3db",
"8cc2c566df38c865e0aa6ddfd46d3440e99442a6d04d567323cbe53ffa470234",
"885cd4d205c35e05f8f738328166b9c65304583704162bcac8944b20690f696f",
"f6d90508da8aa581f7203f4899498c775ed4878544adcdef5e7b53a4ab691dd7",
]);
});
});

0 comments on commit 768ef11

Please sign in to comment.