Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
2f26fda
feat: integrate AWS KMS for enhanced wallet security
blueogin Dec 5, 2025
60f5c68
feat: native eth transfer tx
blueogin Dec 5, 2025
94bd40c
feat: native eth transfer tested
blueogin Dec 5, 2025
8bd7419
feat: implement WETH deposit functionality in KMS transaction tests
blueogin Dec 5, 2025
970cb67
chore: update .gitignore and add yalc.lock for package management
blueogin Dec 8, 2025
a05196e
fix: normalize wallet addresses in KMS and Web3 wallet implementations
blueogin Dec 8, 2025
c4fee86
chore: remove unused EthereumJS packages from package.json
blueogin Dec 9, 2025
fdfe82c
chore: clean up package-lock.json by removing unused EthereumJS depen…
blueogin Dec 9, 2025
b8f6dd2
fix: removed kms client
blueogin Dec 10, 2025
d3bf440
chore: remove unnecessary console log statements from Web3Wallet
blueogin Dec 10, 2025
546d4c2
chore: update kms-ethereum-signing dependency to version 1.0.0 and ad…
blueogin Dec 22, 2025
29dc67a
chore: simplify kms-ethereum-signing resolution in Jest configuration
blueogin Dec 22, 2025
187dcd4
chore: remove kms-ethereum-signing path mapping from Jest configuration
blueogin Dec 22, 2025
1b86d1c
chore: upgrade lockfile version
blueogin Dec 22, 2025
6568bd1
chore: add @gooddollar/kms-ethereum-signing dependency and update rel…
blueogin Dec 22, 2025
6816ae6
chore: update KMSWallet import to use @gooddollar/kms-ethereum-signing
blueogin Dec 22, 2025
5b2bdb3
fix: add error handling for KMS wallet initialization in Web3Wallet
blueogin Dec 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
PORT=3004
ENV=test
ADMIN_PASS=password

# IMPORT_PRIVATE_KEYS_TO_KMS=false
AWS_REGION=us-east-1
AWS_ACCOUNT_ID=902020902029
KMS_KEY_IDS=6fad19b6-8b5b-40a7-8f65-61a09daf4a1b

# MNEMONIC="myth like bonus scare over problem client lizard pioneer submit female collect"
MNEMONIC="test test test test test test test test test test test junk"
NETWORK=dapptest
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ config/*/*
test/jest-envs/*
uploads/*
.idea/

.yalc/
5 changes: 4 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ module.exports = {
"multiformats/cid": "<rootDir>/node_modules/multiformats/src/cid.js",
"ipfs-unixfs": "<rootDir>/node_modules/ipfs-unixfs/dist/index.min.js",
"uint8arrays":"<rootDir>/node_modules/uint8arrays/dist/index.min.js",
// Note: kms-ethereum-signing mapping removed - Jest will resolve it via package.json main field
// If you need to force a specific path, uncomment and adjust:
// "^kms-ethereum-signing$": "<rootDir>/node_modules/kms-ethereum-signing/dist/index.js",
},

// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
Expand Down Expand Up @@ -184,7 +187,7 @@ module.exports = {

// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
transformIgnorePatterns: [
"/node_modules/(?!(@veramo|multiformats|key-did-resolver)/)",
"/node_modules/(?!(@veramo|multiformats|key-did-resolver|@aws-sdk)/)",
"\\.pnp\\.[^\\\/]+$"
],

Expand Down
110,225 changes: 34,470 additions & 75,755 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"@babel/runtime": "^7.16.5",
"@gooddollar/goodcontracts": "^2.6.1",
"@gooddollar/goodprotocol": "^2.1.0",
"@gooddollar/kms-ethereum-signing": "1.0.0",
"@openzeppelin/defender-autotask-client": "^1.50.0",
"@openzeppelin/defender-relay-client": "^1.49.0",
"@sentry/integrations": "^5.30.0",
Expand Down
66 changes: 65 additions & 1 deletion src/server/blockchain/AdminWallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,73 @@ class AdminWallet extends Web3Wallet {
uuid,
balance,
gas,
gasPrice
gasPrice,
isKMS: this.isKMSWallet(address)
})

// Check if this is a KMS wallet
if (this.isKMSWallet(address) && this.kmsWallet) {
// Sign transaction with KMS for mainnet
const kmsTxParams = {
gas,
gasPrice: gasPrice.toString(),
nonce,
chainId: this.networkIdMainnet
}

try {
const txData = tx.encodeABI()
const to = tx._parent._address || tx._parent.options.address

const signedTx = await this.kmsWallet.signTransaction(address, {
to,
data: txData,
...kmsTxParams,
rpcUrl: this.conf.ethereumMainnet.httpWeb3Provider
? this.conf.ethereumMainnet.httpWeb3Provider.split(',')[0]
: undefined
})

const sendTx = this.mainnetWeb3.eth.sendSignedTransaction(signedTx)

return new Promise((res, rej) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reduce duplicate code

sendTx
.on('transactionHash', h => {
release()
log.debug('got tx hash mainnet:', { txhash: h, uuid })
if (onTransactionHash) {
onTransactionHash(h)
}
})
.on('receipt', r => {
log.debug('got tx receipt mainnet:', { uuid })
if (onReceipt) {
onReceipt(r)
}
res(r)
})
.on('confirmation', c => {
if (onConfirmation) {
onConfirmation(c)
}
})
.on('error', async exception => {
const { message } = exception
fail()
if (onError) {
onError(exception)
}
log.error('mainnetWeb3sendTransaction error mainnet:', message, exception, { from: address, uuid })
rej(exception)
})
})
} catch (error) {
fail()
log.error('Failed to send KMS transaction mainnet', { error: error.message, uuid, address })
throw error
}
}

return new Promise((res, rej) => {
tx.send({
gas,
Expand Down
214 changes: 214 additions & 0 deletions src/server/blockchain/KMSWallet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
// @flow
import {
getEthereumAddress,
signTransaction as kmsSignTransaction,
signMessage as kmsSignMessage,
getPublicKey,
importKMSKey
} from '@gooddollar/kms-ethereum-signing'
import logger from '../../imports/logger'

const log = logger.child({ from: 'KMSWallet' })

/**
* KMS Wallet Adapter
* Wraps eth-kms-signer functionality for use with Web3Wallet
*/
export class KMSWallet {
kmsKeys: Map<string, { keyId: string, address: string, region?: string }>
region: ?string

constructor(region?: string) {
this.kmsKeys = new Map()
this.region = region || process.env.AWS_REGION
}

/**
* Initialize KMS wallet with key IDs
* @param keyIds - Array of KMS key IDs or aliases
* @returns Promise resolving to array of addresses
*/
async initialize(keyIds: string[]): Promise<string[]> {
const addresses = []

for (const keyId of keyIds) {
try {
const address = await getEthereumAddress(keyId, this.region)
this.kmsKeys.set(address.toLowerCase(), {
keyId,
address,
region: this.region
})
addresses.push(address)
log.info('KMS wallet initialized', { keyId, address })
} catch (error) {
log.error('Failed to initialize KMS key', { keyId, error: error.message })
throw error
}
}

return addresses
}

/**
* Get KMS key ID for an address
* @param address - Ethereum address
* @returns KMS key ID or null
*/
getKeyId(address: string): ?string {
const keyInfo = this.kmsKeys.get(address.toLowerCase())
return keyInfo ? keyInfo.keyId : null
}

/**
* Get region for an address
* @param address - Ethereum address
* @returns AWS region or null
*/
getRegion(address: string): ?string {
const keyInfo = this.kmsKeys.get(address.toLowerCase())
return keyInfo ? keyInfo.region : null
}

/**
* Check if address is managed by KMS
* @param address - Ethereum address
* @returns boolean
*/
hasAddress(address: string): boolean {
return this.kmsKeys.has(address.toLowerCase())
}

/**
* Get all addresses managed by KMS
* @returns Array of addresses
*/
getAddresses(): string[] {
return Array.from(this.kmsKeys.values()).map(k => k.address)
}

/**
* Sign a transaction using KMS
* @param address - Ethereum address to sign with
* @param transaction - Transaction parameters
* @returns Signed transaction hex string
*/
async signTransaction(
address: string,
transaction: {
to: string,
value?: string,
data?: string,
nonce?: number,
gasLimit?: string,
gasPrice?: string,
maxFeePerGas?: string,
maxPriorityFeePerGas?: string,
chainId: number,
rpcUrl?: string
}
): Promise<string> {
const keyId = this.getKeyId(address)
if (!keyId) {
throw new Error(`No KMS key found for address: ${address}`)
}

const region = this.getRegion(address)

try {
if (transaction.rpcUrl != undefined && transaction.rpcUrl.includes('localhost')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

write a comment why you do this

transaction = {
...transaction,
maxFeePerGas: undefined
}
}
const signedTx = await kmsSignTransaction(keyId, transaction, region)
log.debug('Transaction signed with KMS', { address, keyId, chainId: transaction.chainId })
return signedTx
} catch (error) {
log.error('Failed to sign transaction with KMS', {
address,
keyId,
error: error.message
})
throw error
}
}

/**
* Sign a message using KMS
* @param address - Ethereum address to sign with
* @param message - Message to sign
* @returns Signature hex string
*/
async signMessage(address: string, message: string): Promise<string> {
const keyId = this.getKeyId(address)
if (!keyId) {
throw new Error(`No KMS key found for address: ${address}`)
}

const region = this.getRegion(address)

try {
const signature = await kmsSignMessage(keyId, message, region)
log.debug('Message signed with KMS', { address, keyId })
return signature
} catch (error) {
log.error('Failed to sign message with KMS', {
address,
keyId,
error: error.message
})
throw error
}
}

/**
* Import a private key into KMS
* @param privateKeyHex - Private key in hex format
* @param aliasName - Alias name for the KMS key
* @returns Promise resolving to key ID and address
*/
async importPrivateKey(privateKeyHex: string, aliasName: string): Promise<{ keyId: string, address: string }> {
try {
const result = await importKMSKey(privateKeyHex, aliasName, this.region)

const address = await getEthereumAddress(result.keyId, this.region)

this.kmsKeys.set(address.toLowerCase(), {
keyId: result.keyId,
address,
region: this.region
})

log.info('Private key imported to KMS', {
aliasName,
keyId: result.keyId,
address
})

return { keyId: result.keyId, address }
} catch (error) {
log.error('Failed to import private key to KMS', {
aliasName,
error: error.message
})
throw error
}
}

/**
* Get public key for an address
* @param address - Ethereum address
* @returns Public key hex string
*/
async getPublicKey(address: string): Promise<string> {
const keyId = this.getKeyId(address)
if (!keyId) {
throw new Error(`No KMS key found for address: ${address}`)
}

const region = this.getRegion(address)
return getPublicKey(keyId, region)
}
}
Loading
Loading