@ember-finance/sdk 2.0.6 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  export * from "./interfaces/index.js";
2
2
  export * from "./on-chain-calls/index.js";
3
+ export * from "./signers/index.js";
3
4
  export * from "./utils/index.js";
4
5
  /**
5
6
  * Converts an EVM address to bytes32 format
@@ -1,5 +1,6 @@
1
1
  export * from "./interfaces/index.js";
2
2
  export * from "./on-chain-calls/index.js";
3
+ export * from "./signers/index.js";
3
4
  export * from "./utils/index.js";
4
5
  /**
5
6
  * Converts an EVM address to bytes32 format
@@ -0,0 +1,42 @@
1
+ import { KMSClient } from "@aws-sdk/client-kms";
2
+ import { AbstractSigner, Provider, TransactionRequest, TypedDataDomain, TypedDataField } from "ethers";
3
+ export interface EvmKmsSignerOptions {
4
+ /** KMS key ID, ARN, or alias (e.g. "alias/ember-vaults-ops-evm-manager-wallet"). */
5
+ keyId: string;
6
+ /** AWS region. Defaults to AWS_REGION env or the SDK default. */
7
+ region?: string;
8
+ /** Pre-built KMSClient. If supplied, `region` is ignored. */
9
+ kmsClient?: KMSClient;
10
+ }
11
+ /**
12
+ * EvmKmsSigner is an ethers v6 Signer backed by an AWS KMS asymmetric key
13
+ * with usage `SIGN_VERIFY` and key spec `ECC_SECG_P256K1` (secp256k1). The
14
+ * private material never leaves KMS; every signing operation is one
15
+ * `kms:Sign` call.
16
+ *
17
+ * Caches the public key after first use so subsequent signs do not re-hit
18
+ * KMS for the address.
19
+ *
20
+ * Compatible with ethers v6. Pair with a Provider via `connect(provider)`
21
+ * before submitting transactions.
22
+ */
23
+ export declare class EvmKmsSigner extends AbstractSigner {
24
+ private readonly kms;
25
+ private readonly keyId;
26
+ private addressCache;
27
+ private uncompressedPubKeyCache;
28
+ constructor(opts: EvmKmsSignerOptions, provider?: Provider | null);
29
+ connect(provider: Provider | null): EvmKmsSigner;
30
+ getAddress(): Promise<string>;
31
+ signMessage(message: string | Uint8Array): Promise<string>;
32
+ signTypedData(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, unknown>): Promise<string>;
33
+ signTransaction(tx: TransactionRequest): Promise<string>;
34
+ /** Returns a 65-byte compact signature as a 0x-prefixed hex string. */
35
+ private signDigest;
36
+ /**
37
+ * Sends the digest to KMS, decodes the DER signature, normalises s,
38
+ * and recovers the y-parity by matching against our address.
39
+ */
40
+ private signDigestRaw;
41
+ private getUncompressedPublicKey;
42
+ }
@@ -0,0 +1,143 @@
1
+ import { GetPublicKeyCommand, KMSClient, MessageType, SignCommand, SigningAlgorithmSpec } from "@aws-sdk/client-kms";
2
+ import { secp256k1 } from "@noble/curves/secp256k1.js";
3
+ import asn1 from "asn1.js";
4
+ import { AbstractSigner, computeAddress, getBytes, hashMessage, resolveAddress, Signature, SigningKey, Transaction, TypedDataEncoder } from "ethers";
5
+ // ASN.1 schemas for the DER blobs AWS KMS returns.
6
+ //
7
+ // GetPublicKey returns SubjectPublicKeyInfo (RFC 5480 §2):
8
+ // SEQUENCE { algo: AlgorithmIdentifier, pubKey: BIT STRING }
9
+ // The bit string contains the SEC1 uncompressed point (0x04 || X || Y).
10
+ //
11
+ // Sign returns DER ECDSA signature (RFC 3279 §2.2.3):
12
+ // SEQUENCE { r: INTEGER, s: INTEGER }
13
+ const EcdsaPubKeyAsn = asn1.define("EcdsaPubKey", function () {
14
+ this.seq().obj(this.key("algo")
15
+ .seq()
16
+ .obj(this.key("algorithm").objid(), this.key("parameters").objid()), this.key("pubKey").bitstr());
17
+ });
18
+ const EcdsaSigAsn = asn1.define("EcdsaSig", function () {
19
+ this.seq().obj(this.key("r").int(), this.key("s").int());
20
+ });
21
+ /**
22
+ * EvmKmsSigner is an ethers v6 Signer backed by an AWS KMS asymmetric key
23
+ * with usage `SIGN_VERIFY` and key spec `ECC_SECG_P256K1` (secp256k1). The
24
+ * private material never leaves KMS; every signing operation is one
25
+ * `kms:Sign` call.
26
+ *
27
+ * Caches the public key after first use so subsequent signs do not re-hit
28
+ * KMS for the address.
29
+ *
30
+ * Compatible with ethers v6. Pair with a Provider via `connect(provider)`
31
+ * before submitting transactions.
32
+ */
33
+ export class EvmKmsSigner extends AbstractSigner {
34
+ constructor(opts, provider = null) {
35
+ super(provider);
36
+ this.keyId = opts.keyId;
37
+ this.kms = opts.kmsClient ?? new KMSClient({ region: opts.region });
38
+ }
39
+ connect(provider) {
40
+ const next = new EvmKmsSigner({ keyId: this.keyId, kmsClient: this.kms }, provider);
41
+ // Carry caches over so reconnecting doesn't force a re-fetch.
42
+ next.uncompressedPubKeyCache = this.uncompressedPubKeyCache;
43
+ next.addressCache = this.addressCache;
44
+ return next;
45
+ }
46
+ async getAddress() {
47
+ if (this.addressCache)
48
+ return this.addressCache;
49
+ const pub = await this.getUncompressedPublicKey();
50
+ // ethers' computeAddress accepts a 0x04-prefixed uncompressed key
51
+ // and returns a checksummed address.
52
+ this.addressCache = computeAddress("0x" + Buffer.from(pub).toString("hex"));
53
+ return this.addressCache;
54
+ }
55
+ async signMessage(message) {
56
+ return this.signDigest(hashMessage(message));
57
+ }
58
+ async signTypedData(domain, types, value) {
59
+ // Resolve any ENS-style names that EIP-712 allows in the domain/types.
60
+ const populated = await TypedDataEncoder.resolveNames(domain, types, value, async (name) => {
61
+ if (!this.provider) {
62
+ throw new Error("EvmKmsSigner.signTypedData: name resolution requires a provider");
63
+ }
64
+ const addr = await this.provider.resolveName(name);
65
+ if (!addr)
66
+ throw new Error(`unable to resolve name: ${name}`);
67
+ return addr;
68
+ });
69
+ return this.signDigest(TypedDataEncoder.hash(populated.domain, types, populated.value));
70
+ }
71
+ async signTransaction(tx) {
72
+ // ethers requires `from` to be absent on a serialized signed tx —
73
+ // it gets recovered from the signature. Validate it matches us.
74
+ if (tx.from != null) {
75
+ const expected = await this.getAddress();
76
+ const from = await resolveAddress(tx.from, this.provider);
77
+ if (from.toLowerCase() !== expected.toLowerCase()) {
78
+ throw new Error(`EvmKmsSigner.signTransaction: from=${from} does not match signer=${expected}`);
79
+ }
80
+ tx = { ...tx, from: undefined };
81
+ }
82
+ // ethers' TransactionRequest and TransactionLike overlap structurally
83
+ // but aren't unifiable in the type system; Transaction.from accepts
84
+ // the looser shape at runtime.
85
+ const txObj = Transaction.from(tx);
86
+ const sig = await this.signDigestRaw(txObj.unsignedHash);
87
+ txObj.signature = sig;
88
+ return txObj.serialized;
89
+ }
90
+ /** Returns a 65-byte compact signature as a 0x-prefixed hex string. */
91
+ async signDigest(digest) {
92
+ const sig = await this.signDigestRaw(digest);
93
+ return sig.serialized;
94
+ }
95
+ /**
96
+ * Sends the digest to KMS, decodes the DER signature, normalises s,
97
+ * and recovers the y-parity by matching against our address.
98
+ */
99
+ async signDigestRaw(digest) {
100
+ const digestBytes = getBytes(digest);
101
+ const res = await this.kms.send(new SignCommand({
102
+ KeyId: this.keyId,
103
+ Message: digestBytes,
104
+ MessageType: MessageType.DIGEST,
105
+ SigningAlgorithm: SigningAlgorithmSpec.ECDSA_SHA_256
106
+ }));
107
+ if (!res.Signature) {
108
+ throw new Error("EvmKmsSigner: KMS Sign returned no Signature");
109
+ }
110
+ const decoded = EcdsaSigAsn.decode(Buffer.from(res.Signature), "der");
111
+ const r = BigInt("0x" + decoded.r.toString(16).padStart(64, "0"));
112
+ let s = BigInt("0x" + decoded.s.toString(16).padStart(64, "0"));
113
+ // EIP-2: only accept low-s signatures. KMS happily returns either side.
114
+ const N = secp256k1.Point.Fn.ORDER;
115
+ if (s > N / 2n)
116
+ s = N - s;
117
+ const rHex = "0x" + r.toString(16).padStart(64, "0");
118
+ const sHex = "0x" + s.toString(16).padStart(64, "0");
119
+ // Recover the right y-parity by trying both candidates and matching
120
+ // against the address we know belongs to this KMS key.
121
+ const expectedAddr = (await this.getAddress()).toLowerCase();
122
+ for (const yParity of [0, 1]) {
123
+ const candidate = Signature.from({ r: rHex, s: sHex, yParity });
124
+ const recoveredPubKey = SigningKey.recoverPublicKey(digestBytes, candidate);
125
+ if (computeAddress(recoveredPubKey).toLowerCase() === expectedAddr) {
126
+ return candidate;
127
+ }
128
+ }
129
+ throw new Error("EvmKmsSigner: recovered address does not match KMS key — signature integrity failure");
130
+ }
131
+ async getUncompressedPublicKey() {
132
+ if (this.uncompressedPubKeyCache)
133
+ return this.uncompressedPubKeyCache;
134
+ const res = await this.kms.send(new GetPublicKeyCommand({ KeyId: this.keyId }));
135
+ if (!res.PublicKey) {
136
+ throw new Error("EvmKmsSigner: KMS GetPublicKey returned no PublicKey");
137
+ }
138
+ const decoded = EcdsaPubKeyAsn.decode(Buffer.from(res.PublicKey), "der");
139
+ // pubKey.data is the SEC1 uncompressed point (0x04 || X || Y), 65 bytes.
140
+ this.uncompressedPubKeyCache = new Uint8Array(decoded.pubKey.data);
141
+ return this.uncompressedPubKeyCache;
142
+ }
143
+ }
@@ -0,0 +1 @@
1
+ export * from "./evm-kms-signer.js";
@@ -0,0 +1 @@
1
+ export * from "./evm-kms-signer.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ember-finance/sdk",
3
3
  "description": "Ember Protocol SDK",
4
- "version": "2.0.6",
4
+ "version": "2.0.7",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
7
7
  "main": "./dist/index.js",
@@ -79,14 +79,16 @@
79
79
  "test": "tests"
80
80
  },
81
81
  "dependencies": {
82
+ "@aws-sdk/client-kms": "^3.1044.0",
82
83
  "@firefly-exchange/library-sui": "^3.0.0",
84
+ "@noble/curves": "^2.2.0",
85
+ "asn1.js": "^5.4.1",
83
86
  "axios": "1.13.6",
84
87
  "yarn": "^1.22.19"
85
88
  },
86
89
  "devDependencies": {
87
90
  "@mysten/bcs": "^2.0.3",
88
91
  "@mysten/sui": "^2.0.0",
89
- "ethers": "6.13.4",
90
92
  "@openapitools/openapi-generator-cli": "^2.24.0",
91
93
  "@types/chai": "^4.3.3",
92
94
  "@types/chai-as-promised": "^7.1.5",
@@ -101,6 +103,7 @@
101
103
  "eslint-plugin-import": "^2.25.2",
102
104
  "eslint-plugin-n": "^15.0.0",
103
105
  "eslint-plugin-promise": "^6.0.0",
106
+ "ethers": "6.13.4",
104
107
  "git-format-staged": "^3.0.0",
105
108
  "husky": "^8.0.0",
106
109
  "mocha": "^10.1.0",