diff --git a/packages/aws-kms-wallet/README.md b/packages/aws-kms-wallet/README.md new file mode 100644 index 00000000000..1e8851b0023 --- /dev/null +++ b/packages/aws-kms-wallet/README.md @@ -0,0 +1,44 @@ +# @thirdweb-dev/aws-kms-wallet + +This package provides AWS KMS wallet functionality for thirdweb SDK. + +## Installation + +```bash +npm install @thirdweb-dev/aws-kms-wallet +``` + +## Usage + +```typescript +import { getAwsKmsAccount } from "@thirdweb-dev/aws-kms-wallet"; +import { ThirdwebClient } from "thirdweb"; + +const client = new ThirdwebClient({ + // your client config +}); + +const account = await getAwsKmsAccount({ + keyId: "your-kms-key-id", + config: { + // your AWS KMS config + region: "us-east-1", + }, + client, +}); + +// Use the account for transactions, signing messages, etc. +const tx = await account.sendTransaction({ + // transaction details +}); +``` + +## Requirements + +- Node.js 18+ +- AWS KMS key with ECC_SECG_P256K1 key spec +- AWS credentials configured in your environment + +## License + +Apache-2.0 diff --git a/packages/aws-kms-wallet/package.json b/packages/aws-kms-wallet/package.json new file mode 100644 index 00000000000..994d61b2ee3 --- /dev/null +++ b/packages/aws-kms-wallet/package.json @@ -0,0 +1,53 @@ +{ + "name": "@thirdweb-dev/aws-kms-wallet", + "version": "0.1.0", + "type": "module", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "types": "dist/types/index.d.ts", + "typings": "dist/types/index.d.ts", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "import": "./dist/esm/index.js", + "default": "./dist/cjs/index.js" + }, + "./package.json": "./package.json" + }, + "repository": "https://github.com/thirdweb-dev/js/tree/main/packages/aws-kms-wallet", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/thirdweb-dev/js/issues" + }, + "author": "thirdweb eng ", + "files": ["dist/"], + "sideEffects": false, + "engines": { + "node": ">=18" + }, + "dependencies": { + "@aws-sdk/client-kms": "^3.592.0", + "aws-kms-signer": "0.5.3", + "viem": "2.22.17" + }, + "peerDependencies": { + "thirdweb": "^5.88.1" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@types/node": "^22.13.0", + "typescript": "5.7.3", + "vitest": "3.0.5" + }, + "scripts": { + "format": "biome format ./src --write", + "lint": "biome check ./src && tsc --project ./tsconfig.build.json --module esnext --noEmit", + "fix": "biome check ./src --fix", + "clean": "rm -rf dist/", + "build": "pnpm clean && pnpm build:types && pnpm build:cjs && pnpm build:esm", + "build:cjs": "tsc --noCheck --project ./tsconfig.build.json --module commonjs --outDir ./dist/cjs --verbatimModuleSyntax false && printf '{\"type\":\"commonjs\"}' > ./dist/cjs/package.json", + "build:esm": "tsc --noCheck --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm && printf '{\"type\": \"module\",\"sideEffects\":false}' > ./dist/esm/package.json", + "build:types": "tsc --project ./tsconfig.build.json --module esnext --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap", + "test": "vitest run" + } +} diff --git a/packages/aws-kms-wallet/src/index.ts b/packages/aws-kms-wallet/src/index.ts new file mode 100644 index 00000000000..ba3bac14b03 --- /dev/null +++ b/packages/aws-kms-wallet/src/index.ts @@ -0,0 +1,121 @@ +import { Buffer } from "node:buffer"; +import type { KMSClientConfig } from "@aws-sdk/client-kms"; +import { KmsSigner } from "aws-kms-signer"; +import type { Hex, ThirdwebClient, toSerializableTransaction } from "thirdweb"; +import { + type Address, + eth_sendRawTransaction, + getRpcClient, + keccak256, +} from "thirdweb"; +import { serializeTransaction } from "thirdweb/transaction"; +import { hashMessage } from "thirdweb/utils"; +import type { Account } from "thirdweb/wallets"; +import type { SignableMessage, TypedData, TypedDataDefinition } from "viem"; +import { hashTypedData } from "viem"; +import { getChain } from "./utils/chain"; + +type SendTransactionResult = { + transactionHash: Hex; +}; + +type SerializableTransaction = Awaited< + ReturnType +>; + +type SendTransactionOption = SerializableTransaction & { + chainId: number; +}; + +type AwsKmsAccountOptions = { + keyId: string; + config?: KMSClientConfig; + client: ThirdwebClient; +}; + +type AwsKmsAccount = Account; + +export async function getAwsKmsAccount( + options: AwsKmsAccountOptions, +): Promise { + const { keyId, config, client } = options; + const signer = new KmsSigner(keyId, config); + + // Populate address immediately + const addressUnprefixed = await signer.getAddress(); + const address = `0x${addressUnprefixed}` as Address; + + async function signTransaction(tx: SerializableTransaction): Promise { + const serializedTx = serializeTransaction({ transaction: tx }); + const txHash = keccak256(serializedTx); + const signature = await signer.sign(Buffer.from(txHash.slice(2), "hex")); + + const r = `0x${signature.r.toString("hex")}` as Hex; + const s = `0x${signature.s.toString("hex")}` as Hex; + const v = BigInt(signature.v); + + const yParity: 0 | 1 = signature.v % 2 === 0 ? 1 : 0; + + const signedTx = serializeTransaction({ + transaction: tx, + signature: { + r, + s, + v, + yParity, + }, + }); + + return signedTx; + } + + /** + * Sign a message with the account's private key. + * If the message is a string, it will be prefixed with the Ethereum message prefix. + * If the message is an object with a `raw` property, it will be signed as-is. + */ + async function signMessage({ + message, + }: { + message: SignableMessage; + }): Promise { + const messageHash = hashMessage(message); + const signature = await signer.sign( + Buffer.from(messageHash.slice(2), "hex"), + ); + return `0x${signature.toString()}`; + } + + async function signTypedData< + const typedData extends TypedData | Record, + primaryType extends keyof typedData | "EIP712Domain" = keyof typedData, + >(_typedData: TypedDataDefinition): Promise { + const typedDataHash = hashTypedData(_typedData); + const signature = await signer.sign( + Buffer.from(typedDataHash.slice(2), "hex"), + ); + return `0x${signature.toString()}`; + } + + async function sendTransaction( + tx: SendTransactionOption, + ): Promise { + const rpcRequest = getRpcClient({ + client: client, + chain: await getChain(tx.chainId), + }); + + const signedTx = await signTransaction(tx); + + const transactionHash = await eth_sendRawTransaction(rpcRequest, signedTx); + return { transactionHash }; + } + + return { + address, + sendTransaction, + signMessage, + signTypedData, + signTransaction, + } as AwsKmsAccount satisfies Account; +} diff --git a/packages/aws-kms-wallet/src/utils/chain.ts b/packages/aws-kms-wallet/src/utils/chain.ts new file mode 100644 index 00000000000..ffea421933b --- /dev/null +++ b/packages/aws-kms-wallet/src/utils/chain.ts @@ -0,0 +1,7 @@ +import type { Chain } from "thirdweb"; + +export async function getChain(chainId: number): Promise { + return { + id: chainId, + } as Chain; +} diff --git a/packages/aws-kms-wallet/tsconfig.build.json b/packages/aws-kms-wallet/tsconfig.build.json new file mode 100644 index 00000000000..ac50c079e7d --- /dev/null +++ b/packages/aws-kms-wallet/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.build.json", + "include": ["src"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.bench.ts"], + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "emitDeclarationOnly": true + } +}