diff --git a/packages/cketh/src/index.ts b/packages/cketh/src/index.ts index b7ba3e0a..1874f506 100644 --- a/packages/cketh/src/index.ts +++ b/packages/cketh/src/index.ts @@ -4,3 +4,4 @@ export type { } from "../candid/minter"; export * from "./errors/minter.errors"; export { CkETHMinterCanister } from "./minter.canister"; +export * from "./utils/minter.utils"; diff --git a/packages/cketh/src/utils/minter.utils.spec.ts b/packages/cketh/src/utils/minter.utils.spec.ts new file mode 100644 index 00000000..98d60c41 --- /dev/null +++ b/packages/cketh/src/utils/minter.utils.spec.ts @@ -0,0 +1,36 @@ +import { Principal } from "@dfinity/principal"; +import { encodePrincipalToEthAddress } from "./minter.utils"; + +describe("minter-utils", () => { + const mockPrincipalExample1 = Principal.from( + "esd5n-wtdqq-kimdw-qrxjr-7luer-zjhez-hsauj-pu5ox-2y6ov-g53g4-xqe", + ); + const ethExample1 = + "0x1d638414860ed08dd31fae848e527264f20512fa75d7d63cea9bbb372f020000"; + + const mockPrincipalExample2 = Principal.from( + "auycm-ouxjf-el73p-7cfpm-73w3q-fd7xc-ubevq-ea4vt-vpe2n-rs7e5-qqe", + ); + const ethExample2 = + "0x1d974948bfedff115ecfeedb8147fb8a8125604072b3abc9a6c65f2761020000"; + + const mockPrincipalExample3 = Principal.from( + "ouoed-xoejt-4ssko-nf4tu-w3wbu-x2lz7-qfili-67oyv-kz4kf-nmo7q-yqe", + ); + const ethExample3 = + "0x1dc44cf92929cd2f274b6ec1a5f4bcfe0542d1efbb155678a2b58efc31020000"; + + it("should encode principal into fixed 32-byte representation suitable for calling Ethereum smart contracts", () => { + expect(encodePrincipalToEthAddress(mockPrincipalExample1)).toEqual( + ethExample1, + ); + + expect(encodePrincipalToEthAddress(mockPrincipalExample2)).toEqual( + ethExample2, + ); + + expect(encodePrincipalToEthAddress(mockPrincipalExample3)).toEqual( + ethExample3, + ); + }); +}); diff --git a/packages/cketh/src/utils/minter.utils.ts b/packages/cketh/src/utils/minter.utils.ts new file mode 100644 index 00000000..4648774a --- /dev/null +++ b/packages/cketh/src/utils/minter.utils.ts @@ -0,0 +1,45 @@ +import type { Principal } from "@dfinity/principal"; +import { decodeBase32 } from "@dfinity/utils"; + +/** + * Encode a principal to a byte array as Ethereum data hex (staring with 0x). + * Such a conversion is required to deposit ETH to the ckETH helper contract. + * + * Code adapted from the ckETH minter dashboard JS function: https://github.com/dfinity/ic/blob/master/rs/ethereum/cketh/minter/templates/principal_to_bytes.js + * + * @param principal The principal to encode into a fixed 32-byte representation suitable for calling Ethereum smart contracts. + */ +export const encodePrincipalToEthAddress = (principal: Principal): string => { + const rawBytes = decodeBase32(principal.toText().replace(/-/g, "")); + return bytes32Encode(rawBytes.slice(4)); +}; + +/** + * Appends a hex representation of a number to string. + * @param {string} s A string to append the hex to. + * @param {number} b A byte. + * @return {string} An updated string. + */ +const appendHexByte = (s: string, b: number): string => { + s += ((b >> 4) & 0x0f).toString(16); + s += (b & 0x0f).toString(16); + return s; +}; + +/** + * Encodes a byte array as Ethereum data hex (staring with 0x). + * @param {Array} bytes A byte array. + * @return {string} A hex string. + */ +const bytes32Encode = (bytes: Uint8Array): string => { + const n = bytes.length; + let s = "0x"; + s = appendHexByte(s, n); + for (let i = 0; i < bytes.length; i++) { + s = appendHexByte(s, bytes[i]); + } + for (let i = 0; i < 31 - bytes.length; i++) { + s += "00"; + } + return s; +}; diff --git a/scripts/docs.js b/scripts/docs.js index ab6cb143..b0e3675b 100644 --- a/scripts/docs.js +++ b/scripts/docs.js @@ -40,7 +40,10 @@ const ckBTCInputFiles = [ "./packages/ckbtc/src/utils/btc.utils.ts", ]; -const ckETHInputFiles = ["./packages/cketh/src/minter.canister.ts"]; +const ckETHInputFiles = [ + "./packages/cketh/src/minter.canister.ts", + "./packages/ledger-icrc/src/utils/minter.utils.ts", +]; const icMgmtInputFiles = [ "./packages/ic-management/src/ic-management.canister.ts",