@tetherto/wdk-wallet-evm
Version:
A simple package to manage BIP-32 wallets for evm blockchains.
197 lines (150 loc) • 6 kB
JavaScript
// Copyright 2024 Tether Operations Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {
assert, assertArgument, assertPrivate, BaseWallet, computeHmac, dataSlice, defineProperties,
getBytes, getNumber, hexlify, isBytesLike, ripemd160, sha256
} from 'ethers'
import * as secp256k1 from '@noble/secp256k1'
import MemorySafeSigningKey from './signing-key.js'
const MasterSecret = new Uint8Array([66, 105, 116, 99, 111, 105, 110, 32, 115, 101, 101, 100])
const HardenedBit = 0x80000000
const _guard = { }
function serI (index, chainCode, publicKey, privateKeyBuffer) {
const data = new Uint8Array(37)
if (index & HardenedBit) {
assert(privateKeyBuffer != null, 'cannot derive child of neutered node', 'UNSUPPORTED_OPERATION', {
operation: 'deriveChild'
})
data.set(getBytes(privateKeyBuffer), 1)
} else {
data.set(getBytes(publicKey))
}
for (let i = 24; i >= 0; i -= 8) { data[33 + (i >> 3)] = ((index >> (24 - i)) & 0xff) }
const I = getBytes(computeHmac('sha512', chainCode, data))
return { IL: I.slice(0, 32), IR: I.slice(32) }
}
function derivePath (node, path) {
const components = path.split('/')
assertArgument(components.length > 0, 'invalid path', 'path', path)
if (components[0] === 'm') {
assertArgument(node.depth === 0, `cannot derive root path (i.e. path starting with "m/") for a node at non-zero depth ${node.depth}`, 'path', path)
components.shift()
}
let result = node
for (let i = 0; i < components.length; i++) {
const component = components[i]
if (component.match(/^[0-9]+'$/)) {
const index = parseInt(component.substring(0, component.length - 1))
assertArgument(index < HardenedBit, 'invalid path index', `path[${i}]`, component)
result = result.deriveChild(HardenedBit + index)
} else if (component.match(/^[0-9]+$/)) {
const index = parseInt(component)
assertArgument(index < HardenedBit, 'invalid path index', `path[${i}]`, component)
result = result.deriveChild(index)
} else {
assertArgument(false, 'invalid path component', `path[${i}]`, component)
}
}
return result
}
function addToPrivateKey (privateKey, x) {
let carry = 0
for (let i = 31; i >= 0; i--) {
const sum = privateKey[i] + x[i] + carry
privateKey[i] = sum & 0xff
carry = sum >> 8
}
return carry > 0
}
function subtractCurveOrderFromPrivateKey (privateKey) {
let carry = 0
for (let i = 31; i >= 0; i--) {
const curveOrderByte = Number((secp256k1.CURVE.n >> BigInt(8 * (31 - i))) & 0xffn)
const diff = privateKey[i] - curveOrderByte - carry
privateKey[i] = diff < 0 ? diff + 256 : diff
carry = diff < 0 ? 1 : 0
}
}
function compareWithCurveOrder (buffer, offset = 0) {
for (let i = 0; i < 32; i++) {
const curveOrderByte = Number((secp256k1.CURVE.n >> BigInt(8 * (31 - i))) & 0xffn)
if (buffer[offset + i] > curveOrderByte) return 1
if (buffer[offset + i] < curveOrderByte) return -1
}
return 0
}
/** @internal */
export default class MemorySafeHDNodeWallet extends BaseWallet {
constructor (guard, signingKey, parentFingerprint, chainCode, path, index, depth, mnemonic, provider) {
super(signingKey, provider)
assertPrivate(guard, _guard, 'MemorySafeHDNodeWallet')
defineProperties(this, { publicKey: signingKey.compressedPublicKey })
const fingerprint = dataSlice(ripemd160(sha256(this.publicKey)), 0, 4)
defineProperties(this, {
parentFingerprint,
fingerprint,
chainCode,
path,
index,
depth
})
defineProperties(this, { mnemonic })
}
connect (provider) {
return new MemorySafeHDNodeWallet(_guard, this.signingKey, this.parentFingerprint,
this.chainCode, this.path, this.index, this.depth, this.mnemonic, provider)
}
get privateKeyBuffer () {
return this.signingKey.privateKeyBuffer
}
get publicKeyBuffer () {
return this.signingKey.publicKeyBuffer
}
deriveChild (_index) {
const index = getNumber(_index, 'index')
assertArgument(index <= 0xffffffff, 'invalid index', 'index', index)
let path = this.path
if (path) {
path += '/' + (index & ~HardenedBit)
if (index & HardenedBit) { path += "'" }
}
const { IR, IL } = serI(index, this.chainCode, this.publicKey, this.privateKeyBuffer)
const overflow = addToPrivateKey(this.privateKeyBuffer, IL)
if (overflow || compareWithCurveOrder(this.privateKeyBuffer) >= 0) {
subtractCurveOrderFromPrivateKey(this.privateKeyBuffer)
}
const ki = new MemorySafeSigningKey(this.privateKeyBuffer)
return new MemorySafeHDNodeWallet(_guard, ki, this.fingerprint, hexlify(IR),
path, index, this.depth + 1, this.mnemonic, this.provider)
}
derivePath (path) {
return derivePath(this, path)
}
dispose () {
this.signingKey.dispose()
}
static fromSeed (seed) {
return MemorySafeHDNodeWallet._fromSeed(seed, null)
}
static _fromSeed (_seed, mnemonic) {
assertArgument(isBytesLike(_seed), 'invalid seed', 'seed', '[REDACTED]')
const seed = getBytes(_seed, 'seed')
assertArgument(seed.length >= 16 && seed.length <= 64, 'invalid seed', 'seed', '[REDACTED]')
const I = getBytes(computeHmac('sha512', MasterSecret, seed))
const signingKey = new MemorySafeSigningKey(I.slice(0, 32))
return new MemorySafeHDNodeWallet(_guard, signingKey, '0x00000000', hexlify(I.slice(32)),
'm', 0, 0, mnemonic, null)
}
}