diff --git a/src/index.ts b/src/index.ts index 570548f..9cf73ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import { createTxMsgCancelBond, createTxMsgCreateBond, createTxMsgRefillBond, cr import { RegistryClient } from "./registry-client"; import { Account } from "./account"; import { createTransaction } from "./txbuilder"; +import { createTxMsgReserveAuthority, MessageMsgReserveAuthority } from './nameservice'; const DEFAULT_WRITE_ERROR = 'Unable to write to chiba-clonk.'; @@ -215,6 +216,39 @@ export class Registry { return parseTxResponse(result); } + /** + * Reserve authority. + */ + async reserveAuthority(params: MessageMsgReserveAuthority, 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 = createTxMsgReserveAuthority(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); + } + + /** + * Lookup authorities by names. + */ + async lookupAuthorities(names: string[], auction = false) { + return this._client.lookupAuthorities(names, auction); + } + /** * Submit a generic Tx to the chain. */ @@ -237,3 +271,5 @@ export class Registry { return response; } } + +export { Account } diff --git a/src/nameservice.ts b/src/nameservice.ts new file mode 100644 index 0000000..c8590ad --- /dev/null +++ b/src/nameservice.ts @@ -0,0 +1,118 @@ +import { + createEIP712, + generateFee, + generateMessage, + generateTypes, +} from '@tharsis/eip712' +import { + Chain, + Sender, + Fee, +} from '@tharsis/transactions' +import { createTransaction } from '@tharsis/proto' + +import * as nameserviceTx from './proto/vulcanize/nameservice/v1beta1/tx' + +const MSG_RESERVE_AUTHORITY_TYPES = { + MsgValue: [ + { name: 'name', type: 'string' }, + { name: 'signer', type: 'string' }, + { name: 'owner', type: 'string' }, + ], +} + +export interface MessageMsgReserveAuthority { + name: string + owner: string +} + +export function createTxMsgReserveAuthority( + chain: Chain, + sender: Sender, + fee: Fee, + memo: string, + params: MessageMsgReserveAuthority, +) { + // EIP712 + const feeObject = generateFee( + fee.amount, + fee.denom, + fee.gas, + sender.accountAddress, + ) + const types = generateTypes(MSG_RESERVE_AUTHORITY_TYPES) + + const msg = createMsgReserveAuthority( + params.name, + sender.accountAddress, + params.owner + ) + + const messages = generateMessage( + sender.accountNumber.toString(), + sender.sequence.toString(), + chain.cosmosChainId, + memo, + feeObject, + msg, + ) + const eipToSign = createEIP712(types, chain.chainId, messages) + + // Cosmos + const msgCosmos = protoCreateMsgReserveAuthority( + params.name, + sender.accountAddress, + params.owner + ) + + const tx = createTransaction( + msgCosmos, + memo, + fee.amount, + fee.denom, + parseInt(fee.gas, 10), + 'ethsecp256', + sender.pubkey, + sender.sequence, + sender.accountNumber, + chain.cosmosChainId, + ) + + return { + signDirect: tx.signDirect, + legacyAmino: tx.legacyAmino, + eipToSign, + } +} + +function createMsgReserveAuthority( + name: string, + signer: string, + owner: string +) { + return { + type: 'nameservice/ReserveAuthority', + value: { + name, + signer, + owner + }, + } +} + +const protoCreateMsgReserveAuthority = ( + name: string, + signer: string, + owner: string, +) => { + const reserveAuthorityMessage = new nameserviceTx.vulcanize.nameservice.v1beta1.MsgReserveAuthority({ + name, + signer, + owner + }) + + return { + message: reserveAuthorityMessage, + path: 'vulcanize.nameservice.v1beta1.MsgReserveAuthority', + } +} diff --git a/src/naming.test.ts b/src/naming.test.ts new file mode 100644 index 0000000..471bbd0 --- /dev/null +++ b/src/naming.test.ts @@ -0,0 +1,53 @@ +import { Registry } from './index'; +import { getConfig, wait } from './testing/helper'; + +jest.setTimeout(120 * 1000); + +const { mockServer, chibaClonk: { chainId, restEndpoint, gqlEndpoint, privateKey, accountAddress, fee } } = getConfig(); + +const namingTests = () => { + let registry: Registry; + + let bondId: string; + + let authorityName: string; + + beforeAll(async () => { + registry = new Registry(restEndpoint, gqlEndpoint, chainId); + + // Create bond. + bondId = await registry.getNextBondId(accountAddress); + await registry.createBond({ denom: 'aphoton', amount: '1000000000' }, accountAddress, privateKey, fee); + await wait(5000) + }); + + test('Reserve authority.', async () => { + authorityName = `dxos-${Date.now()}`; + await registry.reserveAuthority({ name: authorityName, owner: accountAddress }, accountAddress, privateKey, fee); + await wait(5000) + }); + + test('Lookup authority.', async () => { + const [record] = await registry.lookupAuthorities([authorityName]); + + expect(record).toBeDefined(); + expect(record.ownerAddress).not.toBe(''); + expect(record.ownerPublicKey).not.toBe(''); + expect(Number(record.height)).toBeGreaterThan(0); + }); + + test('Lookup non existing authority', async () => { + const [record] = await registry.lookupAuthorities(['does-not-exist']); + + expect(record.ownerAddress).toBe(''); + expect(record.ownerPublicKey).toBe(''); + expect(Number(record.height)).toBe(0); + }); +}; + +if (mockServer || process.env.WIRE_AUCTIONS_ENABLED) { + // Required as jest complains if file has no tests. + test('skipping naming tests', () => {}); +} else { + describe('Naming', namingTests); +} diff --git a/src/registry-client.ts b/src/registry-client.ts index d4195f6..dc395ce 100644 --- a/src/registry-client.ts +++ b/src/registry-client.ts @@ -5,6 +5,55 @@ import { generateEndpointAccount, generateEndpointBroadcast, generatePostBodyBro import { Util } from './util'; +const auctionFields = ` + id + status + ownerAddress + createTime + commitsEndTime + revealsEndTime + commitFee { + type + quantity + } + revealFee { + type + quantity + } + minimumBid { + type + quantity + } + winnerAddress + winnerBid { + type + quantity + } + winnerPrice { + type + quantity + } + bids { + bidderAddress + status + commitHash + commitTime + revealTime + commitFee { + type + quantity + } + revealFee { + type + quantity + } + bidAmount { + type + quantity + } + } +`; + /** * Registry */ @@ -51,6 +100,33 @@ export class RegistryClient { return data } + /** + * Lookup authorities by names. + */ + async lookupAuthorities(names: string[], auction = false) { + assert(names.length); + + const query = `query ($names: [String!]) { + lookupAuthorities(names: $names) { + ownerAddress + ownerPublicKey + height + status + bondId + expiryTime + ${auction ? ('auction { ' + auctionFields + ' }') : ''} + } + }`; + + const variables = { + names + }; + + const result = await this._graph(query)(variables); + + return result['lookupAuthorities']; + } + /** * Get bonds by ids. */ diff --git a/src/testing/helper.ts b/src/testing/helper.ts index cffd4f8..8b000ef 100644 --- a/src/testing/helper.ts +++ b/src/testing/helper.ts @@ -1,5 +1,5 @@ -const DEFAULT_PRIVATE_KEY = '3d8e23810daecb66ec4ca97805f6bbfc102015c3f22cdda1a783b1d074c43bdd'; -const DEFAULT_ADDRESS = 'ethm1lrdrh056ce23h9d9d5rx34tp0uwj0u9zumynx3' +const DEFAULT_PRIVATE_KEY = '39e06e1471f69a76491e60d1d22908789bf7801039a9ac2197ed432ad45d2daf'; +const DEFAULT_ADDRESS = 'ethm1p9fqwtlypqptuqgndpce5g6wncj4py9z30wfkt' export const wait = (time: number) => new Promise(resolve => setTimeout(resolve, time))