import { Wallet, Interface, parseEther, parseUnits, JsonRpcProvider, encodeRlp, toBeHex, concat, keccak256, recoverAddress } from 'ethers' import { ecrecover, fromRpcSig, bufferToHex, publicToAddress, hashPersonalMessage } from 'ethereumjs-util' import { recoverTypedSignature, SignTypedDataVersion } from '@metamask/eth-sig-util' import { CreateKeyCommand, KMSClient } from '@aws-sdk/client-kms' import { getEthAddressFromKMS, KMSSigner } from '../src' import { usdcAbi } from './erc20Abi' describe('KMSSigner', () => { let kms: KMSClient let keyId: string let walletAddress: string let kmsSigner: KMSSigner const providerUrl = process.env.ANVIL_ENDPOINT const provider = new JsonRpcProvider(providerUrl) beforeAll(async () => { kms = new KMSClient({ endpoint: process.env.KMS_ENDPOINT, region: 'local', credentials: { accessKeyId: 'AKIAXTTRUF7NU7KDMIED', secretAccessKey: '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' } }) const command = new CreateKeyCommand({ KeyUsage: 'SIGN_VERIFY', CustomerMasterKeySpec: 'ECC_SECG_P256K1' }) const createResponse = await kms.send(command) keyId = createResponse.KeyMetadata.KeyId walletAddress = await getEthAddressFromKMS({ kmsInstance: kms, keyId }) const wallet = Wallet.fromPhrase(process.env.MNEMONIC ?? '', provider) const tx = await wallet.sendTransaction({ to: walletAddress, value: parseEther('10') }) await tx.wait() kmsSigner = await KMSSigner.create(provider, keyId, kms) }) it('should sign transaction using KMS', async () => { expect(walletAddress).toMatch(/0x[0-9a-fA-f]{40}/) const balance = await provider.getBalance(kmsSigner.getAddress()) expect(balance).toEqual(parseEther('10')) const someWallet = Wallet.createRandom() await kmsSigner.sendTransaction({ to: someWallet.address, value: parseEther('1'), type: 2, maxFeePerGas: parseUnits('2', 'gwei'), maxPriorityFeePerGas: parseUnits('1', 'gwei') }) const targetBalance = await provider.getBalance(someWallet.address) expect(targetBalance).toEqual(parseEther('1')) }) it('should support legacy tx', async () => { const someWallet = Wallet.createRandom() const tx = kmsSigner.sendTransaction({ to: someWallet.address, value: parseEther('1'), type: 0, nonce: await kmsSigner.getNonce(), gasPrice: (await provider.getFeeData()).gasPrice }) await expect(tx).resolves.not.toThrow() }) it('should sign message using KMS', async () => { const message = 'hi' const signature = await kmsSigner.signMessage(message) const { v, r, s } = fromRpcSig(signature) const messageBuffer = Buffer.from(message) const messageHash = hashPersonalMessage(messageBuffer) const publicKey = ecrecover(messageHash, v, r, s) const addrBuffer = publicToAddress(publicKey) const recoveredAddress = bufferToHex(addrBuffer) expect(walletAddress.toLowerCase()).toEqual(recoveredAddress.toLowerCase()) }) it('should sign message with utf signs using KMS', async () => { const message = 'zażółć gęślą jaźń' const signature = await kmsSigner.signMessage(message) const { v, r, s } = fromRpcSig(signature) const messageBuffer = Buffer.from(message) const messageHash = hashPersonalMessage(messageBuffer) const publicKey = ecrecover(messageHash, v, r, s) const addrBuffer = publicToAddress(publicKey) const recoveredAddress = bufferToHex(addrBuffer) expect(walletAddress.toLowerCase()).toEqual(recoveredAddress.toLowerCase()) }) it('should sign typed data using KMS', async () => { const chainId = await provider.send('eth_chainId', []) const verifyingContract = '0x1234123412341234123412341234123412341234' const signature = await kmsSigner.signTypedData( { name: 'Place Bid', version: '1', chainId, verifyingContract }, { Bid: [ { name: 'auctionId', type: 'uint32' }, { name: 'value', type: 'uint256' } ] }, { auctionId: 1, value: 4 } ) const typedData = { types: { EIP712Domain: [ { name: 'name', type: 'string' }, { name: 'version', type: 'string' }, { name: 'chainId', type: 'uint256' }, { name: 'verifyingContract', type: 'address' } ], Bid: [ { name: 'auctionId', type: 'uint32' }, { name: 'value', type: 'uint256' } ] }, domain: { name: 'Place Bid', version: '1', chainId: chainId, verifyingContract: verifyingContract }, primaryType: 'Bid' as const, message: { auctionId: '0x1', value: '0x4' } } const recoveredAddress = recoverTypedSignature({ signature, data: typedData, version: SignTypedDataVersion.V4 }) expect(walletAddress.toLowerCase()).toEqual(recoveredAddress.toLowerCase()) }) it('should authorize delegate and send EIP-7702 transaction using KMS', async () => { const delegate = Wallet.createRandom() const currentNonce = await kmsSigner.getNonce() const network = await provider.getNetwork() const erc20Interface = new Interface(usdcAbi) const authorization = await kmsSigner.authorize({ address: delegate.address, nonce: currentNonce + 1 }) expect(authorization.address).toEqual(delegate.address) expect(authorization.nonce).toEqual(BigInt(currentNonce + 1)) expect(authorization.chainId).toEqual(network.chainId) expect(authorization.signature.serialized).toMatch(/^0x[0-9a-fA-F]{130}$/) const encoded = encodeRlp([ toBeHex(authorization.chainId), authorization.address, toBeHex(authorization.nonce) ]) const digest = keccak256(concat(['0x05', encoded])) const recoveredAddress = recoverAddress(digest, authorization.signature) expect(recoveredAddress.toLowerCase()).toEqual(walletAddress.toLowerCase()) const tx = kmsSigner.sendTransaction({ to: kmsSigner.address, authorizationList: [authorization], data: erc20Interface.encodeFunctionData('approve', [ authorization.address, parseEther('1') ]) }) await expect(tx).resolves.not.toThrow() }) })