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
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
encodeCallMsg,
getNetwork,
type HTTPSendRequester,
hexToBase64,
LAST_FINALIZED_BLOCK_NUMBER,
ok,
prepareReportRequest,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,102 @@
import { describe, expect, test } from 'bun:test'
import { EVMClient } from '@cre/sdk/cre'
import {
bigintToProtoBigInt,
blockNumber,
type EncodeCallMsgPayload,
EVM_DEFAULT_REPORT_ENCODER,
encodeCallMsg,
isChainSelectorSupported,
LAST_FINALIZED_BLOCK_NUMBER,
LATEST_BLOCK_NUMBER,
type ProtoBigInt,
prepareReportRequest,
protoBigIntToBigint,
} from './blockchain-helpers'

describe('blockchain-helpers', () => {
describe('bigintToProtoBigInt', () => {
test('should encode number', () => {
const result = bigintToProtoBigInt(123)
expect(result).toEqual({
absVal: Buffer.from([123]).toString('base64'),
})
})

test('should encode string', () => {
const result = bigintToProtoBigInt('9768438')
expect(result).toEqual({
absVal: Buffer.from([0x95, 0x0d, 0xf6]).toString('base64'),
})
})

test('should encode bigint', () => {
const result = bigintToProtoBigInt(9768438n)
expect(result).toEqual({
absVal: Buffer.from([0x95, 0x0d, 0xf6]).toString('base64'),
})
})

test('should encode zero', () => {
const result = bigintToProtoBigInt(0)
expect(result).toEqual({}) // Empty absVal is omitted by toJson
})

test('should take absolute value of negative numbers', () => {
const result = bigintToProtoBigInt(-123)
expect(result).toEqual({
absVal: Buffer.from([123]).toString('base64'),
})
})
})

describe('protoBigIntToBigint', () => {
test('should convert positive protobuf BigInt', () => {
const pb: ProtoBigInt = {
absVal: new Uint8Array([0x95, 0x0d, 0xf6]),
sign: 1n,
}
expect(protoBigIntToBigint(pb)).toBe(9768438n)
})

test('should convert negative protobuf BigInt', () => {
const pb: ProtoBigInt = {
absVal: new Uint8Array([123]),
sign: -1n,
}
expect(protoBigIntToBigint(pb)).toBe(-123n)
})

test('should convert zero', () => {
const pb: ProtoBigInt = {
absVal: new Uint8Array(),
sign: 0n,
}
expect(protoBigIntToBigint(pb)).toBe(0n)
})

test('should roundtrip with bigintToProtoBigInt (positive)', () => {
// Note: bigintToProtoBigInt returns JSON format, not ProtoBigInt
// This tests the conceptual roundtrip
const original = 9768438n
const proto: ProtoBigInt = {
absVal: new Uint8Array([0x95, 0x0d, 0xf6]),
sign: 1n,
}
expect(protoBigIntToBigint(proto)).toBe(original)
})
})

describe('blockNumber (alias)', () => {
test('should be an alias for bigintToProtoBigInt', () => {
expect(blockNumber).toBe(bigintToProtoBigInt)
})

test('should produce same result as bigintToProtoBigInt', () => {
expect(blockNumber(9768438n)).toEqual(bigintToProtoBigInt(9768438n))
})
})

describe('LAST_FINALIZED_BLOCK_NUMBER', () => {
test('should have correct structure for finalized block', () => {
expect(LAST_FINALIZED_BLOCK_NUMBER).toEqual({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,75 @@
import { create, toJson } from '@bufbuild/protobuf'
import type { CallMsgJson } from '@cre/generated/capabilities/blockchain/evm/v1alpha/client_pb'
import type { ReportRequestJson } from '@cre/generated/sdk/v1alpha/sdk_pb'
import { BigIntSchema, type BigInt as GeneratedBigInt } from '@cre/generated/values/v1/values_pb'
import { EVMClient } from '@cre/sdk/cre'
import { hexToBase64 } from '@cre/sdk/utils/hex-utils'
import { bigintToBytes, bytesToBigint, hexToBase64 } from '@cre/sdk/utils/hex-utils'
import type { Address, Hex } from 'viem'

/**
* Protobuf BigInt structure returned by SDK methods (e.g., headerByNumber).
* Uses Pick to extract just the data fields from the generated type.
*/
export type ProtoBigInt = Pick<GeneratedBigInt, 'absVal' | 'sign'>

/**
* Converts a native JS bigint to a protobuf BigInt JSON representation.
* Use this when passing bigint values to SDK methods.
*
* @example
* const response = evmClient.callContract(runtime, {
* call: encodeCallMsg({...}),
* blockNumber: bigintToProtoBigInt(9768438n)
* }).result()
*
* @param n - The native bigint, number, or string value.
* @returns The protobuf BigInt JSON representation.
*/
export const bigintToProtoBigInt = (n: number | bigint | string) => {
const val = BigInt(n)
const abs = val < 0n ? -val : val

const msg = create(BigIntSchema, {
absVal: bigintToBytes(abs),
})

return toJson(BigIntSchema, msg)
}

/**
* Converts a protobuf BigInt to a native JS bigint.
* Use this when extracting bigint values from SDK responses.
*
* @example
* const latestHeader = evmClient.headerByNumber(runtime, {}).result()
* const latestBlockNum = protoBigIntToBigint(latestHeader.header.blockNumber!)
* const customBlock = latestBlockNum - 500n
*
* @param pb - The protobuf BigInt object with absVal and sign fields.
* @returns The native JS bigint value.
*/
export const protoBigIntToBigint = (pb: ProtoBigInt): bigint => {
const result = bytesToBigint(pb.absVal)
return pb.sign < 0n ? -result : result
}

/**
* Convenience alias for `bigintToProtoBigInt`.
* Creates a block number object for EVM capability requests.
*
* @param n - The block number.
* @returns The protobuf BigInt JSON representation.
*/
export const blockNumber = bigintToProtoBigInt

/**
* EVM Capability Helper.
*
* `CallContractRequest`, used by EVM capability, has arguments for reading a contract as specified in the call message at a block height defined by blockNumber.
* That blockNumber can be:
* - the latest mined block (`LATEST_BLOCK_NUMBER`) (default)
* - the last finalized block (`LAST_FINALIZED_BLOCK_NUMBER`)
* - a specific block number (use `blockNumber(n)` or `bigintToProtoBigInt(n)`)
*
* Using this constant will indicate that the call should be executed at the last finalized block.
*/
Expand Down
54 changes: 53 additions & 1 deletion packages/cre-sdk/src/sdk/utils/hex-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'bun:test'
import { bytesToHex, hexToBase64, hexToBytes } from './hex-utils'
import { bigintToBytes, bytesToBigint, bytesToHex, hexToBase64, hexToBytes } from './hex-utils'

describe('hexToBytes', () => {
describe('happy paths', () => {
Expand Down Expand Up @@ -225,3 +225,55 @@ describe('hexToBase64', () => {
})
})
})

describe('bigintToBytes', () => {
it('returns empty array for zero', () => {
expect(bigintToBytes(0n)).toEqual(new Uint8Array())
})

it('converts small numbers', () => {
expect(bigintToBytes(123n)).toEqual(new Uint8Array([123]))
})

it('converts numbers requiring padding', () => {
// 15 = 0xf (single hex char needs padding to 0x0f)
expect(bigintToBytes(15n)).toEqual(new Uint8Array([15]))
})

it('converts realistic block numbers', () => {
// 9768438 = 0x950df6
expect(bigintToBytes(9768438n)).toEqual(new Uint8Array([0x95, 0x0d, 0xf6]))
})

it('handles large block numbers', () => {
// 21000000 = 0x1406f40
expect(bigintToBytes(21000000n)).toEqual(new Uint8Array([0x01, 0x40, 0x6f, 0x40]))
})
})

describe('bytesToBigint', () => {
it('returns 0n for empty array', () => {
expect(bytesToBigint(new Uint8Array())).toBe(0n)
})

it('converts small numbers', () => {
expect(bytesToBigint(new Uint8Array([123]))).toBe(123n)
})

it('converts realistic block numbers', () => {
// 9768438 = 0x950df6
expect(bytesToBigint(new Uint8Array([0x95, 0x0d, 0xf6]))).toBe(9768438n)
})

it('handles large block numbers', () => {
// 21000000 = 0x1406f40
expect(bytesToBigint(new Uint8Array([0x01, 0x40, 0x6f, 0x40]))).toBe(21000000n)
})

it('roundtrips with bigintToBytes', () => {
const values = [0n, 1n, 123n, 9768438n, 21000000n, 2n ** 64n]
for (const val of values) {
expect(bytesToBigint(bigintToBytes(val))).toBe(val)
}
})
})
31 changes: 31 additions & 0 deletions packages/cre-sdk/src/sdk/utils/hex-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,34 @@ export const hexToBase64 = (hex: string): string => {
const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex
return Buffer.from(cleanHex, 'hex').toString('base64')
}

/**
* Convert a bigint to a Uint8Array (big-endian byte order).
* Returns empty array for 0n.
*/
export const bigintToBytes = (n: bigint): Uint8Array => {
if (n === 0n) {
return new Uint8Array()
}
const hex = n.toString(16)
return Buffer.from(hex.padStart(hex.length + (hex.length % 2), '0'), 'hex')
}

/**
* Convert a Uint8Array (big-endian byte order) to a native bigint.
* Returns 0n for empty array.
*
* This is the inverse of `bigintToBytes` and is useful for converting
* protobuf BigInt's `absVal` field back to a native JS bigint.
*
* @example
* // Convert protobuf BigInt from headerByNumber response
* const latestBlockNum = bytesToBigint(latestHeader.header.blockNumber!.absVal)
*/
export const bytesToBigint = (bytes: Uint8Array): bigint => {
let result = 0n
for (const byte of bytes) {
result = (result << 8n) + BigInt(byte)
}
return result
}