Skip to content

Commit bdbab8c

Browse files
authored
feat: Add Authenticator flow for Permissioned Keys (#317)
Adds functionality to enable signing transactions using the authenticator flow on the protocol. - Helper function to add an authenticator - Helper function to remove an authenticator - Changes signing flow to accept an optional Autheticator and Wallet to sign a transaction with - E2E example showing how to add an Authenticator and place a transaction with an authenticated wallet
1 parent 2af188f commit bdbab8c

File tree

13 files changed

+372
-38
lines changed

13 files changed

+372
-38
lines changed

v4-client-js/examples/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export const DYDX_LOCAL_ADDRESS = 'dydx199tqg4wdlnu4qjlxchpd7seg454937hjrknju4';
1515
export const DYDX_LOCAL_MNEMONIC =
1616
'merge panther lobster crazy road hollow amused security before critic about cliff exhibit cause coyote talent happy where lion river tobacco option coconut small';
1717

18+
export const DYDX_TEST_MNEMONIC_2 = 'movie yard still copper exile wear brisk chest ride dizzy novel future menu finish radar lunar claim hub middle force turtle mouse frequent embark';
19+
1820
export const MARKET_BTC_USD: string = 'BTC-USD';
1921
export const PERPETUAL_PAIR_BTC_USD: number = 0;
2022

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { TextEncoder } from 'util';
2+
3+
import { toBase64 } from '@cosmjs/encoding';
4+
import { Order_TimeInForce } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/clob/order';
5+
6+
import { BECH32_PREFIX } from '../src';
7+
import { CompositeClient } from '../src/clients/composite-client';
8+
import { AuthenticatorType, Network, OrderSide, SelectedGasDenom } from '../src/clients/constants';
9+
import LocalWallet from '../src/clients/modules/local-wallet';
10+
import { SubaccountInfo } from '../src/clients/subaccount';
11+
import { DYDX_TEST_MNEMONIC, DYDX_TEST_MNEMONIC_2 } from './constants';
12+
13+
async function test(): Promise<void> {
14+
const wallet1 = await LocalWallet.fromMnemonic(DYDX_TEST_MNEMONIC, BECH32_PREFIX);
15+
const wallet2 = await LocalWallet.fromMnemonic(DYDX_TEST_MNEMONIC_2, BECH32_PREFIX);
16+
17+
const network = Network.staging();
18+
const client = await CompositeClient.connect(network);
19+
client.setSelectedGasDenom(SelectedGasDenom.NATIVE);
20+
21+
console.log('**Client**');
22+
console.log(client);
23+
24+
const subaccount1 = new SubaccountInfo(wallet1, 0);
25+
const subaccount2 = new SubaccountInfo(wallet2, 0);
26+
27+
// Change second wallet pubkey
28+
// Add an authenticator to allow wallet2 to place orders
29+
console.log("** Adding authenticator **");
30+
await addAuthenticator(client, subaccount1, wallet2.pubKey!.value);
31+
32+
const authenticators = await client.getAuthenticators(wallet1.address!);
33+
// Last element in authenticators array is the most recently created
34+
const lastElement = authenticators.accountAuthenticators.length - 1;
35+
const authenticatorID = authenticators.accountAuthenticators[lastElement].id;
36+
37+
// Placing order using subaccount2 for subaccount1 succeeds
38+
console.log("** Placing order with authenticator **");
39+
await placeOrder(client, subaccount2, subaccount1, authenticatorID);
40+
41+
// Remove authenticator
42+
console.log("** Removing authenticator **");
43+
await removeAuthenticator(client, subaccount1, authenticatorID);
44+
45+
// Placing an order using subaccount2 will now fail
46+
console.log("** Placing order with invalid authenticator should fail **");
47+
await placeOrder(client, subaccount2, subaccount1, authenticatorID);
48+
}
49+
50+
async function removeAuthenticator(client: CompositeClient,subaccount: SubaccountInfo, id: Long): Promise<void> {
51+
await client.removeAuthenticator(subaccount, id);
52+
}
53+
54+
async function addAuthenticator(client: CompositeClient, subaccount: SubaccountInfo, authedPubKey:string): Promise<void> {
55+
const subAuthenticators = [{
56+
type: AuthenticatorType.SIGNATURE_VERIFICATION,
57+
config: authedPubKey,
58+
},
59+
{
60+
type: AuthenticatorType.MESSAGE_FILTER,
61+
config: toBase64(new TextEncoder().encode("/dydxprotocol.clob.MsgPlaceOrder")),
62+
},
63+
];
64+
65+
const jsonString = JSON.stringify(subAuthenticators);
66+
const encodedData = new TextEncoder().encode(jsonString);
67+
68+
await client.addAuthenticator(subaccount, AuthenticatorType.ALL_OF, encodedData);
69+
}
70+
71+
async function placeOrder(client: CompositeClient, fromAccount: SubaccountInfo, forAccount: SubaccountInfo, authenticatorId: Long): Promise<void> {
72+
try {
73+
const side = OrderSide.BUY
74+
const price = Number("1000");
75+
const currentBlock = await client.validatorClient.get.latestBlockHeight();
76+
const nextValidBlockHeight = currentBlock + 5;
77+
const goodTilBlock = nextValidBlockHeight + 10;
78+
79+
const timeInForce = Order_TimeInForce.TIME_IN_FORCE_UNSPECIFIED;
80+
81+
const clientId = Math.floor(Math.random() * 10000);
82+
83+
const tx = await client.placeShortTermOrder(
84+
fromAccount,
85+
'ETH-USD',
86+
side,
87+
price,
88+
0.01,
89+
clientId,
90+
goodTilBlock,
91+
timeInForce,
92+
false,
93+
undefined,
94+
{
95+
authenticators: [authenticatorId],
96+
accountForOrder: forAccount,
97+
}
98+
);
99+
console.log('**Order Tx**');
100+
console.log(Buffer.from(tx.hash).toString('hex'));
101+
} catch (error) {
102+
console.log(error.message);
103+
}
104+
}
105+
106+
test()
107+
.then(() => {})
108+
.catch((error) => {
109+
console.log(error.message);
110+
});

v4-client-js/package-lock.json

Lines changed: 10 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

v4-client-js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@
3838
"@cosmjs/stargate": "^0.32.1",
3939
"@cosmjs/tendermint-rpc": "^0.32.1",
4040
"@cosmjs/utils": "^0.32.1",
41+
"@dydxprotocol/v4-proto": "8.0.0",
4142
"@osmonauts/lcd": "^0.6.0",
42-
"@dydxprotocol/v4-proto": "7.0.0-dev.0",
4343
"@scure/bip32": "^1.1.5",
4444
"@scure/bip39": "^1.1.1",
4545
"axios": "1.1.3",

v4-client-js/src/clients/composite-client.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
BroadcastTxAsyncResponse,
66
BroadcastTxSyncResponse,
77
} from '@cosmjs/tendermint-rpc/build/tendermint37';
8+
import { GetAuthenticatorsResponse } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/accountplus/query';
89
import {
910
Order_ConditionType,
1011
Order_TimeInForce,
@@ -17,6 +18,7 @@ import { bigIntToBytes } from '../lib/helpers';
1718
import { isStatefulOrder, verifyOrderFlags } from '../lib/validation';
1819
import { GovAddNewMarketParams, OrderFlags } from '../types';
1920
import {
21+
AuthenticatorType,
2022
Network,
2123
OrderExecution,
2224
OrderSide,
@@ -66,6 +68,11 @@ export interface OrderBatchWithMarketId {
6668
clientIds: number[];
6769
}
6870

71+
export interface PermissionedKeysAccountAuth {
72+
authenticators: Long[];
73+
accountForOrder: SubaccountInfo;
74+
}
75+
6976
export class CompositeClient {
7077
public readonly network: Network;
7178
public gasDenom: SelectedGasDenom = SelectedGasDenom.USDC;
@@ -151,6 +158,7 @@ export class CompositeClient {
151158
memo?: string,
152159
broadcastMode?: BroadcastMode,
153160
account?: () => Promise<Account>,
161+
authenticators?: Long[],
154162
): Promise<BroadcastTxAsyncResponse | BroadcastTxSyncResponse | IndexedTx> {
155163
return this.validatorClient.post.send(
156164
wallet,
@@ -160,6 +168,8 @@ export class CompositeClient {
160168
memo,
161169
broadcastMode,
162170
account,
171+
undefined,
172+
authenticators,
163173
);
164174
}
165175

@@ -297,10 +307,15 @@ export class CompositeClient {
297307
timeInForce: Order_TimeInForce,
298308
reduceOnly: boolean,
299309
memo?: string,
310+
permissionedKeysAccountAuth?: PermissionedKeysAccountAuth,
300311
): Promise<BroadcastTxAsyncResponse | BroadcastTxSyncResponse | IndexedTx> {
312+
// For permissioned orders, use the permissioning account details instead of the subaccount
313+
// This allows placing orders on behalf of another account when using permissioned keys
314+
const accountForOrder = permissionedKeysAccountAuth ? permissionedKeysAccountAuth.accountForOrder : subaccount;
301315
const msgs: Promise<EncodeObject[]> = new Promise((resolve, reject) => {
316+
302317
const msg = this.placeShortTermOrderMessage(
303-
subaccount,
318+
accountForOrder,
304319
marketId,
305320
side,
306321
price,
@@ -311,14 +326,16 @@ export class CompositeClient {
311326
reduceOnly,
312327
);
313328
msg
314-
.then((it) => resolve([it]))
329+
.then((it) => {
330+
resolve([it]);
331+
})
315332
.catch((err) => {
316333
console.log(err);
317334
reject(err);
318335
});
319336
});
320337
const account: Promise<Account> = this.validatorClient.post.account(
321-
subaccount.address,
338+
accountForOrder.address,
322339
undefined,
323340
);
324341
return this.send(
@@ -329,6 +346,7 @@ export class CompositeClient {
329346
memo,
330347
undefined,
331348
() => account,
349+
permissionedKeysAccountAuth?.authenticators,
332350
);
333351
}
334352

@@ -1217,4 +1235,23 @@ export class CompositeClient {
12171235
): Promise<BroadcastTxAsyncResponse | BroadcastTxSyncResponse | IndexedTx> {
12181236
return this.validatorClient.post.createMarketPermissionless(ticker, subaccount, broadcastMode, gasAdjustment, memo);
12191237
}
1238+
1239+
async addAuthenticator(
1240+
subaccount: SubaccountInfo,
1241+
authenticatorType: AuthenticatorType,
1242+
data: Uint8Array,
1243+
): Promise<BroadcastTxAsyncResponse | BroadcastTxSyncResponse | IndexedTx> {
1244+
return this.validatorClient.post.addAuthenticator(subaccount, authenticatorType, data)
1245+
}
1246+
1247+
async removeAuthenticator(
1248+
subaccount: SubaccountInfo,
1249+
id: Long,
1250+
): Promise<BroadcastTxAsyncResponse | BroadcastTxSyncResponse | IndexedTx> {
1251+
return this.validatorClient.post.removeAuthenticator(subaccount, id)
1252+
}
1253+
1254+
async getAuthenticators(address: string): Promise<GetAuthenticatorsResponse>{
1255+
return this.validatorClient.get.getAuthenticators(address);
1256+
}
12201257
}

v4-client-js/src/clients/constants.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ export const TYPE_URL_MSG_UNDELEGATE = '/cosmos.staking.v1beta1.MsgUndelegate';
116116
export const TYPE_URL_MSG_WITHDRAW_DELEGATOR_REWARD =
117117
'/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward';
118118

119+
// x/accountplus
120+
export const TYPE_URL_MSG_ADD_AUTHENTICATOR = '/dydxprotocol.accountplus.MsgAddAuthenticator'
121+
export const TYPE_URL_MSG_REMOVE_AUTHENTICATOR = '/dydxprotocol.accountplus.MsgRemoveAuthenticator'
122+
119123
// ------------ Chain Constants ------------
120124
// The following are same across different networks / deployments.
121125
export const GOV_MODULE_ADDRESS = 'dydx10d07y265gmmuvt4z0w9aw880jnsr700jnmapky';
@@ -196,6 +200,17 @@ export enum PnlTickInterval {
196200
day = 'day',
197201
}
198202

203+
// ----------- Authenticators -------------
204+
205+
export enum AuthenticatorType {
206+
ALL_OF = 'AllOf',
207+
ANY_OF = 'AnyOf',
208+
SIGNATURE_VERIFICATION = 'SignatureVerification',
209+
MESSAGE_FILTER = 'MessageFilter',
210+
CLOB_PAIR_ID_FILTER = 'ClobPairIdFilter',
211+
SUBACCOUNT_FILTER = 'SubaccountFilter',
212+
}
213+
199214
export enum TradingRewardAggregationPeriod {
200215
DAILY = 'DAILY',
201216
WEEKLY = 'WEEKLY',
@@ -285,7 +300,7 @@ export class Network {
285300
const indexerConfig = new IndexerConfig(IndexerApiHost.STAGING, IndexerWSHost.STAGING);
286301
const validatorConfig = new ValidatorConfig(
287302
ValidatorApiHost.STAGING,
288-
TESTNET_CHAIN_ID,
303+
STAGING_CHAIN_ID,
289304
{
290305
CHAINTOKEN_DENOM: 'adv4tnt',
291306
USDC_DENOM: 'ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5',

v4-client-js/src/clients/lib/registry.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { GeneratedType, Registry } from '@cosmjs/proto-signing';
22
import { defaultRegistryTypes } from '@cosmjs/stargate';
3+
import { MsgAddAuthenticator, MsgRemoveAuthenticator } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/accountplus/tx';
34
import { MsgRegisterAffiliate } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/affiliates/tx';
45
import {
56
MsgPlaceOrder,
@@ -38,6 +39,8 @@ import {
3839
TYPE_URL_MSG_WITHDRAW_FROM_MEGAVAULT,
3940
TYPE_URL_MSG_REGISTER_AFFILIATE,
4041
TYPE_URL_MSG_CREATE_MARKET_PERMISSIONLESS,
42+
TYPE_URL_MSG_ADD_AUTHENTICATOR,
43+
TYPE_URL_MSG_REMOVE_AUTHENTICATOR,
4144
} from '../constants';
4245

4346
export const registry: ReadonlyArray<[string, GeneratedType]> = [];
@@ -74,6 +77,11 @@ export function generateRegistry(): Registry {
7477
// affiliates
7578
[TYPE_URL_MSG_REGISTER_AFFILIATE, MsgRegisterAffiliate as GeneratedType],
7679

80+
81+
// authentication
82+
[TYPE_URL_MSG_ADD_AUTHENTICATOR, MsgAddAuthenticator as GeneratedType],
83+
[TYPE_URL_MSG_REMOVE_AUTHENTICATOR, MsgRemoveAuthenticator as GeneratedType],
84+
7785
// default types
7886
...defaultRegistryTypes,
7987
]);

0 commit comments

Comments
 (0)