Skip to content

Commit

Permalink
ETH V4 Signing (#329)
Browse files Browse the repository at this point in the history
Adding ETH V4 signing capability
  • Loading branch information
IndiaJonathan authored Aug 20, 2024
1 parent 846c48a commit ce19dd8
Show file tree
Hide file tree
Showing 26 changed files with 3,669 additions and 31 deletions.
1 change: 0 additions & 1 deletion chain-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
"format": "prettier --config ../.prettierrc 'src/**/*.ts' --write",
"test": "jest"
},
"devDependencies": {},
"nyc": {
"extension": [
".ts",
Expand Down
194 changes: 194 additions & 0 deletions chain-api/src/ethers/address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { keccak256 } from "js-sha3";

import { assertArgument } from "./errors";
import { getBytes } from "./utils/data";

const BN_0 = BigInt(0);
const BN_36 = BigInt(36);

function getChecksumAddress(address: string): string {
address = address.toLowerCase();

const chars = address.substring(2).split("");

const expanded = new Uint8Array(40);
for (let i = 0; i < 40; i++) {
expanded[i] = chars[i].charCodeAt(0);
}

const hashed = getBytes(keccak256(expanded));

for (let i = 0; i < 40; i += 2) {
if (hashed[i >> 1] >> 4 >= 8) {
chars[i] = chars[i].toUpperCase();
}
if ((hashed[i >> 1] & 0x0f) >= 8) {
chars[i + 1] = chars[i + 1].toUpperCase();
}
}

return "0x" + chars.join("");
}

// See: https://en.wikipedia.org/wiki/International_Bank_Account_Number

// Create lookup table
const ibanLookup: { [character: string]: string } = {};
for (let i = 0; i < 10; i++) {
ibanLookup[String(i)] = String(i);
}
for (let i = 0; i < 26; i++) {
ibanLookup[String.fromCharCode(65 + i)] = String(10 + i);
}

// How many decimal digits can we process? (for 64-bit float, this is 15)
// i.e. Math.floor(Math.log10(Number.MAX_SAFE_INTEGER));
const safeDigits = 15;

function ibanChecksum(address: string): string {
address = address.toUpperCase();
address = address.substring(4) + address.substring(0, 2) + "00";

let expanded = address
.split("")
.map((c) => {
return ibanLookup[c];
})
.join("");

// Javascript can handle integers safely up to 15 (decimal) digits
while (expanded.length >= safeDigits) {
const block = expanded.substring(0, safeDigits);
expanded = (parseInt(block, 10) % 97) + expanded.substring(block.length);
}

let checksum = String(98 - (parseInt(expanded, 10) % 97));
while (checksum.length < 2) {
checksum = "0" + checksum;
}

return checksum;
}

const Base36 = (function () {
const result: Record<string, bigint> = {};
for (let i = 0; i < 36; i++) {
const key = "0123456789abcdefghijklmnopqrstuvwxyz"[i];
result[key] = BigInt(i);
}
return result;
})();

function fromBase36(value: string): bigint {
value = value.toLowerCase();

let result = BN_0;
for (let i = 0; i < value.length; i++) {
result = result * BN_36 + Base36[value[i]];
}
return result;
}

/**
* Returns a normalized and checksumed address for %%address%%.
* This accepts non-checksum addresses, checksum addresses and
* [[getIcapAddress]] formats.
*
* The checksum in Ethereum uses the capitalization (upper-case
* vs lower-case) of the characters within an address to encode
* its checksum, which offers, on average, a checksum of 15-bits.
*
* If %%address%% contains both upper-case and lower-case, it is
* assumed to already be a checksum address and its checksum is
* validated, and if the address fails its expected checksum an
* error is thrown.
*
* If you wish the checksum of %%address%% to be ignore, it should
* be converted to lower-case (i.e. ``.toLowercase()``) before
* being passed in. This should be a very rare situation though,
* that you wish to bypass the safegaurds in place to protect
* against an address that has been incorrectly copied from another
* source.
*
* @example:
* // Adds the checksum (via upper-casing specific letters)
* getAddress("0x8ba1f109551bd432803012645ac136ddd64dba72")
* //_result:
*
* // Converts ICAP address and adds checksum
* getAddress("XE65GB6LDNXYOFTX0NSV3FUWKOWIXAMJK36");
* //_result:
*
* // Throws an error if an address contains mixed case,
* // but the checksum fails
* getAddress("0x8Ba1f109551bD432803012645Ac136ddd64DBA72")
* //_error:
*/
export function getAddress(address: string): string {
assertArgument(typeof address === "string", "invalid address", "address", address);

if (address.match(/^(0x)?[0-9a-fA-F]{40}$/)) {
// Missing the 0x prefix
if (!address.startsWith("0x")) {
address = "0x" + address;
}

const result = getChecksumAddress(address);

// It is a checksummed address with a bad checksum
assertArgument(
!address.match(/([A-F].*[a-f])|([a-f].*[A-F])/) || result === address,
"bad address checksum",
"address",
address
);

return result;
}

// Maybe ICAP? (we only support direct mode)
if (address.match(/^XE[0-9]{2}[0-9A-Za-z]{30,31}$/)) {
// It is an ICAP address with a bad checksum
assertArgument(
address.substring(2, 4) === ibanChecksum(address),
"bad icap checksum",
"address",
address
);

let result = fromBase36(address.substring(4)).toString(16);
while (result.length < 40) {
result = "0" + result;
}
return getChecksumAddress("0x" + result);
}

assertArgument(false, "invalid address", "address", address);
}

/**
* The [ICAP Address format](link-icap) format is an early checksum
* format which attempts to be compatible with the banking
* industry [IBAN format](link-wiki-iban) for bank accounts.
*
* It is no longer common or a recommended format.
*
* @example:
* getIcapAddress("0x8ba1f109551bd432803012645ac136ddd64dba72");
* //_result:
*
* getIcapAddress("XE65GB6LDNXYOFTX0NSV3FUWKOWIXAMJK36");
* //_result:
*
* // Throws an error if the ICAP checksum is wrong
* getIcapAddress("XE65GB6LDNXYOFTX0NSV3FUWKOWIXAMJK37");
* //_error:
*/
export function getIcapAddress(address: string): string {
//let base36 = _base16To36(getAddress(address).substring(2)).toUpperCase();
let base36 = BigInt(getAddress(address)).toString(36).toUpperCase();
while (base36.length < 30) {
base36 = "0" + base36;
}
return "XE" + ibanChecksum("XE00" + base36) + base36;
}
6 changes: 6 additions & 0 deletions chain-api/src/ethers/constants/hashes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* A constant for the zero hash.
*
* (**i.e.** ``"0x0000000000000000000000000000000000000000000000000000000000000000"``)
*/
export const ZeroHash = "0x0000000000000000000000000000000000000000000000000000000000000000";
60 changes: 60 additions & 0 deletions chain-api/src/ethers/crypto/keccak_256.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Cryptographic hashing functions
*
* @_subsection: api/crypto:Hash Functions [about-crypto-hashing]
*/

/*
I changed this to save us a bit of space rather than import another keccak lib
import { keccak_256 } from "@noble/hashes/sha3";
*/
import { keccak256 as keccak256_js_sha3 } from "js-sha3";

import { BytesLike, getBytes, hexlify } from "../utils/data";

let locked = false;

const _keccak256 = function (data: Uint8Array): Uint8Array {
return new Uint8Array(keccak256_js_sha3.arrayBuffer(data));
};

let __keccak256: (data: Uint8Array) => BytesLike = _keccak256;

/**
* Compute the cryptographic KECCAK256 hash of %%data%%.
*
* The %%data%% **m
* ust** be a data representation, to compute the
* hash of UTF-8 data use the [[id]] function.
*
* @returns DataHexstring
* @example:
* keccak256("0x")
* //_result:
*
* keccak256("0x1337")
* //_result:
*
* keccak256(new Uint8Array([ 0x13, 0x37 ]))
* //_result:
*
* // Strings are assumed to be DataHexString, otherwise it will
* // throw. To hash UTF-8 data, see the note above.
* keccak256("Hello World")
* //_error:
*/
export function keccak256(_data: BytesLike): string {
const data = getBytes(_data, "data");
return hexlify(__keccak256(data));
}
keccak256._ = _keccak256;
keccak256.lock = function (): void {
locked = true;
};
keccak256.register = function (func: (data: Uint8Array) => BytesLike) {
if (locked) {
throw new TypeError("keccak256 is locked");
}
__keccak256 = func;
};
Object.freeze(keccak256);
Loading

0 comments on commit ce19dd8

Please sign in to comment.