Skip to content

Commit 3fee8df

Browse files
committed
AWS KMS Wallet POC
1 parent dceb47d commit 3fee8df

File tree

5 files changed

+235
-0
lines changed

5 files changed

+235
-0
lines changed

packages/aws-kms-wallet/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# @thirdweb-dev/aws-kms-wallet
2+
3+
This package provides AWS KMS wallet functionality for thirdweb SDK.
4+
5+
## Installation
6+
7+
```bash
8+
npm install @thirdweb-dev/aws-kms-wallet
9+
```
10+
11+
## Usage
12+
13+
```typescript
14+
import { getAwsKmsAccount } from "@thirdweb-dev/aws-kms-wallet";
15+
import { ThirdwebClient } from "thirdweb";
16+
17+
const client = new ThirdwebClient({
18+
// your client config
19+
});
20+
21+
const account = await getAwsKmsAccount({
22+
keyId: "your-kms-key-id",
23+
config: {
24+
// your AWS KMS config
25+
region: "us-east-1",
26+
},
27+
client,
28+
});
29+
30+
// Use the account for transactions, signing messages, etc.
31+
const tx = await account.sendTransaction({
32+
// transaction details
33+
});
34+
```
35+
36+
## Requirements
37+
38+
- Node.js 18+
39+
- AWS KMS key with ECC_SECG_P256K1 key spec
40+
- AWS credentials configured in your environment
41+
42+
## License
43+
44+
Apache-2.0
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"name": "@thirdweb-dev/aws-kms-wallet",
3+
"version": "0.1.0",
4+
"type": "module",
5+
"main": "dist/cjs/index.js",
6+
"module": "dist/esm/index.js",
7+
"types": "dist/types/index.d.ts",
8+
"typings": "dist/types/index.d.ts",
9+
"exports": {
10+
".": {
11+
"types": "./dist/types/index.d.ts",
12+
"import": "./dist/esm/index.js",
13+
"default": "./dist/cjs/index.js"
14+
},
15+
"./package.json": "./package.json"
16+
},
17+
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/aws-kms-wallet",
18+
"license": "Apache-2.0",
19+
"bugs": {
20+
"url": "https://github.com/thirdweb-dev/js/issues"
21+
},
22+
"author": "thirdweb eng <[email protected]>",
23+
"files": ["dist/"],
24+
"sideEffects": false,
25+
"engines": {
26+
"node": ">=18"
27+
},
28+
"dependencies": {
29+
"@aws-sdk/client-kms": "^3.592.0",
30+
"aws-kms-signer": "0.5.3",
31+
"viem": "2.22.17"
32+
},
33+
"peerDependencies": {
34+
"thirdweb": "^5.88.1"
35+
},
36+
"devDependencies": {
37+
"@biomejs/biome": "1.9.4",
38+
"@types/node": "^22.13.0",
39+
"typescript": "5.7.3",
40+
"vitest": "3.0.5"
41+
},
42+
"scripts": {
43+
"format": "biome format ./src --write",
44+
"lint": "biome check ./src && tsc --project ./tsconfig.build.json --module esnext --noEmit",
45+
"fix": "biome check ./src --fix",
46+
"clean": "rm -rf dist/",
47+
"build": "pnpm clean && pnpm build:types && pnpm build:cjs && pnpm build:esm",
48+
"build:cjs": "tsc --noCheck --project ./tsconfig.build.json --module commonjs --outDir ./dist/cjs --verbatimModuleSyntax false && printf '{\"type\":\"commonjs\"}' > ./dist/cjs/package.json",
49+
"build:esm": "tsc --noCheck --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm && printf '{\"type\": \"module\",\"sideEffects\":false}' > ./dist/esm/package.json",
50+
"build:types": "tsc --project ./tsconfig.build.json --module esnext --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap",
51+
"test": "vitest run"
52+
}
53+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { Buffer } from "node:buffer";
2+
import type { KMSClientConfig } from "@aws-sdk/client-kms";
3+
import { KmsSigner } from "aws-kms-signer";
4+
import type { Hex, ThirdwebClient, toSerializableTransaction } from "thirdweb";
5+
import {
6+
type Address,
7+
eth_sendRawTransaction,
8+
getRpcClient,
9+
keccak256,
10+
} from "thirdweb";
11+
import { serializeTransaction } from "thirdweb/transaction";
12+
import { hashMessage } from "thirdweb/utils";
13+
import type { Account } from "thirdweb/wallets";
14+
import type { SignableMessage, TypedData, TypedDataDefinition } from "viem";
15+
import { hashTypedData } from "viem";
16+
import { getChain } from "./utils/chain";
17+
18+
type SendTransactionResult = {
19+
transactionHash: Hex;
20+
};
21+
22+
type SerializableTransaction = Awaited<
23+
ReturnType<typeof toSerializableTransaction>
24+
>;
25+
26+
type SendTransactionOption = SerializableTransaction & {
27+
chainId: number;
28+
};
29+
30+
type AwsKmsAccountOptions = {
31+
keyId: string;
32+
config?: KMSClientConfig;
33+
client: ThirdwebClient;
34+
};
35+
36+
type AwsKmsAccount = Account;
37+
38+
export async function getAwsKmsAccount(
39+
options: AwsKmsAccountOptions,
40+
): Promise<AwsKmsAccount> {
41+
const { keyId, config, client } = options;
42+
const signer = new KmsSigner(keyId, config);
43+
44+
// Populate address immediately
45+
const addressUnprefixed = await signer.getAddress();
46+
const address = `0x${addressUnprefixed}` as Address;
47+
48+
async function signTransaction(tx: SerializableTransaction): Promise<Hex> {
49+
const serializedTx = serializeTransaction({ transaction: tx });
50+
const txHash = keccak256(serializedTx);
51+
const signature = await signer.sign(Buffer.from(txHash.slice(2), "hex"));
52+
53+
const r = `0x${signature.r.toString("hex")}` as Hex;
54+
const s = `0x${signature.s.toString("hex")}` as Hex;
55+
const v = BigInt(signature.v);
56+
57+
const yParity: 0 | 1 = signature.v % 2 === 0 ? 1 : 0;
58+
59+
const signedTx = serializeTransaction({
60+
transaction: tx,
61+
signature: {
62+
r,
63+
s,
64+
v,
65+
yParity,
66+
},
67+
});
68+
69+
return signedTx;
70+
}
71+
72+
/**
73+
* Sign a message with the account's private key.
74+
* If the message is a string, it will be prefixed with the Ethereum message prefix.
75+
* If the message is an object with a `raw` property, it will be signed as-is.
76+
*/
77+
async function signMessage({
78+
message,
79+
}: {
80+
message: SignableMessage;
81+
}): Promise<Hex> {
82+
const messageHash = hashMessage(message);
83+
const signature = await signer.sign(
84+
Buffer.from(messageHash.slice(2), "hex"),
85+
);
86+
return `0x${signature.toString()}`;
87+
}
88+
89+
async function signTypedData<
90+
const typedData extends TypedData | Record<string, unknown>,
91+
primaryType extends keyof typedData | "EIP712Domain" = keyof typedData,
92+
>(_typedData: TypedDataDefinition<typedData, primaryType>): Promise<Hex> {
93+
const typedDataHash = hashTypedData(_typedData);
94+
const signature = await signer.sign(
95+
Buffer.from(typedDataHash.slice(2), "hex"),
96+
);
97+
return `0x${signature.toString()}`;
98+
}
99+
100+
async function sendTransaction(
101+
tx: SendTransactionOption,
102+
): Promise<SendTransactionResult> {
103+
const rpcRequest = getRpcClient({
104+
client: client,
105+
chain: await getChain(tx.chainId),
106+
});
107+
108+
const signedTx = await signTransaction(tx);
109+
110+
const transactionHash = await eth_sendRawTransaction(rpcRequest, signedTx);
111+
return { transactionHash };
112+
}
113+
114+
return {
115+
address,
116+
sendTransaction,
117+
signMessage,
118+
signTypedData,
119+
signTransaction,
120+
} as AwsKmsAccount satisfies Account;
121+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { Chain } from "thirdweb";
2+
3+
export async function getChain(chainId: number): Promise<Chain> {
4+
return {
5+
id: chainId,
6+
} as Chain;
7+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": "../../tsconfig.build.json",
3+
"include": ["src"],
4+
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.bench.ts"],
5+
"compilerOptions": {
6+
"outDir": "dist",
7+
"rootDir": "src",
8+
"emitDeclarationOnly": true
9+
}
10+
}

0 commit comments

Comments
 (0)