diff --git a/packages/cre-sdk-examples/src/workflows/on-chain-write/index.ts b/packages/cre-sdk-examples/src/workflows/on-chain-write/index.ts index 51c4d18..09c3de6 100644 --- a/packages/cre-sdk-examples/src/workflows/on-chain-write/index.ts +++ b/packages/cre-sdk-examples/src/workflows/on-chain-write/index.ts @@ -5,7 +5,6 @@ import { encodeCallMsg, getNetwork, type HTTPSendRequester, - hexToBase64, LAST_FINALIZED_BLOCK_NUMBER, ok, prepareReportRequest, diff --git a/packages/cre-sdk/src/sdk/utils/capabilities/blockchain/blockchain-helpers.test.ts b/packages/cre-sdk/src/sdk/utils/capabilities/blockchain/blockchain-helpers.test.ts index 96e9c9c..09dbce1 100644 --- a/packages/cre-sdk/src/sdk/utils/capabilities/blockchain/blockchain-helpers.test.ts +++ b/packages/cre-sdk/src/sdk/utils/capabilities/blockchain/blockchain-helpers.test.ts @@ -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({ diff --git a/packages/cre-sdk/src/sdk/utils/capabilities/blockchain/blockchain-helpers.ts b/packages/cre-sdk/src/sdk/utils/capabilities/blockchain/blockchain-helpers.ts index 322e7d3..e33f266 100644 --- a/packages/cre-sdk/src/sdk/utils/capabilities/blockchain/blockchain-helpers.ts +++ b/packages/cre-sdk/src/sdk/utils/capabilities/blockchain/blockchain-helpers.ts @@ -1,9 +1,67 @@ +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 + +/** + * 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. * @@ -11,6 +69,7 @@ import type { Address, Hex } from 'viem' * 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. */ diff --git a/packages/cre-sdk/src/sdk/utils/hex-utils.test.ts b/packages/cre-sdk/src/sdk/utils/hex-utils.test.ts index fb8039b..2eb2e95 100644 --- a/packages/cre-sdk/src/sdk/utils/hex-utils.test.ts +++ b/packages/cre-sdk/src/sdk/utils/hex-utils.test.ts @@ -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', () => { @@ -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) + } + }) +}) diff --git a/packages/cre-sdk/src/sdk/utils/hex-utils.ts b/packages/cre-sdk/src/sdk/utils/hex-utils.ts index 3b43d29..8076b1d 100644 --- a/packages/cre-sdk/src/sdk/utils/hex-utils.ts +++ b/packages/cre-sdk/src/sdk/utils/hex-utils.ts @@ -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 +}