@dydxprotocol/v4-client-js 3.0.2 → 3.0.4

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.
@@ -1,19 +1,29 @@
1
- import { toBase64 } from '@cosmjs/encoding';
2
- import { Order_TimeInForce } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/clob/order';
1
+ import Long from 'long';
3
2
 
4
3
  import { BECH32_PREFIX } from '../src';
5
4
  import { CompositeClient } from '../src/clients/composite-client';
6
- import { AuthenticatorType, Network, OrderSide, SelectedGasDenom } from '../src/clients/constants';
5
+ import {
6
+ Network,
7
+ OrderExecution,
8
+ OrderSide,
9
+ OrderTimeInForce,
10
+ OrderType,
11
+ SelectedGasDenom,
12
+ } from '../src/clients/constants';
7
13
  import LocalWallet from '../src/clients/modules/local-wallet';
8
14
  import { SubaccountInfo } from '../src/clients/subaccount';
9
- import { DYDX_TEST_MNEMONIC, DYDX_TEST_MNEMONIC_2, DYDX_TEST_MNEMONIC_3 } from './constants';
15
+ import {
16
+ createNewRandomDydxWallet,
17
+ getAuthorizeNewTradingKeyArguments,
18
+ getAuthorizedTradingKeysMetadata,
19
+ } from '../src/lib/trading-key-utils';
20
+ import { DYDX_TEST_MNEMONIC, DYDX_TEST_MNEMONIC_2 } from './constants';
10
21
 
11
22
  async function test(): Promise<void> {
12
23
  const wallet1 = await LocalWallet.fromMnemonic(DYDX_TEST_MNEMONIC, BECH32_PREFIX);
13
24
  const wallet2 = await LocalWallet.fromMnemonic(DYDX_TEST_MNEMONIC_2, BECH32_PREFIX);
14
- const wallet3 = await LocalWallet.fromMnemonic(DYDX_TEST_MNEMONIC_3, BECH32_PREFIX);
15
25
 
16
- const network = Network.staging();
26
+ const network = Network.testnet();
17
27
  const client = await CompositeClient.connect(network);
18
28
  client.setSelectedGasDenom(SelectedGasDenom.NATIVE);
19
29
 
@@ -22,7 +32,6 @@ async function test(): Promise<void> {
22
32
 
23
33
  const subaccount1 = SubaccountInfo.forLocalWallet(wallet1, 0);
24
34
  const subaccount2 = SubaccountInfo.forLocalWallet(wallet2, 0);
25
- const subaccount3 = SubaccountInfo.forLocalWallet(wallet3, 0);
26
35
 
27
36
  // Change second wallet pubkey
28
37
  // Add an authenticator to allow wallet2 to place orders
@@ -31,7 +40,12 @@ async function test(): Promise<void> {
31
40
  const authsBefore = await client.getAuthenticators(wallet1.address!);
32
41
  const beforeCount = authsBefore.accountAuthenticators.length;
33
42
  console.log(`Authenticators before: ${beforeCount}`);
34
- await addTestAuthenticator(client, subaccount1, wallet2.pubKey!.value);
43
+
44
+ const apiTradingWalletInfo1 = await createApiTradingWallet(client, subaccount1);
45
+ const apiTradingWallet1 = await LocalWallet.fromPrivateKey(
46
+ apiTradingWalletInfo1.privteKeyHex,
47
+ BECH32_PREFIX,
48
+ );
35
49
 
36
50
  console.log('** Waiting 3 seconds for txn to be confirmed **');
37
51
  await new Promise((resolve) => setTimeout(resolve, 3000));
@@ -45,21 +59,29 @@ async function test(): Promise<void> {
45
59
  } else {
46
60
  console.log('Authenticator count incremented by 1 as expected.');
47
61
  }
48
- // Last element in authenticators array is the most recently created
49
- const lastElement = authsAfter.accountAuthenticators.length - 1;
50
- const newAuthenticatorID = authsAfter.accountAuthenticators[lastElement].id;
51
62
 
63
+ const parsedAuths = getAuthorizedTradingKeysMetadata(authsAfter.accountAuthenticators);
64
+ const newAuthenticatorID = parsedAuths.find(
65
+ (a) => apiTradingWallet1.address != null && a.address === apiTradingWallet1.address,
66
+ )?.id;
67
+ if (newAuthenticatorID == null) {
68
+ console.error(
69
+ 'Unable to locate the created authenticator. Address: ',
70
+ apiTradingWallet1.address,
71
+ );
72
+ throw new Error('Unable to locate the new authenticator');
73
+ }
52
74
  console.log(`New authenticator ID: ${newAuthenticatorID}`);
53
75
 
54
76
  // Placing order using subaccount2 for subaccount1 succeeds
55
77
  console.log(
56
78
  '** Placing order for subaccount1 with subaccount2 + authenticator, should succeed **',
57
79
  );
58
- await placeOrder(client, subaccount2, subaccount1, newAuthenticatorID);
80
+ await placeOrder(client, apiTradingWallet1, subaccount1.address, newAuthenticatorID);
59
81
 
60
- // Placing order using subaccount3 for subaccount1 should fail
82
+ // Placing order using subaccount2 for subaccount1 should fail
61
83
  console.log('** Placing order for subaccount1 with subaccount3 + authenticator, should fail **');
62
- await placeOrder(client, subaccount3, subaccount1, newAuthenticatorID);
84
+ await placeOrder(client, subaccount2.signingWallet, subaccount1.address, newAuthenticatorID);
63
85
 
64
86
  // Remove authenticator
65
87
  console.log('** Removing authenticator **');
@@ -70,91 +92,65 @@ async function test(): Promise<void> {
70
92
 
71
93
  // Placing an order using subaccount2 will now fail
72
94
  console.log('** Placing order with removed authenticator should fail **');
73
- await placeOrder(client, subaccount2, subaccount1, newAuthenticatorID);
95
+ await placeOrder(client, apiTradingWallet1, subaccount1.address, newAuthenticatorID);
74
96
  }
75
97
 
76
98
  async function removeAuthenticator(
77
99
  client: CompositeClient,
78
100
  subaccount: SubaccountInfo,
79
- id: Long,
101
+ id: string,
80
102
  ): Promise<void> {
81
- await client.removeAuthenticator(subaccount, id.toString());
103
+ await client.removeAuthenticator(subaccount, id);
82
104
  }
83
105
 
84
- async function addTestAuthenticator(
106
+ async function createApiTradingWallet(
85
107
  client: CompositeClient,
86
108
  subaccount: SubaccountInfo,
87
- authedPubKey: string,
88
- ): Promise<void> {
89
- const msgType = (s: string): string => toBase64(new TextEncoder().encode(s));
90
- const anyOfSubAuth = [
91
- {
92
- type: AuthenticatorType.MESSAGE_FILTER,
93
- config: msgType('/dydxprotocol.clob.MsgPlaceOrder'),
94
- },
95
- {
96
- type: AuthenticatorType.MESSAGE_FILTER,
97
- config: msgType('/dydxprotocol.sending.MsgCreateTransfer'),
98
- },
99
- ];
100
-
101
- // Nested AnyOf config must be base64(JSON([...])) as on-chain expects []byte
102
- const anyOfConfigB64 = toBase64(new TextEncoder().encode(JSON.stringify(anyOfSubAuth)));
103
-
104
- const subAuth = [
105
- {
106
- type: AuthenticatorType.SIGNATURE_VERIFICATION,
107
- config: authedPubKey,
108
- },
109
- {
110
- type: AuthenticatorType.ANY_OF,
111
- config: anyOfConfigB64,
112
- },
113
- ];
114
-
115
- const jsonString = JSON.stringify(subAuth);
116
- const encodedData = new TextEncoder().encode(jsonString);
117
-
118
- await client.addAuthenticator(subaccount, AuthenticatorType.ALL_OF, encodedData);
109
+ ): Promise<{ privteKeyHex: string; forDydxAddress: string }> {
110
+ const wallet = await createNewRandomDydxWallet();
111
+ if (wallet == null) {
112
+ throw new Error("Couldn't create wallet");
113
+ }
114
+ const { data, type } = getAuthorizeNewTradingKeyArguments({
115
+ generatedWalletPubKey: wallet?.publicKey,
116
+ });
117
+ console.log('adding', wallet.address);
118
+ await client.addAuthenticator(subaccount, type, data);
119
+ return { privteKeyHex: wallet.privateKeyHex, forDydxAddress: subaccount.address };
119
120
  }
120
121
 
121
122
  async function placeOrder(
122
123
  client: CompositeClient,
123
- fromAccount: SubaccountInfo,
124
- forAccount: SubaccountInfo,
125
- authenticatorId: Long,
124
+ apiTradingWallet: LocalWallet,
125
+ forAccountAddress: string,
126
+ authenticatorId: string,
126
127
  ): Promise<void> {
127
128
  try {
128
129
  const side = OrderSide.BUY;
129
- const price = Number('1000');
130
- const currentBlock = await client.validatorClient.get.latestBlockHeight();
131
- const nextValidBlockHeight = currentBlock + 5;
132
- const goodTilBlock = nextValidBlockHeight + 10;
133
-
134
- const timeInForce = Order_TimeInForce.TIME_IN_FORCE_UNSPECIFIED;
130
+ const price = Number('10000');
135
131
 
136
132
  const clientId = Math.floor(Math.random() * 10000);
137
133
 
138
- const tx = await client.placeShortTermOrder(
139
- SubaccountInfo.forPermissionedWallet(
140
- fromAccount.signingWallet,
141
- forAccount.address,
142
- forAccount.subaccountNumber,
143
- [authenticatorId],
144
- ),
134
+ const tx = await client.placeOrder(
135
+ SubaccountInfo.forPermissionedWallet(apiTradingWallet, forAccountAddress, 0, [
136
+ Long.fromString(authenticatorId),
137
+ ]),
145
138
  'ETH-USD',
139
+ OrderType.MARKET,
146
140
  side,
147
141
  price,
148
142
  0.01,
149
143
  clientId,
150
- goodTilBlock,
151
- timeInForce,
144
+ OrderTimeInForce.IOC,
145
+ 100,
146
+ OrderExecution.IOC,
147
+ false,
152
148
  false,
153
- undefined,
154
149
  );
155
150
  console.log('**Order Tx**');
156
151
  console.log(Buffer.from(tx.hash).toString('hex'));
157
- } catch (error) {
152
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
153
+ } catch (error: any) {
158
154
  console.log(error.message);
159
155
  }
160
156
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dydxprotocol/v4-client-js",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "description": "General client library for the new dYdX system (v4 decentralized)",
5
5
  "main": "build/cjs/src/index.js",
6
6
  "module": "build/esm/src/index.js",
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export * from './types';
4
4
  // Utility functions.
5
5
  export * as helpers from './lib/helpers';
6
6
  export * as onboarding from './lib/onboarding';
7
+ export * as tradingKeyUtils from './lib/trading-key-utils';
7
8
  export * as utils from './lib/utils';
8
9
  export * as validation from './lib/validation';
9
10
 
@@ -0,0 +1,136 @@
1
+ import { fromBase64, toBase64, toBech32, toHex } from '@cosmjs/encoding';
2
+ import { rawSecp256k1PubkeyToRawAddress } from '@cosmjs/tendermint-rpc';
3
+ import { generateMnemonic } from '@scure/bip39';
4
+ import { wordlist } from '@scure/bip39/wordlists/english';
5
+
6
+ import {
7
+ Authenticator,
8
+ AuthenticatorType,
9
+ TYPE_URL_BATCH_CANCEL,
10
+ TYPE_URL_MSG_CANCEL_ORDER,
11
+ TYPE_URL_MSG_PLACE_ORDER,
12
+ } from '../clients/constants';
13
+ import { type Get } from '../clients/modules/get';
14
+ import LocalWallet from '../clients/modules/local-wallet';
15
+ import { BECH32_PREFIX } from './constants';
16
+ import { deriveHDKeyFromMnemonic } from './onboarding';
17
+
18
+ export const createNewRandomDydxWallet = async (): Promise<
19
+ { privateKeyHex: string; mnemonic: string; publicKey: string; address: string } | undefined
20
+ > => {
21
+ const mnemonic = generateMnemonic(wordlist, 128);
22
+ const { privateKey: privateKeyRaw } = deriveHDKeyFromMnemonic(mnemonic);
23
+ const wallet = await LocalWallet.fromMnemonic(mnemonic, BECH32_PREFIX);
24
+
25
+ const privateKeyHex = privateKeyRaw != null ? `0x${toHex(privateKeyRaw)}` : undefined;
26
+ const publicKey = wallet.pubKey;
27
+ const address = wallet.address;
28
+
29
+ if (privateKeyHex == null || publicKey == null || address == null) {
30
+ return undefined;
31
+ }
32
+
33
+ return {
34
+ // 12 english words
35
+ mnemonic,
36
+ // toHex of the raw private key bits
37
+ privateKeyHex,
38
+ // base64, not hex
39
+ publicKey: publicKey.value,
40
+ // valid dydx address corresponding to the key pair
41
+ address,
42
+ };
43
+ };
44
+
45
+ // arguments to authorize a given wallet public key to trade on behalf of the user.
46
+ // allows place order, cancel order, batch cancel on subaccount 0 only.
47
+ export const getAuthorizeNewTradingKeyArguments = ({
48
+ generatedWalletPubKey,
49
+ }: {
50
+ generatedWalletPubKey: string;
51
+ }): { type: AuthenticatorType; data: Uint8Array } => {
52
+ const wrapAndEncode64 = (s: string): string => toBase64(new TextEncoder().encode(s));
53
+
54
+ const messageFilterSubAuth = [
55
+ {
56
+ type: AuthenticatorType.MESSAGE_FILTER,
57
+ config: wrapAndEncode64(TYPE_URL_MSG_PLACE_ORDER),
58
+ },
59
+ {
60
+ type: AuthenticatorType.MESSAGE_FILTER,
61
+ config: wrapAndEncode64(TYPE_URL_MSG_CANCEL_ORDER),
62
+ },
63
+ {
64
+ type: AuthenticatorType.MESSAGE_FILTER,
65
+ config: wrapAndEncode64(TYPE_URL_BATCH_CANCEL),
66
+ },
67
+ ];
68
+
69
+ const anyOfMessageFilterConfigB64 = wrapAndEncode64(JSON.stringify(messageFilterSubAuth));
70
+
71
+ const subAuth = [
72
+ {
73
+ type: AuthenticatorType.SIGNATURE_VERIFICATION,
74
+ config: generatedWalletPubKey,
75
+ },
76
+ {
77
+ type: AuthenticatorType.ANY_OF,
78
+ config: anyOfMessageFilterConfigB64,
79
+ },
80
+ // we limit to cross markets to make it slightly harder to drain user funds on low liquidity markets with trading keys
81
+ // could undo this in the future if people are angry about it
82
+ {
83
+ type: AuthenticatorType.SUBACCOUNT_FILTER,
84
+ config: wrapAndEncode64('0'),
85
+ },
86
+ ];
87
+
88
+ const jsonString = JSON.stringify(subAuth);
89
+ const encodedData = new TextEncoder().encode(jsonString);
90
+ const topLevelType = AuthenticatorType.ALL_OF;
91
+
92
+ return {
93
+ type: topLevelType,
94
+ data: encodedData,
95
+ };
96
+ };
97
+
98
+ // redeclared to keep sub bundle size small
99
+ type Awaited<T> = T extends Promise<infer U> ? U : T;
100
+ const isTruthy = <T>(n?: T | false | null | undefined | 0): n is T => Boolean(n);
101
+
102
+ // just parses out keys that match the format created in getAuthorizeNewTradingKeyArguments
103
+ export const getAuthorizedTradingKeysMetadata = (
104
+ authorizedKeys: Awaited<ReturnType<Get['getAuthenticators']>>['accountAuthenticators'],
105
+ ): Array<{ id: string; publicKey: string; address: string }> => {
106
+ return authorizedKeys
107
+ .map(({ config, id, type }) => {
108
+ if (type !== AuthenticatorType.ALL_OF) {
109
+ return null;
110
+ }
111
+ const parsedConfig = JSON.parse(new TextDecoder().decode(config)) as
112
+ | Authenticator[]
113
+ | undefined;
114
+ if (parsedConfig == null) {
115
+ return null;
116
+ }
117
+
118
+ const publicKey = parsedConfig.find(
119
+ (t) => t.type === AuthenticatorType.SIGNATURE_VERIFICATION,
120
+ )?.config;
121
+ if (publicKey == null || typeof publicKey !== 'string') {
122
+ return null;
123
+ }
124
+
125
+ const address = toBech32(
126
+ BECH32_PREFIX,
127
+ rawSecp256k1PubkeyToRawAddress(fromBase64(publicKey)),
128
+ );
129
+ return {
130
+ id: id.toString(),
131
+ publicKey,
132
+ address,
133
+ };
134
+ })
135
+ .filter(isTruthy);
136
+ };