@tetherto/wdk-wallet-evm 1.0.0-beta.2

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.
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@tetherto/wdk-wallet-evm",
3
+ "version": "1.0.0-beta.2",
4
+ "description": "A simple package to manage BIP-32 wallets for evm blockchains.",
5
+ "keywords": [
6
+ "wdk",
7
+ "wallet",
8
+ "bip-32",
9
+ "ethereum"
10
+ ],
11
+ "author": "Tether",
12
+ "license": "Apache-2.0",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git://github.com/tetherto/wdk-wallet-evm.git"
16
+ },
17
+ "main": "index.js",
18
+ "type": "module",
19
+ "types": "./types",
20
+ "scripts": {
21
+ "build:types": "tsc",
22
+ "lint": "standard",
23
+ "lint:fix": "standard --fix",
24
+ "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
25
+ "test:coverage": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage",
26
+ "test:integration": "cross-env NODE_OPTIONS=--experimental-vm-modules jest tests/integration",
27
+ "test:integration:coverage": "cross-env NODE_OPTIONS=--experimental-vm-modules jest tests/integration --coverage"
28
+ },
29
+ "dependencies": {
30
+ "@noble/curves": "1.9.2",
31
+ "@noble/hashes": "1.8.0",
32
+ "@noble/secp256k1": "2.2.3",
33
+ "@tetherto/wdk-wallet": "^1.0.0-beta.1",
34
+ "bare-wdk-runtime": "2.0.0",
35
+ "bip39": "3.1.0",
36
+ "ethers": "6.14.3",
37
+ "sodium-universal": "5.0.1"
38
+ },
39
+ "devDependencies": {
40
+ "@nomicfoundation/hardhat-ethers": "3.0.9",
41
+ "cross-env": "7.0.3",
42
+ "hardhat": "2.24.2",
43
+ "jest": "29.7.0",
44
+ "standard": "17.1.2",
45
+ "typescript": "5.8.3"
46
+ },
47
+ "exports": {
48
+ ".": {
49
+ "types": "./types/index.d.ts",
50
+ "bare": "./bare.js",
51
+ "default": "./index.js"
52
+ },
53
+ "./package": {
54
+ "default": "./package.json"
55
+ }
56
+ },
57
+ "publishConfig": {
58
+ "access": "restricted",
59
+ "registry": "https://registry.npmjs.org/"
60
+ },
61
+ "standard": {
62
+ "ignore": [
63
+ "bare.js",
64
+ "tests/**/*.js"
65
+ ]
66
+ }
67
+ }
@@ -0,0 +1,196 @@
1
+ // Copyright 2024 Tether Operations Limited
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ 'use strict'
16
+
17
+ import {
18
+ assert, assertArgument, assertPrivate, BaseWallet, computeHmac, dataSlice, defineProperties,
19
+ getBytes, getNumber, hexlify, isBytesLike, ripemd160, sha256
20
+ } from 'ethers'
21
+
22
+ import * as secp256k1 from '@noble/secp256k1'
23
+
24
+ import MemorySafeSigningKey from './signing-key.js'
25
+
26
+ const MasterSecret = new Uint8Array([66, 105, 116, 99, 111, 105, 110, 32, 115, 101, 101, 100])
27
+
28
+ const HardenedBit = 0x80000000
29
+
30
+ const _guard = { }
31
+
32
+ function serI (index, chainCode, publicKey, privateKeyBuffer) {
33
+ const data = new Uint8Array(37)
34
+
35
+ if (index & HardenedBit) {
36
+ assert(privateKeyBuffer != null, 'cannot derive child of neutered node', 'UNSUPPORTED_OPERATION', {
37
+ operation: 'deriveChild'
38
+ })
39
+
40
+ data.set(getBytes(privateKeyBuffer), 1)
41
+ } else {
42
+ data.set(getBytes(publicKey))
43
+ }
44
+
45
+ for (let i = 24; i >= 0; i -= 8) { data[33 + (i >> 3)] = ((index >> (24 - i)) & 0xff) }
46
+ const I = getBytes(computeHmac('sha512', chainCode, data))
47
+
48
+ return { IL: I.slice(0, 32), IR: I.slice(32) }
49
+ }
50
+
51
+ function derivePath (node, path) {
52
+ const components = path.split('/')
53
+
54
+ assertArgument(components.length > 0, 'invalid path', 'path', path)
55
+
56
+ if (components[0] === 'm') {
57
+ assertArgument(node.depth === 0, `cannot derive root path (i.e. path starting with "m/") for a node at non-zero depth ${node.depth}`, 'path', path)
58
+ components.shift()
59
+ }
60
+
61
+ let result = node
62
+ for (let i = 0; i < components.length; i++) {
63
+ const component = components[i]
64
+
65
+ if (component.match(/^[0-9]+'$/)) {
66
+ const index = parseInt(component.substring(0, component.length - 1))
67
+ assertArgument(index < HardenedBit, 'invalid path index', `path[${i}]`, component)
68
+ result = result.deriveChild(HardenedBit + index)
69
+ } else if (component.match(/^[0-9]+$/)) {
70
+ const index = parseInt(component)
71
+ assertArgument(index < HardenedBit, 'invalid path index', `path[${i}]`, component)
72
+ result = result.deriveChild(index)
73
+ } else {
74
+ assertArgument(false, 'invalid path component', `path[${i}]`, component)
75
+ }
76
+ }
77
+
78
+ return result
79
+ }
80
+
81
+ function addToPrivateKey (privateKey, x) {
82
+ let carry = 0
83
+
84
+ for (let i = 31; i >= 0; i--) {
85
+ const sum = privateKey[i] + x[i] + carry
86
+ privateKey[i] = sum & 0xff
87
+ carry = sum >> 8
88
+ }
89
+
90
+ return carry > 0
91
+ }
92
+
93
+ function subtractCurveOrderFromPrivateKey (privateKey) {
94
+ let carry = 0
95
+
96
+ for (let i = 31; i >= 0; i--) {
97
+ const curveOrderByte = Number((secp256k1.CURVE.n >> BigInt(8 * (31 - i))) & 0xffn)
98
+ const diff = privateKey[i] - curveOrderByte - carry
99
+ privateKey[i] = diff < 0 ? diff + 256 : diff
100
+ carry = diff < 0 ? 1 : 0
101
+ }
102
+ }
103
+
104
+ function compareWithCurveOrder (buffer, offset = 0) {
105
+ for (let i = 0; i < 32; i++) {
106
+ const curveOrderByte = Number((secp256k1.CURVE.n >> BigInt(8 * (31 - i))) & 0xffn)
107
+ if (buffer[offset + i] > curveOrderByte) return 1
108
+ if (buffer[offset + i] < curveOrderByte) return -1
109
+ }
110
+
111
+ return 0
112
+ }
113
+
114
+ /** @internal */
115
+ export default class MemorySafeHDNodeWallet extends BaseWallet {
116
+ constructor (guard, signingKey, parentFingerprint, chainCode, path, index, depth, mnemonic, provider) {
117
+ super(signingKey, provider)
118
+ assertPrivate(guard, _guard, 'MemorySafeHDNodeWallet')
119
+
120
+ defineProperties(this, { publicKey: signingKey.compressedPublicKey })
121
+
122
+ const fingerprint = dataSlice(ripemd160(sha256(this.publicKey)), 0, 4)
123
+ defineProperties(this, {
124
+ parentFingerprint,
125
+ fingerprint,
126
+ chainCode,
127
+ path,
128
+ index,
129
+ depth
130
+ })
131
+
132
+ defineProperties(this, { mnemonic })
133
+ }
134
+
135
+ connect (provider) {
136
+ return new MemorySafeHDNodeWallet(_guard, this.signingKey, this.parentFingerprint,
137
+ this.chainCode, this.path, this.index, this.depth, this.mnemonic, provider)
138
+ }
139
+
140
+ get privateKeyBuffer () {
141
+ return this.signingKey.privateKeyBuffer
142
+ }
143
+
144
+ get publicKeyBuffer () {
145
+ return this.signingKey.publicKeyBuffer
146
+ }
147
+
148
+ deriveChild (_index) {
149
+ const index = getNumber(_index, 'index')
150
+ assertArgument(index <= 0xffffffff, 'invalid index', 'index', index)
151
+
152
+ let path = this.path
153
+ if (path) {
154
+ path += '/' + (index & ~HardenedBit)
155
+ if (index & HardenedBit) { path += "'" }
156
+ }
157
+
158
+ const { IR, IL } = serI(index, this.chainCode, this.publicKey, this.privateKeyBuffer)
159
+
160
+ const overflow = addToPrivateKey(this.privateKeyBuffer, IL)
161
+
162
+ if (overflow || compareWithCurveOrder(this.privateKeyBuffer) >= 0) {
163
+ subtractCurveOrderFromPrivateKey(this.privateKeyBuffer)
164
+ }
165
+
166
+ const ki = new MemorySafeSigningKey(this.privateKeyBuffer)
167
+
168
+ return new MemorySafeHDNodeWallet(_guard, ki, this.fingerprint, hexlify(IR),
169
+ path, index, this.depth + 1, this.mnemonic, this.provider)
170
+ }
171
+
172
+ derivePath (path) {
173
+ return derivePath(this, path)
174
+ }
175
+
176
+ dispose () {
177
+ this.signingKey.dispose()
178
+ }
179
+
180
+ static fromSeed (seed) {
181
+ return MemorySafeHDNodeWallet._fromSeed(seed, null)
182
+ }
183
+
184
+ static _fromSeed (_seed, mnemonic) {
185
+ assertArgument(isBytesLike(_seed), 'invalid seed', 'seed', '[REDACTED]')
186
+
187
+ const seed = getBytes(_seed, 'seed')
188
+ assertArgument(seed.length >= 16 && seed.length <= 64, 'invalid seed', 'seed', '[REDACTED]')
189
+
190
+ const I = getBytes(computeHmac('sha512', MasterSecret, seed))
191
+ const signingKey = new MemorySafeSigningKey(I.slice(0, 32))
192
+
193
+ return new MemorySafeHDNodeWallet(_guard, signingKey, '0x00000000', hexlify(I.slice(32)),
194
+ 'm', 0, 0, mnemonic, null)
195
+ }
196
+ }
@@ -0,0 +1,77 @@
1
+ // Copyright 2024 Tether Operations Limited
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ 'use strict'
16
+
17
+ import { hmac } from '@noble/hashes/hmac'
18
+ import { sha256 } from '@noble/hashes/sha256'
19
+ import * as secp256k1 from '@noble/secp256k1'
20
+
21
+ import { assertArgument, dataLength, getBytesCopy, Signature, SigningKey, toBeHex } from 'ethers'
22
+
23
+ // eslint-disable-next-line camelcase
24
+ import { sodium_memzero } from 'sodium-universal'
25
+
26
+ const NULL = '0x0000000000000000000000000000000000000000000000000000000000000000'
27
+
28
+ secp256k1.etc.hmacSha256Sync = (key, ...messages) => {
29
+ return hmac(sha256, key, secp256k1.etc.concatBytes(...messages))
30
+ }
31
+
32
+ /** @internal */
33
+ export default class MemorySafeSigningKey extends SigningKey {
34
+ constructor (privateKeyBuffer) {
35
+ super(NULL)
36
+
37
+ this._privateKeyBuffer = privateKeyBuffer
38
+
39
+ this._publicKeyBuffer = secp256k1.getPublicKey(privateKeyBuffer, true)
40
+ }
41
+
42
+ get publicKey () {
43
+ return SigningKey.computePublicKey(this._privateKeyBuffer)
44
+ }
45
+
46
+ get compressedPublicKey () {
47
+ return SigningKey.computePublicKey(this._privateKeyBuffer, true)
48
+ }
49
+
50
+ get privateKeyBuffer () {
51
+ return this._privateKeyBuffer
52
+ }
53
+
54
+ get publicKeyBuffer () {
55
+ return this._publicKeyBuffer
56
+ }
57
+
58
+ sign (digest) {
59
+ assertArgument(dataLength(digest) === 32, 'invalid digest length', 'digest', digest)
60
+
61
+ const sig = secp256k1.sign(getBytesCopy(digest), this._privateKeyBuffer, {
62
+ lowS: true
63
+ })
64
+
65
+ return Signature.from({
66
+ r: toBeHex(sig.r, 32),
67
+ s: toBeHex(sig.s, 32),
68
+ v: (sig.recovery ? 0x1c : 0x1b)
69
+ })
70
+ }
71
+
72
+ dispose () {
73
+ sodium_memzero(this._privateKeyBuffer)
74
+
75
+ this._privateKeyBuffer = undefined
76
+ }
77
+ }
@@ -0,0 +1,200 @@
1
+ // Copyright 2024 Tether Operations Limited
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ 'use strict'
16
+
17
+ import { verifyMessage } from 'ethers'
18
+
19
+ import * as bip39 from 'bip39'
20
+
21
+ import WalletAccountReadOnlyEvm from './wallet-account-read-only-evm.js'
22
+
23
+ import MemorySafeHDNodeWallet from './memory-safe/hd-node-wallet.js'
24
+
25
+ /** @typedef {import('ethers').HDNodeWallet} HDNodeWallet */
26
+
27
+ /** @typedef {import('@tetherto/wdk-wallet').IWalletAccount} IWalletAccount */
28
+
29
+ /** @typedef {import('@tetherto/wdk-wallet').KeyPair} KeyPair */
30
+ /** @typedef {import('@tetherto/wdk-wallet').TransactionResult} TransactionResult */
31
+ /** @typedef {import('@tetherto/wdk-wallet').TransferOptions} TransferOptions */
32
+ /** @typedef {import('@tetherto/wdk-wallet').TransferResult} TransferResult */
33
+
34
+ /** @typedef {import('./wallet-account-read-only-evm.js').EvmTransaction} EvmTransaction */
35
+ /** @typedef {import('./wallet-account-read-only-evm.js').EvmWalletConfig} EvmWalletConfig */
36
+
37
+ const BIP_44_ETH_DERIVATION_PATH_PREFIX = "m/44'/60'"
38
+
39
+ /** @implements {IWalletAccount} */
40
+ export default class WalletAccountEvm extends WalletAccountReadOnlyEvm {
41
+ /**
42
+ * Creates a new evm wallet account.
43
+ *
44
+ * @param {string | Uint8Array} seed - The wallet's [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) seed phrase.
45
+ * @param {string} path - The BIP-44 derivation path (e.g. "0'/0/0").
46
+ * @param {EvmWalletConfig} [config] - The configuration object.
47
+ */
48
+ constructor (seed, path, config = {}) {
49
+ if (typeof seed === 'string') {
50
+ if (!bip39.validateMnemonic(seed)) {
51
+ throw new Error('The seed phrase is invalid.')
52
+ }
53
+
54
+ seed = bip39.mnemonicToSeedSync(seed)
55
+ }
56
+
57
+ path = BIP_44_ETH_DERIVATION_PATH_PREFIX + '/' + path
58
+
59
+ const account = MemorySafeHDNodeWallet.fromSeed(seed)
60
+ .derivePath(path)
61
+
62
+ super(account.address, config)
63
+
64
+ /**
65
+ * The wallet account configuration.
66
+ *
67
+ * @protected
68
+ * @type {EvmWalletConfig}
69
+ */
70
+ this._config = config
71
+
72
+ /**
73
+ * The account.
74
+ *
75
+ * @protected
76
+ * @type {HDNodeWallet}
77
+ */
78
+ this._account = account
79
+
80
+ if (this._provider) {
81
+ this._account = this._account.connect(this._provider)
82
+ }
83
+ }
84
+
85
+ /**
86
+ * The derivation path's index of this account.
87
+ *
88
+ * @type {number}
89
+ */
90
+ get index () {
91
+ return this._account.index
92
+ }
93
+
94
+ /**
95
+ * The derivation path of this account (see [BIP-44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki)).
96
+ *
97
+ * @type {string}
98
+ */
99
+ get path () {
100
+ return this._account.path
101
+ }
102
+
103
+ /**
104
+ * The account's key pair.
105
+ *
106
+ * @type {KeyPair}
107
+ */
108
+ get keyPair () {
109
+ return {
110
+ privateKey: this._account.privateKeyBuffer,
111
+ publicKey: this._account.publicKeyBuffer
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Signs a message.
117
+ *
118
+ * @param {string} message - The message to sign.
119
+ * @returns {Promise<string>} The message's signature.
120
+ */
121
+ async sign (message) {
122
+ return await this._account.signMessage(message)
123
+ }
124
+
125
+ /**
126
+ * Verifies a message's signature.
127
+ *
128
+ * @param {string} message - The original message.
129
+ * @param {string} signature - The signature to verify.
130
+ * @returns {Promise<boolean>} True if the signature is valid.
131
+ */
132
+ async verify (message, signature) {
133
+ const address = await verifyMessage(message, signature)
134
+
135
+ return address.toLowerCase() === this._account.address.toLowerCase()
136
+ }
137
+
138
+ /**
139
+ * Sends a transaction.
140
+ *
141
+ * @param {EvmTransaction} tx - The transaction.
142
+ * @returns {Promise<TransactionResult>} The transaction's result.
143
+ */
144
+ async sendTransaction (tx) {
145
+ if (!this._account.provider) {
146
+ throw new Error('The wallet must be connected to a provider to send transactions.')
147
+ }
148
+
149
+ const { fee } = await this.quoteSendTransaction(tx)
150
+
151
+ const { hash } = await this._account.sendTransaction({
152
+ from: await this.getAddress(),
153
+ ...tx
154
+ })
155
+
156
+ return { hash, fee }
157
+ }
158
+
159
+ /**
160
+ * Transfers a token to another address.
161
+ *
162
+ * @param {TransferOptions} options - The transfer's options.
163
+ * @returns {Promise<TransferResult>} The transfer's result.
164
+ */
165
+ async transfer (options) {
166
+ if (!this._account.provider) {
167
+ throw new Error('The wallet must be connected to a provider to transfer tokens.')
168
+ }
169
+
170
+ const tx = await WalletAccountEvm._getTransferTransaction(options)
171
+
172
+ const { fee } = await this.quoteSendTransaction(tx)
173
+
174
+ if (this._config.transferMaxFee !== undefined && fee >= this._config.transferMaxFee) {
175
+ throw new Error('Exceeded maximum fee cost for transfer operation.')
176
+ }
177
+
178
+ const { hash } = await this._account.sendTransaction(tx)
179
+
180
+ return { hash, fee }
181
+ }
182
+
183
+ /**
184
+ * Returns a read-only copy of the account.
185
+ *
186
+ * @returns {Promise<WalletAccountReadOnlyEvm>} The read-only account.
187
+ */
188
+ async toReadOnlyAccount () {
189
+ const readOnlyAccount = new WalletAccountReadOnlyEvm(this._account.address, this._config)
190
+
191
+ return readOnlyAccount
192
+ }
193
+
194
+ /**
195
+ * Disposes the wallet account, erasing the private key from the memory.
196
+ */
197
+ dispose () {
198
+ this._account.dispose()
199
+ }
200
+ }