Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create NobleClient for interacting with noble #78

Merged
merged 4 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 188 additions & 27 deletions v4-client-js/__native__/__ios__/v4-native-client.js

Large diffs are not rendered by default.

117 changes: 117 additions & 0 deletions v4-client-js/examples/noble_example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { EncodeObject } from '@cosmjs/proto-signing';
import Long from 'long';

import { Network } from '../src/clients/constants';
import LocalWallet from '../src/clients/modules/local-wallet';
import { NobleClient } from '../src/clients/noble-client';
import { ValidatorClient } from '../src/clients/validator-client';
import { BECH32_PREFIX, NOBLE_BECH32_PREFIX } from '../src/lib/constants';
import { sleep } from '../src/lib/utils';
import { DYDX_TEST_MNEMONIC } from './constants';

async function test(): Promise<void> {
const dydxClient = await ValidatorClient.connect(
Network.testnet().validatorConfig,
);

const dydxWallet = await LocalWallet.fromMnemonic(
DYDX_TEST_MNEMONIC,
BECH32_PREFIX,
);
const nobleWallet = await LocalWallet.fromMnemonic(
DYDX_TEST_MNEMONIC,
NOBLE_BECH32_PREFIX,
);

const client = await NobleClient.connect('https://rpc.testnet.noble.strange.love', nobleWallet);

if (nobleWallet.address === undefined || dydxWallet.address === undefined) {
throw new Error('Wallet not found');
}

// IBC to noble

const ibcToNobleMsg: EncodeObject = {
typeUrl: '/ibc.applications.transfer.v1.MsgTransfer',
value: {
sourcePort: 'transfer',
sourceChannel: 'channel-0',
token: {
denom:
'ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5',
amount: '1000000',
},
sender: dydxWallet.address,
receiver: nobleWallet.address,
timeoutTimestamp: Long.fromNumber(
Math.floor(Date.now() / 1000) * 1e9 + 10 * 60 * 1e9,
),
},
};

const msgs = [ibcToNobleMsg];
const encodeObjects: Promise<EncodeObject[]> = new Promise((resolve) => resolve(msgs),
);

await dydxClient.post.send(
dydxWallet,
() => {
return encodeObjects;
},
false,
undefined,
undefined,
);

await sleep(30000);

try {
const coins = await client.getAccountBalances(nobleWallet.address);
console.log('Balances');
console.log(JSON.stringify(coins));

// IBC from noble

const ibcFromNobleMsg: EncodeObject = {
typeUrl: '/ibc.applications.transfer.v1.MsgTransfer',
value: {
sourcePort: 'transfer',
sourceChannel: 'channel-21',
token: {
denom: 'uusdc',
amount: coins[0].amount,
},
sender: nobleWallet.address,
receiver: dydxWallet.address,
timeoutTimestamp: Long.fromNumber(
Math.floor(Date.now() / 1000) * 1e9 + 10 * 60 * 1e9,
),
},
};
const fee = await client.simulateTransaction([ibcFromNobleMsg]);

ibcFromNobleMsg.value.token.amount = (parseInt(ibcFromNobleMsg.value.token.amount, 10) -
Math.floor(parseInt(fee.amount[0].amount, 10) * 1.4)).toString();

await client.send([ibcFromNobleMsg]);
} catch (error) {
console.log(JSON.stringify(error.message));
}

await sleep(30000);

try {
const coin = await client.getAccountBalance(nobleWallet.address, 'uusdc');
console.log('Balance');
console.log(JSON.stringify(coin));
} catch (error) {
console.log(JSON.stringify(error.message));
}
}

test()
.then(() => {})
.catch((error) => {
console.log(error.message);
console.log(error);
});
4 changes: 2 additions & 2 deletions v4-client-js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion v4-client-js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dydxprotocol/v4-client-js",
"version": "1.0.4",
"version": "1.0.5",
"description": "General client library for the new dYdX system (v4 decentralized)",
"main": "build/src/index.js",
"scripts": {
Expand Down
2 changes: 2 additions & 0 deletions v4-client-js/src/clients/modules/local-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default class LocalWallet {
address?: string;
pubKey?: Secp256k1Pubkey;
signer?: TransactionSigner;
offlineSigner?: OfflineSigner;

static async fromOfflineSigner(signer:OfflineSigner): Promise<LocalWallet> {
const wallet = new LocalWallet();
Expand All @@ -42,6 +43,7 @@ export default class LocalWallet {
}

async setSigner(signer: OfflineSigner): Promise<void> {
this.offlineSigner = signer;
const stargateClient = await SigningStargateClient.offline(
signer,
{
Expand Down
82 changes: 81 additions & 1 deletion v4-client-js/src/clients/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Order_Side, Order_TimeInForce } from '@dydxprotocol/v4-proto/src/codege
import * as AuthModule from 'cosmjs-types/cosmos/auth/v1beta1/query';
import Long from 'long';

import { BECH32_PREFIX } from '../lib/constants';
import { BECH32_PREFIX, GAS_MULTIPLIER, NOBLE_BECH32_PREFIX } from '../lib/constants';
import { UserError } from '../lib/errors';
import { ByteArrayEncoding, encodeJson } from '../lib/helpers';
import { deriveHDKeyFromEthereumSignature } from '../lib/onboarding';
Expand All @@ -19,6 +19,7 @@ import {
} from './constants';
import { FaucetClient } from './faucet-client';
import LocalWallet from './modules/local-wallet';
import { NobleClient } from './noble-client';
import { SubaccountInfo } from './subaccount';
import { OrderFlags } from './types';

Expand All @@ -29,6 +30,27 @@ declare global {
var faucetClient: FaucetClient | null;
// eslint-disable-next-line vars-on-top, no-var
var wallet: LocalWallet;

// eslint-disable-next-line vars-on-top, no-var
var nobleValidatorUrl: string | undefined;
// eslint-disable-next-line vars-on-top, no-var
var nobleWallet: LocalWallet | undefined;
// eslint-disable-next-line vars-on-top, no-var
var nobleClient: NobleClient | undefined;
}

async function getNobleClient(): Promise<NobleClient | undefined> {
if (
globalThis.nobleClient === undefined &&
globalThis.nobleValidatorUrl !== undefined &&
globalThis.nobleWallet !== undefined
) {
globalThis.nobleClient = await NobleClient.connect(
globalThis.nobleValidatorUrl,
globalThis.nobleWallet,
);
}
return Promise.resolve(globalThis.nobleClient);
}

export async function connectClient(
Expand All @@ -53,6 +75,7 @@ export async function connectNetwork(
validatorUrl,
chainId,
faucetUrl,
nobleValidatorUrl,
USDC_DENOM,
USDC_DECIMALS,
USDC_GAS_DENOM,
Expand Down Expand Up @@ -90,6 +113,8 @@ export async function connectNetwork(
} else {
globalThis.faucetClient = null;
}
globalThis.nobleValidatorUrl = nobleValidatorUrl;
globalThis.nobleClient = undefined;
return encodeJson(config);
} catch (e) {
return wrappedError(e);
Expand All @@ -101,6 +126,10 @@ export async function connectWallet(
): Promise<string> {
try {
globalThis.wallet = await LocalWallet.fromMnemonic(mnemonic, BECH32_PREFIX);
globalThis.nobleWallet = await LocalWallet.fromMnemonic(
mnemonic,
NOBLE_BECH32_PREFIX,
);
const address = globalThis.wallet.address!;
return encodeJson({ address });
} catch (e) {
Expand Down Expand Up @@ -988,3 +1017,54 @@ export async function getMarketPrice(
return wrappedError(e);
}
}
export async function getNobleBalance(): Promise<String> {
try {
const client = await getNobleClient();
if (client === undefined) {
throw new UserError(
'client is not connected.',
);
}
if (globalThis.nobleWallet?.address === undefined) {
throw new UserError('wallet is not set. Call connectWallet() first');
}
const coin = await client.getAccountBalance(globalThis.nobleWallet.address, 'uusdc');
return encodeJson(coin);
} catch (error) {
return wrappedError(error);
}
}

export async function sendNobleIBC(squidPayload: string): Promise<String> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this only accept squidPayloads or does it except generic payloads as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it does specifically have to be a squid IBC payload as I parse the json here with that expectation. Similar to withdrawToIBC in this file

try {
const client = await getNobleClient();
if (client === undefined) {
throw new UserError(
'client is not connected.',
);
}
if (globalThis.nobleWallet?.address === undefined) {
throw new UserError('wallet is not set. Call connectWallet() first');
}

const decode = (str: string): string => Buffer.from(str, 'base64').toString('binary');
const decoded = decode(squidPayload);

const json = JSON.parse(decoded);

const ibcMsg: EncodeObject = {
typeUrl: json.msgTypeUrl, // '/ibc.applications.transfer.v1.MsgTransfer',
value: json.msg,
};
const fee = await client.simulateTransaction([ibcMsg]);

// take out fee from amount before sweeping
ibcMsg.value.token.amount = (parseInt(ibcMsg.value.token.amount, 10) -
rosepuppy marked this conversation as resolved.
Show resolved Hide resolved
Math.floor(parseInt(fee.amount[0].amount, 10) * GAS_MULTIPLIER)).toString();

const tx = await client.send([ibcMsg]);
return encodeJson(tx);
} catch (error) {
return wrappedError(error);
}
}
100 changes: 100 additions & 0 deletions v4-client-js/src/clients/noble-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { EncodeObject, Registry, Coin } from '@cosmjs/proto-signing';
import {
calculateFee,
DeliverTxResponse,
GasPrice,
StdFee,
defaultRegistryTypes,
SigningStargateClient,
} from '@cosmjs/stargate';

import { GAS_MULTIPLIER } from './constants';
import LocalWallet from './modules/local-wallet';

export class NobleClient {
private stargateClient?: SigningStargateClient;
private wallet?: LocalWallet;

static async connect(
restEndpoint: string,
wallet: LocalWallet,
): Promise<NobleClient> {
const client = new NobleClient();
await client.initialize(restEndpoint, wallet);
return client;
}

private async initialize(
restEndpoint: string,
wallet: LocalWallet,
): Promise<void> {
if (wallet?.offlineSigner === undefined) {
throw new Error('Wallet signer not found');
}
this.wallet = wallet;
this.stargateClient = await SigningStargateClient.connectWithSigner(
restEndpoint,
wallet.offlineSigner,
{ registry: new Registry(defaultRegistryTypes) },
);
}

getAccountBalances(address: string): Promise<readonly Coin[]> {
if (!this.stargateClient) {
throw new Error('stargateClient not initialized');
}
return this.stargateClient.getAllBalances(address);
}

getAccountBalance(address: string, denom: string): Promise<Coin> {
if (!this.stargateClient) {
throw new Error('stargateClient not initialized');
}
return this.stargateClient.getBalance(address, denom);
}

async send(
messages: EncodeObject[],
gasPrice: GasPrice = GasPrice.fromString('0.025uusdc'),
memo?: string,
): Promise<DeliverTxResponse> {
if (!this.stargateClient) {
throw new Error('NobleClient stargateClient not initialized');
}
if (this.wallet?.address === undefined) {
throw new Error('NobleClient wallet not initialized');
}
// Simulate to get the gas estimate
const fee = await this.simulateTransaction(messages, gasPrice, memo);

// Sign and broadcast the transaction
return this.stargateClient.signAndBroadcast(
this.wallet.address,
messages,
fee,
memo ?? '',
);
}

async simulateTransaction(
messages: readonly EncodeObject[],
gasPrice: GasPrice = GasPrice.fromString('0.025uusdc'),
memo?: string,
): Promise<StdFee> {
if (!this.stargateClient) {
throw new Error('NobleClient stargateClient not initialized');
}
if (this.wallet?.address === undefined) {
throw new Error('NobleClient wallet not initialized');
}
// Get simulated response
const gasEstimate = await this.stargateClient.simulate(
this.wallet?.address,
messages,
memo,
);

// Calculate and return the fee
return calculateFee(Math.floor(gasEstimate * GAS_MULTIPLIER), gasPrice);
}
}
1 change: 1 addition & 0 deletions v4-client-js/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { StdFee } from '@cosmjs/stargate';

// Bech32 Prefix
export const BECH32_PREFIX = 'dydx';
export const NOBLE_BECH32_PREFIX = 'noble';

// Broadcast Defaults
export const BROADCAST_POLL_INTERVAL_MS: number = 300;
Expand Down