forked from cerc-io/registry-sdk
Implement pattern from dxns-registry-client
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import assert from 'assert';
|
||||
import { MessageTypes, signTypedData, SignTypedDataVersion } from '@metamask/eth-sig-util';
|
||||
import { Secp256k1 } from "@cosmjs/crypto";
|
||||
|
||||
interface TypedMessageDomain {
|
||||
name?: string;
|
||||
version?: string;
|
||||
chainId?: number;
|
||||
verifyingContract?: string;
|
||||
salt?: ArrayBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry account.
|
||||
*/
|
||||
// TODO(egor): This is a wrapper around the private key and doesn't have any account related stuff (e.g. account number/sequence). Maybe rename to Key?
|
||||
export class Account {
|
||||
_privateKey: Buffer
|
||||
_publicKey?: Uint8Array
|
||||
|
||||
/**
|
||||
* New Account.
|
||||
* @param {buffer} privateKey
|
||||
*/
|
||||
constructor(privateKey: Buffer) {
|
||||
assert(privateKey);
|
||||
|
||||
this._privateKey = privateKey;
|
||||
}
|
||||
|
||||
get privateKey() {
|
||||
return this._privateKey;
|
||||
}
|
||||
|
||||
async init () {
|
||||
// Generate public key.
|
||||
const keypair = await Secp256k1.makeKeypair(this._privateKey);
|
||||
|
||||
const compressed = Secp256k1.compressPubkey(keypair.pubkey);
|
||||
this._publicKey = compressed
|
||||
}
|
||||
|
||||
/**
|
||||
* Get private key.
|
||||
*/
|
||||
getPrivateKey() {
|
||||
return this._privateKey.toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign message.
|
||||
*/
|
||||
sign(message: any) {
|
||||
assert(message);
|
||||
const eipMessageDomain: any = message.eipToSign.domain;
|
||||
|
||||
const signature = signTypedData({
|
||||
data: {
|
||||
types: message.eipToSign.types as MessageTypes,
|
||||
primaryType: message.eipToSign.primaryType,
|
||||
domain: eipMessageDomain as TypedMessageDomain,
|
||||
message: message.eipToSign.message as Record<string, unknown>
|
||||
},
|
||||
privateKey: this._privateKey,
|
||||
version: SignTypedDataVersion.V4
|
||||
})
|
||||
|
||||
return signature;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Registry } from './index';
|
||||
import { getConfig } from './testing/helper';
|
||||
|
||||
const { mockServer, chibaClonk: { chainId, endpoint, privateKey, accountAddress, fee } } = getConfig();
|
||||
|
||||
jest.setTimeout(90 * 1000);
|
||||
|
||||
const bondTests = () => {
|
||||
let registry: Registry;
|
||||
let bondId1: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
registry = new Registry(endpoint, chainId);
|
||||
});
|
||||
|
||||
test('Create bond.', async () => {
|
||||
bondId1 = await registry.getNextBondId(accountAddress);
|
||||
expect(bondId1).toBeDefined();
|
||||
await registry.createBond({ denom: 'aphoton', amount: '100' }, accountAddress, privateKey, fee);
|
||||
})
|
||||
};
|
||||
|
||||
if (mockServer) {
|
||||
// Required as jest complains if file has no tests.
|
||||
test('skipping bond tests', () => {});
|
||||
} else {
|
||||
describe('Bonds', bondTests);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { createBond, sendDeposit, sendTokens, sendVote } from './index'
|
||||
|
||||
const SENDER_ADDRESS = 'ethm1kgwzff36qmx5tvfvfr7wvdurp5mr25csyqxgdm';
|
||||
const SENDER_PRIVATE_KEY = '12e94bcc0daecd936b499f3eeb3b3b76ac1410cbaff2ee6c6f64d768453db0cf';
|
||||
const TO_ADDRESS = 'ethm1e6r855un2ufnne9cdpujvan5srxjand37pepuz';
|
||||
|
||||
test('Send tokens', async () => {
|
||||
await sendTokens(SENDER_PRIVATE_KEY, SENDER_ADDRESS, TO_ADDRESS)
|
||||
});
|
||||
|
||||
describe('Gov module', () => {
|
||||
test('Send deposit', async () => {
|
||||
const depositParams = {
|
||||
proposalId: 1,
|
||||
amount: '10',
|
||||
denom: 'aphoton',
|
||||
}
|
||||
|
||||
await sendDeposit(SENDER_PRIVATE_KEY, SENDER_ADDRESS, depositParams)
|
||||
})
|
||||
|
||||
test('Send vote', async () => {
|
||||
const voteParams = {
|
||||
proposalId: 1,
|
||||
option: 1
|
||||
}
|
||||
|
||||
await sendVote(SENDER_PRIVATE_KEY, SENDER_ADDRESS, voteParams)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Bond module', () => {
|
||||
test('Create bond', async () => {
|
||||
const bondParams = {
|
||||
amount: '100',
|
||||
denom: 'aphoton',
|
||||
}
|
||||
|
||||
await createBond(SENDER_PRIVATE_KEY, SENDER_ADDRESS, bondParams)
|
||||
})
|
||||
})
|
||||
+111
-166
@@ -1,199 +1,144 @@
|
||||
import axios from "axios";
|
||||
import { MessageTypes, signTypedData, SignTypedDataVersion } from "@metamask/eth-sig-util";
|
||||
import { generateEndpointAccount, generateEndpointBroadcast, generatePostBodyBroadcast } from '@tharsis/provider';
|
||||
|
||||
import isUrl from 'is-url';
|
||||
import { sha256 } from 'js-sha256';
|
||||
import { generatePostBodyBroadcast } from '@tharsis/provider';
|
||||
import {
|
||||
createMessageSend,
|
||||
createTxRawEIP712,
|
||||
signatureToWeb3Extension,
|
||||
createTxMsgVote,
|
||||
Chain,
|
||||
Sender,
|
||||
MessageMsgVote
|
||||
Fee,
|
||||
} from '@tharsis/transactions'
|
||||
|
||||
import { createTxMsgDeposit, MessageMsgDeposit } from "./gov";
|
||||
import { createTxMsgCreateBond, createTxMsgRefillBond, MessageMsgCreateBond, MessageMsgRefillBond } from "./bond";
|
||||
import { createTxMsgCreateBond, MessageMsgCreateBond } from "./bond";
|
||||
import { RegistryClient } from "./registry-client";
|
||||
import { Account } from "./account";
|
||||
import { createTransaction } from "./txbuilder";
|
||||
|
||||
const ETHERMINT_REST_ENDPOINT = 'http://127.0.0.1:1317'
|
||||
const DEFAULT_WRITE_ERROR = 'Unable to write to chiba-clonk.';
|
||||
|
||||
interface TypedMessageDomain {
|
||||
name?: string;
|
||||
version?: string;
|
||||
chainId?: number;
|
||||
verifyingContract?: string;
|
||||
salt?: ArrayBuffer;
|
||||
}
|
||||
|
||||
export const sendTokens = async (senderPrivateKey: string, senderAddress: string, destinationAddress: string) => {
|
||||
let { data: addrData} = await axios.get(`${ETHERMINT_REST_ENDPOINT}${generateEndpointAccount(senderAddress)}`)
|
||||
|
||||
const chain = {
|
||||
chainId: 9000,
|
||||
cosmosChainId: 'ethermint_9000-1',
|
||||
}
|
||||
|
||||
const sender = {
|
||||
accountAddress: addrData.account.base_account.address,
|
||||
sequence: addrData.account.base_account.sequence,
|
||||
accountNumber: addrData.account.base_account.account_number,
|
||||
pubkey: addrData.account.base_account.pub_key.key,
|
||||
}
|
||||
export const DEFAULT_CHAIN_ID = 'ethermint_9000-1';
|
||||
|
||||
// Parse Tx response from cosmos-sdk.
|
||||
export const parseTxResponse = (result: any) => {
|
||||
const { txhash: hash, height, ...txResponse } = result;
|
||||
txResponse.data = txResponse.data && Buffer.from(txResponse.data, 'base64').toString('utf8');
|
||||
txResponse.log = JSON.parse(txResponse.raw_log);
|
||||
|
||||
txResponse.events.forEach((event:any) => {
|
||||
event.attributes = event.attributes.map(({ key, value }: { key: string, value: string }) => ({
|
||||
key: Buffer.from(key, 'base64').toString('utf8'),
|
||||
value: Buffer.from(value, 'base64').toString('utf8')
|
||||
}));
|
||||
});
|
||||
|
||||
return { hash, height, ...txResponse };
|
||||
};
|
||||
|
||||
export const isKeyValid = (key: string) => key && key.match(/^[0-9a-fA-F]{64}$/);
|
||||
|
||||
export class Registry {
|
||||
_endpoint: string
|
||||
_chain: Chain
|
||||
_client: RegistryClient
|
||||
|
||||
static processWriteError(error: Error) {
|
||||
/**
|
||||
Example:
|
||||
|
||||
const fee = {
|
||||
amount: '20',
|
||||
denom: 'aphoton',
|
||||
gas: '200000',
|
||||
{
|
||||
message: '{"code":18,"data":null,"log":"invalid request: Name already reserved.: failed to execute message; message index: 0","info":"","gasWanted":"200000","gasUsed":"86717","events":[],"codespace":"sdk"}',
|
||||
path: [ 'submit' ]
|
||||
}g
|
||||
*/
|
||||
const message = JSON.parse(error.message);
|
||||
return message.log || DEFAULT_WRITE_ERROR;
|
||||
}
|
||||
|
||||
const memo = ''
|
||||
|
||||
const params = {
|
||||
destinationAddress: destinationAddress,
|
||||
amount: '10',
|
||||
denom: 'aphoton',
|
||||
}
|
||||
|
||||
// Create a MsgSend transaction.
|
||||
const msg = createMessageSend(chain, sender, fee, memo, params)
|
||||
|
||||
await signAndSendMessage(senderPrivateKey, chain, sender, msg)
|
||||
}
|
||||
constructor(url: string, cosmosChainId = DEFAULT_CHAIN_ID) {
|
||||
if (!isUrl(url)) {
|
||||
throw new Error('Path to a registry GQL endpoint should be provided.');
|
||||
}
|
||||
|
||||
export const sendVote = async (senderPrivateKey: string, senderAddress: string, params: MessageMsgVote) => {
|
||||
let { data: addrData} = await axios.get(`${ETHERMINT_REST_ENDPOINT}${generateEndpointAccount(senderAddress)}`)
|
||||
this._endpoint = url;
|
||||
this._client = new RegistryClient(url);
|
||||
|
||||
const chain = {
|
||||
chainId: 9000,
|
||||
cosmosChainId: 'ethermint_9000-1',
|
||||
this._chain = {
|
||||
chainId: 9000,
|
||||
cosmosChainId
|
||||
}
|
||||
}
|
||||
|
||||
const sender = {
|
||||
accountAddress: addrData.account.base_account.address,
|
||||
sequence: addrData.account.base_account.sequence,
|
||||
accountNumber: addrData.account.base_account.account_number,
|
||||
pubkey: addrData.account.base_account.pub_key.key,
|
||||
/**
|
||||
* Get account by addresses.
|
||||
*/
|
||||
async getAccount(address: string) {
|
||||
return this._client.getAccount(address);
|
||||
}
|
||||
|
||||
const fee = {
|
||||
amount: '20',
|
||||
denom: 'aphoton',
|
||||
gas: '200000',
|
||||
}
|
||||
|
||||
const memo = ''
|
||||
/**
|
||||
* Computes the next bondId for the given account private key.
|
||||
*/
|
||||
async getNextBondId(address: string) {
|
||||
let result;
|
||||
|
||||
const msg = createTxMsgVote(chain, sender, fee, memo, params)
|
||||
await signAndSendMessage(senderPrivateKey, chain, sender, msg)
|
||||
}
|
||||
try {
|
||||
const { account } = await this.getAccount(address);
|
||||
const accountObj = account.base_account;
|
||||
|
||||
export const sendDeposit = async (senderPrivateKey: string, senderAddress: string, params: MessageMsgDeposit) => {
|
||||
let { data: addrData} = await axios.get(`${ETHERMINT_REST_ENDPOINT}${generateEndpointAccount(senderAddress)}`)
|
||||
|
||||
const chain = {
|
||||
chainId: 9000,
|
||||
cosmosChainId: 'ethermint_9000-1',
|
||||
}
|
||||
const nextSeq = parseInt(accountObj.sequence, 10) + 1;
|
||||
result = sha256(`${accountObj.address}:${accountObj.number}:${nextSeq}`);
|
||||
} catch (err: any) {
|
||||
const error = err[0] || err;
|
||||
throw new Error(Registry.processWriteError(error));
|
||||
}
|
||||
|
||||
const sender = {
|
||||
accountAddress: addrData.account.base_account.address,
|
||||
sequence: addrData.account.base_account.sequence,
|
||||
accountNumber: addrData.account.base_account.account_number,
|
||||
pubkey: addrData.account.base_account.pub_key.key,
|
||||
return result;
|
||||
}
|
||||
|
||||
const fee = {
|
||||
amount: '20',
|
||||
denom: 'aphoton',
|
||||
gas: '200000',
|
||||
}
|
||||
|
||||
const memo = ''
|
||||
|
||||
const msg = createTxMsgDeposit(chain, sender, fee, memo, params)
|
||||
await signAndSendMessage(senderPrivateKey, chain, sender, msg)
|
||||
}
|
||||
/**
|
||||
* Create bond.
|
||||
*/
|
||||
async createBond(params: MessageMsgCreateBond, senderAddress: string, privateKey: string, fee: Fee) {
|
||||
let result;
|
||||
|
||||
export const createBond = async (senderPrivateKey: string, senderAddress: string, params: MessageMsgCreateBond) => {
|
||||
let { data: addrData} = await axios.get(`${ETHERMINT_REST_ENDPOINT}${generateEndpointAccount(senderAddress)}`)
|
||||
try {
|
||||
const { account: { base_account: accountInfo } } = await this.getAccount(senderAddress);
|
||||
|
||||
const chain = {
|
||||
chainId: 9000,
|
||||
cosmosChainId: 'ethermint_9000-1',
|
||||
}
|
||||
const sender = {
|
||||
accountAddress: accountInfo.address,
|
||||
sequence: accountInfo.sequence,
|
||||
accountNumber: accountInfo.account_number,
|
||||
pubkey: accountInfo.pub_key.key,
|
||||
}
|
||||
|
||||
const sender = {
|
||||
accountAddress: addrData.account.base_account.address,
|
||||
sequence: addrData.account.base_account.sequence,
|
||||
accountNumber: addrData.account.base_account.account_number,
|
||||
pubkey: addrData.account.base_account.pub_key.key,
|
||||
}
|
||||
const msg = createTxMsgCreateBond(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));
|
||||
}
|
||||
|
||||
const fee = {
|
||||
amount: '20',
|
||||
denom: 'aphoton',
|
||||
gas: '200000',
|
||||
return parseTxResponse(result);
|
||||
}
|
||||
|
||||
const memo = ''
|
||||
/**
|
||||
* Submit a generic Tx to the chain.
|
||||
*/
|
||||
async _submitTx(message: any, privateKey: string, sender: Sender) {
|
||||
// Check private key.
|
||||
if (!isKeyValid(privateKey)) {
|
||||
throw new Error('Registry privateKey should be a hex string.');
|
||||
}
|
||||
|
||||
const msg = createTxMsgCreateBond(chain, sender, fee, memo, params)
|
||||
await signAndSendMessage(senderPrivateKey, chain, sender, msg)
|
||||
}
|
||||
// Check that the account exists on-chain.
|
||||
const account = new Account(Buffer.from(privateKey, 'hex'));
|
||||
|
||||
export const refillBond = async (senderPrivateKey: string, senderAddress: string, params: MessageMsgRefillBond) => {
|
||||
let { data: addrData} = await axios.get(`${ETHERMINT_REST_ENDPOINT}${generateEndpointAccount(senderAddress)}`)
|
||||
// Generate signed Tx.
|
||||
const transaction = createTransaction(message, account, sender, this._chain);
|
||||
|
||||
const chain = {
|
||||
chainId: 9000,
|
||||
cosmosChainId: 'ethermint_9000-1',
|
||||
}
|
||||
const tx = generatePostBodyBroadcast(transaction)
|
||||
|
||||
const sender = {
|
||||
accountAddress: addrData.account.base_account.address,
|
||||
sequence: addrData.account.base_account.sequence,
|
||||
accountNumber: addrData.account.base_account.account_number,
|
||||
pubkey: addrData.account.base_account.pub_key.key,
|
||||
// Submit Tx to chain.
|
||||
const { tx_response: response } = await this._client.submit(tx);
|
||||
return response;
|
||||
}
|
||||
|
||||
const fee = {
|
||||
amount: '20',
|
||||
denom: 'aphoton',
|
||||
gas: '200000',
|
||||
}
|
||||
|
||||
const memo = ''
|
||||
|
||||
const msg = createTxMsgRefillBond(chain, sender, fee, memo, params)
|
||||
await signAndSendMessage(senderPrivateKey, chain, sender, msg)
|
||||
}
|
||||
|
||||
const signAndSendMessage = async (senderPrivateKey: string, chain: Chain, sender: Sender, msg: any) => {
|
||||
const eipMessageDomain: any = msg.eipToSign.domain;
|
||||
|
||||
// Sign transaction.
|
||||
const signature = signTypedData({
|
||||
data: {
|
||||
types: msg.eipToSign.types as MessageTypes,
|
||||
primaryType: msg.eipToSign.primaryType,
|
||||
domain: eipMessageDomain as TypedMessageDomain,
|
||||
message: msg.eipToSign.message as Record<string, unknown>
|
||||
},
|
||||
privateKey: Buffer.from(senderPrivateKey, 'hex'),
|
||||
version: SignTypedDataVersion.V4
|
||||
})
|
||||
|
||||
let extension = signatureToWeb3Extension(chain, sender, signature)
|
||||
|
||||
// Create the txRaw.
|
||||
let rawTx = createTxRawEIP712(msg.legacyAmino.body, msg.legacyAmino.authInfo, extension)
|
||||
|
||||
const body = generatePostBodyBroadcast(rawTx)
|
||||
|
||||
// Broadcast transaction.
|
||||
return axios.post(
|
||||
`${ETHERMINT_REST_ENDPOINT}${generateEndpointBroadcast()}`,
|
||||
JSON.parse(body)
|
||||
)
|
||||
|
||||
// TODO: Check for successful broadcast.
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import assert from 'assert';
|
||||
import axios from 'axios';
|
||||
import { generateEndpointAccount, generateEndpointBroadcast, generatePostBodyBroadcast } from '@tharsis/provider';
|
||||
|
||||
/**
|
||||
* Registry
|
||||
*/
|
||||
export class RegistryClient {
|
||||
_endpoint: string
|
||||
|
||||
/**
|
||||
* New Client.
|
||||
* @param {string} endpoint
|
||||
* @param {object} options
|
||||
*/
|
||||
constructor(endpoint: string) {
|
||||
assert(endpoint);
|
||||
|
||||
this._endpoint = endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Account.
|
||||
*/
|
||||
async getAccount(address: string) {
|
||||
assert(address);
|
||||
|
||||
let { data } = await axios.get(`${this._endpoint}${generateEndpointAccount(address)}`)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit transaction.
|
||||
*/
|
||||
async submit(tx: string) {
|
||||
assert(tx);
|
||||
|
||||
// Broadcast transaction.
|
||||
const { data } = await axios.post(
|
||||
`${this._endpoint}${generateEndpointBroadcast()}`,
|
||||
tx
|
||||
)
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
const DEFAULT_PRIVATE_KEY = '0451f0bd95c855d52e76cdc8dd06f29097b944bfef26d3455725157f9133f4e0';
|
||||
const DEFAULT_ADDRESS = 'ethm19n3je0lhuk0w9kmkftsuw4etn8lmpu3jjfayeh'
|
||||
|
||||
export const getConfig = () => ({
|
||||
mockServer: process.env.MOCK_SERVER || false,
|
||||
chibaClonk: {
|
||||
chainId: process.env.CHIBA_CLONK_CHAIN_ID || 'ethermint_9000-1',
|
||||
privateKey: DEFAULT_PRIVATE_KEY,
|
||||
accountAddress: DEFAULT_ADDRESS,
|
||||
endpoint: process.env.CHIBA_CLONK_ENDPOINT || 'http://localhost:1317',
|
||||
fee: {
|
||||
amount: '20',
|
||||
denom: 'aphoton',
|
||||
gas: '200000',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import assert from 'assert';
|
||||
import {
|
||||
createTxRawEIP712,
|
||||
signatureToWeb3Extension,
|
||||
Chain,
|
||||
Sender
|
||||
} from '@tharsis/transactions'
|
||||
|
||||
import { Account } from './account';
|
||||
|
||||
/**
|
||||
* Generate a cosmos-sdk transaction.
|
||||
*/
|
||||
export const createTransaction = (message: any, account: Account, sender: Sender, chain: Chain) => {
|
||||
assert(message);
|
||||
assert(account);
|
||||
|
||||
// Sign transaction.
|
||||
const signature = account.sign(message);
|
||||
|
||||
let extension = signatureToWeb3Extension(chain, sender, signature)
|
||||
|
||||
// Create the txRaw.
|
||||
return createTxRawEIP712(message.legacyAmino.body, message.legacyAmino.authInfo, extension)
|
||||
};
|
||||
Reference in New Issue
Block a user