diff --git a/README.md b/README.md index d872883..5144129 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Follow these steps to run the tests: - Checkout to appropriate branch for running tests. ```bash - git checkout ng-chiba-clonk-client + git checkout ng-v12-chiba-clonk-client ``` - Run the chain using `./init.sh`. diff --git a/package.json b/package.json index fb26993..b39f33b 100644 --- a/package.json +++ b/package.json @@ -24,13 +24,17 @@ "@cosmjs/stargate": "^0.28.0", "@metamask/eth-sig-util": "^4.0.0", "axios": "^0.26.1", + "bech32": "^2.0.0", + "bip32": "^3.0.1", + "bip39": "^3.0.4", "ethers": "^5.6.1", "evmosjs": "^0.2.2", "graphql.js": "^0.6.8", "is-url": "^1.2.4", "js-sha256": "^0.9.0", "ripemd160": "^2.0.2", - "secp256k1": "^4.0.3" + "secp256k1": "^4.0.3", + "tiny-secp256k1": "^2.2.1" }, "scripts": { "test": "jest --runInBand", diff --git a/src/account.ts b/src/account.ts index 76cd363..0bec06e 100644 --- a/src/account.ts +++ b/src/account.ts @@ -1,6 +1,15 @@ import assert from 'assert'; +import BIP32Factory from 'bip32'; +import * as ecc from 'tiny-secp256k1'; +import * as bip39 from 'bip39'; import { MessageTypes, signTypedData, SignTypedDataVersion } from '@metamask/eth-sig-util'; -import { Secp256k1 } from "@cosmjs/crypto"; +import { Ripemd160, Secp256k1 } from "@cosmjs/crypto"; +import { toBech32, toHex } from '@cosmjs/encoding'; +import { rawSecp256k1PubkeyToRawAddress } from "@cosmjs/amino"; + +const HDPATH = "m/44'/60'/0'/0"; + +const bip32 = BIP32Factory(ecc); interface TypedMessageDomain { name?: string; @@ -17,6 +26,30 @@ interface TypedMessageDomain { export class Account { _privateKey: Buffer _publicKey?: Uint8Array + _cosmosAddress?: string + _formattedCosmosAddress?: string + + /** + * Generate bip39 mnemonic. + */ + static generateMnemonic() { + return bip39.generateMnemonic(); + } + + /** + * Generate private key from mnemonic. + */ + static async generateFromMnemonic(mnemonic: string) { + assert(mnemonic); + + const seed = await bip39.mnemonicToSeed(mnemonic); + const wallet = bip32.fromSeed(seed); + const account = wallet.derivePath(HDPATH); + const { privateKey } = account; + assert(privateKey); + + return new Account(privateKey); + } /** * New Account. @@ -32,12 +65,23 @@ export class Account { return this._privateKey; } + get formattedCosmosAddress() { + return this._formattedCosmosAddress; + } + async init () { // Generate public key. const keypair = await Secp256k1.makeKeypair(this._privateKey); const compressed = Secp256k1.compressPubkey(keypair.pubkey); this._publicKey = compressed + + // 2. Generate cosmos-sdk address. + // let publicKeySha256 = sha256(this._publicKey); + this._cosmosAddress = new Ripemd160().update(keypair.pubkey).digest().toString(); + + // 3. Generate cosmos-sdk formatted address. + this._formattedCosmosAddress = toBech32('ethm', rawSecp256k1PubkeyToRawAddress(this._publicKey)); } /** diff --git a/src/index.ts b/src/index.ts index 9cf73ed..35b7f7a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,8 @@ import { Chain, Sender, Fee, + createMessageSend, + MessageSendParams } from '@tharsis/transactions' import { createTxMsgCancelBond, createTxMsgCreateBond, createTxMsgRefillBond, createTxMsgWithdrawBond, MessageMsgCancelBond, MessageMsgCreateBond, MessageMsgRefillBond, MessageMsgWithdrawBond } from "./bond"; @@ -78,6 +80,36 @@ export class Registry { return this._client.getAccount(address); } + /** + * Send coins. + * @param {object[]} amount + * @param {string} toAddress + * @param {string} privateKey + * @param {object} fee + */ + async sendCoins(params: MessageSendParams, senderAddress: string, privateKey: string, fee: Fee) { + let result; + + try { + const { account: { base_account: accountInfo } } = await this.getAccount(senderAddress); + + const sender = { + accountAddress: accountInfo.address, + sequence: accountInfo.sequence, + accountNumber: accountInfo.account_number, + pubkey: accountInfo.pub_key.key, + } + + const msg = createMessageSend(this._chain, sender, fee, '', params) + result = await this._submitTx(msg, privateKey, sender); + } catch (err: any) { + const error = err[0] || err; + throw new Error(Registry.processWriteError(error)); + } + + return parseTxResponse(result); + } + /** * Computes the next bondId for the given account private key. */ diff --git a/src/naming.test.ts b/src/naming.test.ts index 471bbd0..11dead8 100644 --- a/src/naming.test.ts +++ b/src/naming.test.ts @@ -1,3 +1,6 @@ +import assert from 'assert'; + +import { Account } from './account'; import { Registry } from './index'; import { getConfig, wait } from './testing/helper'; @@ -43,6 +46,50 @@ const namingTests = () => { expect(record.ownerPublicKey).toBe(''); expect(Number(record.height)).toBe(0); }); + + xtest('Reserve already reserved authority', async () => { + await expect(registry.reserveAuthority({ name: authorityName, owner: accountAddress }, accountAddress, privateKey, fee)).rejects.toThrow('Name already reserved.'); + }); + + test('Reserve sub-authority.', async () => { + const subAuthority = `echo.${authorityName}`; + await registry.reserveAuthority({ name: subAuthority, owner: accountAddress }, accountAddress, privateKey, fee); + await wait(5000) + + const [record] = await registry.lookupAuthorities([subAuthority]); + expect(record).toBeDefined(); + expect(record.ownerAddress).not.toBe(''); + expect(record.ownerPublicKey).not.toBe(''); + expect(Number(record.height)).toBeGreaterThan(0); + }); + + test('Reserve sub-authority with different owner.', async () => { + // Create another account, send tx to set public key on the account. + const mnenonic1 = Account.generateMnemonic(); + const otherAccount1 = await Account.generateFromMnemonic(mnenonic1); + await otherAccount1.init() + assert(otherAccount1.formattedCosmosAddress) + await registry.sendCoins({ denom: 'aphoton', amount: '1000000000', destinationAddress: otherAccount1.formattedCosmosAddress }, accountAddress, privateKey, fee); + + const mnenonic2 = Account.generateMnemonic(); + const otherAccount2 = await Account.generateFromMnemonic(mnenonic2); + await otherAccount2.init() + assert(otherAccount2.formattedCosmosAddress) + await registry.sendCoins({ denom: 'aphoton', amount: '10', destinationAddress: otherAccount2.formattedCosmosAddress }, accountAddress, privateKey, fee); + + await wait(5000) + + const subAuthority = `halo.${authorityName}`; + await registry.reserveAuthority({ name: subAuthority, owner: otherAccount1.formattedCosmosAddress }, accountAddress, privateKey, fee); + await wait(5000) + + const [record] = await registry.lookupAuthorities([subAuthority]); + expect(record).toBeDefined(); + expect(record.ownerAddress).toBeDefined(); + expect(record.ownerAddress).toBe(otherAccount1.formattedCosmosAddress); + expect(record.ownerPublicKey).toBeDefined(); + expect(Number(record.height)).toBeGreaterThan(0); + }); }; if (mockServer || process.env.WIRE_AUCTIONS_ENABLED) { diff --git a/yarn.lock b/yarn.lock index 8145a01..e99605b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1256,6 +1256,16 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.23.tgz#3b41a6e643589ac6442bdbd7a4a3ded62f33f7da" integrity sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw== +"@types/node@10.12.18": + version "10.12.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67" + integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ== + +"@types/node@11.11.6": + version "11.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.6.tgz#df929d1bb2eee5afdda598a41930fe50b43eaa6a" + integrity sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ== + "@types/node@^13.7.0": version "13.13.52" resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.52.tgz#03c13be70b9031baaed79481c0c0cfb0045e53f7" @@ -1512,6 +1522,28 @@ big-integer@1.6.36: resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.36.tgz#78631076265d4ae3555c04f85e7d9d2f3a071a36" integrity sha512-t70bfa7HYEA1D9idDbmuv7YbsbVkQ+Hp+8KFSul4aE5e/i1bjCNIRYJZlA8Q8p0r9T8cF/RVvwUgRA//FydEyg== +bip32@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/bip32/-/bip32-3.0.1.tgz#1d1121469cce6e910e0ec3a5a1990dd62687e2a3" + integrity sha512-Uhpp9aEx3iyiO7CpbNGFxv9WcMIVdGoHG04doQ5Ln0u60uwDah7jUSc3QMV/fSZGm/Oo01/OeAmYevXV+Gz5jQ== + dependencies: + "@types/node" "10.12.18" + bs58check "^2.1.1" + create-hash "^1.2.0" + create-hmac "^1.1.7" + typeforce "^1.11.5" + wif "^2.0.6" + +bip39@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.0.4.tgz#5b11fed966840b5e1b8539f0f54ab6392969b2a0" + integrity sha512-YZKQlb752TrUWqHWj7XAwCSjYEgGAk+/Aas3V7NyjQeZYsztO8JnQUaCWhcnL4T+jL8nvB8typ2jRPzTlgugNw== + dependencies: + "@types/node" "11.11.6" + create-hash "^1.1.0" + pbkdf2 "^3.0.9" + randombytes "^2.0.1" + blakejs@^1.1.0: version "1.2.1" resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.2.1.tgz#5057e4206eadb4a97f7c0b6e197a505042fc3814" @@ -1589,7 +1621,7 @@ bs58@^4.0.0, bs58@^4.0.1: dependencies: base-x "^3.0.2" -bs58check@^2.1.2: +bs58check@<3.0.0, bs58check@^2.1.1, bs58check@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" integrity sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA== @@ -3217,7 +3249,7 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -pbkdf2@^3.0.17: +pbkdf2@^3.0.17, pbkdf2@^3.0.9: version "3.1.2" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== @@ -3325,7 +3357,7 @@ punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -randombytes@^2.1.0: +randombytes@^2.0.1, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== @@ -3682,6 +3714,13 @@ throat@^6.0.1: resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== +tiny-secp256k1@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tiny-secp256k1/-/tiny-secp256k1-2.2.1.tgz#a61d4791b7031aa08a9453178a131349c3e10f9b" + integrity sha512-/U4xfVqnVxJXN4YVsru0E6t5wVncu2uunB8+RVR40fYUxkKYUPS10f+ePQZgFBoE/Jbf9H1NBveupF2VmB58Ng== + dependencies: + uint8array-tools "0.0.7" + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -3782,11 +3821,21 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typeforce@^1.11.5: + version "1.18.0" + resolved "https://registry.yarnpkg.com/typeforce/-/typeforce-1.18.0.tgz#d7416a2c5845e085034d70fcc5b6cc4a90edbfdc" + integrity sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g== + typescript@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.2.tgz#fe12d2727b708f4eef40f51598b3398baa9611d4" integrity sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg== +uint8array-tools@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/uint8array-tools/-/uint8array-tools-0.0.7.tgz#a7a2bb5d8836eae2fade68c771454e6a438b390d" + integrity sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ== + universalify@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -3870,6 +3919,13 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +wif@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/wif/-/wif-2.0.6.tgz#08d3f52056c66679299726fade0d432ae74b4704" + integrity sha1-CNP1IFbGZnkplyb63g1DKudLRwQ= + dependencies: + bs58check "<3.0.0" + word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"