diff --git a/packages/sdk/wallets/wallet-base/package.json b/packages/sdk/wallets/wallet-base/package.json index 57bd947e6..580274608 100644 --- a/packages/sdk/wallets/wallet-base/package.json +++ b/packages/sdk/wallets/wallet-base/package.json @@ -28,8 +28,10 @@ "@celo/base": "^6.0.0", "@celo/connect": "^5.1.2", "@celo/utils": "^6.0.0", - "@ethereumjs/rlp": "^5.0.0", + "@ethereumjs/rlp": "^5.0.2", "@ethereumjs/util": "8.0.5", + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3", "@types/debug": "^4.1.5", "bignumber.js": "^9.0.0", "debug": "^4.1.1", diff --git a/packages/sdk/wallets/wallet-base/src/signing-utils.ts b/packages/sdk/wallets/wallet-base/src/signing-utils.ts index b7ad953d8..a31cf451f 100644 --- a/packages/sdk/wallets/wallet-base/src/signing-utils.ts +++ b/packages/sdk/wallets/wallet-base/src/signing-utils.ts @@ -15,25 +15,20 @@ import { } from '@celo/connect/lib/utils/formatter' import { EIP712TypedData, generateTypedDataHash } from '@celo/utils/lib/sign-typed-data-utils' import { parseSignatureWithoutPrefix } from '@celo/utils/lib/signatureUtils' +import { publicKeyToAddress } from '@celo/utils/src/address' // @ts-ignore-next-line import * as ethUtil from '@ethereumjs/util' +import { secp256k1 } from '@noble/curves/secp256k1' import { keccak_256 } from '@noble/hashes/sha3' import { hexToBytes } from '@noble/hashes/utils' import debugFactory from 'debug' // @ts-ignore-next-line eth-lib types not found -import { account as Account, bytes as Bytes, RLP } from 'eth-lib' +import { bytes as Bytes, RLP } from 'eth-lib' +// TODO: replace by @ethereumjs/rlp import Web3 from 'web3' // TODO try to do this without web3 direct import Accounts from 'web3-eth-accounts' -const { - Address, - ecrecover, - fromRpcSig, - hashPersonalMessage, - pubToAddress, - toBuffer, - toChecksumAddress, -} = ethUtil +const { ecrecover, fromRpcSig, hashPersonalMessage, toBuffer } = ethUtil const debug = debugFactory('wallet-base:tx:sign') // Original code taken from @@ -57,7 +52,7 @@ export function chainIdTransformationForSigning(chainId: number): number { return chainId * 2 + 35 } -export function getHashFromEncoded(rlpEncode: string): string { +export function getHashFromEncoded(rlpEncode: string): Hex { const rlpBytes = hexToBytes(trimLeading0x(rlpEncode)) const hash = Buffer.from(keccak_256(rlpBytes)) return `0x${hash.toString('hex')}` @@ -469,13 +464,17 @@ export function recoverTransaction(rawTx: string): [CeloTx, string] { data: rawValues[8], chainId, } - const { r, v, s } = extractSignatureFromDecoded(rawValues) - const signature = Account.encodeSignature([v, r, s]) + const { r, v: _v, s } = extractSignatureFromDecoded(rawValues) + let v = parseInt(_v || '0x0', 16) + const signature = new secp256k1.Signature(BigInt(r), BigInt(s)).addRecoveryBit( + v - chainIdTransformationForSigning(chainId) + ) const extraData = recovery < 35 ? [] : [chainId, '0x', '0x'] const signingData = rawValues.slice(0, 9).concat(extraData) const signingDataHex = RLP.encode(signingData) - const signer = Account.recover(getHashFromEncoded(signingDataHex), signature) - return [celoTx, signer] + const signingDataHash = getHashFromEncoded(signingDataHex) + const publicKey = signature.recoverPublicKey(trimLeading0x(signingDataHash)).toHex(false) + return [celoTx, publicKeyToAddress(publicKey)] } } @@ -505,7 +504,8 @@ export function getSignerFromTxEIP2718TX(serializedTransaction: string): string transactionArray, determineTXType(serializedTransaction) ) - return toChecksumAddress(Address.fromPublicKey(signer).toString()) + + return publicKeyToAddress(signer.toString('hex')) } function determineTXType(serializedTransaction: string): TransactionTypes { @@ -670,8 +670,8 @@ export function recoverMessageSigner(signingDataHex: string, signedData: string) const signature = fromRpcSig(signedData) const publicKey = ecrecover(msgHashBuff, signature.v, signature.r, signature.s) - const address = pubToAddress(publicKey, true) - return ensureLeading0x(address.toString('hex')) + const address = publicKeyToAddress(publicKey.toString('hex')) + return ensureLeading0x(address) } export function verifyEIP712TypedDataSigner( @@ -696,12 +696,37 @@ export function verifySignatureWithoutPrefix( } } -export function decodeSig(sig: any) { - const [v, r, s] = Account.decodeSignature(sig) +function bigintToPaddedHex(n: bigint, length: number): string { + const hex = n.toString(16) + if (hex.length >= length) { + return hex + } + const padded = new Array(length).fill('0').join('') + hex + return padded.slice(-length) +} + +export function decodeSig(sig: Hex | ReturnType, addToV = 0) { + const { recovery, r, s } = typeof sig === 'string' ? secp256k1.Signature.fromCompact(sig) : sig return { - v: parseInt(v, 16), - r: toBuffer(r) as Buffer, - s: toBuffer(s) as Buffer, + v: recovery! + addToV, + r: Buffer.from(bigintToPaddedHex(r, 64), 'hex'), + s: Buffer.from(bigintToPaddedHex(s, 64), 'hex'), } + // const [v, r, s] = Account.decodeSignature(sig) + + // return { + // v: parseInt(v, 16), + // r: toBuffer(r) as Buffer, + // s: toBuffer(s) as Buffer, + // } +} + +export function signTransaction(hash: Hex, privateKey: Hex, addToV = 0) { + const signature = secp256k1.sign( + trimLeading0x(hash), + hexToBytes(trimLeading0x(privateKey)), + { lowS: true } // canonical:true + ) + return decodeSig(signature, addToV) } diff --git a/packages/sdk/wallets/wallet-ledger/package.json b/packages/sdk/wallets/wallet-ledger/package.json index 46d5c8a00..cc91a7533 100644 --- a/packages/sdk/wallets/wallet-ledger/package.json +++ b/packages/sdk/wallets/wallet-ledger/package.json @@ -31,11 +31,11 @@ "@ledgerhq/errors": "^5.50.0", "@ledgerhq/hw-app-eth": "~5.11.0", "@ledgerhq/hw-transport": "~5.11.0", - "debug": "^4.1.1", - "eth-lib": "^0.2.8" + "debug": "^4.1.1" }, "devDependencies": { "@ledgerhq/hw-transport-node-hid": "^6.27.4", + "@noble/hashes": "^1.3.3", "web3": "1.10.0" }, "engines": { diff --git a/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts b/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts index 4ff801529..fb6adad3a 100644 --- a/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts +++ b/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts @@ -7,6 +7,7 @@ import debugFactory from 'debug' import { transportErrorFriendlyMessage } from './ledger-utils' import { AddressValidation } from './ledger-wallet' import { compareLedgerAppVersions, tokenInfoByAddressAndChainId } from './tokens' +import { ILedger } from './types' const debug = debugFactory('kit:wallet:ledger') const CELO_APP_ACCEPTS_CONTRACT_DATA_FROM_VERSION = '1.0.2' @@ -15,14 +16,14 @@ const CELO_APP_ACCEPTS_CONTRACT_DATA_FROM_VERSION = '1.0.2' * Signs the EVM transaction with a Ledger device */ export class LedgerSigner implements Signer { - private ledger: any + private ledger: ILedger private derivationPath: string private validated: boolean = false private ledgerAddressValidation: AddressValidation private appConfiguration: { arbitraryDataEnabled: number; version: string } constructor( - ledger: any, + ledger: ILedger, derivationPath: string, ledgerAddressValidation: AddressValidation, appConfiguration: { arbitraryDataEnabled: number; version: string } = { @@ -57,9 +58,8 @@ export class LedgerSigner implements Signer { if (rv !== addToV && (rv & addToV) !== rv) { addToV += 1 // add signature v bit. } - signature.v = addToV.toString(10) return { - v: signature.v, + v: addToV, r: ethUtil.toBuffer(ensureLeading0x(signature.r)) as Buffer, s: ethUtil.toBuffer(ensureLeading0x(signature.s)) as Buffer, } @@ -88,7 +88,7 @@ export class LedgerSigner implements Signer { ) return { - v: signature.v, + v: parseInt(signature.v, 16), r: ethUtil.toBuffer(ensureLeading0x(signature.r)) as Buffer, s: ethUtil.toBuffer(ensureLeading0x(signature.s)) as Buffer, } @@ -116,7 +116,7 @@ export class LedgerSigner implements Signer { ) return { - v: parseInt(sig.v, 10), + v: parseInt(sig.v, 16), r: ethUtil.toBuffer(ensureLeading0x(sig.r)) as Buffer, s: ethUtil.toBuffer(ensureLeading0x(sig.s)) as Buffer, } diff --git a/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.test.ts b/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.test.ts index 2b2a9108a..d12ce69f2 100644 --- a/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.test.ts +++ b/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.test.ts @@ -1,20 +1,20 @@ import { ensureLeading0x, normalizeAddressWith0x, trimLeading0x } from '@celo/base/lib/address' -import { CeloTx, EncodedTransaction } from '@celo/connect' +import { CeloTx, EncodedTransaction, Hex } from '@celo/connect' import { privateKeyToAddress } from '@celo/utils/lib/address' import { verifySignature } from '@celo/utils/lib/signatureUtils' import { chainIdTransformationForSigning, getHashFromEncoded, recoverTransaction, + signTransaction, verifyEIP712TypedDataSigner, } from '@celo/wallet-base' import * as ethUtil from '@ethereumjs/util' import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' import { keccak_256 } from '@noble/hashes/sha3' -// @ts-ignore-next-line eth-lib types not found -import { account as Account } from 'eth-lib' import Web3 from 'web3' import { AddressValidation, LedgerWallet } from './ledger-wallet' +import { ILedger } from './types' // Update this variable when testing using a physical device const USE_PHYSICAL_LEDGER = false @@ -34,7 +34,7 @@ const ACCOUNT_ADDRESS5 = normalizeAddressWith0x(privateKeyToAddress(PRIVATE_KEY5 const PRIVATE_KEY_NEVER = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890ffffff' const ACCOUNT_ADDRESS_NEVER = normalizeAddressWith0x(privateKeyToAddress(PRIVATE_KEY_NEVER)) -const ledgerAddresses: { [myKey: string]: { address: string; privateKey: string } } = { +const ledgerAddresses: { [myKey: string]: { address: Hex; privateKey: Hex } } = { "44'/52752'/0'/0/0": { address: ACCOUNT_ADDRESS1, privateKey: PRIVATE_KEY1, @@ -100,68 +100,76 @@ const TYPED_DATA = { } function mockLedger(wallet: LedgerWallet, mockForceValidation: () => void) { - jest.spyOn(wallet, 'generateNewLedger').mockImplementation((_transport: any) => { - return { - getAddress: async (derivationPath: string, forceValidation?: boolean) => { - if (forceValidation) { - mockForceValidation() - } - if (ledgerAddresses[derivationPath]) { - return { address: ledgerAddresses[derivationPath].address, derivationPath } - } - return {} - }, - signTransaction: async (derivationPath: string, data: string) => { - if (ledgerAddresses[derivationPath]) { - const hash = getHashFromEncoded(ensureLeading0x(data)) - const signature = Account.makeSigner(chainIdTransformationForSigning(CHAIN_ID))( - hash, - ledgerAddresses[derivationPath].privateKey + jest + .spyOn(wallet, 'generateNewLedger') + .mockImplementation((_transport: any): ILedger => { + return { + getAddress: async (derivationPath: string, forceValidation?: boolean) => { + if (forceValidation) { + mockForceValidation() + } + if (ledgerAddresses[derivationPath]) { + return { address: ledgerAddresses[derivationPath].address, derivationPath } + } + return {} + }, + signTransaction: async (derivationPath: string, data: string) => { + if (ledgerAddresses[derivationPath]) { + const { r, s, v } = signTransaction( + getHashFromEncoded(ensureLeading0x(data)), + ledgerAddresses[derivationPath].privateKey, + chainIdTransformationForSigning(CHAIN_ID) + ) + return { + v: v.toString(16), + r: r.toString('hex'), + s: s.toString('hex'), + } + } + throw new Error('Invalid Path') + }, + signPersonalMessage: async (derivationPath: string, data: string) => { + if (ledgerAddresses[derivationPath]) { + const dataBuff = ethUtil.toBuffer(ensureLeading0x(data)) + const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff) + + const trimmedKey = trimLeading0x(ledgerAddresses[derivationPath].privateKey) + const pkBuffer = Buffer.from(trimmedKey, 'hex') + const signature = ethUtil.ecsign(msgHashBuff, pkBuffer) + return { + v: signature.v.toString(16), + r: signature.r.toString('hex'), + s: signature.s.toString('hex'), + } + } + throw new Error('Invalid Path') + }, + signEIP712HashedMessage: async ( + derivationPath: string, + domainSeparator: Buffer, + structHash: Buffer + ) => { + const messageHash = Buffer.from( + keccak_256(Buffer.concat([Buffer.from('1901', 'hex'), domainSeparator, structHash])) ) - const [v, r, s] = Account.decodeSignature(signature) - return { v, r, s } - } - throw new Error('Invalid Path') - }, - signPersonalMessage: async (derivationPath: string, data: string) => { - if (ledgerAddresses[derivationPath]) { - const dataBuff = ethUtil.toBuffer(ensureLeading0x(data)) - const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff) const trimmedKey = trimLeading0x(ledgerAddresses[derivationPath].privateKey) const pkBuffer = Buffer.from(trimmedKey, 'hex') - const signature = ethUtil.ecsign(msgHashBuff, pkBuffer) + const signature = ethUtil.ecsign(messageHash, pkBuffer) return { - v: signature.v, + v: signature.v.toString(16), r: signature.r.toString('hex'), s: signature.s.toString('hex'), } - } - throw new Error('Invalid Path') - }, - signEIP712HashedMessage: async ( - derivationPath: string, - domainSeparator: Buffer, - structHash: Buffer - ) => { - const messageHash = keccak_256( - Buffer.concat([Buffer.from('1901', 'hex'), domainSeparator, structHash]) - ) as Buffer - - const trimmedKey = trimLeading0x(ledgerAddresses[derivationPath].privateKey) - const pkBuffer = Buffer.from(trimmedKey, 'hex') - const signature = ethUtil.ecsign(messageHash, pkBuffer) - return { - v: signature.v, - r: signature.r.toString('hex'), - s: signature.s.toString('hex'), - } - }, - getAppConfiguration: async () => { - return { arbitraryDataEnabled: 1, version: '0.0.0' } - }, - } - }) + }, + getAppConfiguration: async () => { + return { arbitraryDataEnabled: 1, version: '0.0.0' } + }, + provideERC20TokenInformation: async (_token) => { + return {} + }, + } + }) } describe('LedgerWallet class', () => { @@ -252,8 +260,8 @@ describe('LedgerWallet class', () => { } await wallet.init() if (USE_PHYSICAL_LEDGER) { - knownAddress = wallet.getAccounts()[0] - otherAddress = wallet.getAccounts()[1] + knownAddress = wallet.getAccounts()[0] as Hex + otherAddress = wallet.getAccounts()[1] as Hex } }, TEST_TIMEOUT_IN_MS) diff --git a/packages/sdk/wallets/wallet-ledger/src/types.d.ts b/packages/sdk/wallets/wallet-ledger/src/types.d.ts new file mode 100644 index 000000000..991902965 --- /dev/null +++ b/packages/sdk/wallets/wallet-ledger/src/types.d.ts @@ -0,0 +1,18 @@ +import { Hex } from '@celo/connect' + +type LedgerSignature = { v: string; r: string; s: string } +export interface ILedger { + getAddress( + derivationPath: string, + forceValidation?: boolean + ): Promise<{ address?: Hex; derivationPath?: string }> + signTransaction(derivationPath: string, data: string): Promise + signPersonalMessage(derivationPath: string, data: string): Promise + signEIP712HashedMessage( + derivationPath: string, + domainSeparator: Buffer, + structHash: Buffer + ): Promise + getAppConfiguration(): Promise<{ arbitraryDataEnabled: number; version: string }> + provideERC20TokenInformation(TokenInfo): Promise +} diff --git a/packages/sdk/wallets/wallet-local/src/local-signer.ts b/packages/sdk/wallets/wallet-local/src/local-signer.ts index 1bb598659..5f2199f0a 100644 --- a/packages/sdk/wallets/wallet-local/src/local-signer.ts +++ b/packages/sdk/wallets/wallet-local/src/local-signer.ts @@ -1,21 +1,19 @@ -import { RLPEncodedTx, Signer } from '@celo/connect' +import { Hex, RLPEncodedTx, Signer } from '@celo/connect' import { ensureLeading0x, trimLeading0x } from '@celo/utils/lib/address' import { computeSharedSecret as computeECDHSecret } from '@celo/utils/lib/ecdh' import { Decrypt } from '@celo/utils/lib/ecies' import { EIP712TypedData, generateTypedDataHash } from '@celo/utils/lib/sign-typed-data-utils' -import { decodeSig, getHashFromEncoded } from '@celo/wallet-base' +import { getHashFromEncoded, signTransaction } from '@celo/wallet-base' import * as ethUtil from '@ethereumjs/util' -// @ts-ignore eth-lib types not found -import { account as Account } from 'eth-lib' /** * Signs the EVM transaction using the provided private key */ export class LocalSigner implements Signer { - private privateKey: string + private privateKey: Hex constructor(privateKey: string) { - this.privateKey = privateKey + this.privateKey = ensureLeading0x(privateKey) } getNativeKey(): string { @@ -26,9 +24,7 @@ export class LocalSigner implements Signer { addToV: number, encodedTx: RLPEncodedTx ): Promise<{ v: number; r: Buffer; s: Buffer }> { - const hash = getHashFromEncoded(encodedTx.rlpEncode) - const signature = Account.makeSigner(addToV)(hash, this.privateKey) - return decodeSig(signature) + return signTransaction(getHashFromEncoded(encodedTx.rlpEncode), this.privateKey, addToV) } async signPersonalMessage(data: string): Promise<{ v: number; r: Buffer; s: Buffer }> { diff --git a/packages/sdk/wallets/wallet-rpc/src/rpc-signer.ts b/packages/sdk/wallets/wallet-rpc/src/rpc-signer.ts index 3fef77474..80178d981 100644 --- a/packages/sdk/wallets/wallet-rpc/src/rpc-signer.ts +++ b/packages/sdk/wallets/wallet-rpc/src/rpc-signer.ts @@ -1,5 +1,5 @@ import { ensureLeading0x, normalizeAddressWith0x, trimLeading0x } from '@celo/base/lib/address' -import { CeloTx, EncodedTransaction, RpcCaller, Signer } from '@celo/connect' +import { CeloTx, EncodedTransaction, Hex, RpcCaller, Signer } from '@celo/connect' import { EIP712TypedData } from '@celo/utils/lib/sign-typed-data-utils' import { decodeSig } from '@celo/wallet-base' import BigNumber from 'bignumber.js' @@ -109,7 +109,7 @@ export class RpcSigner implements Signer { typedData, ]) - return decodeSig(result) + return decodeSig(result as Hex) } async signPersonalMessage(data: string): Promise<{ v: number; r: Buffer; s: Buffer }> { @@ -117,7 +117,7 @@ export class RpcSigner implements Signer { this.account, data, ]) - return decodeSig(result) + return decodeSig(result as Hex) } getNativeKey = () => this.account diff --git a/yarn.lock b/yarn.lock index 85e3fb45d..554201bb8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1980,6 +1980,7 @@ __metadata: dependencies: "@celo/base": "npm:^6.0.0" "@celo/typescript": "npm:0.0.1" + "@ethereumjs/rlp": "npm:^5.0.2" "@ethereumjs/util": "npm:8.0.5" "@noble/ciphers": "npm:0.4.1" "@noble/curves": "npm:1.3.0" @@ -1989,7 +1990,6 @@ __metadata: bignumber.js: "npm:^9.0.0" fp-ts: "npm:2.1.1" io-ts: "npm:2.0.1" - rlp: "npm:^2.2.4" web3-eth-abi: "npm:1.10.0" web3-utils: "npm:1.10.0" languageName: unknown @@ -2002,8 +2002,10 @@ __metadata: "@celo/base": "npm:^6.0.0" "@celo/connect": "npm:^5.1.2" "@celo/utils": "npm:^6.0.0" - "@ethereumjs/rlp": "npm:^5.0.0" + "@ethereumjs/rlp": "npm:^5.0.2" "@ethereumjs/util": "npm:8.0.5" + "@noble/curves": "npm:^1.3.0" + "@noble/hashes": "npm:^1.3.3" "@types/debug": "npm:^4.1.5" bignumber.js: "npm:^9.0.0" debug: "npm:^4.1.1" @@ -2121,8 +2123,8 @@ __metadata: "@ledgerhq/hw-app-eth": "npm:~5.11.0" "@ledgerhq/hw-transport": "npm:~5.11.0" "@ledgerhq/hw-transport-node-hid": "npm:^6.27.4" + "@noble/hashes": "npm:^1.3.3" debug: "npm:^4.1.1" - eth-lib: "npm:^0.2.8" web3: "npm:1.10.0" languageName: unknown linkType: soft @@ -2603,12 +2605,12 @@ __metadata: languageName: node linkType: hard -"@ethereumjs/rlp@npm:^5.0.0": - version: 5.0.0 - resolution: "@ethereumjs/rlp@npm:5.0.0" +"@ethereumjs/rlp@npm:^5.0.2": + version: 5.0.2 + resolution: "@ethereumjs/rlp@npm:5.0.2" bin: - rlp: bin/rlp - checksum: ed2478580489f0adbb037b8c054b178ab2f22e0b1417470fb6659e655436a3a5c9762030f29c5ab64db6a902911fcad92eeaebdfe59d0561a6ecc149d1a42f06 + rlp: bin/rlp.cjs + checksum: 2af80d98faf7f64dfb6d739c2df7da7350ff5ad52426c3219897e843ee441215db0ffa346873200a6be6d11142edb9536e66acd62436b5005fa935baaf7eb6bd languageName: node linkType: hard @@ -3767,7 +3769,7 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:1.3.0, @noble/curves@npm:~1.3.0": +"@noble/curves@npm:1.3.0, @noble/curves@npm:^1.3.0, @noble/curves@npm:~1.3.0": version: 1.3.0 resolution: "@noble/curves@npm:1.3.0" dependencies: @@ -3797,7 +3799,7 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.3.3, @noble/hashes@npm:~1.3.2": +"@noble/hashes@npm:1.3.3, @noble/hashes@npm:^1.3.3, @noble/hashes@npm:~1.3.2": version: 1.3.3 resolution: "@noble/hashes@npm:1.3.3" checksum: 1025ddde4d24630e95c0818e63d2d54ee131b980fe113312d17ed7468bc18f54486ac86c907685759f8a7e13c2f9b9e83ec7b67d1cc20836f36b5e4a65bb102d